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