@magnusekdahl/parallix 1.1.0 → 1.2.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 (68) hide show
  1. package/config/integration-pipelines.json +9 -0
  2. package/config/workflow.config.schema.json +12 -0
  3. package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
  4. package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
  5. package/docs/adr/0041-integration-pipeline-gates.md +1 -1
  6. package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
  7. package/docs/adr/index.md +1 -0
  8. package/docs/use-cases.md +44 -0
  9. package/lib/agents/agents.js +149 -100
  10. package/lib/agents/claude-telemetry.js +14 -10
  11. package/lib/agents/claude.js +32 -11
  12. package/lib/agents/codex-telemetry.js +32 -19
  13. package/lib/agents/codex.js +36 -11
  14. package/lib/agents/limit-hit.js +42 -25
  15. package/lib/agents/mistral-telemetry.js +1 -1
  16. package/lib/agents/mistral.js +13 -3
  17. package/lib/agents/opencode-export.js +11 -5
  18. package/lib/agents/opencode-telemetry.js +75 -90
  19. package/lib/agents/opencode.js +86 -29
  20. package/lib/agents/stage-telemetry.js +3 -7
  21. package/lib/commands/active.js +139 -94
  22. package/lib/commands/checkpoint.js +3 -1
  23. package/lib/commands/config.js +5 -3
  24. package/lib/commands/coverage-gate.js +17 -10
  25. package/lib/commands/diff.js +11 -5
  26. package/lib/commands/draft.js +95 -55
  27. package/lib/commands/handoff.js +253 -65
  28. package/lib/commands/integrate.js +288 -188
  29. package/lib/commands/mission-start.js +39 -34
  30. package/lib/commands/rebase.js +32 -21
  31. package/lib/commands/repair-handoff.js +21 -18
  32. package/lib/commands/resolve-conflict.js +5 -2
  33. package/lib/commands/review.js +1 -0
  34. package/lib/commands/setup-review.js +1 -0
  35. package/lib/commands/stats-backfill.js +50 -41
  36. package/lib/commands/stats.js +653 -204
  37. package/lib/commands/status.js +33 -28
  38. package/lib/core/fmt.js +58 -15
  39. package/lib/core/git.js +7 -5
  40. package/lib/core/gitignore.js +6 -4
  41. package/lib/core/mission-utils.js +161 -88
  42. package/lib/core/nels.js +199 -0
  43. package/lib/core/persistent-data-migration.js +55 -23
  44. package/lib/core/product-config.js +44 -23
  45. package/lib/core/runtime-matrix.js +15 -2
  46. package/lib/core/spawn-tee.js +40 -13
  47. package/lib/core/state-map.js +32 -13
  48. package/lib/core/storage.js +19 -5
  49. package/lib/core/subagent-limit.js +28 -0
  50. package/lib/core/verification.js +29 -12
  51. package/lib/review/rebase.js +11 -3
  52. package/lib/review/review-adapter.js +93 -6
  53. package/lib/review/review-artifacts.js +89 -23
  54. package/lib/review/review-commands.js +139 -45
  55. package/lib/review/review-events.js +103 -52
  56. package/lib/review/review-loop.js +117 -87
  57. package/lib/review/review-polling.js +25 -5
  58. package/lib/review/review-prompts.js +36 -5
  59. package/lib/review/review-state.js +39 -15
  60. package/lib/tools/backlog.js +162 -71
  61. package/lib/tools/forgejo.js +290 -129
  62. package/lib/tools/gatekeeper.js +15 -0
  63. package/lib/tools/redgreen.js +220 -0
  64. package/lib/tools/sessions.js +12 -5
  65. package/lib/tools/setup-review.js +170 -70
  66. package/package.json +11 -3
  67. package/prompts/draft.md +6 -0
  68. package/templates/mission-scaffold.md +1 -1
@@ -36,6 +36,7 @@
36
36
 
37
37
  const PROVIDER = 'anthropic';
38
38
 
