@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.
Files changed (127) hide show
  1. package/index.js +190 -185
  2. package/lib/agents/agents.js +737 -838
  3. package/lib/agents/agents.ts +930 -0
  4. package/lib/agents/claude-telemetry.js +149 -154
  5. package/lib/agents/claude-telemetry.ts +233 -0
  6. package/lib/agents/claude.js +121 -131
  7. package/lib/agents/claude.ts +156 -0
  8. package/lib/agents/codex-telemetry.js +176 -164
  9. package/lib/agents/codex-telemetry.ts +205 -0
  10. package/lib/agents/codex.js +168 -201
  11. package/lib/agents/codex.ts +236 -0
  12. package/lib/agents/limit-hit.js +216 -226
  13. package/lib/agents/limit-hit.ts +262 -0
  14. package/lib/agents/mistral-telemetry.js +7 -10
  15. package/lib/agents/mistral-telemetry.ts +44 -0
  16. package/lib/agents/mistral.js +40 -58
  17. package/lib/agents/mistral.ts +84 -0
  18. package/lib/agents/opencode-export.js +126 -127
  19. package/lib/agents/opencode-export.ts +160 -0
  20. package/lib/agents/opencode-telemetry.js +282 -261
  21. package/lib/agents/opencode-telemetry.ts +350 -0
  22. package/lib/agents/opencode.js +263 -305
  23. package/lib/agents/opencode.ts +370 -0
  24. package/lib/agents/stage-telemetry.js +23 -24
  25. package/lib/agents/stage-telemetry.ts +47 -0
  26. package/lib/commands/active.js +502 -579
  27. package/lib/commands/active.ts +665 -0
  28. package/lib/commands/checkpoint.js +98 -74
  29. package/lib/commands/checkpoint.ts +79 -0
  30. package/lib/commands/config.js +61 -31
  31. package/lib/commands/config.ts +47 -0
  32. package/lib/commands/coverage-gate.js +271 -246
  33. package/lib/commands/coverage-gate.ts +362 -0
  34. package/lib/commands/diff.js +140 -119
  35. package/lib/commands/diff.ts +125 -0
  36. package/lib/commands/draft.js +770 -923
  37. package/lib/commands/draft.ts +1021 -0
  38. package/lib/commands/handoff.js +588 -608
  39. package/lib/commands/handoff.ts +694 -0
  40. package/lib/commands/integrate.js +1332 -1463
  41. package/lib/commands/integrate.ts +1688 -0
  42. package/lib/commands/mission-start.js +279 -237
  43. package/lib/commands/mission-start.ts +263 -0
  44. package/lib/commands/rebase.js +606 -582
  45. package/lib/commands/rebase.ts +635 -0
  46. package/lib/commands/repair-handoff.js +181 -179
  47. package/lib/commands/repair-handoff.ts +228 -0
  48. package/lib/commands/resolve-conflict.js +121 -102
  49. package/lib/commands/resolve-conflict.ts +112 -0
  50. package/lib/commands/review.js +3 -4
  51. package/lib/commands/review.ts +14 -0
  52. package/lib/commands/setup-review.js +4 -5
  53. package/lib/commands/setup-review.ts +14 -0
  54. package/lib/commands/setup.js +3 -3
  55. package/lib/commands/setup.ts +2 -0
  56. package/lib/commands/stats-backfill.js +394 -359
  57. package/lib/commands/stats-backfill.ts +418 -0
  58. package/lib/commands/stats.js +1424 -1665
  59. package/lib/commands/stats.ts +2186 -0
  60. package/lib/commands/status.js +219 -200
  61. package/lib/commands/status.ts +219 -0
  62. package/lib/commands/verify.js +3 -1
  63. package/lib/commands/verify.ts +2 -0
  64. package/lib/core/fmt.js +140 -211
  65. package/lib/core/fmt.ts +189 -0
  66. package/lib/core/git.js +121 -102
  67. package/lib/core/git.ts +146 -0
  68. package/lib/core/gitignore.js +59 -106
  69. package/lib/core/gitignore.ts +103 -0
  70. package/lib/core/mission-utils.js +833 -875
  71. package/lib/core/mission-utils.ts +998 -0
  72. package/lib/core/nels.js +112 -127
  73. package/lib/core/nels.ts +189 -0
  74. package/lib/core/persistent-data-migration.js +247 -205
  75. package/lib/core/persistent-data-migration.ts +255 -0
  76. package/lib/core/product-config.js +400 -476
  77. package/lib/core/product-config.ts +518 -0
  78. package/lib/core/runtime-matrix.js +41 -87
  79. package/lib/core/runtime-matrix.ts +84 -0
  80. package/lib/core/spawn-tee.js +152 -192
  81. package/lib/core/spawn-tee.ts +202 -0
  82. package/lib/core/state-map.js +92 -92
  83. package/lib/core/state-map.ts +115 -0
  84. package/lib/core/storage.js +101 -129
  85. package/lib/core/storage.ts +182 -0
  86. package/lib/core/subagent-limit.js +15 -16
  87. package/lib/core/subagent-limit.ts +25 -0
  88. package/lib/core/verification.js +148 -138
  89. package/lib/core/verification.ts +200 -0
  90. package/lib/index.js +142 -59
  91. package/lib/index.ts +134 -0
  92. package/lib/review/rebase.js +142 -145
  93. package/lib/review/rebase.ts +188 -0
  94. package/lib/review/review-adapter.js +99 -87
  95. package/lib/review/review-adapter.ts +198 -0
  96. package/lib/review/review-artifacts.js +486 -629
  97. package/lib/review/review-artifacts.ts +622 -0
  98. package/lib/review/review-commands.js +1220 -1373
  99. package/lib/review/review-commands.ts +1579 -0
  100. package/lib/review/review-events.js +544 -886
  101. package/lib/review/review-events.ts +954 -0
  102. package/lib/review/review-loop.js +945 -1020
  103. package/lib/review/review-loop.ts +1174 -0
  104. package/lib/review/review-polling.js +86 -115
  105. package/lib/review/review-polling.ts +194 -0
  106. package/lib/review/review-prompts.js +154 -189
  107. package/lib/review/review-prompts.ts +182 -0
  108. package/lib/review/review-state.js +246 -225
  109. package/lib/review/review-state.ts +353 -0
  110. package/lib/review/review.js +63 -84
  111. package/lib/review/review.ts +138 -0
  112. package/lib/tools/backlog.js +694 -680
  113. package/lib/tools/backlog.ts +835 -0
  114. package/lib/tools/forgejo.js +1270 -1405
  115. package/lib/tools/forgejo.ts +1793 -0
  116. package/lib/tools/gatekeeper.js +106 -96
  117. package/lib/tools/gatekeeper.ts +124 -0
  118. package/lib/tools/redgreen.js +200 -181
  119. package/lib/tools/redgreen.ts +221 -0
  120. package/lib/tools/sessions.js +58 -52
  121. package/lib/tools/sessions.ts +81 -0
  122. package/lib/tools/setup-review.js +902 -975
  123. package/lib/tools/setup-review.ts +1152 -0
  124. package/package.json +7 -5
  125. package/prompts/draft.md +1 -0
  126. package/prompts/portfolio.md +1 -1
  127. package/px.js +226 -210
