@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.
- package/config/integration-pipelines.json +9 -0
- package/config/workflow.config.schema.json +12 -0
- package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
- package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
- package/docs/adr/0041-integration-pipeline-gates.md +1 -1
- package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
- package/docs/adr/index.md +1 -0
- package/docs/use-cases.md +44 -0
- package/lib/agents/agents.js +149 -100
- package/lib/agents/claude-telemetry.js +14 -10
- package/lib/agents/claude.js +32 -11
- package/lib/agents/codex-telemetry.js +32 -19
- package/lib/agents/codex.js +36 -11
- package/lib/agents/limit-hit.js +42 -25
- package/lib/agents/mistral-telemetry.js +1 -1
- package/lib/agents/mistral.js +13 -3
- package/lib/agents/opencode-export.js +11 -5
- package/lib/agents/opencode-telemetry.js +75 -90
- package/lib/agents/opencode.js +86 -29
- package/lib/agents/stage-telemetry.js +3 -7
- package/lib/commands/active.js +139 -94
- package/lib/commands/checkpoint.js +3 -1
- package/lib/commands/config.js +5 -3
- package/lib/commands/coverage-gate.js +17 -10
- package/lib/commands/diff.js +11 -5
- package/lib/commands/draft.js +95 -55
- package/lib/commands/handoff.js +253 -65
- package/lib/commands/integrate.js +288 -188
- package/lib/commands/mission-start.js +39 -34
- package/lib/commands/rebase.js +32 -21
- package/lib/commands/repair-handoff.js +21 -18
- package/lib/commands/resolve-conflict.js +5 -2
- package/lib/commands/review.js +1 -0
- package/lib/commands/setup-review.js +1 -0
- package/lib/commands/stats-backfill.js +50 -41
- package/lib/commands/stats.js +653 -204
- package/lib/commands/status.js +33 -28
- package/lib/core/fmt.js +58 -15
- package/lib/core/git.js +7 -5
- package/lib/core/gitignore.js +6 -4
- package/lib/core/mission-utils.js +161 -88
- package/lib/core/nels.js +199 -0
- package/lib/core/persistent-data-migration.js +55 -23
- package/lib/core/product-config.js +44 -23
- package/lib/core/runtime-matrix.js +15 -2
- package/lib/core/spawn-tee.js +40 -13
- package/lib/core/state-map.js +32 -13
- package/lib/core/storage.js +19 -5
- package/lib/core/subagent-limit.js +28 -0
- package/lib/core/verification.js +29 -12
- package/lib/review/rebase.js +11 -3
- package/lib/review/review-adapter.js +93 -6
- package/lib/review/review-artifacts.js +89 -23
- package/lib/review/review-commands.js +139 -45
- package/lib/review/review-events.js +103 -52
- package/lib/review/review-loop.js +117 -87
- package/lib/review/review-polling.js +25 -5
- package/lib/review/review-prompts.js +36 -5
- package/lib/review/review-state.js +39 -15
- package/lib/tools/backlog.js +162 -71
- package/lib/tools/forgejo.js +290 -129
- package/lib/tools/gatekeeper.js +15 -0
- package/lib/tools/redgreen.js +220 -0
- package/lib/tools/sessions.js +12 -5
- package/lib/tools/setup-review.js +170 -70
- package/package.json +11 -3
- package/prompts/draft.md +6 -0
- package/templates/mission-scaffold.md +1 -1
package/lib/agents/opencode.js
CHANGED
|
@@ -3,20 +3,28 @@ const { extractOpencodeTelemetryFromExport } = require('./opencode-telemetry');
|
|
|
3
3
|
const { captureOpencodeExport } = require('./opencode-export');
|
|
4
4
|
const sessions = require('../tools/sessions');
|
|
5
5
|
const { detectLimitHit } = require('./limit-hit');
|
|
6
|
+
const { buildSubagentLimitPrefix } = require('../core/subagent-limit');
|
|
6
7
|
|
|
7
8
|
// Injectable I/O for tests. Production uses the real spawn-tee / export capture.
|
|
9
|
+
/** @type {Function} */
|
|
8
10
|
let _spawnAndTee = spawnAndTee;
|
|
11
|
+
/** @type {Function} */
|
|
9
12
|
let _captureExport = captureOpencodeExport;
|
|
13
|
+
/** @type {object} */
|
|
10
14
|
let _sessions = sessions;
|
|
11
15
|
|
|
12
16
|
// Test hooks: override the launcher's I/O without touching the public signature.
|
|
17
|
+
/** @param {Function} fn */
|
|
13
18
|
function __setSpawnAndTeeForTest(fn) { _spawnAndTee = fn || spawnAndTee; }
|
|
19
|
+
/** @param {Function} fn */
|
|
14
20
|
function __setExportCaptureForTest(fn) { _captureExport = fn || captureOpencodeExport; }
|
|
21
|
+
/** @param {object} mod */
|
|
15
22
|
function __setSessionsForTest(mod) { _sessions = mod || sessions; }
|
|
16
23
|
|
|
17
24
|
// Test hook: override the cached feature-detect for `--format json` support.
|
|
18
25
|
// Set to true/false to force inclusion/exclusion of the flag regardless of
|
|
19
26
|
// the real binary. Pass null to reset to the live detect.
|
|
27
|
+
/** @param {boolean|null} val */
|
|
20
28
|
function __setJsonFormatSupportForTest(val) {
|
|
21
29
|
_jsonFormatSupported = val;
|
|
22
30
|
_jsonFormatDetectFn = null;
|
|
@@ -24,6 +32,7 @@ function __setJsonFormatSupportForTest(val) {
|
|
|
24
32
|
|
|
25
33
|
// Test hook: inject a feature-detect function for tests. This replaces the
|
|
26
34
|
// live shell-out with a stubbed function, making tests hermetic.
|
|
35
|
+
/** @param {Function} fn */
|
|
27
36
|
function __setJsonFormatDetectForTest(fn) {
|
|
28
37
|
_jsonFormatDetectFn = fn;
|
|
29
38
|
_jsonFormatSupported = null; // invalidate cache so the detect fn runs
|
|
@@ -45,10 +54,11 @@ function __setJsonFormatDetectForTest(fn) {
|
|
|
45
54
|
const OPENCODE_SESSION_ID_RE = /opencode\s+-s\s+(ses_\S+)/i;
|
|
46
55
|
const OPENCODE_JSON_SESSION_ID_RE = /"sessionID"\s*:\s*"(ses_[^"]+)"/;
|
|
47
56
|
|
|
57
|
+
/** @param {string} stdout */
|
|
48
58
|
function extractOpencodeSessionId(stdout) {
|
|
49
|
-
if (!stdout) return null;
|
|
59
|
+
if (!stdout) {return null;}
|
|
50
60
|
const footer = OPENCODE_SESSION_ID_RE.exec(stdout);
|
|
51
|
-
if (footer) return footer[1];
|
|
61
|
+
if (footer) {return footer[1];}
|
|
52
62
|
const json = OPENCODE_JSON_SESSION_ID_RE.exec(stdout);
|
|
53
63
|
return json ? json[1] : null;
|
|
54
64
|
}
|
|
@@ -56,10 +66,12 @@ function extractOpencodeSessionId(stdout) {
|
|
|
56
66
|
// Cached feature-detect for `--format json` support. Set to `false` only when
|
|
57
67
|
// a real invocation proves the flag is rejected (older opencode versions).
|
|
58
68
|
// Tests can inject a canned result via __setJsonFormatSupportForTest.
|
|
69
|
+
/** @type {boolean | null} */
|
|
59
70
|
let _jsonFormatSupported = null;
|
|
60
71
|
|
|
61
72
|
// Injectable feature-detect function for tests. When set, checkJsonFormatSupport()
|
|
62
73
|
// calls this function instead of shelling out, making tests hermetic.
|
|
74
|
+
/** @type {Function | null} */
|
|
63
75
|
let _jsonFormatDetectFn = null;
|
|
64
76
|
|
|
65
77
|
function resolveOpencodeCommand() {
|
|
@@ -72,7 +84,7 @@ function resolveOpencodeCommand() {
|
|
|
72
84
|
// Results are cached so the check runs at most once in production.
|
|
73
85
|
// Tests can inject a canned result via __setJsonFormatSupportForTest.
|
|
74
86
|
function checkJsonFormatSupport() {
|
|
75
|
-
if (_jsonFormatSupported !== null) return _jsonFormatSupported;
|
|
87
|
+
if (_jsonFormatSupported !== null) {return _jsonFormatSupported;}
|
|
76
88
|
if (_jsonFormatDetectFn) {
|
|
77
89
|
_jsonFormatSupported = _jsonFormatDetectFn();
|
|
78
90
|
return _jsonFormatSupported;
|
|
@@ -99,6 +111,9 @@ function checkJsonFormatSupport() {
|
|
|
99
111
|
}
|
|
100
112
|
}
|
|
101
113
|
|
|
114
|
+
/**
|
|
115
|
+
* @param {{ prompt: string, worktree: string, env?: object, resume?: boolean, sessionId?: string | null, model?: string | null, preferJson?: boolean }} opts
|
|
116
|
+
*/
|
|
102
117
|
function buildOpencodeInvocation({ prompt, worktree, env, resume = false, sessionId = null, model = null, preferJson = true }) {
|
|
103
118
|
// `--format json` makes opencode stream NDJSON events that each carry a
|
|
104
119
|
// "sessionID":"ses_..." field. opencode v2.0.0's default `run` output no
|
|
@@ -111,8 +126,8 @@ function buildOpencodeInvocation({ prompt, worktree, env, resume = false, sessio
|
|
|
111
126
|
// invocation, preserving launch behaviour at the cost of telemetry.
|
|
112
127
|
const useJson = preferJson && checkJsonFormatSupport();
|
|
113
128
|
const args = ['run', '--pure', '--dangerously-skip-permissions'];
|
|
114
|
-
if (useJson) args.push('--format', 'json');
|
|
115
|
-
if (model) args.push('-m', model);
|
|
129
|
+
if (useJson) {args.push('--format', 'json');}
|
|
130
|
+
if (model) {args.push('-m', model);}
|
|
116
131
|
// Only resume when the marker explicitly says so (resume=true).
|
|
117
132
|
// sessionId from a stale cross-family marker must be ignored when resume=false.
|
|
118
133
|
if (resume && sessionId) {
|
|
@@ -170,13 +185,19 @@ const HARD_OPENCODE_PATTERNS = [
|
|
|
170
185
|
/\bauthentication (?:failed|error)\b/i,
|
|
171
186
|
];
|
|
172
187
|
|
|
188
|
+
/**
|
|
189
|
+
* @param {{stdout?: string, stderr?: string}} result
|
|
190
|
+
*/
|
|
173
191
|
function failureText(result) {
|
|
174
192
|
return `${(result && result.stdout) || ''}\n${(result && result.stderr) || ''}`;
|
|
175
193
|
}
|
|
176
194
|
|
|
195
|
+
/**
|
|
196
|
+
* @param {{stdout?: string, stderr?: string, error?: {code?: string}}} result
|
|
197
|
+
*/
|
|
177
198
|
function isHardOpencodeFailure(result) {
|
|
178
|
-
if (!result) return false;
|
|
179
|
-
if (result.error && (result.error.code === 'ENOENT' || result.error.code === 'EACCES')) return true;
|
|
199
|
+
if (!result) {return false;}
|
|
200
|
+
if (result.error && (result.error.code === 'ENOENT' || result.error.code === 'EACCES')) {return true;}
|
|
180
201
|
const text = failureText(result);
|
|
181
202
|
return HARD_OPENCODE_PATTERNS.some((re) => re.test(text));
|
|
182
203
|
}
|
|
@@ -186,16 +207,22 @@ function isHardOpencodeFailure(result) {
|
|
|
186
207
|
// When the transcript contains a valid "reason":"stop" completion event and
|
|
187
208
|
// no "type":"error" event, the exit is spurious – the agent actually
|
|
188
209
|
// completed successfully. See opencode issues #31446 and #33653.
|
|
210
|
+
/**
|
|
211
|
+
* @param {{status?: number, stdout?: string, stderr?: string}} result
|
|
212
|
+
*/
|
|
189
213
|
function isSpuriousOpencodeExit(result) {
|
|
190
|
-
if (!result || result.status !== 1) return false;
|
|
214
|
+
if (!result || result.status !== 1) {return false;}
|
|
191
215
|
const text = failureText(result);
|
|
192
216
|
const hasStop = /"reason"\s*:\s*"stop"/.test(text);
|
|
193
217
|
const hasError = /"type"\s*:\s*"error"/.test(text);
|
|
194
218
|
return hasStop && !hasError;
|
|
195
219
|
}
|
|
196
220
|
|
|
221
|
+
/**
|
|
222
|
+
* @param {{stdout?: string, stderr?: string, error?: {code?: string}, signal?: string, status?: number}} result
|
|
223
|
+
*/
|
|
197
224
|
function isTransientOpencodeFailure(result) {
|
|
198
|
-
if (!result) return false;
|
|
225
|
+
if (!result) {return false;}
|
|
199
226
|
const text = failureText(result);
|
|
200
227
|
return TRANSIENT_OPENCODE_PATTERNS.some((re) => re.test(text));
|
|
201
228
|
}
|
|
@@ -204,11 +231,12 @@ function isTransientOpencodeFailure(result) {
|
|
|
204
231
|
// Only genuine non-zero exits carrying a transient backend signature qualify;
|
|
205
232
|
// clean exits, spawn errors (ENOENT/EACCES), killing signals, limit-hits, and
|
|
206
233
|
// hard errors never retry — so real failures still surface to the reroute loop.
|
|
234
|
+
/** @param {{stdout?: string, stderr?: string, status?: number | null, signal?: string | null, error?: {code?: string}, sessionId?: string | null}} result */
|
|
207
235
|
function shouldRetryOpencodeFailure(result) {
|
|
208
|
-
if (!result) return false;
|
|
209
|
-
if (result.error && (result.error.code === 'ENOENT' || result.error.code === 'EACCES')) return false;
|
|
210
|
-
if (result.signal) return false;
|
|
211
|
-
if (!(typeof result.status === 'number' && result.status !== 0)) return false;
|
|
236
|
+
if (!result) {return false;}
|
|
237
|
+
if (result.error && (result.error.code === 'ENOENT' || result.error.code === 'EACCES')) {return false;}
|
|
238
|
+
if (result.signal) {return false;}
|
|
239
|
+
if (!(typeof result.status === 'number' && result.status !== 0)) {return false;}
|
|
212
240
|
if (detectLimitHit({
|
|
213
241
|
agent: 'custom',
|
|
214
242
|
stdout: result.stdout,
|
|
@@ -216,11 +244,14 @@ function shouldRetryOpencodeFailure(result) {
|
|
|
216
244
|
status: result.status,
|
|
217
245
|
signal: result.signal,
|
|
218
246
|
error: result.error,
|
|
219
|
-
})) return false;
|
|
220
|
-
if (isHardOpencodeFailure(result)) return false;
|
|
221
|
-
return isTransientOpencodeFailure(result);
|
|
247
|
+
})) {return false;}
|
|
248
|
+
if (isHardOpencodeFailure(result)) {return false;}
|
|
249
|
+
return isTransientOpencodeFailure(/** @type {any} */(result));
|
|
222
250
|
}
|
|
223
251
|
|
|
252
|
+
/**
|
|
253
|
+
* @param {{ prompt: string, worktree: string, env?: object, resume?: boolean, sessionId?: string | null, model?: string | null, teeOptions?: object, slug?: string | null, role?: string | null, maxTransientRetries?: number }} opts
|
|
254
|
+
*/
|
|
224
255
|
function startOpencodeAgent({
|
|
225
256
|
prompt,
|
|
226
257
|
worktree,
|
|
@@ -233,18 +264,32 @@ function startOpencodeAgent({
|
|
|
233
264
|
role = null,
|
|
234
265
|
maxTransientRetries = 1
|
|
235
266
|
}) {
|
|
267
|
+
// Prepend the subagent-limit advisory prefix to the prompt.
|
|
268
|
+
// When maxParallel is unset/null/zero the prefix is empty (no-op).
|
|
269
|
+
const subagentPrefix = buildSubagentLimitPrefix();
|
|
270
|
+
const injectedPrompt = subagentPrefix + prompt;
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @param {{stdout?: string, stderr?: string}} result
|
|
274
|
+
*/
|
|
236
275
|
function isStaleSessionResult(result) {
|
|
237
|
-
if (!result) return false;
|
|
276
|
+
if (!result) {return false;}
|
|
238
277
|
const stderr = result.stderr || '';
|
|
239
278
|
const stdout = result.stdout || '';
|
|
240
279
|
return stderr.includes('Session not found') || stdout.includes('Session not found');
|
|
241
280
|
}
|
|
242
281
|
|
|
282
|
+
/**
|
|
283
|
+
* @param {{command: string, args: string[], options?: object}} invocation
|
|
284
|
+
*/
|
|
243
285
|
function runInvocation(invocation) {
|
|
244
286
|
const spawnOptions = { ...invocation.options, ...teeOptions };
|
|
245
287
|
return _spawnAndTee(invocation.command, invocation.args, spawnOptions);
|
|
246
288
|
}
|
|
247
289
|
|
|
290
|
+
/**
|
|
291
|
+
* @param {{command: string, args: string[], options?: object}} invocation
|
|
292
|
+
*/
|
|
248
293
|
async function runInvocationWithRetry(invocation) {
|
|
249
294
|
let attempts = 0;
|
|
250
295
|
let result = await runInvocation(invocation);
|
|
@@ -252,7 +297,7 @@ function startOpencodeAgent({
|
|
|
252
297
|
attempts += 1;
|
|
253
298
|
result = await runInvocation(invocation);
|
|
254
299
|
}
|
|
255
|
-
if (result) result.transientRetries = attempts;
|
|
300
|
+
if (result) {result.transientRetries = attempts;}
|
|
256
301
|
return result;
|
|
257
302
|
}
|
|
258
303
|
|
|
@@ -260,25 +305,31 @@ function startOpencodeAgent({
|
|
|
260
305
|
// that wasn't caught by the feature-detect). If so, retry the invocation
|
|
261
306
|
// without the flag so the prompt still runs (telemetry will be lost but the
|
|
262
307
|
// agent won't fail outright).
|
|
308
|
+
/**
|
|
309
|
+
* @param {{stdout?: string, stderr?: string}} result
|
|
310
|
+
*/
|
|
263
311
|
function isJsonFlagError(result) {
|
|
264
|
-
if (!result) return false;
|
|
312
|
+
if (!result) {return false;}
|
|
265
313
|
const text = failureText(result);
|
|
266
314
|
return (/\bunrecognized option\b|\bunknown option\b|\bno such option\b|\binvalid option\b|\bunrecognized flag\b|\bunknown flag\b/i).test(text);
|
|
267
315
|
}
|
|
268
316
|
|
|
317
|
+
/**
|
|
318
|
+
* @param {{stdout?: string, sessionId?: string, telemetry?: object, model?: string, provider?: string, transientRetries?: number, _jsonFallback?: boolean}} result
|
|
319
|
+
*/
|
|
269
320
|
async function processResult(result) {
|
|
270
321
|
if (result && result.stdout) {
|
|
271
|
-
result.sessionId = extractOpencodeSessionId(result.stdout);
|
|
322
|
+
result.sessionId = extractOpencodeSessionId(result.stdout) || undefined;
|
|
272
323
|
}
|
|
273
324
|
if (result && result.sessionId) {
|
|
274
325
|
try {
|
|
275
326
|
const exportJson = await _captureExport(result.sessionId, { worktree, env });
|
|
276
327
|
if (exportJson) {
|
|
277
|
-
const telemetry = extractOpencodeTelemetryFromExport(exportJson, model);
|
|
328
|
+
const telemetry = extractOpencodeTelemetryFromExport(exportJson, model || undefined);
|
|
278
329
|
if (telemetry) {
|
|
279
|
-
result.telemetry = telemetry;
|
|
280
|
-
if (telemetry.model) result.model = telemetry.model;
|
|
281
|
-
if (telemetry.provider) result.provider = telemetry.provider;
|
|
330
|
+
/** @type {any} */(result).telemetry = telemetry;
|
|
331
|
+
if (/** @type {any} */(telemetry).model) {/** @type {any} */(result).model = /** @type {any} */(telemetry).model;}
|
|
332
|
+
if (/** @type {any} */(telemetry).provider) {/** @type {any} */(result).provider = /** @type {any} */(telemetry).provider;}
|
|
282
333
|
}
|
|
283
334
|
}
|
|
284
335
|
} catch (_) {
|
|
@@ -288,34 +339,40 @@ function startOpencodeAgent({
|
|
|
288
339
|
return result;
|
|
289
340
|
}
|
|
290
341
|
|
|
342
|
+
/**
|
|
343
|
+
* @param {{command: string, args: string[], options?: object}} invocation
|
|
344
|
+
*/
|
|
291
345
|
async function runWithJsonFallback(invocation) {
|
|
292
346
|
let result = await runInvocationWithRetry(invocation);
|
|
293
347
|
// Runtime fallback: if the first invocation used --format json but the
|
|
294
348
|
// binary rejected it, retry without the flag so the prompt still executes.
|
|
295
349
|
if (isJsonFlagError(result) && invocation.args.includes('--format')) {
|
|
296
350
|
const legacyInv = buildOpencodeInvocation({
|
|
297
|
-
prompt, worktree, env, resume, sessionId, model, preferJson: false,
|
|
351
|
+
prompt: injectedPrompt, worktree, env, resume, sessionId, model, preferJson: false,
|
|
298
352
|
});
|
|
299
353
|
result = await runInvocationWithRetry(legacyInv);
|
|
300
354
|
// Mark that we fell back so callers know telemetry may be absent.
|
|
301
|
-
if (result) result._jsonFallback = true;
|
|
355
|
+
if (result) {result._jsonFallback = true;}
|
|
302
356
|
}
|
|
303
357
|
return result;
|
|
304
358
|
}
|
|
305
359
|
|
|
360
|
+
/**
|
|
361
|
+
* @param {{command: string, args: string[], options?: object}} invocation
|
|
362
|
+
*/
|
|
306
363
|
async function staleSessionHandler(invocation) {
|
|
307
364
|
let result = await runWithJsonFallback(invocation);
|
|
308
365
|
if (isStaleSessionResult(result) && worktree && resume) {
|
|
309
366
|
try {
|
|
310
|
-
_sessions.clearSession(worktree, slug, role);
|
|
367
|
+
/** @type {any} */(_sessions).clearSession(worktree, slug || '', role || '');
|
|
311
368
|
} catch (_) { /* best-effort */ }
|
|
312
|
-
const freshInv = buildOpencodeInvocation({ prompt, worktree, env, resume: false, sessionId: null, model });
|
|
369
|
+
const freshInv = buildOpencodeInvocation({ prompt: injectedPrompt, worktree, env, resume: false, sessionId: null, model });
|
|
313
370
|
result = await runWithJsonFallback(freshInv);
|
|
314
371
|
}
|
|
315
372
|
return processResult(result);
|
|
316
373
|
}
|
|
317
374
|
|
|
318
|
-
const invocation = buildOpencodeInvocation({ prompt, worktree, env, resume, sessionId, model });
|
|
375
|
+
const invocation = buildOpencodeInvocation({ prompt: injectedPrompt, worktree, env, resume, sessionId, model });
|
|
319
376
|
const resultPromise = staleSessionHandler(invocation);
|
|
320
377
|
|
|
321
378
|
return { invocation, resultPromise };
|
|
@@ -17,15 +17,11 @@
|
|
|
17
17
|
const { codexHomeRoot, extractCodexTelemetry } = require('./codex');
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* @param {object} opts
|
|
21
|
-
* @param {string} opts.worktree - worktree root used to locate the codex home.
|
|
22
|
-
* @param {object} opts.result - launcher result; `result.telemetry` is the
|
|
23
|
-
* launcher-attached fallback and `result.startedAt` bounds the codex window.
|
|
24
|
-
* @param {number} [opts.sinceMs] - epoch ms marking the start of this stage.
|
|
20
|
+
* @param {{ worktree: string, result: { telemetry?: { provider?: string } & object }, sinceMs?: number }} opts
|
|
25
21
|
* @returns {object|null} normalized telemetry, or null when none is available.
|
|
26
22
|
*/
|
|
27
|
-
function resolveStageTelemetry({ worktree, result, sinceMs = 0 } = {}) {
|
|
28
|
-
if (!result || !result.telemetry) return null;
|
|
23
|
+
function resolveStageTelemetry({ worktree, result, sinceMs = 0 } = /** @type {{ worktree: string, result: { telemetry?: { provider?: string } & object }, sinceMs?: number }} */({})) {
|
|
24
|
+
if (!result || !result.telemetry) {return null;}
|
|
29
25
|
try {
|
|
30
26
|
const codexT = extractCodexTelemetry(codexHomeRoot(worktree), { sinceMs: sinceMs || 0 });
|
|
31
27
|
// Guard against stale Codex rollouts bleeding into non-Codex stages:
|