@magnusekdahl/parallix 1.2.0 → 1.2.1

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 (51) hide show
  1. package/lib/agents/agents.js +737 -838
  2. package/lib/agents/agents.ts +930 -0
  3. package/lib/agents/claude-telemetry.js +149 -154
  4. package/lib/agents/claude-telemetry.ts +233 -0
  5. package/lib/agents/claude.js +121 -131
  6. package/lib/agents/claude.ts +156 -0
  7. package/lib/agents/codex-telemetry.js +176 -164
  8. package/lib/agents/codex-telemetry.ts +205 -0
  9. package/lib/agents/codex.js +168 -201
  10. package/lib/agents/codex.ts +236 -0
  11. package/lib/agents/limit-hit.js +216 -226
  12. package/lib/agents/limit-hit.ts +262 -0
  13. package/lib/agents/mistral-telemetry.js +7 -10
  14. package/lib/agents/mistral-telemetry.ts +44 -0
  15. package/lib/agents/mistral.js +40 -58
  16. package/lib/agents/mistral.ts +84 -0
  17. package/lib/agents/opencode-export.js +126 -127
  18. package/lib/agents/opencode-export.ts +160 -0
  19. package/lib/agents/opencode-telemetry.js +282 -261
  20. package/lib/agents/opencode-telemetry.ts +350 -0
  21. package/lib/agents/opencode.js +263 -305
  22. package/lib/agents/opencode.ts +370 -0
  23. package/lib/agents/stage-telemetry.js +23 -24
  24. package/lib/agents/stage-telemetry.ts +47 -0
  25. package/lib/commands/integrate.js +9 -3
  26. package/lib/core/fmt.js +140 -211
  27. package/lib/core/fmt.ts +189 -0
  28. package/lib/core/git.js +121 -102
  29. package/lib/core/git.ts +146 -0
  30. package/lib/core/gitignore.js +59 -106
  31. package/lib/core/gitignore.ts +103 -0
  32. package/lib/core/mission-utils.js +833 -875
  33. package/lib/core/mission-utils.ts +998 -0
  34. package/lib/core/persistent-data-migration.js +247 -205
  35. package/lib/core/persistent-data-migration.ts +255 -0
  36. package/lib/core/product-config.js +400 -476
  37. package/lib/core/product-config.ts +518 -0
  38. package/lib/core/runtime-matrix.js +41 -87
  39. package/lib/core/runtime-matrix.ts +84 -0
  40. package/lib/core/spawn-tee.js +152 -192
  41. package/lib/core/spawn-tee.ts +202 -0
  42. package/lib/core/state-map.js +92 -92
  43. package/lib/core/state-map.ts +115 -0
  44. package/lib/core/storage.js +101 -129
  45. package/lib/core/storage.ts +182 -0
  46. package/lib/core/verification.js +148 -138
  47. package/lib/core/verification.ts +200 -0
  48. package/package.json +4 -2
  49. package/prompts/draft.md +1 -0
  50. package/prompts/portfolio.md +1 -1
  51. package/px.js +0 -2
