flowviant 0.125.5 → 0.125.7
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/README.md +4 -5
- package/bin/lib/agentInternalWait.mjs +1 -5
- package/bin/lib/agentPlan.mjs +13 -163
- package/bin/lib/agentRepositories.mjs +3 -1
- package/bin/lib/claudeInput.mjs +89 -10
- package/bin/lib/claudeMachineMcp.mjs +85 -0
- package/bin/lib/claudePersonal.mjs +6 -57
- package/bin/lib/claudePosture.mjs +6 -15
- package/bin/lib/claudeSettingsArg.mjs +67 -0
- package/bin/lib/claudeStream.mjs +30 -1
- package/bin/lib/claudeSubagentProgress.mjs +12 -28
- package/bin/lib/claudeSubagents.mjs +4 -2
- package/bin/lib/codexContract.mjs +52 -0
- package/bin/lib/codexFencedMount.mjs +275 -0
- package/bin/lib/codexPersonal.mjs +1 -265
- package/bin/lib/codexProjectRead.mjs +5 -4
- package/bin/lib/codexResearch.mjs +9 -3
- package/bin/lib/codexSubagentEvents.mjs +11 -7
- package/bin/lib/codexSubagents.mjs +55 -52
- package/bin/lib/codexToml.mjs +328 -0
- package/bin/lib/config.mjs +22 -1
- package/bin/lib/conversationIds.mjs +10 -0
- package/bin/lib/fleet.mjs +2 -2
- package/bin/lib/git.mjs +32 -0
- package/bin/lib/heldTool.mjs +49 -0
- package/bin/lib/hooks/mcpAllow.mjs +11 -5
- package/bin/lib/hooks/orchestrator.mjs +20 -10
- package/bin/lib/intakeDraft.mjs +1 -1
- package/bin/lib/jsonlTail.mjs +52 -0
- package/bin/lib/mcpInventory.mjs +11 -6
- package/bin/lib/mcpReasons.mjs +26 -0
- package/bin/lib/mcpReportBounds.mjs +10 -0
- package/bin/lib/mcpStdio.mjs +35 -0
- package/bin/lib/mergeBatch.mjs +6 -24
- package/bin/lib/mergeOrder.mjs +18 -0
- package/bin/lib/modelJson.mjs +173 -0
- package/bin/lib/nativeAgentTools.mjs +72 -0
- package/bin/lib/orchestrator.mjs +221 -133
- package/bin/lib/orchestratorClient.mjs +17 -21
- package/bin/lib/orchestratorHooks.mjs +21 -4
- package/bin/lib/orchestratorNative.mjs +58 -0
- package/bin/lib/orchestratorPrompt.mjs +7 -4
- package/bin/lib/orchestratorProtocol.mjs +25 -3
- package/bin/lib/postureTools.mjs +58 -13
- package/bin/lib/prWorkflow.mjs +13 -2
- package/bin/lib/processGroup.mjs +41 -0
- package/bin/lib/projectRepositories.mjs +2 -2
- package/bin/lib/projectToolRuntimes.mjs +92 -245
- package/bin/lib/projectTools.mjs +60 -19
- package/bin/lib/prompts.mjs +47 -18
- package/bin/lib/reportJournal.mjs +61 -0
- package/bin/lib/repositoryDelivery.mjs +6 -4
- package/bin/lib/repositoryMerge.mjs +8 -12
- package/bin/lib/repositoryPlaces.mjs +2 -2
- package/bin/lib/repositoryPrompts.mjs +42 -0
- package/bin/lib/repositoryTool.mjs +9 -32
- package/bin/lib/researchUse.mjs +39 -0
- package/bin/lib/resumeLost.mjs +21 -1
- package/bin/lib/resumeMarker.mjs +43 -0
- package/bin/lib/runTurn.mjs +55 -52
- package/bin/lib/runtimeAntigravity.mjs +1 -1
- package/bin/lib/runtimeCapabilities.mjs +13 -18
- package/bin/lib/runtimeClaude.mjs +68 -24
- package/bin/lib/runtimeCodex.mjs +12 -6
- package/bin/lib/runtimeEvents.mjs +13 -2
- package/bin/lib/runtimeOfferings.mjs +12 -5
- package/bin/lib/runtimes.mjs +3 -1
- package/bin/lib/startProject.mjs +10 -0
- package/bin/lib/subagentBounds.mjs +20 -0
- package/bin/lib/trace.mjs +30 -5
- package/bin/lib/turnDeclaration.mjs +1 -1
- package/bin/lib/turnDiagnostics.mjs +9 -12
- package/bin/lib/turnReceipts.mjs +64 -0
- package/bin/lib/turnSubagents.mjs +91 -0
- package/bin/lib/turnUsage.mjs +9 -6
- package/bin/lib/update.mjs +18 -4
- package/bin/lib/versionOrder.mjs +11 -1
- package/bin/lib/work.mjs +9 -9
- package/bin/lib/workAgentMerges.mjs +5 -7
- package/bin/lib/workAgentPrecheck.mjs +5 -5
- package/bin/lib/workAgentRecovery.mjs +2 -1
- package/bin/lib/workAgentResume.mjs +121 -0
- package/bin/lib/workAgentTools.mjs +111 -0
- package/bin/lib/workAgentTurnExecution.mjs +82 -246
- package/bin/lib/workAgentTurnOutcome.mjs +30 -3
- package/bin/lib/workAgentTurnReports.mjs +16 -0
- package/bin/lib/workAgentTurnShape.mjs +90 -0
- package/bin/lib/workAgentTurns.mjs +28 -12
- package/bin/lib/workArtifactReconcile.mjs +3 -2
- package/bin/lib/workAttachments.mjs +27 -5
- package/bin/lib/workMergeBatch.mjs +7 -5
- package/bin/lib/workNarration.mjs +32 -0
- package/bin/lib/workPlaceReports.mjs +3 -3
- package/bin/lib/workPlaces.mjs +14 -0
- package/bin/lib/workReportQueue.mjs +18 -1
- package/bin/lib/workSessionRuntime.mjs +2 -5
- package/bin/lib/workSessionTurns.mjs +68 -80
- package/bin/lib/workShip.mjs +8 -18
- package/bin/lib/workTurnShape.mjs +6 -4
- package/bin/lib/worktreeDiff.mjs +29 -5
- package/package.json +1 -1
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
*/
|
|
57
57
|
import { readFileSync, statSync } from 'node:fs';
|
|
58
58
|
import { join } from 'node:path';
|
|
59
|
-
import {
|
|
59
|
+
import { isTable, newTable, parseCodexToml, tomlValue } from './codexToml.mjs';
|
|
60
60
|
import { codexHome } from './codexHome.mjs';
|
|
61
61
|
|
|
62
62
|
/** Where the login lives and which endpoint it signs into — top-level keys,
|
|
@@ -104,259 +104,6 @@ export const CODEX_HEADER_ENV_PREFIX = 'FLOWVIANT_CODEX_SECRET_HEADER_';
|
|
|
104
104
|
|
|
105
105
|
/** Far past any real config.toml; a bigger one is not read. */
|
|
106
106
|
const CONFIG_MAX_BYTES = 1024 * 1024;
|
|
107
|
-
/** Arrays and inline tables nest no deeper than this in a file we read. */
|
|
108
|
-
const MAX_DEPTH = 32;
|
|
109
|
-
|
|
110
|
-
/** A number, date or time, kept as written so it is carried as written. */
|
|
111
|
-
export class TomlRaw {
|
|
112
|
-
constructor(text) {
|
|
113
|
-
this.text = text;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
const newTable = () => Object.create(null);
|
|
117
|
-
const isTable = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof TomlRaw);
|
|
118
|
-
|
|
119
|
-
const BARE = /[A-Za-z0-9_-]+/y;
|
|
120
|
-
// First match wins, so the forms that begin like an integer come first.
|
|
121
|
-
const SCALAR =
|
|
122
|
-
/[+-]?(?:inf|nan)|0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*|\d{4}-\d{2}-\d{2}(?:[Tt ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:[Zz]|[+-]\d{2}:\d{2})?)?|\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?|[+-]?\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?/y;
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* A BOUNDED TOML READER — the repo carries no TOML library, and the one file
|
|
126
|
-
* it reads is Codex's own config. Whole documents: tables, arrays of tables
|
|
127
|
-
* (the daemon's own `[[skills.config]]` in an agent's home), dotted and quoted
|
|
128
|
-
* keys, the four string forms, arrays, inline tables, booleans, and numbers,
|
|
129
|
-
* dates and times kept as written. Anything else throws, and the caller
|
|
130
|
-
* carries nothing — a partial read is never a license to guess.
|
|
131
|
-
*/
|
|
132
|
-
export function parseCodexToml(text) {
|
|
133
|
-
const src = String(text).replace(/^/, '');
|
|
134
|
-
const n = src.length;
|
|
135
|
-
let i = 0;
|
|
136
|
-
const fail = (why) => {
|
|
137
|
-
throw new Error(`config.toml: ${why} at offset ${i}`);
|
|
138
|
-
};
|
|
139
|
-
const ws = () => {
|
|
140
|
-
while (i < n && (src[i] === ' ' || src[i] === '\t')) i++;
|
|
141
|
-
};
|
|
142
|
-
const comment = () => {
|
|
143
|
-
if (src[i] === '#') while (i < n && src[i] !== '\n') i++;
|
|
144
|
-
};
|
|
145
|
-
const newline = () => {
|
|
146
|
-
if (src[i] === '\n') return (i += 1), true;
|
|
147
|
-
if (src[i] === '\r' && src[i + 1] === '\n') return (i += 2), true;
|
|
148
|
-
return false;
|
|
149
|
-
};
|
|
150
|
-
/** Blank lines, comments and whitespace, as between array items. */
|
|
151
|
-
const gap = () => {
|
|
152
|
-
for (;;) {
|
|
153
|
-
ws();
|
|
154
|
-
comment();
|
|
155
|
-
if (!newline()) return;
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
const endOfLine = () => {
|
|
159
|
-
ws();
|
|
160
|
-
comment();
|
|
161
|
-
if (i < n && !newline()) fail('expected the end of the line');
|
|
162
|
-
};
|
|
163
|
-
const hexChar = (len) => {
|
|
164
|
-
const h = src.slice(i, i + len);
|
|
165
|
-
if (h.length !== len || !/^[0-9A-Fa-f]+$/.test(h)) fail('bad escape');
|
|
166
|
-
i += len;
|
|
167
|
-
const cp = parseInt(h, 16);
|
|
168
|
-
if (cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff)) fail('bad escape');
|
|
169
|
-
return String.fromCodePoint(cp);
|
|
170
|
-
};
|
|
171
|
-
const escape = () => {
|
|
172
|
-
const e = src[i + 1];
|
|
173
|
-
i += 2;
|
|
174
|
-
switch (e) {
|
|
175
|
-
case 'b': return '\b';
|
|
176
|
-
case 't': return '\t';
|
|
177
|
-
case 'n': return '\n';
|
|
178
|
-
case 'f': return '\f';
|
|
179
|
-
case 'r': return '\r';
|
|
180
|
-
case 'e': return '\x1b';
|
|
181
|
-
case '"': return '"';
|
|
182
|
-
case '\\': return '\\';
|
|
183
|
-
case 'x': return hexChar(2);
|
|
184
|
-
case 'u': return hexChar(4);
|
|
185
|
-
case 'U': return hexChar(8);
|
|
186
|
-
default: return fail('bad escape');
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
const basicString = () => {
|
|
190
|
-
i += 1;
|
|
191
|
-
let s = '';
|
|
192
|
-
for (;;) {
|
|
193
|
-
if (i >= n) fail('unterminated string');
|
|
194
|
-
const ch = src[i];
|
|
195
|
-
if (ch === '"') return (i += 1), s;
|
|
196
|
-
if (ch === '\n' || ch === '\r') fail('newline in a string');
|
|
197
|
-
if (ch === '\\') s += escape();
|
|
198
|
-
else (s += ch), (i += 1);
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
const literalString = () => {
|
|
202
|
-
const end = src.indexOf("'", i + 1);
|
|
203
|
-
if (end < 0) fail('unterminated string');
|
|
204
|
-
const s = src.slice(i + 1, end);
|
|
205
|
-
if (/[\r\n]/.test(s)) fail('newline in a string');
|
|
206
|
-
i = end + 1;
|
|
207
|
-
return s;
|
|
208
|
-
};
|
|
209
|
-
const LINE_END_BACKSLASH = /\\[ \t]*\r?\n/y;
|
|
210
|
-
const multiline = (quote, escapes) => {
|
|
211
|
-
i += 3;
|
|
212
|
-
newline(); // a newline straight after the opening quotes is not content
|
|
213
|
-
let s = '';
|
|
214
|
-
for (;;) {
|
|
215
|
-
if (i >= n) fail('unterminated string');
|
|
216
|
-
if (src.startsWith(quote.repeat(3), i)) {
|
|
217
|
-
let q = 3;
|
|
218
|
-
while (q < 5 && src[i + q] === quote) q++; // up to two belong to the content
|
|
219
|
-
s += quote.repeat(q - 3);
|
|
220
|
-
i += q;
|
|
221
|
-
return s;
|
|
222
|
-
}
|
|
223
|
-
if (escapes && src[i] === '\\') {
|
|
224
|
-
LINE_END_BACKSLASH.lastIndex = i;
|
|
225
|
-
if (LINE_END_BACKSLASH.test(src)) {
|
|
226
|
-
i = LINE_END_BACKSLASH.lastIndex;
|
|
227
|
-
while (i < n && /[ \t\r\n]/.test(src[i])) i++;
|
|
228
|
-
} else s += escape();
|
|
229
|
-
} else (s += src[i]), (i += 1);
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
const key = () => {
|
|
233
|
-
const parts = [];
|
|
234
|
-
for (;;) {
|
|
235
|
-
ws();
|
|
236
|
-
if (src[i] === '"') {
|
|
237
|
-
if (src.startsWith('"""', i)) fail('a multi-line key');
|
|
238
|
-
parts.push(basicString());
|
|
239
|
-
} else if (src[i] === "'") {
|
|
240
|
-
if (src.startsWith("'''", i)) fail('a multi-line key');
|
|
241
|
-
parts.push(literalString());
|
|
242
|
-
} else {
|
|
243
|
-
BARE.lastIndex = i;
|
|
244
|
-
const m = BARE.exec(src);
|
|
245
|
-
if (!m) fail('expected a key');
|
|
246
|
-
parts.push(m[0]);
|
|
247
|
-
i = BARE.lastIndex;
|
|
248
|
-
}
|
|
249
|
-
ws();
|
|
250
|
-
if (src[i] !== '.') return parts;
|
|
251
|
-
i += 1;
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
const assign = (target, path, v) => {
|
|
255
|
-
let t = target;
|
|
256
|
-
for (const k of path.slice(0, -1)) {
|
|
257
|
-
if (!Object.hasOwn(t, k)) t[k] = newTable();
|
|
258
|
-
else if (!isTable(t[k])) fail(`"${k}" is not a table`);
|
|
259
|
-
t = t[k];
|
|
260
|
-
}
|
|
261
|
-
const last = path.at(-1);
|
|
262
|
-
if (Object.hasOwn(t, last)) fail(`"${last}" is defined twice`);
|
|
263
|
-
t[last] = v;
|
|
264
|
-
};
|
|
265
|
-
let depth = 0;
|
|
266
|
-
const nested = (open) => {
|
|
267
|
-
if (++depth > MAX_DEPTH) fail('nested too deep');
|
|
268
|
-
try {
|
|
269
|
-
return open();
|
|
270
|
-
} finally {
|
|
271
|
-
depth -= 1;
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
const array = () => {
|
|
275
|
-
i += 1;
|
|
276
|
-
const out = [];
|
|
277
|
-
for (;;) {
|
|
278
|
-
gap();
|
|
279
|
-
if (src[i] === ']') return (i += 1), out;
|
|
280
|
-
out.push(value());
|
|
281
|
-
gap();
|
|
282
|
-
if (src[i] === ',') i += 1;
|
|
283
|
-
else if (src[i] === ']') return (i += 1), out;
|
|
284
|
-
else fail('expected , or ]');
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
const inlineTable = () => {
|
|
288
|
-
i += 1;
|
|
289
|
-
const t = newTable();
|
|
290
|
-
for (;;) {
|
|
291
|
-
gap();
|
|
292
|
-
if (src[i] === '}') return (i += 1), t;
|
|
293
|
-
keyValue(t);
|
|
294
|
-
gap();
|
|
295
|
-
if (src[i] === ',') i += 1;
|
|
296
|
-
else if (src[i] === '}') return (i += 1), t;
|
|
297
|
-
else fail('expected , or }');
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
const word = (w) => src.startsWith(w, i) && !/[A-Za-z0-9_-]/.test(src[i + w.length] ?? '');
|
|
301
|
-
const value = () => {
|
|
302
|
-
const ch = src[i];
|
|
303
|
-
if (ch === '"') return src.startsWith('"""', i) ? multiline('"', true) : basicString();
|
|
304
|
-
if (ch === "'") return src.startsWith("'''", i) ? multiline("'", false) : literalString();
|
|
305
|
-
if (ch === '[') return nested(array);
|
|
306
|
-
if (ch === '{') return nested(inlineTable);
|
|
307
|
-
if (word('true')) return (i += 4), true;
|
|
308
|
-
if (word('false')) return (i += 5), false;
|
|
309
|
-
SCALAR.lastIndex = i;
|
|
310
|
-
const m = SCALAR.exec(src);
|
|
311
|
-
if (!m) fail('a value this reader does not know');
|
|
312
|
-
i = SCALAR.lastIndex;
|
|
313
|
-
if (/[A-Za-z0-9_.:+-]/.test(src[i] ?? '')) fail('a value this reader does not know');
|
|
314
|
-
return new TomlRaw(m[0]);
|
|
315
|
-
};
|
|
316
|
-
const keyValue = (target) => {
|
|
317
|
-
const path = key();
|
|
318
|
-
if (src[i] !== '=') fail('expected =');
|
|
319
|
-
i += 1;
|
|
320
|
-
ws();
|
|
321
|
-
assign(target, path, value());
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
const root = newTable();
|
|
325
|
-
let current = root;
|
|
326
|
-
for (;;) {
|
|
327
|
-
gap();
|
|
328
|
-
if (i >= n) return root;
|
|
329
|
-
if (src[i] !== '[') {
|
|
330
|
-
keyValue(current);
|
|
331
|
-
endOfLine();
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
const many = src[i + 1] === '[';
|
|
335
|
-
i += many ? 2 : 1;
|
|
336
|
-
const path = key();
|
|
337
|
-
if (many ? !src.startsWith(']]', i) : src[i] !== ']') fail('an unclosed table header');
|
|
338
|
-
i += many ? 2 : 1;
|
|
339
|
-
endOfLine();
|
|
340
|
-
let t = root;
|
|
341
|
-
for (const k of path.slice(0, -1)) {
|
|
342
|
-
if (!Object.hasOwn(t, k)) t[k] = newTable();
|
|
343
|
-
const v = Array.isArray(t[k]) ? t[k].at(-1) : t[k];
|
|
344
|
-
if (!isTable(v)) fail(`"${k}" is not a table`);
|
|
345
|
-
t = v;
|
|
346
|
-
}
|
|
347
|
-
const last = path.at(-1);
|
|
348
|
-
if (many) {
|
|
349
|
-
if (!Object.hasOwn(t, last)) t[last] = [];
|
|
350
|
-
if (!Array.isArray(t[last])) fail(`"${last}" is not an array of tables`);
|
|
351
|
-
current = newTable();
|
|
352
|
-
t[last].push(current);
|
|
353
|
-
} else {
|
|
354
|
-
if (!Object.hasOwn(t, last)) t[last] = newTable();
|
|
355
|
-
else if (!isTable(t[last])) fail(`"${last}" is not a table`);
|
|
356
|
-
current = t[last];
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
107
|
|
|
361
108
|
/**
|
|
362
109
|
* The person's config.toml, where the turn's Codex would look —
|
|
@@ -374,17 +121,6 @@ export function readPersonalCodexConfig(env = process.env) {
|
|
|
374
121
|
}
|
|
375
122
|
}
|
|
376
123
|
|
|
377
|
-
/** One value as TOML, as a `-c` override's value is parsed. */
|
|
378
|
-
const bareKeyOr = (k) => (/^[A-Za-z0-9_-]+$/.test(k) ? k : toml(k));
|
|
379
|
-
export function tomlValue(v) {
|
|
380
|
-
if (typeof v === 'string') return toml(v);
|
|
381
|
-
if (typeof v === 'boolean') return String(v);
|
|
382
|
-
if (v instanceof TomlRaw) return v.text;
|
|
383
|
-
if (Array.isArray(v)) return `[${v.map(tomlValue).join(',')}]`;
|
|
384
|
-
if (isTable(v)) return `{${Object.keys(v).map((k) => `${bareKeyOr(k)}=${tomlValue(v[k])}`).join(',')}}`;
|
|
385
|
-
throw new Error('not a TOML value');
|
|
386
|
-
}
|
|
387
|
-
|
|
388
124
|
const isString = (v) => typeof v === 'string';
|
|
389
125
|
const isStringList = (v) => Array.isArray(v) && v.every(isString);
|
|
390
126
|
const nonEmpty = (v) => typeof v === 'string' && v.trim() !== '';
|
|
@@ -6,16 +6,17 @@ import { execFileSync } from 'node:child_process';
|
|
|
6
6
|
import { lstatSync, readdirSync, realpathSync } from 'node:fs';
|
|
7
7
|
import { isAbsolute, join, dirname } from 'node:path';
|
|
8
8
|
import { git } from './git.mjs';
|
|
9
|
-
import { codexResearchHelper } from './codexResearch.mjs';
|
|
9
|
+
import { CODEX_HELPER_MIN, codexResearchHelper } from './codexResearch.mjs';
|
|
10
|
+
import { versionAtLeast } from './versionOrder.mjs';
|
|
10
11
|
import { unattendedEnv } from './noWindowEnv.mjs';
|
|
11
12
|
|
|
12
13
|
export function projectReadBoundary({ cwd, knowledgeDir, env = process.env }) {
|
|
13
14
|
if (process.platform !== 'linux') throw new Error('Project-scoped Codex reads require Linux or WSL.');
|
|
14
15
|
if (!isAbsolute(cwd ?? '')) throw new Error('Project-scoped Codex reads require an absolute project directory.');
|
|
15
16
|
const helper = codexResearchHelper(env);
|
|
16
|
-
const version = execFileSync(helper, ['--version'], { encoding: 'utf8', timeout: 10000, env: unattendedEnv(env) }).match(/^codex-cli (\d
|
|
17
|
-
if (!version || version
|
|
18
|
-
throw new Error(
|
|
17
|
+
const version = execFileSync(helper, ['--version'], { encoding: 'utf8', timeout: 10000, env: unattendedEnv(env) }).match(/^codex-cli (\d+\.\d+\.\d+)/);
|
|
18
|
+
if (!version || !versionAtLeast(version[1], CODEX_HELPER_MIN))
|
|
19
|
+
throw new Error(`Project-scoped reads need Codex CLI ${CODEX_HELPER_MIN} or newer. Update Codex before retrying.`);
|
|
19
20
|
const project = realpathSync(cwd);
|
|
20
21
|
// Resolve both: linked worktrees have their own HEAD/index and shared
|
|
21
22
|
// objects/refs in the common directory. Never grant the parent checkout.
|
|
@@ -10,6 +10,7 @@ import { ensureRealSubdir, realDirState } from './ownedDirs.mjs';
|
|
|
10
10
|
import { closeSync, constants, copyFileSync, existsSync, fstatSync, openSync, readFileSync, readSync, lstatSync, mkdirSync, readdirSync, realpathSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
|
+
import { versionAtLeast } from './versionOrder.mjs';
|
|
13
14
|
|
|
14
15
|
const MAX_FILES = 20_000;
|
|
15
16
|
const MAX_BYTES = 128 * 1024 * 1024;
|
|
@@ -51,6 +52,11 @@ export function copyResearchView(source, destination, budget = { files: 0, bytes
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/** The helper both the research fence and project-scoped reads need
|
|
56
|
+
* (codexProjectRead.mjs): one floor, so neither runs on a Codex the other
|
|
57
|
+
* refuses. */
|
|
58
|
+
export const CODEX_HELPER_MIN = '0.159.2';
|
|
59
|
+
|
|
54
60
|
/** Resolve the actual sandbox helper without granting the person's bin/home.
|
|
55
61
|
* Support native installs and the npm wrapper's platform package. Unknown
|
|
56
62
|
* installations refuse; granting the whole home to find a helper is no fence.
|
|
@@ -147,9 +153,9 @@ export async function runIsolatedResearch(opts, run, {
|
|
|
147
153
|
if (process.platform !== 'linux') throw new Error('Isolated Codex research requires Linux or WSL.');
|
|
148
154
|
const original = realpathSync(opts.cwd);
|
|
149
155
|
const helper = resolveHelper();
|
|
150
|
-
const version = readVersion(helper).match(/codex-cli (\d
|
|
151
|
-
if (!version || version
|
|
152
|
-
throw new Error(
|
|
156
|
+
const version = readVersion(helper).match(/codex-cli (\d+\.\d+\.\d+)/);
|
|
157
|
+
if (!version || !versionAtLeast(version[1], CODEX_HELPER_MIN))
|
|
158
|
+
throw new Error(`Isolated research needs Codex CLI ${CODEX_HELPER_MIN} or newer. Restart after updating Codex.`);
|
|
153
159
|
// Both publication parents must be real directories in this checkout, or
|
|
154
160
|
// absent — never a link, dangling or not (ownedDirs.mjs).
|
|
155
161
|
for (const dir of [join(original, '.flowviant'), join(original, '.flowviant/artifacts')])
|
|
@@ -1,24 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { CODEX_CHILD_THREAD_RE } from './conversationIds.mjs';
|
|
2
|
+
import { NATIVE_AGENT_TOOLS } from './nativeAgentTools.mjs';
|
|
3
|
+
import { SUBAGENT_CAP } from './subagentBounds.mjs';
|
|
4
|
+
|
|
3
5
|
const snake = s => String(s ?? '').replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
|
|
6
|
+
// A collab item as exec (`collab_tool_call`) or a rollout (`CollabAgentToolCall`) spells it.
|
|
7
|
+
const isCollabItem = item => ['collab_tool_call', 'CollabAgentToolCall'].includes(item?.type ?? item?.item_type);
|
|
4
8
|
export const statusOf = (s) => ({ pending_init: 'waiting', running: 'running', completed: 'done',
|
|
5
9
|
interrupted: 'stopped', errored: 'stopped', shutdown: 'stopped', not_found: 'stopped',
|
|
6
10
|
waiting: 'waiting', asked: 'asked' })[snake(typeof s === 'string' ? s : s && Object.keys(s)[0])];
|
|
7
11
|
|
|
8
12
|
export function codexSpawnResult(item) {
|
|
9
|
-
if (!
|
|
13
|
+
if (!isCollabItem(item) || snake(item.tool) !== 'spawn_agent' || !item.id) return null;
|
|
10
14
|
if (['failed', 'errored'].includes(snake(item.status))) return { callId: item.id, success: false };
|
|
11
15
|
const nativeId = item.receiver_thread_ids?.[0];
|
|
12
16
|
return nativeId ? { callId: item.id, nativeId, success: true } : null;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export function codexCollabUpdates(item) {
|
|
16
|
-
if (!
|
|
20
|
+
if (!isCollabItem(item)) return [];
|
|
17
21
|
const tool = snake(item.tool);
|
|
18
22
|
const states = item.agents_states ?? {};
|
|
19
|
-
const ids = [...new Set([...(item.receiver_thread_ids ?? []), ...Object.keys(states)])].slice(0,
|
|
23
|
+
const ids = [...new Set([...(item.receiver_thread_ids ?? []), ...Object.keys(states)])].slice(0, SUBAGENT_CAP);
|
|
20
24
|
return ids.flatMap(id => {
|
|
21
|
-
if (typeof id !== 'string' || !
|
|
25
|
+
if (typeof id !== 'string' || !CODEX_CHILD_THREAD_RE.test(id)) return [];
|
|
22
26
|
const state = states[id];
|
|
23
27
|
const receiver = item.receiver_agents?.find(a => a.thread_id === id);
|
|
24
28
|
const status = statusOf(state?.status ?? state);
|
|
@@ -27,7 +31,7 @@ export function codexCollabUpdates(item) {
|
|
|
27
31
|
...(receiver?.agent_path || receiver?.agent_nickname || item.title ? { title: receiver?.agent_path ?? receiver?.agent_nickname ?? item.title } : {}),
|
|
28
32
|
...(receiver?.agent_role || item.agent_type ? { kind: receiver?.agent_role ?? item.agent_type } : {}),
|
|
29
33
|
} : {}),
|
|
30
|
-
...(status ? { status } :
|
|
34
|
+
...(status ? { status } : NATIVE_AGENT_TOOLS.codex.stop.includes(tool) ? { status: 'stopped' } : {}),
|
|
31
35
|
...(typeof message === 'string' ? { now: message, message } : {}),
|
|
32
36
|
}];
|
|
33
37
|
});
|
|
@@ -3,15 +3,22 @@
|
|
|
3
3
|
* rollouts named by this turn's native spawn receipts, never a global latest.
|
|
4
4
|
* rust-v0.160.0: exec_events.rs CollabToolCallItem; core/agent/child_config.rs
|
|
5
5
|
* reapplies the live turn's approval and permission profile after role config.
|
|
6
|
+
* V2 rollout shapes measured on codex-cli 0.163.0 (codexSubagents.test.mjs).
|
|
6
7
|
*/
|
|
7
8
|
import { open } from 'node:fs/promises';
|
|
8
9
|
import { codexRolloutFile } from './runtimeLimits.mjs';
|
|
10
|
+
import { CODEX_CHILD_THREAD_RE } from './conversationIds.mjs';
|
|
11
|
+
import { followJsonl } from './jsonlTail.mjs';
|
|
9
12
|
import { codexCollabUpdates, codexSpawnResult, statusOf } from './codexSubagentEvents.mjs';
|
|
13
|
+
import { NATIVE_AGENT_TOOLS } from './nativeAgentTools.mjs';
|
|
14
|
+
import { SUBAGENT_CAP } from './subagentBounds.mjs';
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
16
|
+
const CODEX = NATIVE_AGENT_TOOLS.codex;
|
|
17
|
+
// A rollout shows a superset of what the gate sees: send_message and the
|
|
18
|
+
// stop tools too.
|
|
19
|
+
const COLLAB_CALLS = [...CODEX.spawn, ...CODEX.followup, ...CODEX.message, ...CODEX.stop];
|
|
20
|
+
// The calls whose output names a child's receipt.
|
|
21
|
+
const RECEIPT_CALLS = [...CODEX.spawn, ...CODEX.followup];
|
|
15
22
|
|
|
16
23
|
export function createCodexSubagentReader({ since = Date.now(), env = process.env, onSubagent, onActivity, onToolEvent, onSpawnResult,
|
|
17
24
|
findFile = codexRolloutFile } = {}) {
|
|
@@ -24,8 +31,9 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
24
31
|
let stopped = false;
|
|
25
32
|
let chain = Promise.resolve();
|
|
26
33
|
const report = update => {
|
|
27
|
-
if (!update?.id || !
|
|
28
|
-
|
|
34
|
+
if (!update?.id || !CODEX_CHILD_THREAD_RE.test(update.id)) return;
|
|
35
|
+
// The root thread plus the run's children.
|
|
36
|
+
if (!readers.has(update.id) && readers.size >= SUBAGENT_CAP + 1) return;
|
|
29
37
|
if (!readers.has(update.id))
|
|
30
38
|
readers.set(update.id, { offset: null, rest: Buffer.alloc(0), seen: new Set(), live: false });
|
|
31
39
|
const { message, ...record } = update;
|
|
@@ -35,7 +43,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
35
43
|
targets.set(path, id);
|
|
36
44
|
if (targets.size > 128) targets.delete(targets.keys().next().value);
|
|
37
45
|
};
|
|
38
|
-
const targetId = value => targets.get(value) ?? (
|
|
46
|
+
const targetId = value => targets.get(value) ?? (CODEX_CHILD_THREAD_RE.test(value ?? '') ? value : null);
|
|
39
47
|
|
|
40
48
|
function observe(ev, owner) {
|
|
41
49
|
const at = Date.parse(ev?.timestamp ?? '');
|
|
@@ -68,40 +76,52 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
68
76
|
if (command) step(p.call_id, Array.isArray(command) ? command.join(' ') : command);
|
|
69
77
|
return;
|
|
70
78
|
}
|
|
71
|
-
if (owner !== root && name
|
|
79
|
+
if (owner !== root && CODEX.ask.includes(name)) {
|
|
72
80
|
questions.set(p.call_id, owner);
|
|
73
81
|
if (questions.size > 128) questions.delete(questions.keys().next().value);
|
|
74
82
|
report({ id: owner, status: 'asked' }); return;
|
|
75
83
|
}
|
|
76
|
-
if (!
|
|
84
|
+
if (!COLLAB_CALLS.includes(name)) return;
|
|
77
85
|
let args;
|
|
78
86
|
try { args = JSON.parse(p.arguments); } catch { return; }
|
|
79
87
|
calls.set(p.call_id, { name, args });
|
|
80
88
|
if (calls.size > 128) calls.delete(calls.keys().next().value);
|
|
81
89
|
const id = targetId(args.target ?? args.id);
|
|
82
90
|
if (id && name !== 'spawn_agent') {
|
|
83
|
-
const stopped = name
|
|
91
|
+
const stopped = CODEX.stop.includes(name);
|
|
84
92
|
report({ id, ...(stopped ? { status: 'stopped' } : name === 'followup_task' ? { status: 'running' } : {}) });
|
|
85
93
|
}
|
|
86
94
|
}
|
|
87
95
|
if (ev.type === 'response_item' && p.type === 'function_call_output') {
|
|
88
96
|
if (questions.has(p.call_id)) { report({ id: questions.get(p.call_id), status: 'running' }); questions.delete(p.call_id); }
|
|
89
97
|
const call = calls.get(p.call_id);
|
|
90
|
-
if (!
|
|
91
|
-
let output;
|
|
98
|
+
if (!RECEIPT_CALLS.includes(call?.name)) return;
|
|
99
|
+
let output = null;
|
|
92
100
|
try {
|
|
93
101
|
const body = Array.isArray(p.output) ? p.output.map(b => b.text ?? '').join('') : p.output;
|
|
94
102
|
output = typeof body === 'string' ? JSON.parse(body) : body;
|
|
95
|
-
} catch {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
} catch { /* the tool's own words */ }
|
|
104
|
+
// ONLY AN EXPLICIT FAILURE FREES A SLOT (2026-10-17). These tools answer
|
|
105
|
+
// JSON on success (their output schemas), so plain text is the native
|
|
106
|
+
// error ("collab spawn failed: …"); V2 followup_task declares no output,
|
|
107
|
+
// and a JSON answer naming no receipt is unknown. Neither is a failure.
|
|
108
|
+
if (!output || typeof output !== 'object') {
|
|
109
|
+
if (call.name !== 'followup_task') onSpawnResult?.({ callId: p.call_id, success: false });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// V2 (measured on 0.163) answers only the canonical task path; the
|
|
113
|
+
// thread arrived first, on the spawn's SubAgentActivity.
|
|
114
|
+
const path = output.task_name ?? output.agent_path;
|
|
115
|
+
const id = output.agent_id ?? output.thread_id ?? (typeof path === 'string' ? targets.get(path) : undefined);
|
|
116
|
+
const receipt = call.name === 'spawn_agent' ? output.agent_id ?? output.thread_id ?? path
|
|
117
|
+
: output.submission_id || output.status ? call.args.target ?? call.args.id : null;
|
|
118
|
+
if (receipt) onSpawnResult?.({ callId: p.call_id, success: true, nativeId: receipt });
|
|
99
119
|
if (call.name !== 'spawn_agent') return;
|
|
100
120
|
if (!id) return;
|
|
101
121
|
const args = call.args;
|
|
102
122
|
const title = args.task_name ?? args.description ?? output.agent_nickname ?? '';
|
|
103
123
|
if (args.task_name) rememberTarget(args.task_name, id);
|
|
104
|
-
if (
|
|
124
|
+
if (typeof path === 'string') rememberTarget(path, id);
|
|
105
125
|
report({ id, kind: args.agent_type ?? '', title, brief: args.message ?? '', status: 'running', now: '' });
|
|
106
126
|
return;
|
|
107
127
|
}
|
|
@@ -112,7 +132,13 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
112
132
|
if (ev.type !== 'event_msg') return;
|
|
113
133
|
const activityState = item => {
|
|
114
134
|
const kind = String(item.kind ?? '').toLowerCase();
|
|
115
|
-
if (item.agent_thread_id)
|
|
135
|
+
if (!item.agent_thread_id) return;
|
|
136
|
+
if (item.agent_path) rememberTarget(item.agent_path, item.agent_thread_id);
|
|
137
|
+
// A start carries the spawn's own call id (measured on 0.163): the
|
|
138
|
+
// controller binds the native thread to the record that call reserved.
|
|
139
|
+
const callId = kind === 'started' ? item.call_id ?? item.id : null;
|
|
140
|
+
report({ id: item.agent_thread_id, title: item.agent_path,
|
|
141
|
+
...(typeof callId === 'string' && callId ? { callId } : {}),
|
|
116
142
|
...(['started', 'completed', 'interrupted'].includes(kind) ? {
|
|
117
143
|
status: kind === 'completed' ? 'done' : kind === 'interrupted' ? 'stopped' : 'running',
|
|
118
144
|
} : {}),
|
|
@@ -163,41 +189,18 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
163
189
|
if (!file) return;
|
|
164
190
|
state.file = file;
|
|
165
191
|
fh = await open(file, 'r');
|
|
166
|
-
const { size } = await fh.stat();
|
|
167
192
|
const initial = state.offset === null;
|
|
168
193
|
const initialEvents = [];
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (!bytesRead) break;
|
|
180
|
-
state.offset += bytesRead;
|
|
181
|
-
const data = Buffer.concat([state.rest, buffer.subarray(0, bytesRead)]);
|
|
182
|
-
let start = 0, nl;
|
|
183
|
-
while ((nl = data.indexOf(10, start)) >= 0) {
|
|
184
|
-
const line = data.subarray(start, nl).toString('utf8');
|
|
185
|
-
start = nl + 1;
|
|
186
|
-
if (state.skip) { state.skip = false; continue; }
|
|
187
|
-
try {
|
|
188
|
-
const ev = JSON.parse(line);
|
|
189
|
-
if (initial && id !== root) initialEvents.push(ev);
|
|
190
|
-
else if (id === root || state.live || ev.type === 'session_meta') observe(ev, id);
|
|
191
|
-
else if (ev.type === 'event_msg' && ['task_started', 'turn_started'].includes(ev.payload?.type)) {
|
|
192
|
-
state.live = true; observe(ev, id);
|
|
193
|
-
}
|
|
194
|
-
} catch { /* non-JSON/unknown native events */ }
|
|
195
|
-
}
|
|
196
|
-
// Keep bytes across UTF-8 and partial-line boundaries. Oversized lines
|
|
197
|
-
// are discarded through their newline rather than reparsed as a suffix.
|
|
198
|
-
state.rest = data.subarray(start);
|
|
199
|
-
if (state.rest.length > READ_BYTES) { state.rest = Buffer.alloc(0); state.skip = true; }
|
|
200
|
-
}
|
|
194
|
+
await followJsonl(fh, state, line => {
|
|
195
|
+
try {
|
|
196
|
+
const ev = JSON.parse(line);
|
|
197
|
+
if (initial && id !== root) initialEvents.push(ev);
|
|
198
|
+
else if (id === root || state.live || ev.type === 'session_meta') observe(ev, id);
|
|
199
|
+
else if (ev.type === 'event_msg' && ['task_started', 'turn_started'].includes(ev.payload?.type)) {
|
|
200
|
+
state.live = true; observe(ev, id);
|
|
201
|
+
}
|
|
202
|
+
} catch { /* non-JSON/unknown native events */ }
|
|
203
|
+
});
|
|
201
204
|
if (initial && id !== root) {
|
|
202
205
|
// A fork copies the parent's old EventMsgs. Only the child's newest
|
|
203
206
|
// native turn belongs to this card; inherited history is never replayed.
|
|
@@ -219,7 +222,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
219
222
|
};
|
|
220
223
|
return {
|
|
221
224
|
thread(id) {
|
|
222
|
-
if (!
|
|
225
|
+
if (!CODEX_CHILD_THREAD_RE.test(id ?? '') || root) return;
|
|
223
226
|
root = id;
|
|
224
227
|
readers.set(id, { offset: null, rest: Buffer.alloc(0), seen: new Set(), live: false });
|
|
225
228
|
if (!stopped) { timer = setInterval(() => void flush(), 1_000); timer.unref?.(); }
|