@magnusekdahl/parallix 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +190 -185
- package/lib/agents/agents.js +737 -838
- package/lib/agents/agents.ts +930 -0
- package/lib/agents/claude-telemetry.js +149 -154
- package/lib/agents/claude-telemetry.ts +233 -0
- package/lib/agents/claude.js +121 -131
- package/lib/agents/claude.ts +156 -0
- package/lib/agents/codex-telemetry.js +176 -164
- package/lib/agents/codex-telemetry.ts +205 -0
- package/lib/agents/codex.js +168 -201
- package/lib/agents/codex.ts +236 -0
- package/lib/agents/limit-hit.js +216 -226
- package/lib/agents/limit-hit.ts +262 -0
- package/lib/agents/mistral-telemetry.js +7 -10
- package/lib/agents/mistral-telemetry.ts +44 -0
- package/lib/agents/mistral.js +40 -58
- package/lib/agents/mistral.ts +84 -0
- package/lib/agents/opencode-export.js +126 -127
- package/lib/agents/opencode-export.ts +160 -0
- package/lib/agents/opencode-telemetry.js +282 -261
- package/lib/agents/opencode-telemetry.ts +350 -0
- package/lib/agents/opencode.js +263 -305
- package/lib/agents/opencode.ts +370 -0
- package/lib/agents/stage-telemetry.js +23 -24
- package/lib/agents/stage-telemetry.ts +47 -0
- package/lib/commands/active.js +502 -579
- package/lib/commands/active.ts +665 -0
- package/lib/commands/checkpoint.js +98 -74
- package/lib/commands/checkpoint.ts +79 -0
- package/lib/commands/config.js +61 -31
- package/lib/commands/config.ts +47 -0
- package/lib/commands/coverage-gate.js +271 -246
- package/lib/commands/coverage-gate.ts +362 -0
- package/lib/commands/diff.js +140 -119
- package/lib/commands/diff.ts +125 -0
- package/lib/commands/draft.js +770 -923
- package/lib/commands/draft.ts +1021 -0
- package/lib/commands/handoff.js +588 -608
- package/lib/commands/handoff.ts +694 -0
- package/lib/commands/integrate.js +1332 -1463
- package/lib/commands/integrate.ts +1688 -0
- package/lib/commands/mission-start.js +279 -237
- package/lib/commands/mission-start.ts +263 -0
- package/lib/commands/rebase.js +606 -582
- package/lib/commands/rebase.ts +635 -0
- package/lib/commands/repair-handoff.js +181 -179
- package/lib/commands/repair-handoff.ts +228 -0
- package/lib/commands/resolve-conflict.js +121 -102
- package/lib/commands/resolve-conflict.ts +112 -0
- package/lib/commands/review.js +3 -4
- package/lib/commands/review.ts +14 -0
- package/lib/commands/setup-review.js +4 -5
- package/lib/commands/setup-review.ts +14 -0
- package/lib/commands/setup.js +3 -3
- package/lib/commands/setup.ts +2 -0
- package/lib/commands/stats-backfill.js +394 -359
- package/lib/commands/stats-backfill.ts +418 -0
- package/lib/commands/stats.js +1424 -1665
- package/lib/commands/stats.ts +2186 -0
- package/lib/commands/status.js +219 -200
- package/lib/commands/status.ts +219 -0
- package/lib/commands/verify.js +3 -1
- package/lib/commands/verify.ts +2 -0
- package/lib/core/fmt.js +140 -211
- package/lib/core/fmt.ts +189 -0
- package/lib/core/git.js +121 -102
- package/lib/core/git.ts +146 -0
- package/lib/core/gitignore.js +59 -106
- package/lib/core/gitignore.ts +103 -0
- package/lib/core/mission-utils.js +833 -875
- package/lib/core/mission-utils.ts +998 -0
- package/lib/core/nels.js +112 -127
- package/lib/core/nels.ts +189 -0
- package/lib/core/persistent-data-migration.js +247 -205
- package/lib/core/persistent-data-migration.ts +255 -0
- package/lib/core/product-config.js +400 -476
- package/lib/core/product-config.ts +518 -0
- package/lib/core/runtime-matrix.js +41 -87
- package/lib/core/runtime-matrix.ts +84 -0
- package/lib/core/spawn-tee.js +152 -192
- package/lib/core/spawn-tee.ts +202 -0
- package/lib/core/state-map.js +92 -92
- package/lib/core/state-map.ts +115 -0
- package/lib/core/storage.js +101 -129
- package/lib/core/storage.ts +182 -0
- package/lib/core/subagent-limit.js +15 -16
- package/lib/core/subagent-limit.ts +25 -0
- package/lib/core/verification.js +148 -138
- package/lib/core/verification.ts +200 -0
- package/lib/index.js +142 -59
- package/lib/index.ts +134 -0
- package/lib/review/rebase.js +142 -145
- package/lib/review/rebase.ts +188 -0
- package/lib/review/review-adapter.js +99 -87
- package/lib/review/review-adapter.ts +198 -0
- package/lib/review/review-artifacts.js +486 -629
- package/lib/review/review-artifacts.ts +622 -0
- package/lib/review/review-commands.js +1220 -1373
- package/lib/review/review-commands.ts +1579 -0
- package/lib/review/review-events.js +544 -886
- package/lib/review/review-events.ts +954 -0
- package/lib/review/review-loop.js +945 -1020
- package/lib/review/review-loop.ts +1174 -0
- package/lib/review/review-polling.js +86 -115
- package/lib/review/review-polling.ts +194 -0
- package/lib/review/review-prompts.js +154 -189
- package/lib/review/review-prompts.ts +182 -0
- package/lib/review/review-state.js +246 -225
- package/lib/review/review-state.ts +353 -0
- package/lib/review/review.js +63 -84
- package/lib/review/review.ts +138 -0
- package/lib/tools/backlog.js +694 -680
- package/lib/tools/backlog.ts +835 -0
- package/lib/tools/forgejo.js +1270 -1405
- package/lib/tools/forgejo.ts +1793 -0
- package/lib/tools/gatekeeper.js +106 -96
- package/lib/tools/gatekeeper.ts +124 -0
- package/lib/tools/redgreen.js +200 -181
- package/lib/tools/redgreen.ts +221 -0
- package/lib/tools/sessions.js +58 -52
- package/lib/tools/sessions.ts +81 -0
- package/lib/tools/setup-review.js +902 -975
- package/lib/tools/setup-review.ts +1152 -0
- package/package.json +7 -5
- package/prompts/draft.md +1 -0
- package/prompts/portfolio.md +1 -1
- package/px.js +226 -210
package/lib/agents/codex.js
CHANGED
|
@@ -1,244 +1,211 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.extractCodexTelemetry = void 0;
|
|
7
|
+
exports.codexAuthPath = codexAuthPath;
|
|
8
|
+
exports.buildCodexDraftInvocation = buildCodexDraftInvocation;
|
|
9
|
+
exports.codexConfigPath = codexConfigPath;
|
|
10
|
+
exports.codexHomeRoot = codexHomeRoot;
|
|
11
|
+
exports.ensureCodexHome = ensureCodexHome;
|
|
12
|
+
exports.extractCodexSessionId = extractCodexSessionId;
|
|
13
|
+
exports.headlessCodexConfig = headlessCodexConfig;
|
|
14
|
+
exports.resolveCodexCommand = resolveCodexCommand;
|
|
15
|
+
exports.startCodexDraftAgent = startCodexDraftAgent;
|
|
16
|
+
exports.__setSpawnAndTeeForTest = __setSpawnAndTeeForTest;
|
|
17
|
+
exports.__setSessionsForTest = __setSessionsForTest;
|
|
18
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
19
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
20
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
21
|
+
const spawn_tee_js_1 = require("../core/spawn-tee.js");
|
|
22
|
+
const codex_telemetry_js_1 = require("./codex-telemetry.js");
|
|
23
|
+
Object.defineProperty(exports, "extractCodexTelemetry", { enumerable: true, get: function () { return codex_telemetry_js_1.extractCodexTelemetry; } });
|
|
24
|
+
// tools/sessions is still CJS (not converted in this wave); require keeps it
|
|
25
|
+
// untyped (any) without pulling a non-included .js into the typecheck program.
|
|
6
26
|
const sessions = require('../tools/sessions');
|
|
7
|
-
|
|
8
27
|
// Injectable I/O for tests. Production uses the real spawn-tee / export capture.
|
|
9
|
-
|
|
10
|
-
let _spawnAndTee = spawnAndTee;
|
|
11
|
-
/** @type {object} */
|
|
28
|
+
let _spawnAndTee = spawn_tee_js_1.spawnAndTee;
|
|
12
29
|
let _sessions = sessions;
|
|
13
|
-
|
|
14
30
|
// Test hooks: override the launcher's I/O without touching the public signature.
|
|
15
|
-
|
|
16
|
-
function __setSpawnAndTeeForTest(fn) { _spawnAndTee = fn || spawnAndTee; }
|
|
17
|
-
/** @param {object} mod */
|
|
31
|
+
function __setSpawnAndTeeForTest(fn) { _spawnAndTee = fn || spawn_tee_js_1.spawnAndTee; }
|
|
18
32
|
function __setSessionsForTest(mod) { _sessions = mod || sessions; }
|
|
19
|
-
|
|
20
33
|
// Codex outputs "To continue this session, run codex resume <id>" at the end.
|
|
21
34
|
// Also captures the session ID from the "Interaction Summary" block.
|
|
22
35
|
const CODEX_SESSION_ID_RE = /codex\s+resume\s+([0-9a-f-]+)/i;
|
|
23
36
|
const CODEX_SESSION_ID_ALT_RE = /Session ID:\s*([0-9a-f-]+)/i;
|
|
24
|
-
|
|
25
|
-
/** @param {string} stdout */
|
|
26
37
|
function extractCodexSessionId(stdout) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
if (!stdout) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const m = CODEX_SESSION_ID_RE.exec(stdout);
|
|
42
|
+
if (m) {
|
|
43
|
+
return m[1];
|
|
44
|
+
}
|
|
45
|
+
const m2 = CODEX_SESSION_ID_ALT_RE.exec(stdout);
|
|
46
|
+
if (m2) {
|
|
47
|
+
return m2[1];
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
33
50
|
}
|
|
34
|
-
|
|
35
51
|
function resolveCodexCommand() {
|
|
36
|
-
|
|
52
|
+
return 'codex';
|
|
37
53
|
}
|
|
38
|
-
|
|
39
54
|
function hasLiveTty() {
|
|
40
|
-
|
|
55
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
41
56
|
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @param {{ prompt: string, worktree: string, interactive?: boolean, env?: {[key: string]: string}, resume?: boolean, sessionId?: string | null, model?: string | null }} opts
|
|
45
|
-
*/
|
|
46
57
|
function buildCodexDraftInvocation({ prompt, worktree, interactive = hasLiveTty(), env = {}, resume = false, sessionId = null, model = null }) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
if (resume) {
|
|
59
|
+
const args = ['exec', 'resume'];
|
|
60
|
+
if (sessionId) {
|
|
61
|
+
args.push(sessionId);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
args.push('--last');
|
|
65
|
+
}
|
|
66
|
+
if (model) {
|
|
67
|
+
args.push('-m', model);
|
|
68
|
+
}
|
|
69
|
+
args.push(prompt);
|
|
70
|
+
return {
|
|
71
|
+
command: resolveCodexCommand(),
|
|
72
|
+
args,
|
|
73
|
+
options: {
|
|
74
|
+
stdio: 'inherit',
|
|
75
|
+
cwd: worktree,
|
|
76
|
+
env: { ...process.env, ...env, HOME: codexHomeRoot(worktree) }
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const modelArgs = model ? ['-m', model] : [];
|
|
81
|
+
const args = interactive
|
|
82
|
+
? ['--full-auto', ...modelArgs, '--cd', worktree, prompt]
|
|
83
|
+
: ['exec', '--sandbox', 'danger-full-access', ...modelArgs, '--cd', worktree, prompt];
|
|
84
|
+
const baseEnv = { ...process.env };
|
|
85
|
+
if (!interactive) {
|
|
86
|
+
baseEnv.HOME = codexHomeRoot(worktree);
|
|
53
87
|
}
|
|
54
|
-
if (model) {args.push('-m', model);}
|
|
55
|
-
args.push(prompt);
|
|
56
88
|
return {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
89
|
+
command: resolveCodexCommand(),
|
|
90
|
+
args,
|
|
91
|
+
options: {
|
|
92
|
+
stdio: 'inherit',
|
|
93
|
+
cwd: worktree,
|
|
94
|
+
env: { ...baseEnv, ...env }
|
|
95
|
+
}
|
|
64
96
|
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const modelArgs = model ? ['-m', model] : [];
|
|
68
|
-
const args = interactive
|
|
69
|
-
? ['--full-auto', ...modelArgs, '--cd', worktree, prompt]
|
|
70
|
-
: ['exec', '--sandbox', 'danger-full-access', ...modelArgs, '--cd', worktree, prompt];
|
|
71
|
-
|
|
72
|
-
const baseEnv = { ...process.env };
|
|
73
|
-
if (!interactive) {
|
|
74
|
-
baseEnv.HOME = codexHomeRoot(worktree);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
command: resolveCodexCommand(),
|
|
79
|
-
args,
|
|
80
|
-
options: {
|
|
81
|
-
stdio: 'inherit',
|
|
82
|
-
cwd: worktree,
|
|
83
|
-
env: { ...baseEnv, ...env }
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
97
|
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* @param {{ prompt: string, worktree: string, env?: {[key: string]: string}, resume?: boolean, sessionId?: string | null, model?: string | null, teeOptions?: object, slug?: string | null, role?: string | null }} opts
|
|
90
|
-
*/
|
|
91
98
|
function startCodexDraftAgent({ prompt, worktree, env = {}, resume = false, sessionId = null, model = null, teeOptions = {}, slug = null, role = null }) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
*/
|
|
101
|
-
function isStaleSessionResult(result) {
|
|
102
|
-
if (!result) {return false;}
|
|
103
|
-
const stderr = result.stderr || '';
|
|
104
|
-
const stdout = result.stdout || '';
|
|
105
|
-
return (stderr.includes('Session not found') || stdout.includes('Session not found'));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* @param {{stdout?: string, sessionId?: string, telemetry?: object, model?: string, provider?: string}} result
|
|
110
|
-
*/
|
|
111
|
-
function processResult(result) {
|
|
112
|
-
if (result && result.stdout) {
|
|
113
|
-
result.sessionId = extractCodexSessionId(result.stdout) || undefined;
|
|
114
|
-
}
|
|
115
|
-
if (result) {
|
|
116
|
-
// Real usage telemetry comes from the codex rollout JSONL written under the
|
|
117
|
-
// worktree-scoped codex-home, not the stdout stream (see codex-telemetry.js).
|
|
118
|
-
try {
|
|
119
|
-
const telemetry = extractCodexTelemetry(codexHomeRoot(worktree), { sinceMs: Date.now() });
|
|
120
|
-
if (telemetry) {
|
|
121
|
-
result.telemetry = telemetry;
|
|
122
|
-
if (telemetry.model) {result.model = telemetry.model;}
|
|
123
|
-
if (telemetry.provider) {result.provider = telemetry.provider;}
|
|
99
|
+
// The launcher always tees through spawnAndTee for limit-hit detection, which
|
|
100
|
+
// forces child stdio to ['inherit', 'pipe', 'pipe']. Codex's `--full-auto`
|
|
101
|
+
// interactive UI requires a TTY on stdout, so we always use the headless
|
|
102
|
+
// `exec` path here regardless of whether the parent has a TTY.
|
|
103
|
+
ensureCodexHome(worktree);
|
|
104
|
+
function isStaleSessionResult(result) {
|
|
105
|
+
if (!result) {
|
|
106
|
+
return false;
|
|
124
107
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
108
|
+
const stderr = result.stderr || '';
|
|
109
|
+
const stdout = result.stdout || '';
|
|
110
|
+
return (stderr.includes('Session not found') || stdout.includes('Session not found'));
|
|
128
111
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
112
|
+
function processResult(result) {
|
|
113
|
+
if (result && result.stdout) {
|
|
114
|
+
result.sessionId = extractCodexSessionId(result.stdout) || undefined;
|
|
115
|
+
}
|
|
116
|
+
if (result) {
|
|
117
|
+
// Real usage telemetry comes from the codex rollout JSONL written under the
|
|
118
|
+
// worktree-scoped codex-home, not the stdout stream (see codex-telemetry.js).
|
|
119
|
+
try {
|
|
120
|
+
const telemetry = (0, codex_telemetry_js_1.extractCodexTelemetry)(codexHomeRoot(worktree), { sinceMs: Date.now() });
|
|
121
|
+
if (telemetry) {
|
|
122
|
+
result.telemetry = telemetry;
|
|
123
|
+
if (telemetry.model) {
|
|
124
|
+
result.model = telemetry.model;
|
|
125
|
+
}
|
|
126
|
+
if (telemetry.provider) {
|
|
127
|
+
result.provider = telemetry.provider;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (_) {
|
|
132
|
+
// Telemetry is best-effort; never let it break the launch result.
|
|
133
|
+
}
|
|
144
134
|
}
|
|
145
135
|
return result;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
136
|
+
}
|
|
137
|
+
function staleSessionHandler(invocation) {
|
|
138
|
+
return _spawnAndTee(invocation.command, invocation.args, { ...invocation.options, ...teeOptions })
|
|
139
|
+
.then((result) => {
|
|
140
|
+
if (isStaleSessionResult(result) && worktree && resume) {
|
|
141
|
+
try {
|
|
142
|
+
_sessions.clearSession(worktree, slug, role);
|
|
143
|
+
}
|
|
144
|
+
catch (_) { /* best-effort */ }
|
|
145
|
+
const freshInv = buildCodexDraftInvocation({ prompt, worktree, interactive: false, env, resume: false, sessionId: null, model });
|
|
146
|
+
return _spawnAndTee(freshInv.command, freshInv.args, { ...freshInv.options, ...teeOptions });
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
})
|
|
150
|
+
.then(processResult);
|
|
151
|
+
}
|
|
152
|
+
const invocation = buildCodexDraftInvocation({ prompt, worktree, interactive: false, env, resume, sessionId, model });
|
|
153
|
+
const resultPromise = staleSessionHandler(invocation);
|
|
154
|
+
return { invocation, resultPromise };
|
|
154
155
|
}
|
|
155
|
-
|
|
156
|
-
/** @param {string} worktree */
|
|
157
156
|
function codexHomeRoot(worktree) {
|
|
158
|
-
|
|
157
|
+
return node_path_1.default.join(worktree, '.workflow', 'codex-home');
|
|
159
158
|
}
|
|
160
|
-
|
|
161
|
-
/** @param {string} worktree */
|
|
162
159
|
function codexConfigPath(worktree) {
|
|
163
|
-
|
|
160
|
+
return node_path_1.default.join(codexHomeRoot(worktree), '.codex', 'config.toml');
|
|
164
161
|
}
|
|
165
|
-
|
|
166
|
-
/** @param {string} worktree */
|
|
167
162
|
function codexAuthPath(worktree) {
|
|
168
|
-
|
|
163
|
+
return node_path_1.default.join(codexHomeRoot(worktree), '.codex', 'auth.json');
|
|
169
164
|
}
|
|
170
|
-
|
|
171
165
|
function userCodexAuthPath() {
|
|
172
|
-
|
|
166
|
+
return node_path_1.default.join(node_os_1.default.homedir(), '.codex', 'auth.json');
|
|
173
167
|
}
|
|
174
|
-
|
|
175
|
-
/** @param {*} value */
|
|
176
168
|
function tomlString(value) {
|
|
177
|
-
|
|
169
|
+
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
178
170
|
}
|
|
179
|
-
|
|
180
171
|
// Graphify's Codex skill requires multi_agent = true for spawn_agent subagent
|
|
181
172
|
// dispatch (skill-codex.md line 233). Without this, the copied Graphify skill
|
|
182
173
|
// cannot launch semantic extraction subagents and parallel graph building fails.
|
|
183
|
-
/** @param {string} worktree */
|
|
184
174
|
function headlessCodexConfig(worktree) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
175
|
+
const worktreeParent = node_path_1.default.dirname(node_path_1.default.resolve(worktree));
|
|
176
|
+
return [
|
|
177
|
+
'sandbox_mode = "danger-full-access"',
|
|
178
|
+
'',
|
|
179
|
+
'[features]',
|
|
180
|
+
'multi_agent = true',
|
|
181
|
+
'',
|
|
182
|
+
`[projects.${tomlString(worktreeParent)}]`,
|
|
183
|
+
'trust_level = "trusted"',
|
|
184
|
+
'approval_policy = "never"',
|
|
185
|
+
'',
|
|
186
|
+
`[projects.${tomlString(node_path_1.default.resolve(worktree))}]`,
|
|
187
|
+
'trust_level = "trusted"',
|
|
188
|
+
'approval_policy = "never"',
|
|
189
|
+
''
|
|
190
|
+
].join('\n');
|
|
201
191
|
}
|
|
202
|
-
|
|
203
|
-
/** @param {string} worktree */
|
|
204
192
|
function ensureCodexHome(worktree) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
fs.cpSync(
|
|
224
|
-
sourceSkillPath,
|
|
225
|
-
path.join(codexHomeRoot(worktree), '.agents', 'skills', 'graphify'),
|
|
226
|
-
{ recursive: true }
|
|
227
|
-
);
|
|
228
|
-
}
|
|
193
|
+
node_fs_1.default.mkdirSync(codexHomeRoot(worktree), { recursive: true });
|
|
194
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(codexConfigPath(worktree)), { recursive: true });
|
|
195
|
+
node_fs_1.default.writeFileSync(codexConfigPath(worktree), headlessCodexConfig(worktree), 'utf8');
|
|
196
|
+
const sourceAuthPath = userCodexAuthPath();
|
|
197
|
+
if (node_fs_1.default.existsSync(sourceAuthPath)) {
|
|
198
|
+
node_fs_1.default.copyFileSync(sourceAuthPath, codexAuthPath(worktree));
|
|
199
|
+
}
|
|
200
|
+
// Seed the globally-installed Graphify skill into the isolated worktree HOME,
|
|
201
|
+
// the same way auth.json is carried over from the operator's real home. Codex
|
|
202
|
+
// runs with HOME=codexHomeRoot, so it looks for skills under
|
|
203
|
+
// <home>/.agents/skills; without this copy the one-time global install at
|
|
204
|
+
// ~/.agents/skills is invisible to codex. This is a plain filesystem copy of
|
|
205
|
+
// an existing install, not a `graphify install` invocation, and skips cleanly
|
|
206
|
+
// when the skill is absent.
|
|
207
|
+
const sourceSkillPath = node_path_1.default.join(node_os_1.default.homedir(), '.agents', 'skills', 'graphify');
|
|
208
|
+
if (node_fs_1.default.existsSync(sourceSkillPath)) {
|
|
209
|
+
node_fs_1.default.cpSync(sourceSkillPath, node_path_1.default.join(codexHomeRoot(worktree), '.agents', 'skills', 'graphify'), { recursive: true });
|
|
210
|
+
}
|
|
229
211
|
}
|
|
230
|
-
|
|
231
|
-
module.exports = {
|
|
232
|
-
codexAuthPath,
|
|
233
|
-
buildCodexDraftInvocation,
|
|
234
|
-
codexConfigPath,
|
|
235
|
-
codexHomeRoot,
|
|
236
|
-
ensureCodexHome,
|
|
237
|
-
extractCodexSessionId,
|
|
238
|
-
extractCodexTelemetry,
|
|
239
|
-
headlessCodexConfig,
|
|
240
|
-
resolveCodexCommand,
|
|
241
|
-
startCodexDraftAgent,
|
|
242
|
-
__setSpawnAndTeeForTest,
|
|
243
|
-
__setSessionsForTest
|
|
244
|
-
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnAndTee } from '../core/spawn-tee.js';
|
|
5
|
+
import { extractCodexTelemetry } from './codex-telemetry.js';
|
|
6
|
+
// tools/sessions is still CJS (not converted in this wave); require keeps it
|
|
7
|
+
// untyped (any) without pulling a non-included .js into the typecheck program.
|
|
8
|
+
const sessions = require('../tools/sessions');
|
|
9
|
+
|
|
10
|
+
interface CodexInvocationOptions {
|
|
11
|
+
prompt: string;
|
|
12
|
+
worktree: string;
|
|
13
|
+
interactive?: boolean;
|
|
14
|
+
env?: {[key: string]: string};
|
|
15
|
+
resume?: boolean;
|
|
16
|
+
sessionId?: string | null;
|
|
17
|
+
model?: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface StartCodexAgentOptions extends CodexInvocationOptions {
|
|
21
|
+
teeOptions?: object;
|
|
22
|
+
slug?: string | null;
|
|
23
|
+
role?: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Injectable I/O for tests. Production uses the real spawn-tee / export capture.
|
|
27
|
+
let _spawnAndTee: any = spawnAndTee;
|
|
28
|
+
let _sessions: any = sessions;
|
|
29
|
+
|
|
30
|
+
// Test hooks: override the launcher's I/O without touching the public signature.
|
|
31
|
+
function __setSpawnAndTeeForTest(fn: any) { _spawnAndTee = fn || spawnAndTee; }
|
|
32
|
+
function __setSessionsForTest(mod: any) { _sessions = mod || sessions; }
|
|
33
|
+
|
|
34
|
+
// Codex outputs "To continue this session, run codex resume <id>" at the end.
|
|
35
|
+
// Also captures the session ID from the "Interaction Summary" block.
|
|
36
|
+
const CODEX_SESSION_ID_RE = /codex\s+resume\s+([0-9a-f-]+)/i;
|
|
37
|
+
const CODEX_SESSION_ID_ALT_RE = /Session ID:\s*([0-9a-f-]+)/i;
|
|
38
|
+
|
|
39
|
+
function extractCodexSessionId(stdout: string) {
|
|
40
|
+
if (!stdout) {return null;}
|
|
41
|
+
const m = CODEX_SESSION_ID_RE.exec(stdout);
|
|
42
|
+
if (m) {return m[1];}
|
|
43
|
+
const m2 = CODEX_SESSION_ID_ALT_RE.exec(stdout);
|
|
44
|
+
if (m2) {return m2[1];}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveCodexCommand() {
|
|
49
|
+
return 'codex';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasLiveTty() {
|
|
53
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildCodexDraftInvocation({ prompt, worktree, interactive = hasLiveTty(), env = {}, resume = false, sessionId = null, model = null }: CodexInvocationOptions) {
|
|
57
|
+
if (resume) {
|
|
58
|
+
const args = ['exec', 'resume'];
|
|
59
|
+
if (sessionId) {
|
|
60
|
+
args.push(sessionId);
|
|
61
|
+
} else {
|
|
62
|
+
args.push('--last');
|
|
63
|
+
}
|
|
64
|
+
if (model) {args.push('-m', model);}
|
|
65
|
+
args.push(prompt);
|
|
66
|
+
return {
|
|
67
|
+
command: resolveCodexCommand(),
|
|
68
|
+
args,
|
|
69
|
+
options: {
|
|
70
|
+
stdio: 'inherit',
|
|
71
|
+
cwd: worktree,
|
|
72
|
+
env: { ...process.env, ...env, HOME: codexHomeRoot(worktree) }
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const modelArgs = model ? ['-m', model] : [];
|
|
78
|
+
const args = interactive
|
|
79
|
+
? ['--full-auto', ...modelArgs, '--cd', worktree, prompt]
|
|
80
|
+
: ['exec', '--sandbox', 'danger-full-access', ...modelArgs, '--cd', worktree, prompt];
|
|
81
|
+
|
|
82
|
+
const baseEnv = { ...process.env };
|
|
83
|
+
if (!interactive) {
|
|
84
|
+
baseEnv.HOME = codexHomeRoot(worktree);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
command: resolveCodexCommand(),
|
|
89
|
+
args,
|
|
90
|
+
options: {
|
|
91
|
+
stdio: 'inherit',
|
|
92
|
+
cwd: worktree,
|
|
93
|
+
env: { ...baseEnv, ...env }
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function startCodexDraftAgent({ prompt, worktree, env = {}, resume = false, sessionId = null, model = null, teeOptions = {}, slug = null, role = null }: StartCodexAgentOptions) {
|
|
99
|
+
// The launcher always tees through spawnAndTee for limit-hit detection, which
|
|
100
|
+
// forces child stdio to ['inherit', 'pipe', 'pipe']. Codex's `--full-auto`
|
|
101
|
+
// interactive UI requires a TTY on stdout, so we always use the headless
|
|
102
|
+
// `exec` path here regardless of whether the parent has a TTY.
|
|
103
|
+
ensureCodexHome(worktree);
|
|
104
|
+
|
|
105
|
+
function isStaleSessionResult(result: any) {
|
|
106
|
+
if (!result) {return false;}
|
|
107
|
+
const stderr = result.stderr || '';
|
|
108
|
+
const stdout = result.stdout || '';
|
|
109
|
+
return (stderr.includes('Session not found') || stdout.includes('Session not found'));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function processResult(result: any) {
|
|
113
|
+
if (result && result.stdout) {
|
|
114
|
+
result.sessionId = extractCodexSessionId(result.stdout) || undefined;
|
|
115
|
+
}
|
|
116
|
+
if (result) {
|
|
117
|
+
// Real usage telemetry comes from the codex rollout JSONL written under the
|
|
118
|
+
// worktree-scoped codex-home, not the stdout stream (see codex-telemetry.js).
|
|
119
|
+
try {
|
|
120
|
+
const telemetry = extractCodexTelemetry(codexHomeRoot(worktree), { sinceMs: Date.now() });
|
|
121
|
+
if (telemetry) {
|
|
122
|
+
result.telemetry = telemetry;
|
|
123
|
+
if (telemetry.model) {result.model = telemetry.model;}
|
|
124
|
+
if (telemetry.provider) {result.provider = telemetry.provider;}
|
|
125
|
+
}
|
|
126
|
+
} catch (_) {
|
|
127
|
+
// Telemetry is best-effort; never let it break the launch result.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function staleSessionHandler(invocation: any) {
|
|
134
|
+
return _spawnAndTee(invocation.command, invocation.args, { ...invocation.options, ...teeOptions })
|
|
135
|
+
.then((result: any) => {
|
|
136
|
+
if (isStaleSessionResult(result) && worktree && resume) {
|
|
137
|
+
try {
|
|
138
|
+
_sessions.clearSession(worktree, slug, role);
|
|
139
|
+
} catch (_) { /* best-effort */ }
|
|
140
|
+
const freshInv = buildCodexDraftInvocation({ prompt, worktree, interactive: false, env, resume: false, sessionId: null, model });
|
|
141
|
+
return _spawnAndTee(freshInv.command, freshInv.args, { ...freshInv.options, ...teeOptions });
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
})
|
|
145
|
+
.then(processResult);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const invocation = buildCodexDraftInvocation({ prompt, worktree, interactive: false, env, resume, sessionId, model });
|
|
149
|
+
const resultPromise = staleSessionHandler(invocation);
|
|
150
|
+
|
|
151
|
+
return { invocation, resultPromise };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function codexHomeRoot(worktree: string) {
|
|
155
|
+
return path.join(worktree, '.workflow', 'codex-home');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function codexConfigPath(worktree: string) {
|
|
159
|
+
return path.join(codexHomeRoot(worktree), '.codex', 'config.toml');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function codexAuthPath(worktree: string) {
|
|
163
|
+
return path.join(codexHomeRoot(worktree), '.codex', 'auth.json');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function userCodexAuthPath() {
|
|
167
|
+
return path.join(os.homedir(), '.codex', 'auth.json');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function tomlString(value: any) {
|
|
171
|
+
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Graphify's Codex skill requires multi_agent = true for spawn_agent subagent
|
|
175
|
+
// dispatch (skill-codex.md line 233). Without this, the copied Graphify skill
|
|
176
|
+
// cannot launch semantic extraction subagents and parallel graph building fails.
|
|
177
|
+
function headlessCodexConfig(worktree: string) {
|
|
178
|
+
const worktreeParent = path.dirname(path.resolve(worktree));
|
|
179
|
+
return [
|
|
180
|
+
'sandbox_mode = "danger-full-access"',
|
|
181
|
+
'',
|
|
182
|
+
'[features]',
|
|
183
|
+
'multi_agent = true',
|
|
184
|
+
'',
|
|
185
|
+
`[projects.${tomlString(worktreeParent)}]`,
|
|
186
|
+
'trust_level = "trusted"',
|
|
187
|
+
'approval_policy = "never"',
|
|
188
|
+
'',
|
|
189
|
+
`[projects.${tomlString(path.resolve(worktree))}]`,
|
|
190
|
+
'trust_level = "trusted"',
|
|
191
|
+
'approval_policy = "never"',
|
|
192
|
+
''
|
|
193
|
+
].join('\n');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function ensureCodexHome(worktree: string) {
|
|
197
|
+
fs.mkdirSync(codexHomeRoot(worktree), { recursive: true });
|
|
198
|
+
fs.mkdirSync(path.dirname(codexConfigPath(worktree)), { recursive: true });
|
|
199
|
+
fs.writeFileSync(codexConfigPath(worktree), headlessCodexConfig(worktree), 'utf8');
|
|
200
|
+
|
|
201
|
+
const sourceAuthPath = userCodexAuthPath();
|
|
202
|
+
if (fs.existsSync(sourceAuthPath)) {
|
|
203
|
+
fs.copyFileSync(sourceAuthPath, codexAuthPath(worktree));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Seed the globally-installed Graphify skill into the isolated worktree HOME,
|
|
207
|
+
// the same way auth.json is carried over from the operator's real home. Codex
|
|
208
|
+
// runs with HOME=codexHomeRoot, so it looks for skills under
|
|
209
|
+
// <home>/.agents/skills; without this copy the one-time global install at
|
|
210
|
+
// ~/.agents/skills is invisible to codex. This is a plain filesystem copy of
|
|
211
|
+
// an existing install, not a `graphify install` invocation, and skips cleanly
|
|
212
|
+
// when the skill is absent.
|
|
213
|
+
const sourceSkillPath = path.join(os.homedir(), '.agents', 'skills', 'graphify');
|
|
214
|
+
if (fs.existsSync(sourceSkillPath)) {
|
|
215
|
+
fs.cpSync(
|
|
216
|
+
sourceSkillPath,
|
|
217
|
+
path.join(codexHomeRoot(worktree), '.agents', 'skills', 'graphify'),
|
|
218
|
+
{ recursive: true }
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export {
|
|
224
|
+
codexAuthPath,
|
|
225
|
+
buildCodexDraftInvocation,
|
|
226
|
+
codexConfigPath,
|
|
227
|
+
codexHomeRoot,
|
|
228
|
+
ensureCodexHome,
|
|
229
|
+
extractCodexSessionId,
|
|
230
|
+
extractCodexTelemetry,
|
|
231
|
+
headlessCodexConfig,
|
|
232
|
+
resolveCodexCommand,
|
|
233
|
+
startCodexDraftAgent,
|
|
234
|
+
__setSpawnAndTeeForTest,
|
|
235
|
+
__setSessionsForTest
|
|
236
|
+
};
|