39
+ /** @param {*} value */
39
40
  function num(value) {
40
41
  return typeof value === 'number' && Number.isFinite(value) ? value : 0;
41
42
  }
@@ -45,6 +46,7 @@ function num(value) {
45
46
  * `--include-partial-messages` form nests the real event under `stream_event`;
46
47
  * the bare form has it at the top level. Returns the inner event object.
47
48
  */
49
+ /** @param {any} evt */
48
50
  function unwrapEvent(evt) {
49
51
  if (evt && evt.type === 'stream_event' && evt.event && typeof evt.event === 'object') {
50
52
  return evt.event;
@@ -66,8 +68,9 @@ function unwrapEvent(evt) {
66
68
  * resultUsage: { inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens } | null
67
69
  * }
68
70
  */
71
+ /** @param {string} content */
69
72
  function parseClaudeStreamJson(content) {
70
- if (!content) return null;
73
+ if (!content) {return null;}
71
74
 
72
75
  let sessionId = null;
73
76
  let model = null;
@@ -79,19 +82,19 @@ function parseClaudeStreamJson(content) {
79
82
 
80
83
  for (const line of String(content).split('\n')) {
81
84
  const trimmed = line.trim();
82
- if (!trimmed.startsWith('{')) continue;
85
+ if (!trimmed.startsWith('{')) {continue;}
83
86
  let outer;
84
87
  try {
85
88
  outer = JSON.parse(trimmed);
86
89
  } catch (_) {
87
90
  continue;
88
91
  }
89
- if (!outer || typeof outer !== 'object') continue;
92
+ if (!outer || typeof outer !== 'object') {continue;}
90
93
 
91
94
  // Top-level CLI envelope fields (present on system/result and sometimes the
92
95
  // wrapper) carry session/model metadata.
93
- if (outer.session_id) sessionId = outer.session_id;
94
- if (outer.type === 'system' && outer.model) model = outer.model;
96
+ if (outer.session_id) {sessionId = outer.session_id;}
97
+ if (outer.type === 'system' && outer.model) {model = outer.model;}
95
98
 
96
99
  // The final `result` event carries `total_cost_usd` (direct from the CLI)
97
100
  // and an aggregate usage object (fallback when partial events were evicted).
@@ -110,13 +113,13 @@ function parseClaudeStreamJson(content) {
110
113
  }
111
114
 
112
115
  const evt = unwrapEvent(outer);
113
- if (!evt || typeof evt !== 'object') continue;
116
+ if (!evt || typeof evt !== 'object') {continue;}
114
117
 
115
118
  switch (evt.type) {
116
119
  case 'message_start': {
117
120
  const message = evt.message || {};
118
121
  const usage = message.usage || {};
119
- if (message.model) model = message.model;
122
+ if (message.model) {model = message.model;}
120
123
  messageStarts.push({
121
124
  inputTokens: num(usage.input_tokens),
122
125
  outputTokens: num(usage.output_tokens),
@@ -133,7 +136,7 @@ function parseClaudeStreamJson(content) {
133
136
  }
134
137
  case 'content_block_start': {
135
138
  const block = evt.content_block || {};
136
- if (block.type === 'tool_use') toolCalls += 1;
139
+ if (block.type === 'tool_use') {toolCalls += 1;}
137
140
  break;
138
141
  }
139
142
  default:
@@ -142,7 +145,7 @@ function parseClaudeStreamJson(content) {
142
145
  }
143
146
 
144
147
  const hasSignal = messageStarts.length > 0 || messageDeltas.length > 0 || resultUsage || model || sessionId;
145
- if (!hasSignal) return null;
148
+ if (!hasSignal) {return null;}
146
149
 
147
150
  return {
148
151
  sessionId,
@@ -174,9 +177,10 @@ function parseClaudeStreamJson(content) {
174
177
  *
175
178
  * Returns null when the stream yields no usable token signal.
176
179
  */
180
+ /** @param {string} stdout */
177
181
  function extractClaudeTelemetryFromStdout(stdout) {
178
182
  const parsed = parseClaudeStreamJson(stdout);
179
- if (!parsed) return null;
183
+ if (!parsed) {return null;}
180
184
 
181
185
  const { messageStarts, messageDeltas, resultUsage, resultCostUsd } = parsed;
182
186
 
@@ -3,11 +3,15 @@ const { extractClaudeTelemetryFromStdout } = require('./claude-telemetry');
3
3
  const sessions = require('../tools/sessions');
4
4
 
5
5
  // Injectable I/O for tests. Production uses the real spawn-tee / export capture.
6
+ /** @type {Function} */
6
7
  let _spawnAndTee = spawnAndTee;
8
+ /** @type {object} */
7
9
  let _sessions = sessions;
8
10
 
9
11
  // Test hooks: override the launcher's I/O without touching the public signature.
12
+ /** @param {Function} fn */
10
13
  function __setSpawnAndTeeForTest(fn) { _spawnAndTee = fn || spawnAndTee; }
14
+ /** @param {object} mod */
11
15
  function __setSessionsForTest(mod) { _sessions = mod || sessions; }
12
16
 
13
17
  // Claude usage telemetry is parsed from the `stream-json` stdout stream, which
@@ -20,16 +24,17 @@ const CLAUDE_TELEMETRY_TAIL_BYTES = 8 * 1024 * 1024;
20
24
  // Claude outputs "Resume this session with: claude --resume <id>" at the end.
21
25
  const CLAUDE_SESSION_ID_RE = /claude\s+--resume\s+([0-9a-f-]+)/i;
22
26
 
27
+ /** @param {string} stdout */
23
28
  function extractClaudeSessionIdFromStreamJson(stdout) {
24
- if (!stdout) return null;
29
+ if (!stdout) {return null;}
25
30
  const lines = stdout.split('\n');
26
31
  for (const line of lines) {
27
32
  const trimmed = line.trim();
28
- if (!trimmed.startsWith('{')) continue;
33
+ if (!trimmed.startsWith('{')) {continue;}
29
34
  try {
30
35
  const evt = JSON.parse(trimmed);
31
36
  // stream-json 'result' events carry session_id at the top level.
32
- if (evt.type === 'result' && evt.session_id) return evt.session_id;
37
+ if (evt.type === 'result' && evt.session_id) {return evt.session_id;}
33
38
  } catch {
34
39
  /* not JSON — skip */
35
40
  }
@@ -37,11 +42,12 @@ function extractClaudeSessionIdFromStreamJson(stdout) {
37
42
  return null;
38
43
  }
39
44
 
45
+ /** @param {string} stdout */
40
46
  function extractClaudeSessionId(stdout) {
41
- if (!stdout) return null;
47
+ if (!stdout) {return null;}
42
48
  // Try stream-json result event first (new default format).
43
49
  const fromStream = extractClaudeSessionIdFromStreamJson(stdout);
44
- if (fromStream) return fromStream;
50
+ if (fromStream) {return fromStream;}
45
51
  // Fall back to legacy plain-text regex.
46
52
  const m = CLAUDE_SESSION_ID_RE.exec(stdout);
47
53
  return m ? m[1] : null;
@@ -51,9 +57,12 @@ function resolveClaudeCommand() {
51
57
  return 'claude';
52
58
  }
53
59
 
60
+ /**
61
+ * @param {{ prompt: string, worktree: string, env?: object, resume?: boolean, sessionId?: string | null, model?: string | null }} opts
62
+ */
54
63
  function buildClaudeInvocation({ prompt, worktree, env, resume = false, sessionId = null, model = null }) {
55
64
  const args = ['--dangerously-skip-permissions'];
56
- if (model) args.push('--model', model);
65
+ if (model) {args.push('--model', model);}
57
66
  // Only resume when the marker explicitly says so (resume=true).
58
67
  // sessionId from a stale cross-family marker must be ignored when resume=false.
59
68
  if (resume && sessionId) {
@@ -80,14 +89,23 @@ function buildClaudeInvocation({ prompt, worktree, env, resume = false, sessionI
80
89
  };
81
90
  }
82
91
 
92
+ /**
93
+ * @param {{ prompt: string, worktree: string, env?: object, resume?: boolean, sessionId?: string | null, model?: string | null, teeOptions?: object, slug?: string | null, role?: string | null }} opts
94
+ */
83
95
  function startClaudeAgent({ prompt, worktree, env, resume = false, sessionId = null, model = null, teeOptions = {}, slug = null, role = null }) {
96
+ /**
97
+ * @param {{stdout?: string, stderr?: string}} result
98
+ */
84
99
  function isStaleSessionResult(result) {
85
- if (!result) return false;
100
+ if (!result) {return false;}
86
101
  const stderr = result.stderr || '';
87
102
  const stdout = result.stdout || '';
88
103
  return (stderr.includes('Session not found') || stdout.includes('Session not found'));
89
104
  }
90
105
 
106
+ /**
107
+ * @param {{stdout?: string, sessionId?: string, telemetry?: object, model?: string, provider?: string}} result
108
+ */
91
109
  function processResult(result) {
92
110
  if (result && result.stdout) {
93
111
  result.sessionId = extractClaudeSessionId(result.stdout);
@@ -95,8 +113,8 @@ function startClaudeAgent({ prompt, worktree, env, resume = false, sessionId = n
95
113
  const telemetry = extractClaudeTelemetryFromStdout(result.stdout);
96
114
  if (telemetry) {
97
115
  result.telemetry = telemetry;
98
- if (telemetry.model) result.model = telemetry.model;
99
- if (telemetry.provider) result.provider = telemetry.provider;
116
+ if (telemetry.model) {result.model = telemetry.model;}
117
+ if (telemetry.provider) {result.provider = telemetry.provider;}
100
118
  }
101
119
  } catch (_) {
102
120
  // Telemetry is best-effort; never let it break the launch result.
@@ -105,13 +123,16 @@ function startClaudeAgent({ prompt, worktree, env, resume = false, sessionId = n
105
123
  return result;
106
124
  }
107
125
 
126
+ /**
127
+ * @param {{command: string, args: string[], options?: object}} invocation
128
+ */
108
129
  function staleSessionHandler(invocation) {
109
130
  const teeWithTail = { maxTailBytes: CLAUDE_TELEMETRY_TAIL_BYTES, ...invocation.options, ...teeOptions };
110
131
  return _spawnAndTee(invocation.command, invocation.args, teeWithTail)
111
- .then(result => {
132
+ .then(/** @param {object} result */ (result) => {
112
133
  if (isStaleSessionResult(result) && worktree && resume) {
113
134
  try {
114
- _sessions.clearSession(worktree, slug, role);
135
+ /** @type {any} */(_sessions).clearSession(worktree, slug, role);
115
136
  } catch (_) { /* best-effort */ }
116
137
  const freshInv = buildClaudeInvocation({ prompt, worktree, env, resume: false, sessionId: null, model });
117
138
  const freshTee = { maxTailBytes: CLAUDE_TELEMETRY_TAIL_BYTES, ...freshInv.options, ...teeOptions };
@@ -16,6 +16,7 @@
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
18
 
19
+ /** @param {string} codexHome */
19
20
  function codexSessionsDir(codexHome) {
20
21
  return path.join(codexHome, '.codex', 'sessions');
21
22
  }
@@ -24,8 +25,9 @@ function codexSessionsDir(codexHome) {
24
25
  * Parse a rollout JSONL string into a telemetry object. Returns null when the
25
26
  * content yields no usable signal (e.g. a failed turn with no token_count).
26
27
  */
28
+ /** @param {string} content */
27
29
  function parseCodexRollout(content) {
28
- if (!content) return null;
30
+ if (!content) {return null;}
29
31
 
30
32
  let sessionId = null;
31
33
  let provider = null;
@@ -38,7 +40,7 @@ function parseCodexRollout(content) {
38
40
 
39
41
  for (const line of String(content).split('\n')) {
40
42
  const trimmed = line.trim();
41
- if (!trimmed.startsWith('{')) continue;
43
+ if (!trimmed.startsWith('{')) {continue;}
42
44
  let evt;
43
45
  try {
44
46
  evt = JSON.parse(trimmed);
@@ -46,21 +48,21 @@ function parseCodexRollout(content) {
46
48
  continue;
47
49
  }
48
50
  const payload = evt && evt.payload;
49
- if (!payload) continue;
51
+ if (!payload) {continue;}
50
52
 
51
53
  switch (evt.type) {
52
54
  case 'session_meta':
53
55
  sessionId = payload.id || sessionId;
54
56
  provider = payload.model_provider || provider;
55
- if (payload.model) model = payload.model;
57
+ if (payload.model) {model = payload.model;}
56
58
  break;
57
59
  case 'turn_context':
58
60
  // The latest turn_context reflects the model/effort actually used.
59
- if (payload.model) model = payload.model;
60
- if (payload.effort) effort = payload.effort;
61
+ if (payload.model) {model = payload.model;}
62
+ if (payload.effort) {effort = payload.effort;}
61
63
  break;
62
64
  case 'response_item':
63
- if (payload.type === 'function_call') toolCalls += 1;
65
+ if (payload.type === 'function_call') {toolCalls += 1;}
64
66
  break;
65
67
  case 'event_msg':
66
68
  if (payload.type === 'token_count') {
@@ -73,7 +75,7 @@ function parseCodexRollout(content) {
73
75
  const pct = payload.rate_limits
74
76
  && payload.rate_limits.primary
75
77
  && payload.rate_limits.primary.used_percent;
76
- if (typeof pct === 'number') usagePercent = pct;
78
+ if (typeof pct === 'number') {usagePercent = pct;}
77
79
  }
78
80
  break;
79
81
  default:
@@ -81,7 +83,7 @@ function parseCodexRollout(content) {
81
83
  }
82
84
  }
83
85
 
84
- if (!lastUsage && !model && !sessionId) return null;
86
+ if (!lastUsage && !model && !sessionId) {return null;}
85
87
 
86
88
  const usage = lastUsage || {};
87
89
  return {
@@ -107,8 +109,12 @@ function parseCodexRollout(content) {
107
109
  * `total_token_usage` counter, so a multi-round stage produces several files
108
110
  * whose totals must be summed to get the stage's real quota consumption.
109
111
  */
112
+ /**
113
+ * @param {string} sessionsDir
114
+ * @param {{ sinceMs?: number }} [opts]
115
+ */
110
116
  function collectRolloutFiles(sessionsDir, { sinceMs = 0 } = {}) {
111
- if (!sessionsDir || !fs.existsSync(sessionsDir)) return [];
117
+ if (!sessionsDir || !fs.existsSync(sessionsDir)) {return [];}
112
118
 
113
119
  const found = [];
114
120
  const stack = [sessionsDir];
@@ -116,12 +122,14 @@ function collectRolloutFiles(sessionsDir, { sinceMs = 0 } = {}) {
116
122
  const dir = stack.pop();
117
123
  let entries;
118
124
  try {
119
- entries = fs.readdirSync(dir, { withFileTypes: true });
125
+ entries = /** @type {Array<import('fs').Dirent>} */(/** @type {any} */(fs).readdirSync(dir, { withFileTypes: true }));
120
126
  } catch (_) {
121
127
  continue;
122
128
  }
123
- for (const entry of entries) {
124
- const full = path.join(dir, entry.name);
129
+ for (const entry of /** @type {Array<{name: string, isDirectory: () => boolean, isFile: () => boolean}>} */(entries)) {
130
+ if (!entry.name) {continue;}
131
+ const /** @type {string} */ name = entry.name ?? '';
132
+ const full = path.join(dir || '', name);
125
133
  if (entry.isDirectory()) {
126
134
  stack.push(full);
127
135
  } else if (entry.isFile() && entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) {
@@ -132,7 +140,7 @@ function collectRolloutFiles(sessionsDir, { sinceMs = 0 } = {}) {
132
140
  continue;
133
141
  }
134
142
  const mtime = stat.mtimeMs;
135
- if (mtime + 1000 < sinceMs) continue; // 1s grace for clock skew
143
+ if (mtime + 1000 < sinceMs) {continue;} // 1s grace for clock skew
136
144
  found.push({ full, mtime });
137
145
  }
138
146
  }
@@ -152,15 +160,20 @@ function collectRolloutFiles(sessionsDir, { sinceMs = 0 } = {}) {
152
160
  * re-running a stage (e.g. after the workflow process is resumed mid-mission)
153
161
  * yields the same total rather than double-counting.
154
162
  */
163
+ /**
164
+ * @param {string} codexHome
165
+ * @param {{ sinceMs?: number }} [opts]
166
+ */
155
167
  function extractCodexTelemetry(codexHome, { sinceMs = 0 } = {}) {
156
168
  const files = collectRolloutFiles(codexSessionsDir(codexHome), { sinceMs });
157
- if (files.length === 0) return null;
169
+ if (files.length === 0) {return null;}
158
170
 
159
171
  const agg = {
160
172
  inputTokens: 0, outputTokens: 0, cachedTokens: 0,
161
173
  reasoningTokens: 0, totalTokens: 0, toolCalls: 0,
162
174
  };
163
175
  let newest = null;
176
+ let newestPath = null;
164
177
  for (const file of files) {
165
178
  let content;
166
179
  try {
@@ -169,7 +182,7 @@ function extractCodexTelemetry(codexHome, { sinceMs = 0 } = {}) {
169
182
  continue;
170
183
  }
171
184
  const t = parseCodexRollout(content);
172
- if (!t) continue;
185
+ if (!t) {continue;}
173
186
  agg.inputTokens += t.inputTokens;
174
187
  agg.outputTokens += t.outputTokens;
175
188
  agg.cachedTokens += t.cachedTokens;
@@ -177,9 +190,9 @@ function extractCodexTelemetry(codexHome, { sinceMs = 0 } = {}) {
177
190
  agg.totalTokens += t.totalTokens;
178
191
  agg.toolCalls += t.toolCalls;
179
192
  newest = t; // files are oldest-first, so the last assignment is the newest
180
- newest.rolloutPath = file;
193
+ newestPath = file;
181
194
  }
182
- if (!newest) return null;
195
+ if (!newest) {return null;}
183
196
 
184
197
  return {
185
198
  sessionId: newest.sessionId,
@@ -188,7 +201,7 @@ function extractCodexTelemetry(codexHome, { sinceMs = 0 } = {}) {
188
201
  effort: newest.effort,
189
202
  contextWindow: newest.contextWindow,
190
203
  usagePercent: newest.usagePercent,
191
- rolloutPath: newest.rolloutPath,
204
+ rolloutPath: newestPath,
192
205
  rolloutCount: files.length,
193
206
  ...agg,
194
207
  };
@@ -6,11 +6,15 @@ const { extractCodexTelemetry } = require('./codex-telemetry');
6
6
  const sessions = require('../tools/sessions');
7
7
 
8
8
  // Injectable I/O for tests. Production uses the real spawn-tee / export capture.
9
+ /** @type {Function} */
9
10
  let _spawnAndTee = spawnAndTee;
11
+ /** @type {object} */
10
12
  let _sessions = sessions;
11
13
 
12
14
  // Test hooks: override the launcher's I/O without touching the public signature.
15
+ /** @param {Function} fn */
13
16
  function __setSpawnAndTeeForTest(fn) { _spawnAndTee = fn || spawnAndTee; }
17
+ /** @param {object} mod */
14
18
  function __setSessionsForTest(mod) { _sessions = mod || sessions; }
15
19
 
16
20
  // Codex outputs "To continue this session, run codex resume <id>" at the end.
@@ -18,12 +22,13 @@ function __setSessionsForTest(mod) { _sessions = mod || sessions; }
18
22
  const CODEX_SESSION_ID_RE = /codex\s+resume\s+([0-9a-f-]+)/i;
19
23
  const CODEX_SESSION_ID_ALT_RE = /Session ID:\s*([0-9a-f-]+)/i;
20
24
 
25
+ /** @param {string} stdout */
21
26
  function extractCodexSessionId(stdout) {
22
- if (!stdout) return null;
27
+ if (!stdout) {return null;}
23
28
  const m = CODEX_SESSION_ID_RE.exec(stdout);
24
- if (m) return m[1];
29
+ if (m) {return m[1];}
25
30
  const m2 = CODEX_SESSION_ID_ALT_RE.exec(stdout);
26
- if (m2) return m2[1];
31
+ if (m2) {return m2[1];}
27
32
  return null;
28
33
  }
29
34
 
@@ -35,6 +40,9 @@ function hasLiveTty() {
35
40
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
36
41
  }
37
42
 
43
+ /**
44
+ * @param {{ prompt: string, worktree: string, interactive?: boolean, env?: {[key: string]: string}, resume?: boolean, sessionId?: string | null, model?: string | null }} opts
45
+ */
38
46
  function buildCodexDraftInvocation({ prompt, worktree, interactive = hasLiveTty(), env = {}, resume = false, sessionId = null, model = null }) {
39
47
  if (resume) {
40
48
  const args = ['exec', 'resume'];
@@ -43,7 +51,7 @@ function buildCodexDraftInvocation({ prompt, worktree, interactive = hasLiveTty(
43
51
  } else {
44
52
  args.push('--last');
45
53
  }
46
- if (model) args.push('-m', model);
54
+ if (model) {args.push('-m', model);}
47
55
  args.push(prompt);
48
56
  return {
49
57
  command: resolveCodexCommand(),
@@ -77,6 +85,9 @@ function buildCodexDraftInvocation({ prompt, worktree, interactive = hasLiveTty(
77
85
  };
78
86
  }
79
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
+ */
80
91
  function startCodexDraftAgent({ prompt, worktree, env = {}, resume = false, sessionId = null, model = null, teeOptions = {}, slug = null, role = null }) {
81
92
  // The launcher always tees through spawnAndTee for limit-hit detection, which
82
93
  // forces child stdio to ['inherit', 'pipe', 'pipe']. Codex's `--full-auto`
@@ -84,16 +95,22 @@ function startCodexDraftAgent({ prompt, worktree, env = {}, resume = false, sess
84
95
  // `exec` path here regardless of whether the parent has a TTY.
85
96
  ensureCodexHome(worktree);
86
97
 
98
+ /**
99
+ * @param {{stdout?: string, stderr?: string}} result
100
+ */
87
101
  function isStaleSessionResult(result) {
88
- if (!result) return false;
102
+ if (!result) {return false;}
89
103
  const stderr = result.stderr || '';
90
104
  const stdout = result.stdout || '';
91
105
  return (stderr.includes('Session not found') || stdout.includes('Session not found'));
92
106
  }
93
107
 
108
+ /**
109
+ * @param {{stdout?: string, sessionId?: string, telemetry?: object, model?: string, provider?: string}} result
110
+ */
94
111
  function processResult(result) {
95
112
  if (result && result.stdout) {
96
- result.sessionId = extractCodexSessionId(result.stdout);
113
+ result.sessionId = extractCodexSessionId(result.stdout) || undefined;
97
114
  }
98
115
  if (result) {
99
116
  // Real usage telemetry comes from the codex rollout JSONL written under the
@@ -102,8 +119,8 @@ function startCodexDraftAgent({ prompt, worktree, env = {}, resume = false, sess
102
119
  const telemetry = extractCodexTelemetry(codexHomeRoot(worktree), { sinceMs: Date.now() });
103
120
  if (telemetry) {
104
121
  result.telemetry = telemetry;
105
- if (telemetry.model) result.model = telemetry.model;
106
- if (telemetry.provider) result.provider = telemetry.provider;
122
+ if (telemetry.model) {result.model = telemetry.model;}
123
+ if (telemetry.provider) {result.provider = telemetry.provider;}
107
124
  }
108
125
  } catch (_) {
109
126
  // Telemetry is best-effort; never let it break the launch result.
@@ -112,12 +129,15 @@ function startCodexDraftAgent({ prompt, worktree, env = {}, resume = false, sess
112
129
  return result;
113
130
  }
114
131
 
132
+ /**
133
+ * @param {{command: string, args: string[], options?: object}} invocation
134
+ */
115
135
  function staleSessionHandler(invocation) {
116
136
  return _spawnAndTee(invocation.command, invocation.args, { ...invocation.options, ...teeOptions })
117
- .then(result => {
137
+ .then(/** @param {object} result */ (result) => {
118
138
  if (isStaleSessionResult(result) && worktree && resume) {
119
139
  try {
120
- _sessions.clearSession(worktree, slug, role);
140
+ /** @type {any} */(_sessions).clearSession(worktree, slug, role);
121
141
  } catch (_) { /* best-effort */ }
122
142
  const freshInv = buildCodexDraftInvocation({ prompt, worktree, interactive: false, env, resume: false, sessionId: null, model });
123
143
  return _spawnAndTee(freshInv.command, freshInv.args, { ...freshInv.options, ...teeOptions });
@@ -127,21 +147,23 @@ function startCodexDraftAgent({ prompt, worktree, env = {}, resume = false, sess
127
147
  .then(processResult);
128
148
  }
129
149
 
130
- const startedMs = Date.now();
131
150
  const invocation = buildCodexDraftInvocation({ prompt, worktree, interactive: false, env, resume, sessionId, model });
132
151
  const resultPromise = staleSessionHandler(invocation);
133
152
 
134
153
  return { invocation, resultPromise };
135
154
  }
136
155
 
156
+ /** @param {string} worktree */
137
157
  function codexHomeRoot(worktree) {
138
158
  return path.join(worktree, '.workflow', 'codex-home');
139
159
  }
140
160
 
161
+ /** @param {string} worktree */
141
162
  function codexConfigPath(worktree) {
142
163
  return path.join(codexHomeRoot(worktree), '.codex', 'config.toml');
143
164
  }
144
165
 
166
+ /** @param {string} worktree */
145
167
  function codexAuthPath(worktree) {
146
168
  return path.join(codexHomeRoot(worktree), '.codex', 'auth.json');
147
169
  }
@@ -150,6 +172,7 @@ function userCodexAuthPath() {
150
172
  return path.join(os.homedir(), '.codex', 'auth.json');
151
173
  }
152
174
 
175
+ /** @param {*} value */
153
176
  function tomlString(value) {
154
177
  return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
155
178
  }
@@ -157,6 +180,7 @@ function tomlString(value) {
157
180
  // Graphify's Codex skill requires multi_agent = true for spawn_agent subagent
158
181
  // dispatch (skill-codex.md line 233). Without this, the copied Graphify skill
159
182
  // cannot launch semantic extraction subagents and parallel graph building fails.
183
+ /** @param {string} worktree */
160
184
  function headlessCodexConfig(worktree) {
161
185
  const worktreeParent = path.dirname(path.resolve(worktree));
162
186
  return [
@@ -176,6 +200,7 @@ function headlessCodexConfig(worktree) {
176
200
  ].join('\n');
177
201
  }
178
202
 
203
+ /** @param {string} worktree */
179
204
  function ensureCodexHome(worktree) {
180
205
  fs.mkdirSync(codexHomeRoot(worktree), { recursive: true });
181
206
  fs.mkdirSync(path.dirname(codexConfigPath(worktree)), { recursive: true });