@@ -1,244 +1,211 @@
1
- const fs = require('fs');
2
- const os = require('os');
3
- const path = require('path');
4
- const { spawnAndTee } = require('../core/spawn-tee');
5
- const { extractCodexTelemetry } = require('./codex-telemetry');
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
- /** @type {Function} */
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
- /** @param {Function} fn */
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
- if (!stdout) {return null;}
28
- const m = CODEX_SESSION_ID_RE.exec(stdout);
29
- if (m) {return m[1];}
30
- const m2 = CODEX_SESSION_ID_ALT_RE.exec(stdout);
31
- if (m2) {return m2[1];}
32
- return null;
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
- return 'codex';
52
+ return 'codex';
37
53
  }
38
-
39
54
  function hasLiveTty() {
40
- return Boolean(process.stdin.isTTY && process.stdout.isTTY);
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
- if (resume) {
48
- const args = ['exec', 'resume'];
49
- if (sessionId) {
50
- args.push(sessionId);
51
- } else {
52
- args.push('--last');
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
- command: resolveCodexCommand(),
58
- args,
59
- options: {
60
- stdio: 'inherit',
61
- cwd: worktree,
62
- env: { ...process.env, ...env, HOME: codexHomeRoot(worktree) }
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
- // The launcher always tees through spawnAndTee for limit-hit detection, which
93
- // forces child stdio to ['inherit', 'pipe', 'pipe']. Codex's `--full-auto`
94
- // interactive UI requires a TTY on stdout, so we always use the headless
95
- // `exec` path here regardless of whether the parent has a TTY.
96
- ensureCodexHome(worktree);
97
-
98
- /**
99
- * @param {{stdout?: string, stderr?: string}} result
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
- } catch (_) {
126
- // Telemetry is best-effort; never let it break the launch result.
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
- return result;
130
- }
131
-
132
- /**
133
- * @param {{command: string, args: string[], options?: object}} invocation
134
- */
135
- function staleSessionHandler(invocation) {
136
- return _spawnAndTee(invocation.command, invocation.args, { ...invocation.options, ...teeOptions })
137
- .then(/** @param {object} result */ (result) => {
138
- if (isStaleSessionResult(result) && worktree && resume) {
139
- try {
140
- /** @type {any} */(_sessions).clearSession(worktree, slug, role);
141
- } catch (_) { /* best-effort */ }
142
- const freshInv = buildCodexDraftInvocation({ prompt, worktree, interactive: false, env, resume: false, sessionId: null, model });
143
- return _spawnAndTee(freshInv.command, freshInv.args, { ...freshInv.options, ...teeOptions });
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
- .then(processResult);
148
- }
149
-
150
- const invocation = buildCodexDraftInvocation({ prompt, worktree, interactive: false, env, resume, sessionId, model });
151
- const resultPromise = staleSessionHandler(invocation);
152
-
153
- return { invocation, resultPromise };
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
- return path.join(worktree, '.workflow', 'codex-home');
157
+ return node_path_1.default.join(worktree, '.workflow', 'codex-home');
159
158
  }
160
-
161
- /** @param {string} worktree */
162
159
  function codexConfigPath(worktree) {
163
- return path.join(codexHomeRoot(worktree), '.codex', 'config.toml');
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
- return path.join(codexHomeRoot(worktree), '.codex', 'auth.json');
163
+ return node_path_1.default.join(codexHomeRoot(worktree), '.codex', 'auth.json');
169
164
  }
170
-
171
165
  function userCodexAuthPath() {
172
- return path.join(os.homedir(), '.codex', 'auth.json');
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
- return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
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
- const worktreeParent = path.dirname(path.resolve(worktree));
186
- return [
187
- 'sandbox_mode = "danger-full-access"',
188
- '',
189
- '[features]',
190
- 'multi_agent = true',
191
- '',
192
- `[projects.${tomlString(worktreeParent)}]`,
193
- 'trust_level = "trusted"',
194
- 'approval_policy = "never"',
195
- '',
196
- `[projects.${tomlString(path.resolve(worktree))}]`,
197
- 'trust_level = "trusted"',
198
- 'approval_policy = "never"',
199
- ''
200
- ].join('\n');
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
- fs.mkdirSync(codexHomeRoot(worktree), { recursive: true });
206
- fs.mkdirSync(path.dirname(codexConfigPath(worktree)), { recursive: true });
207
- fs.writeFileSync(codexConfigPath(worktree), headlessCodexConfig(worktree), 'utf8');
208
-
209
- const sourceAuthPath = userCodexAuthPath();
210
- if (fs.existsSync(sourceAuthPath)) {
211
- fs.copyFileSync(sourceAuthPath, codexAuthPath(worktree));
212
- }
213
-
214
- // Seed the globally-installed Graphify skill into the isolated worktree HOME,
215
- // the same way auth.json is carried over from the operator's real home. Codex
216
- // runs with HOME=codexHomeRoot, so it looks for skills under
217
- // <home>/.agents/skills; without this copy the one-time global install at
218
- // ~/.agents/skills is invisible to codex. This is a plain filesystem copy of
219
- // an existing install, not a `graphify install` invocation, and skips cleanly
220
- // when the skill is absent.
221
- const sourceSkillPath = path.join(os.homedir(), '.agents', 'skills', 'graphify');
222
- if (fs.existsSync(sourceSkillPath)) {
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
+ };