@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
@@ -0,0 +1,370 @@
1
+ import { spawnAndTee } from '../core/spawn-tee.js';
2
+ import { extractOpencodeTelemetryFromExport } from './opencode-telemetry.js';
3
+ import { captureOpencodeExport } from './opencode-export.js';
4
+ import { detectLimitHit } from './limit-hit.js';
5
+ // tools/sessions and core/subagent-limit are still CJS (not converted in this
6
+ // wave); require keeps them untyped (any) without pulling non-included .js into
7
+ // the typecheck program.
8
+ const sessions = require('../tools/sessions');
9
+ const { buildSubagentLimitPrefix } = require('../core/subagent-limit');
10
+
11
+ interface BuildOpencodeInvocationOptions {
12
+ prompt: string;
13
+ worktree: string;
14
+ env?: object;
15
+ resume?: boolean;
16
+ sessionId?: string | null;
17
+ model?: string | null;
18
+ preferJson?: boolean;
19
+ }
20
+
21
+ interface StartOpencodeAgentOptions {
22
+ prompt: string;
23
+ worktree: string;
24
+ env?: object;
25
+ resume?: boolean;
26
+ sessionId?: string | null;
27
+ model?: string | null;
28
+ teeOptions?: object;
29
+ slug?: string | null;
30
+ role?: string | null;
31
+ maxTransientRetries?: number;
32
+ }
33
+
34
+ // Injectable I/O for tests. Production uses the real spawn-tee / export capture.
35
+ let _spawnAndTee: any = spawnAndTee;
36
+ let _captureExport: any = captureOpencodeExport;
37
+ let _sessions: any = sessions;
38
+
39
+ // Test hooks: override the launcher's I/O without touching the public signature.
40
+ function __setSpawnAndTeeForTest(fn: any) { _spawnAndTee = fn || spawnAndTee; }
41
+ function __setExportCaptureForTest(fn: any) { _captureExport = fn || captureOpencodeExport; }
42
+ function __setSessionsForTest(mod: any) { _sessions = mod || sessions; }
43
+
44
+ // Test hook: override the cached feature-detect for `--format json` support.
45
+ // Set to true/false to force inclusion/exclusion of the flag regardless of
46
+ // the real binary. Pass null to reset to the live detect.
47
+ function __setJsonFormatSupportForTest(val: boolean | null) {
48
+ _jsonFormatSupported = val;
49
+ _jsonFormatDetectFn = null;
50
+ }
51
+
52
+ // Test hook: inject a feature-detect function for tests. This replaces the
53
+ // live shell-out with a stubbed function, making tests hermetic.
54
+ function __setJsonFormatDetectForTest(fn: any) {
55
+ _jsonFormatDetectFn = fn;
56
+ _jsonFormatSupported = null; // invalidate cache so the detect fn runs
57
+ }
58
+
59
+ // Session-id recovery for telemetry. After session completion `opencode export`
60
+ // is invoked to extract token-usage data (see opencode-telemetry.js), so the
61
+ // session id MUST be recoverable from the launcher's stdout. Stats hooks in
62
+ // active.js and review-loop.js call recordStageStats which defaults to '0' for
63
+ // tokens when telemetry is null.
64
+ //
65
+ // Two shapes are recognized:
66
+ // 1. Legacy footer: "Continue opencode -s ses_<id>" (older opencode TUI).
67
+ // 2. JSON stream: `"sessionID":"ses_<id>"` (opencode v2.0.0
68
+ // `run --format json`). v2.0.0's plain `run` no longer prints the footer,
69
+ // so without the JSON field the id is lost and custom telemetry drops to all
70
+ // zeros (task-1339). We request `--format json` in buildOpencodeInvocation
71
+ // so this field is always present.
72
+ const OPENCODE_SESSION_ID_RE = /opencode\s+-s\s+(ses_\S+)/i;
73
+ const OPENCODE_JSON_SESSION_ID_RE = /"sessionID"\s*:\s*"(ses_[^"]+)"/;
74
+
75
+ function extractOpencodeSessionId(stdout: string) {
76
+ if (!stdout) {return null;}
77
+ const footer = OPENCODE_SESSION_ID_RE.exec(stdout);
78
+ if (footer) {return footer[1];}
79
+ const json = OPENCODE_JSON_SESSION_ID_RE.exec(stdout);
80
+ return json ? json[1] : null;
81
+ }
82
+
83
+ // Cached feature-detect for `--format json` support. Set to `false` only when
84
+ // a real invocation proves the flag is rejected (older opencode versions).
85
+ // Tests can inject a canned result via __setJsonFormatSupportForTest.
86
+ let _jsonFormatSupported: boolean | null = null;
87
+
88
+ // Injectable feature-detect function for tests. When set, checkJsonFormatSupport()
89
+ // calls this function instead of shelling out, making tests hermetic.
90
+ let _jsonFormatDetectFn: (() => any) | null = null;
91
+
92
+ function resolveOpencodeCommand() {
93
+ return 'opencode';
94
+ }
95
+
96
+ // Lightweight feature-detect: try `opencode --format json --help`.
97
+ // Returns true if the flag is accepted (exit 0 or non-zero for unrelated reasons),
98
+ // false if the flag itself is rejected ("unrecognized" / "unknown option").
99
+ // Results are cached so the check runs at most once in production.
100
+ // Tests can inject a canned result via __setJsonFormatSupportForTest.
101
+ function checkJsonFormatSupport() {
102
+ if (_jsonFormatSupported !== null) {return _jsonFormatSupported;}
103
+ if (_jsonFormatDetectFn) {
104
+ _jsonFormatSupported = _jsonFormatDetectFn();
105
+ return _jsonFormatSupported;
106
+ }
107
+ try {
108
+ const { spawnSync } = require('node:child_process');
109
+ const result = spawnSync('opencode', ['--format', 'json', '--help'], {
110
+ timeout: 3000,
111
+ stdio: ['ignore', 'pipe', 'pipe'],
112
+ });
113
+ const stderr = (result.stderr && result.stderr.toString()) || '';
114
+ const combined = stderr;
115
+ // Flag rejected → fallback to legacy invocation.
116
+ if (/unrecognized|unknown option|no such option|invalid option/i.test(combined)) {
117
+ _jsonFormatSupported = false;
118
+ return false;
119
+ }
120
+ _jsonFormatSupported = true;
121
+ return true;
122
+ } catch (_) {
123
+ // ENOENT or other spawn errors → assume legacy (no JSON flag).
124
+ _jsonFormatSupported = false;
125
+ return false;
126
+ }
127
+ }
128
+
129
+ function buildOpencodeInvocation({ prompt, worktree, env, resume = false, sessionId = null, model = null, preferJson = true }: BuildOpencodeInvocationOptions) {
130
+ // `--format json` makes opencode stream NDJSON events that each carry a
131
+ // "sessionID":"ses_..." field. opencode v2.0.0's default `run` output no
132
+ // longer prints the "Continue opencode -s ses_..." footer, so JSON is the
133
+ // only reliable way to recover the session id needed for `opencode export`
134
+ // telemetry capture (task-1339).
135
+ //
136
+ // Compatibility guard: feature-detect the flag on first call; older opencode
137
+ // versions that reject `--format json` will silently fall back to the legacy
138
+ // invocation, preserving launch behaviour at the cost of telemetry.
139
+ const useJson = preferJson && checkJsonFormatSupport();
140
+ const args = ['run', '--pure', '--dangerously-skip-permissions'];
141
+ if (useJson) {args.push('--format', 'json');}
142
+ if (model) {args.push('-m', model);}
143
+ // Only resume when the marker explicitly says so (resume=true).
144
+ // sessionId from a stale cross-family marker must be ignored when resume=false.
145
+ if (resume && sessionId) {
146
+ args.push('-s', sessionId);
147
+ } else if (resume) {
148
+ args.push('--continue');
149
+ }
150
+ args.push(prompt);
151
+ return {
152
+ command: resolveOpencodeCommand(),
153
+ args,
154
+ options: {
155
+ stdio: 'inherit',
156
+ cwd: worktree,
157
+ env: { ...process.env, ...env }
158
+ }
159
+ };
160
+ }
161
+
162
+ // Transient custom/vLLM backend failures that opencode surfaces as a non-zero
163
+ // exit. These are recoverable: the same prompt frequently succeeds on a second
164
+ // attempt, so the launcher retries once in-family before agents.js reroutes to a
165
+ // different agent family. Limit-hit phrases are intentionally NOT listed here —
166
+ // detectLimitHit() in agents.js owns that accounting (lib/agents/limit-hit.js).
167
+ // Hard errors (model-not-found, auth, ENOENT/EACCES) are never retried.
168
+ const TRANSIENT_OPENCODE_PATTERNS = [
169
+ /\bECONNRESET\b/i,
170
+ /\bECONNREFUSED\b/i,
171
+ /\bETIMEDOUT\b/i,
172
+ /\bENETUNREACH\b/i,
173
+ /\bENOTFOUND\b/i,
174
+ /\bEAI_AGAIN\b/i,
175
+ /\bEPIPE\b/i,
176
+ /socket hang ?up/i,
177
+ /\bfetch failed\b/i,
178
+ /network (?:error|timeout)/i,
179
+ /connection (?:error|reset|closed|refused|timed? ?out)/i,
180
+ /\b(?:502|503|504|529)\b/,
181
+ /\bbad gateway\b/i,
182
+ /\bservice (?:is )?unavailable\b/i,
183
+ /\bgateway time-?out\b/i,
184
+ /\b(?:server |api |model )?overloaded\b/i,
185
+ /\btemporarily unavailable\b/i,
186
+ /\bplease (?:try|retry) again\b/i,
187
+ /\bretry[- ]after\b/i,
188
+ ];
189
+
190
+ // Hard, non-retryable failures: retrying cannot help and only wastes time/quota.
191
+ const HARD_OPENCODE_PATTERNS = [
192
+ /\bmodel not found\b/i,
193
+ /\bno such model\b/i,
194
+ /\bunknown model\b/i,
195
+ /\binvalid (?:api )?key\b/i,
196
+ /\b401\b[^\n]*\bunauthorized\b/i,
197
+ /\bauthentication (?:failed|error)\b/i,
198
+ ];
199
+
200
+ function failureText(result: any) {
201
+ return `${(result && result.stdout) || ''}\n${(result && result.stderr) || ''}`;
202
+ }
203
+
204
+ function isHardOpencodeFailure(result: any) {
205
+ if (!result) {return false;}
206
+ if (result.error && (result.error.code === 'ENOENT' || result.error.code === 'EACCES')) {return true;}
207
+ const text = failureText(result);
208
+ return HARD_OPENCODE_PATTERNS.some((re) => re.test(text));
209
+ }
210
+
211
+ // Opencode v2.0.0 JSON-mode sometimes exits with code 1 during post-run
212
+ // cleanup (race closing the stdout pipe, EPIPE, or draining pending events).
213
+ // When the transcript contains a valid "reason":"stop" completion event and
214
+ // no "type":"error" event, the exit is spurious – the agent actually
215
+ // completed successfully. See opencode issues #31446 and #33653.
216
+ function isSpuriousOpencodeExit(result: any) {
217
+ if (!result || result.status !== 1) {return false;}
218
+ const text = failureText(result);
219
+ const hasStop = /"reason"\s*:\s*"stop"/.test(text);
220
+ const hasError = /"type"\s*:\s*"error"/.test(text);
221
+ return hasStop && !hasError;
222
+ }
223
+
224
+ function isTransientOpencodeFailure(result: any) {
225
+ if (!result) {return false;}
226
+ const text = failureText(result);
227
+ return TRANSIENT_OPENCODE_PATTERNS.some((re) => re.test(text));
228
+ }
229
+
230
+ // Decide whether a failed opencode run should be retried once in-family.
231
+ // Only genuine non-zero exits carrying a transient backend signature qualify;
232
+ // clean exits, spawn errors (ENOENT/EACCES), killing signals, limit-hits, and
233
+ // hard errors never retry — so real failures still surface to the reroute loop.
234
+ function shouldRetryOpencodeFailure(result: any) {
235
+ if (!result) {return false;}
236
+ if (result.error && (result.error.code === 'ENOENT' || result.error.code === 'EACCES')) {return false;}
237
+ if (result.signal) {return false;}
238
+ if (!(typeof result.status === 'number' && result.status !== 0)) {return false;}
239
+ if (detectLimitHit({
240
+ agent: 'custom',
241
+ stdout: result.stdout,
242
+ stderr: result.stderr,
243
+ status: result.status,
244
+ signal: result.signal,
245
+ error: result.error,
246
+ })) {return false;}
247
+ if (isHardOpencodeFailure(result)) {return false;}
248
+ return isTransientOpencodeFailure(result);
249
+ }
250
+
251
+ function startOpencodeAgent({
252
+ prompt,
253
+ worktree,
254
+ env,
255
+ resume = false,
256
+ sessionId = null,
257
+ model = null,
258
+ teeOptions = {},
259
+ slug = null,
260
+ role = null,
261
+ maxTransientRetries = 1
262
+ }: StartOpencodeAgentOptions) {
263
+ // Prepend the subagent-limit advisory prefix to the prompt.
264
+ // When maxParallel is unset/null/zero the prefix is empty (no-op).
265
+ const subagentPrefix = buildSubagentLimitPrefix();
266
+ const injectedPrompt = subagentPrefix + prompt;
267
+
268
+ function isStaleSessionResult(result: any) {
269
+ if (!result) {return false;}
270
+ const stderr = result.stderr || '';
271
+ const stdout = result.stdout || '';
272
+ return stderr.includes('Session not found') || stdout.includes('Session not found');
273
+ }
274
+
275
+ function runInvocation(invocation: any) {
276
+ const spawnOptions = { ...invocation.options, ...teeOptions };
277
+ return _spawnAndTee(invocation.command, invocation.args, spawnOptions);
278
+ }
279
+
280
+ async function runInvocationWithRetry(invocation: any) {
281
+ let attempts = 0;
282
+ let result = await runInvocation(invocation);
283
+ while (attempts < maxTransientRetries && shouldRetryOpencodeFailure(result)) {
284
+ attempts += 1;
285
+ result = await runInvocation(invocation);
286
+ }
287
+ if (result) {result.transientRetries = attempts;}
288
+ return result;
289
+ }
290
+
291
+ // Detect whether `--format json` was rejected at runtime (e.g. older opencode
292
+ // that wasn't caught by the feature-detect). If so, retry the invocation
293
+ // without the flag so the prompt still runs (telemetry will be lost but the
294
+ // agent won't fail outright).
295
+ function isJsonFlagError(result: any) {
296
+ if (!result) {return false;}
297
+ const text = failureText(result);
298
+ return (/\bunrecognized option\b|\bunknown option\b|\bno such option\b|\binvalid option\b|\bunrecognized flag\b|\bunknown flag\b/i).test(text);
299
+ }
300
+
301
+ async function processResult(result: any) {
302
+ if (result && result.stdout) {
303
+ result.sessionId = extractOpencodeSessionId(result.stdout) || undefined;
304
+ }
305
+ if (result && result.sessionId) {
306
+ try {
307
+ const exportJson = await _captureExport(result.sessionId, { worktree, env });
308
+ if (exportJson) {
309
+ const telemetry = extractOpencodeTelemetryFromExport(exportJson, model || undefined);
310
+ if (telemetry) {
311
+ result.telemetry = telemetry;
312
+ if ((telemetry as any).model) {result.model = (telemetry as any).model;}
313
+ if ((telemetry as any).provider) {result.provider = (telemetry as any).provider;}
314
+ }
315
+ }
316
+ } catch (_) {
317
+ return result;
318
+ }
319
+ }
320
+ return result;
321
+ }
322
+
323
+ async function runWithJsonFallback(invocation: any) {
324
+ let result = await runInvocationWithRetry(invocation);
325
+ // Runtime fallback: if the first invocation used --format json but the
326
+ // binary rejected it, retry without the flag so the prompt still executes.
327
+ if (isJsonFlagError(result) && invocation.args.includes('--format')) {
328
+ const legacyInv = buildOpencodeInvocation({
329
+ prompt: injectedPrompt, worktree, env, resume, sessionId, model, preferJson: false,
330
+ });
331
+ result = await runInvocationWithRetry(legacyInv);
332
+ // Mark that we fell back so callers know telemetry may be absent.
333
+ if (result) {result._jsonFallback = true;}
334
+ }
335
+ return result;
336
+ }
337
+
338
+ async function staleSessionHandler(invocation: any) {
339
+ let result = await runWithJsonFallback(invocation);
340
+ if (isStaleSessionResult(result) && worktree && resume) {
341
+ try {
342
+ _sessions.clearSession(worktree, slug || '', role || '');
343
+ } catch (_) { /* best-effort */ }
344
+ const freshInv = buildOpencodeInvocation({ prompt: injectedPrompt, worktree, env, resume: false, sessionId: null, model });
345
+ result = await runWithJsonFallback(freshInv);
346
+ }
347
+ return processResult(result);
348
+ }
349
+
350
+ const invocation = buildOpencodeInvocation({ prompt: injectedPrompt, worktree, env, resume, sessionId, model });
351
+ const resultPromise = staleSessionHandler(invocation);
352
+
353
+ return { invocation, resultPromise };
354
+ }
355
+
356
+ export {
357
+ buildOpencodeInvocation,
358
+ extractOpencodeSessionId,
359
+ resolveOpencodeCommand,
360
+ startOpencodeAgent,
361
+ isSpuriousOpencodeExit,
362
+ isTransientOpencodeFailure,
363
+ isHardOpencodeFailure,
364
+ shouldRetryOpencodeFailure,
365
+ __setSpawnAndTeeForTest,
366
+ __setExportCaptureForTest,
367
+ __setSessionsForTest,
368
+ __setJsonFormatSupportForTest,
369
+ __setJsonFormatDetectForTest,
370
+ };
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
-
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveStageTelemetry = resolveStageTelemetry;
3
4
  // Shared resolution of the most faithful telemetry for a recorded workflow