@@ -0,0 +1,262 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_FALLBACK_HOURS = 1;
4
+ const SIGINT_SHORT_BLOCK_MINUTES = 15;
5
+
6
+ const PATTERN_SETS = Object.freeze({
7
+ claude: [
8
+ /claude (?:ai )?usage limit reached/i,
9
+ /\b5-hour limit (?:reached|exceeded)\b/i,
10
+ /you(?:'|')?ve hit your limit(?:\b|$)/i,
11
+ /you(?:'|')?ve hit your (?:weekly|daily|monthly|usage) limit/i,
12
+ /(?:usage|rate)\s*limit (?:has been )?(?:reached|exceeded)/i,
13
+ /\b429\b[^\n]*?\b(?:rate|usage|quota)\b/i
14
+ ],
15
+ codex: [
16
+ /you(?:'|’)?ve hit your (?:weekly|daily|monthly|usage) limit/i,
17
+ /\brate[_ ]?limit(?:_| )?(?:exceeded|reached)\b/i,
18
+ /\bweekly (?:codex )?usage limit\b/i,
19
+ /\b429\b[^\n]*?\b(?:rate|quota|usage)\b/i,
20
+ /resource_exhausted/i
21
+ ],
22
+
23
+ custom: [
24
+ /\b(?:rate|usage|quota)\s*limit\s*(?:reached|exceeded)\b/i,
25
+ /\b429\b[^\n]*?\b(?:rate|quota|usage)\b/i,
26
+ /\b429\b/i,
27
+ /\binsufficient[_ ]quota\b/i
28
+ ],
29
+ mistral: [
30
+ /\bmistral (?:ai )?usage limit reached/i,
31
+ /\bmistral (?:ai )?quota exceeded/i,
32
+ /\bmistral (?:ai )?rate limit(?: reached| exceeded)/i,
33
+ /you(?:'|')?ve hit your (?:mistral|vibe) (?:weekly|daily|monthly|usage) limit/i,
34
+ /\b429\b[^\n]*?\b(?:rate|quota|usage)\b/i,
35
+ /\bvibe\b[^\n]*?\b(?:quota exceeded|rate limit exceeded)\b/i,
36
+ /\bmistral\b[^\n]*?\b(?:quota exceeded|rate limit exceeded)\b/i,
37
+ /\bresource[_ ]has[_ ]been[_ ]exhausted\b/i,
38
+ /\bresource_exhausted\b/i
39
+ ]
40
+ });
41
+
42
+ function getPatternsForAgent(agent: string) {
43
+ return PATTERN_SETS[agent as keyof typeof PATTERN_SETS] || [];
44
+ }
45
+
46
+ function findLimitHitMatch(agent: string, text: string) {
47
+ const patterns = getPatternsForAgent(agent);
48
+ for (const pattern of patterns) {
49
+ const match = pattern.exec(text);
50
+ if (match) {
51
+ return { pattern, index: match.index, length: match[0].length };
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ function clipContext(text: string, index: number, length: number) {
58
+ const before = Math.max(0, index - 200);
59
+ const after = Math.min(text.length, index + length + 200);
60
+ return text.slice(before, after);
61
+ }
62
+
63
+ const ISO_PATTERN = /(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})?/;
64
+ const TWELVE_HOUR_PATTERN = /\b(\d{1,2})(?::(\d{2}))?\s*(am|pm|AM|PM)\b/;
65
+ const TWENTY_FOUR_HOUR_PATTERN = /\b([01]?\d|2[0-3]):([0-5]\d)\b/;
66
+ const RELATIVE_PATTERN = /\bin\s+(\d+)\s+(hour|hours|minute|minutes|second|seconds)\b/i;
67
+ const RETRY_AFTER_PATTERN = /retry[- ]?after[:\s]+(\d+)\s*(seconds?|s|minutes?|m)?/i;
68
+ const RETRY_AT_DATE_PATTERN = /(?:retry|reset|try again|reset(?:s)? at)[^\n]{0,40}?(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)/i;
69
+
70
+ function parseIsoOffset(value: string | undefined) {
71
+ if (!value) {return null;}
72
+ if (value === 'Z') {return 0;}
73
+ const m = /([+-])(\d{2}):?(\d{2})/.exec(value);
74
+ if (!m) {return null;}
75
+ const sign = m[1] === '-' ? -1 : 1;
76
+ return sign * (Number(m[2]) * 60 + Number(m[3]));
77
+ }
78
+
79
+ function parseIso(text: string) {
80
+ const m = ISO_PATTERN.exec(text);
81
+ if (!m) {return null;}
82
+ const [, year, month, day, hour, minute, second, offset] = m;
83
+ const offsetMinutes = parseIsoOffset(offset);
84
+ if (offsetMinutes === null) {
85
+ return new Date(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second || 0), 0);
86
+ }
87
+ const utcMs = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second || 0), 0);
88
+ return new Date(utcMs - offsetMinutes * 60 * 1000);
89
+ }
90
+
91
+ function parseTwelveHour(text: string, now: Date) {
92
+ const m = TWELVE_HOUR_PATTERN.exec(text);
93
+ if (!m) {return null;}
94
+ let hour = Number(m[1]);
95
+ const minute = Number(m[2] || 0);
96
+ const meridiem = m[3].toLowerCase();
97
+ if (hour < 1 || hour > 12) {return null;}
98
+ if (meridiem === 'pm' && hour !== 12) {hour += 12;}
99
+ if (meridiem === 'am' && hour === 12) {hour = 0;}
100
+ return projectClockTime(now, hour, minute);
101
+ }
102
+
103
+ function parseTwentyFourHour(text: string, now: Date) {
104
+ const m = TWENTY_FOUR_HOUR_PATTERN.exec(text);
105
+ if (!m) {return null;}
106
+ const hour = Number(m[1]);
107
+ const minute = Number(m[2]);
108
+ return projectClockTime(now, hour, minute);
109
+ }
110
+
111
+ function parseRelative(text: string, now: Date) {
112
+ const m = RELATIVE_PATTERN.exec(text);
113
+ if (!m) {return null;}
114
+ const amount = Number(m[1]);
115
+ const unit = m[2].toLowerCase();
116
+ let ms;
117
+ if (unit.startsWith('hour')) {ms = amount * 60 * 60 * 1000;}
118
+ else if (unit.startsWith('minute')) {ms = amount * 60 * 1000;}
119
+ else {ms = amount * 1000;}
120
+ return new Date(now.getTime() + ms);
121
+ }
122
+
123
+ function parseRetryAfter(text: string, now: Date) {
124
+ const m = RETRY_AFTER_PATTERN.exec(text);
125
+ if (!m) {return null;}
126
+ const amount = Number(m[1]);
127
+ const unit = (m[2] || 's').toLowerCase();
128
+ let ms;
129
+ if (unit.startsWith('m') && unit !== 's') {ms = amount * 60 * 1000;}
130
+ else {ms = amount * 1000;}
131
+ return new Date(now.getTime() + ms);
132
+ }
133
+
134
+ function parseRetryAtDate(text: string) {
135
+ const m = RETRY_AT_DATE_PATTERN.exec(text);
136
+ if (!m) {return null;}
137
+ return parseIso(m[1]);
138
+ }
139
+
140
+ function projectClockTime(now: Date, hour: number, minute: number) {
141
+ const candidate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute, 0, 0);
142
+ if (candidate.getTime() <= now.getTime()) {
143
+ candidate.setDate(candidate.getDate() + 1);
144
+ }
145
+ return candidate;
146
+ }
147
+
148
+ function parseResetTime(text: string, now: Date = new Date()) {
149
+ if (!text) {return null;}
150
+ return (
151
+ parseRetryAtDate(text) ||
152
+ parseIso(text) ||
153
+ parseTwelveHour(text, now) ||
154
+ parseTwentyFourHour(text, now) ||
155
+ parseRelative(text, now) ||
156
+ parseRetryAfter(text, now)
157
+ );
158
+ }
159
+
160
+ function formatBlockUntil(date: Date) {
161
+ const year = date.getFullYear();
162
+ const month = String(date.getMonth() + 1).padStart(2, '0');
163
+ const day = String(date.getDate()).padStart(2, '0');
164
+ const hour = String(date.getHours()).padStart(2, '0');
165
+ return `${year}-${month}-${day} ${hour}`;
166
+ }
167
+
168
+ function ceilToNextHour(date: Date) {
169
+ const ceil = new Date(date.getTime());
170
+ if (ceil.getMinutes() === 0 && ceil.getSeconds() === 0 && ceil.getMilliseconds() === 0) {
171
+ return ceil;
172
+ }
173
+ ceil.setMinutes(0, 0, 0);
174
+ ceil.setHours(ceil.getHours() + 1);
175
+ return ceil;
176
+ }
177
+
178
+ interface DetectLimitHitOptions {
179
+ agent: string;
180
+ stdout?: string;
181
+ stderr?: string;
182
+ status?: number;
183
+ signal?: string | null;
184
+ error?: { code?: string } | null;
185
+ now?: Date;
186
+ }
187
+
188
+ function detectLimitHit({
189
+ agent,
190
+ stdout = '',
191
+ stderr = '',
192
+ status,
193
+ signal,
194
+ error,
195
+ now = new Date()
196
+ }: DetectLimitHitOptions = {} as DetectLimitHitOptions) {
197
+ if (!agent) {return null;}
198
+ // Gate detection on a failed launcher invocation. A successful child (exit 0,
199
+ // no signal, no spawn error) means any limit-hit phrases in the transcript
200
+ // are quoted text — typically when the agent itself reviewed code, tests, or
201
+ // logs containing those strings. Treating that as a real limit hit would
202
+ // wrongly block a healthy agent in agents.local.json.
203
+ // `status === undefined` means the caller did not pass exit metadata; preserve
204
+ // the legacy behavior so callers that haven't been updated still get detection.
205
+ const launcherFailed =
206
+ error !== null && error !== undefined ||
207
+ signal !== null && signal !== undefined ||
208
+ (typeof status === 'number' && status !== 0) ||
209
+ typeof status === 'undefined';
210
+ if (!launcherFailed) {return null;}
211
+
212
+ const combined = `${stdout || ''}\n${stderr || ''}`;
213
+ const match = findLimitHitMatch(agent, combined);
214
+
215
+ let target;
216
+ let source;
217
+
218
+ if (match) {
219
+ const context = clipContext(combined, match.index, match.length);
220
+ const parsed = parseResetTime(context, now);
221
+
222
+ if (parsed && !Number.isNaN(parsed.getTime()) && parsed.getTime() > now.getTime()) {
223
+ target = parsed;
224
+ source = 'parsed';
225
+ } else {
226
+ target = new Date(now.getTime() + DEFAULT_FALLBACK_HOURS * 60 * 60 * 1000);
227
+ source = 'fallback';
228
+ }
229
+ } else if (signal !== null && signal !== undefined) {
230
+ // Agent was killed by a signal (e.g. SIGINT/Ctrl-C) but no limit-hit
231
+ // pattern matched. Apply a short-term block so the next invocation
232
+ // selects a different agent instead of re-picking the same one.
233
+ target = new Date(now.getTime() + SIGINT_SHORT_BLOCK_MINUTES * 60 * 1000);
234
+ source = 'sigint';
235
+ } else {
236
+ return null;
237
+ }
238
+
239
+ const ceiled = ceilToNextHour(target);
240
+ return { until: formatBlockUntil(ceiled), source };
241
+ }
242
+
243
+ export {
244
+ detectLimitHit,
245
+ parseResetTime,
246
+ formatBlockUntil,
247
+ ceilToNextHour,
248
+ findLimitHitMatch,
249
+ clipContext,
250
+ parseIsoOffset,
251
+ parseIso,
252
+ parseTwelveHour,
253
+ parseTwentyFourHour,
254
+ parseRelative,
255
+ parseRetryAfter,
256
+ parseRetryAtDate,
257
+ projectClockTime,
258
+ getPatternsForAgent,
259
+ PATTERN_SETS,
260
+ DEFAULT_FALLBACK_HOURS,
261
+ SIGINT_SHORT_BLOCK_MINUTES
262
+ };
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  /**
2
3
  * Mistral (Vibe) Telemetry Stub
3
4
  *
@@ -15,7 +16,9 @@
15
16
  * telemetry once usage-unblocked"), which names the missing evidence and the
16
17
  * reason it could not be collected in the task-1285 environment.
17
18
  */