4
5
  // stage (execute/review). Codex's authoritative usage is the windowed sum read
5
6
  // from its on-disk rollout — `sinceMs` bounds the read to just this stage — so
@@ -13,30 +14,28 @@
13
14
  // fixes was active.js computing `sinceMs` and then discarding it, recording
14
15
  // cumulative Codex usage for the execute phase instead of the windowed delta
15
16
  // (task-1285 review F1/F6).
16
-
17
- const { codexHomeRoot, extractCodexTelemetry } = require('./codex');
18
-
17
+ const codex_js_1 = require("./codex.js");
19
18
  /**
20
- * @param {{ worktree: string, result: { telemetry?: { provider?: string } & object }, sinceMs?: number }} opts
21
- * @returns {object|null} normalized telemetry, or null when none is available.
19
+ * @returns normalized telemetry, or null when none is available.
22
20
  */
23
- function resolveStageTelemetry({ worktree, result, sinceMs = 0 } = /** @type {{ worktree: string, result: { telemetry?: { provider?: string } & object }, sinceMs?: number }} */({})) {
24
- if (!result || !result.telemetry) {return null;}
25
- try {
26
- const codexT = extractCodexTelemetry(codexHomeRoot(worktree), { sinceMs: sinceMs || 0 });
27
- // Guard against stale Codex rollouts bleeding into non-Codex stages:
28
- // if the launcher attached telemetry with a provider that differs from
29
- // what Codex would produce (e.g. anthropic vs openai), discard the Codex
30
- // read and use the launcher-attached telemetry instead.
31
- if (codexT && codexT.provider && result.telemetry.provider
32
- && codexT.provider.toLowerCase() !== result.telemetry.provider.toLowerCase()) {
33
- return result.telemetry;
21
+ function resolveStageTelemetry({ worktree, result, sinceMs = 0 } = {}) {
22
+ if (!result || !result.telemetry) {
23
+ return null;
24
+ }
25
+ try {
26
+ const codexT = (0, codex_js_1.extractCodexTelemetry)((0, codex_js_1.codexHomeRoot)(worktree), { sinceMs: sinceMs || 0 });
27
+ // Guard against stale Codex rollouts bleeding into non-Codex stages:
28
+ // if the launcher attached telemetry with a provider that differs from
29
+ // what Codex would produce (e.g. anthropic vs openai), discard the Codex
30
+ // read and use the launcher-attached telemetry instead.
31
+ if (codexT && codexT.provider && result.telemetry.provider
32
+ && codexT.provider.toLowerCase() !== result.telemetry.provider.toLowerCase()) {
33
+ return result.telemetry;
34
+ }
35
+ return codexT || result.telemetry;
36
+ }
37
+ catch (_) {
38
+ // Codex rollout unreadable: fall back to this stage's launcher telemetry.
39
+ return result.telemetry;
34
40
  }
35
- return codexT || result.telemetry;
36
- } catch (_) {
37
- // Codex rollout unreadable: fall back to this stage's launcher telemetry.
38
- return result.telemetry;
39
- }
40
41
  }
41
-
42
- module.exports = { resolveStageTelemetry };
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ // Shared resolution of the most faithful telemetry for a recorded workflow
4
+ // stage (execute/review). Codex's authoritative usage is the windowed sum read
5
+ // from its on-disk rollout — `sinceMs` bounds the read to just this stage — so
6
+ // prefer it when present. For non-Codex agents (e.g. Claude, whose usage the
7
+ // launcher parses from stdout via claude-telemetry.js) no codex rollout exists
8
+ // and extractCodexTelemetry returns null *without throwing*, so we fall back to
9
+ // the telemetry the launcher already attached.
10
+ //
11
+ // This lives in one place so the execute hook (commands/active.js) and the
12
+ // review-loop hook (review/review-loop.js) cannot drift apart — the bug this
13
+ // fixes was active.js computing `sinceMs` and then discarding it, recording
14
+ // cumulative Codex usage for the execute phase instead of the windowed delta
15
+ // (task-1285 review F1/F6).
16
+
17
+ import { codexHomeRoot, extractCodexTelemetry } from './codex.js';
18
+
19
+ interface StageTelemetryOptions {
20
+ worktree: string;
21
+ result: { telemetry?: { provider?: string } & Record<string, any> };
22
+ sinceMs?: number;
23
+ }
24
+
25
+ /**
26
+ * @returns normalized telemetry, or null when none is available.
27
+ */
28
+ function resolveStageTelemetry({ worktree, result, sinceMs = 0 }: StageTelemetryOptions = {} as StageTelemetryOptions) {
29
+ if (!result || !result.telemetry) {return null;}
30
+ try {
31
+ const codexT = extractCodexTelemetry(codexHomeRoot(worktree), { sinceMs: sinceMs || 0 });
32
+ // Guard against stale Codex rollouts bleeding into non-Codex stages:
33
+ // if the launcher attached telemetry with a provider that differs from
34
+ // what Codex would produce (e.g. anthropic vs openai), discard the Codex
35
+ // read and use the launcher-attached telemetry instead.
36
+ if (codexT && codexT.provider && result.telemetry.provider
37
+ && codexT.provider.toLowerCase() !== result.telemetry.provider.toLowerCase()) {
38
+ return result.telemetry;
39
+ }
40
+ return codexT || result.telemetry;
41
+ } catch (_) {
42
+ // Codex rollout unreadable: fall back to this stage's launcher telemetry.
43
+ return result.telemetry;
44
+ }
45
+ }
46
+
47
+ export { resolveStageTelemetry };