18
-
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.extractMistralTelemetry = extractMistralTelemetry;
21
+ exports.getMistralProviderModel = getMistralProviderModel;
19
22
  /**
20
23
  * Attempt to extract telemetry from a mistral launcher result.
21
24
  * Since mistral/vibe provides no parseable usage data, always returns null.
@@ -24,10 +27,9 @@
24
27
  * @returns {null} Always null — mistral/vibe has no telemetry parser
25
28
  */
26
29
  function extractMistralTelemetry() {
27
- // Mistral Vibe does not emit token usage data. Honest zero.
28
- return null;
30
+ // Mistral Vibe does not emit token usage data. Honest zero.
31
+ return null;
29
32
  }
30
-
31
33
  /**
32
34
  * Return the provider/model pair for mistral tasks.
33
35
  * Used as fallback when telemetry is null.
@@ -35,10 +37,5 @@ function extractMistralTelemetry() {
35
37
  * @returns {{provider: string, model: string}}
36
38
  */
37
39
  function getMistralProviderModel() {
38
- return { provider: 'mistral', model: 'mistral' };
40
+ return { provider: 'mistral', model: 'mistral' };
39
41
  }
40
-
41
- module.exports = {
42
- extractMistralTelemetry,
43
- getMistralProviderModel,
44
- };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Mistral (Vibe) Telemetry Stub
3
+ *
4
+ * Mistral Vibe in programmatic mode does not output token-usage data to
5
+ * stdout/stderr, and has no CLI endpoint for querying session usage.
6
+ * This stub records honest zeros for all token columns so the stats CSV
7
+ * never contains fabricated data.
8
+ *
9
+ * Provider and model fields are set to "mistral" to indicate the agent family.
10
+ * The actual model is determined at runtime via VIBE_ACTIVE_MODEL env var
11
+ * but is not captured per-session, so model falls back to "mistral".
12
+ *
13
+ * See task-1285 for the full telemetry credibility design. The blocked
14
+ * verification is tracked as follow-up task-1288 ("Verify vibe/mistral
15
+ * telemetry once usage-unblocked"), which names the missing evidence and the
16
+ * reason it could not be collected in the task-1285 environment.
17
+ */
18
+
19
+ /**
20
+ * Attempt to extract telemetry from a mistral launcher result.
21
+ * Since mistral/vibe provides no parseable usage data, always returns null.
22
+ *
23
+ * @param {object} result - The launcher result object
24
+ * @returns {null} Always null — mistral/vibe has no telemetry parser
25
+ */
26
+ function extractMistralTelemetry() {
27
+ // Mistral Vibe does not emit token usage data. Honest zero.
28
+ return null;
29
+ }
30
+
31
+ /**
32
+ * Return the provider/model pair for mistral tasks.
33
+ * Used as fallback when telemetry is null.
34
+ *
35
+ * @returns {{provider: string, model: string}}
36
+ */
37
+ function getMistralProviderModel() {
38
+ return { provider: 'mistral', model: 'mistral' };
39
+ }
40
+
41
+ export {
42
+ extractMistralTelemetry,
43
+ getMistralProviderModel,
44
+ };
@@ -1,5 +1,10 @@
1
- const { spawnAndTee } = require('../core/spawn-tee');
2
-
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildMistralInvocation = buildMistralInvocation;
4
+ exports.extractMistralSessionId = extractMistralSessionId;
5
+ exports.resolveMistralCommand = resolveMistralCommand;
6
+ exports.startMistralAgent = startMistralAgent;
7
+ const spawn_tee_js_1 = require("../core/spawn-tee.js");
3
8
  // Mistral Vibe in programmatic mode (-p/--prompt) does NOT output a resume
4
9
  // hint to stdout/stderr like other agents do. Session IDs are stored in
5
10
  // ~/.vibe/logs/session/session_<timestamp>_<short_id>/meta.json with UUID format,
@@ -12,67 +17,44 @@ const { spawnAndTee } = require('../core/spawn-tee');
12
17
  // Telemetry: mistral/vibe does not expose token-usage data. See mistral-telemetry.js
13
18
  // for the honest-zero stub. Stats hooks in active.js and review-loop.js call
14
19
  // recordStageStats which defaults to '0' for tokens when telemetry is null.
15
-
16
-
17
- /** @param {string} stdout */
18
20
  function extractMistralSessionId(stdout) {
19
- void stdout;
20
- // Vibe does not currently emit a resume hint in programmatic mode.
21
- // Return null to indicate no resume capability via stdout parsing.
22
- return null;
21
+ void stdout;
22
+ // Vibe does not currently emit a resume hint in programmatic mode.
23
+ // Return null to indicate no resume capability via stdout parsing.
24
+ return null;
23
25
  }
24
-
25
26
  function resolveMistralCommand() {
26
- return 'vibe';
27
+ return 'vibe';
27
28
  }
28
-
29
- /**
30
- * @param {{ prompt: string, worktree: string, env?: object, resume?: boolean, sessionId?: string | null, model?: string | null }} opts
31
- */
32
29
  function buildMistralInvocation({ prompt, worktree, env, resume, sessionId, model = null }) {
33
- void resume;
34
- void sessionId;
35
- const args = ['--prompt', prompt, '--trust', '--output', 'text'];
36
-
37
- // Vibe programmatic mode does not support --resume flag in the same way
38
- // as other agents. The --resume flag exists but requires interactive selection
39
- // or a session picker. Since we cannot reliably pass a session ID via
40
- // programmatic mode, we do not add resume flags here.
41
- // If resume capability is proven in a future version, update this.
42
-
43
- // Vibe has no CLI model flag in programmatic mode; the active model is
44
- // selected via the VIBE_ACTIVE_MODEL env var (verified from `vibe --help`).
45
- const modelEnv = model ? { VIBE_ACTIVE_MODEL: model } : {};
46
-
47
- return {
48
- command: resolveMistralCommand(),
49
- args,
50
- options: {
51
- stdio: 'inherit',
52
- cwd: worktree,
53
- env: { ...process.env, ...env, ...modelEnv }
54
- }
55
- };
30
+ void resume;
31
+ void sessionId;
32
+ const args = ['--prompt', prompt, '--trust', '--output', 'text'];
33
+ // Vibe programmatic mode does not support --resume flag in the same way
34
+ // as other agents. The --resume flag exists but requires interactive selection
35
+ // or a session picker. Since we cannot reliably pass a session ID via
36
+ // programmatic mode, we do not add resume flags here.
37
+ // If resume capability is proven in a future version, update this.
38
+ // Vibe has no CLI model flag in programmatic mode; the active model is
39
+ // selected via the VIBE_ACTIVE_MODEL env var (verified from `vibe --help`).
40
+ const modelEnv = model ? { VIBE_ACTIVE_MODEL: model } : {};
41
+ return {
42
+ command: resolveMistralCommand(),
43
+ args,
44
+ options: {
45
+ stdio: 'inherit',
46
+ cwd: worktree,
47
+ env: { ...process.env, ...env, ...modelEnv }
48
+ }
49
+ };
56
50
  }
57
-
58
- /**
59
- * @param {{ prompt: string, worktree: string, env?: object, resume?: boolean, sessionId?: string | null, model?: string | null, teeOptions?: object }} opts
60
- */
61
51
  function startMistralAgent({ prompt, worktree, env, resume = false, sessionId = null, model = null, teeOptions = {} }) {
62
- const invocation = buildMistralInvocation({ prompt, worktree, env, resume, sessionId, model });
63
- const resultPromise = spawnAndTee(invocation.command, invocation.args, /** @type {any} */({ ...invocation.options, ...teeOptions })).then(result => {
64
- if (result && result.stdout) {
65
- result.sessionId = extractMistralSessionId(result.stdout);
66
- }
67
- return result;
68
- });
69
-
70
- return { invocation, resultPromise };
52
+ const invocation = buildMistralInvocation({ prompt, worktree, env, resume, sessionId, model });
53
+ const resultPromise = (0, spawn_tee_js_1.spawnAndTee)(invocation.command, invocation.args, { ...invocation.options, ...teeOptions }).then((result) => {
54
+ if (result && result.stdout) {
55
+ result.sessionId = extractMistralSessionId(result.stdout);
56
+ }
57
+ return result;
58
+ });
59
+ return { invocation, resultPromise };
71
60
  }
72
-
73
- module.exports = {
74
- buildMistralInvocation,
75
- extractMistralSessionId,
76
- resolveMistralCommand,
77
- startMistralAgent
78
- };
@@ -0,0 +1,84 @@
1
+ import { spawnAndTee } from '../core/spawn-tee.js';
2
+
3
+ interface MistralInvocationOptions {
4
+ prompt: string;
5
+ worktree: string;
6
+ env?: Record<string, string>;
7
+ resume?: boolean;
8
+ sessionId?: string | null;
9
+ model?: string | null;
10
+ }
11
+
12
+ interface StartMistralAgentOptions extends MistralInvocationOptions {
13
+ teeOptions?: object;
14
+ }
15
+
16
+ // Mistral Vibe in programmatic mode (-p/--prompt) does NOT output a resume
17
+ // hint to stdout/stderr like other agents do. Session IDs are stored in
18
+ // ~/.vibe/logs/session/session_<timestamp>_<short_id>/meta.json with UUID format,
19
+ // but there is no reliable stdout pattern to extract. Therefore, Mistral is
20
+ // NOT marked as RESUME_CAPABLE in agents.js. If future Vibe versions add
21
+ // a resume hint, update this regex and add 'mistral' to RESUME_CAPABLE.
22
+ // Current session ID format in meta.json: UUID like "a3dd3d4d-f97d-d57d-4942-a1f694e3a922"
23
+ // Directory naming uses first 8 chars: session_20260521_162703_a3dd3d4d
24
+ // No stdout marker detected in testing, so we leave this as null.
25
+ // Telemetry: mistral/vibe does not expose token-usage data. See mistral-telemetry.js
26
+ // for the honest-zero stub. Stats hooks in active.js and review-loop.js call
27
+ // recordStageStats which defaults to '0' for tokens when telemetry is null.
28
+
29
+
30
+ function extractMistralSessionId(stdout: string) {
31
+ void stdout;
32
+ // Vibe does not currently emit a resume hint in programmatic mode.
33
+ // Return null to indicate no resume capability via stdout parsing.
34
+ return null;
35
+ }
36
+
37
+ function resolveMistralCommand() {
38
+ return 'vibe';
39
+ }
40
+
41
+ function buildMistralInvocation({ prompt, worktree, env, resume, sessionId, model = null }: MistralInvocationOptions) {
42
+ void resume;
43
+ void sessionId;
44
+ const args = ['--prompt', prompt, '--trust', '--output', 'text'];
45
+
46
+ // Vibe programmatic mode does not support --resume flag in the same way
47
+ // as other agents. The --resume flag exists but requires interactive selection
48
+ // or a session picker. Since we cannot reliably pass a session ID via
49
+ // programmatic mode, we do not add resume flags here.
50
+ // If resume capability is proven in a future version, update this.
51
+
52
+ // Vibe has no CLI model flag in programmatic mode; the active model is
53
+ // selected via the VIBE_ACTIVE_MODEL env var (verified from `vibe --help`).
54
+ const modelEnv = model ? { VIBE_ACTIVE_MODEL: model } : {};
55
+
56
+ return {
57
+ command: resolveMistralCommand(),
58
+ args,
59
+ options: {
60
+ stdio: 'inherit',
61
+ cwd: worktree,
62
+ env: { ...process.env, ...env, ...modelEnv }
63
+ }
64
+ };
65
+ }
66
+
67
+ function startMistralAgent({ prompt, worktree, env, resume = false, sessionId = null, model = null, teeOptions = {} }: StartMistralAgentOptions) {
68
+ const invocation = buildMistralInvocation({ prompt, worktree, env, resume, sessionId, model });
69
+ const resultPromise = spawnAndTee(invocation.command, invocation.args, { ...invocation.options, ...teeOptions } as any).then((result: any) => {
70
+ if (result && result.stdout) {
71
+ result.sessionId = extractMistralSessionId(result.stdout);
72
+ }
73
+ return result;
74
+ });
75
+
76
+ return { invocation, resultPromise };
77
+ }
78
+
79
+ export {
80
+ buildMistralInvocation,
81
+ extractMistralSessionId,
82
+ resolveMistralCommand,
83
+ startMistralAgent
84
+ };