flowviant 0.125.0 → 0.125.2
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/bin/cli.mjs +10 -0
- package/bin/lib/agentAsk.mjs +6 -12
- package/bin/lib/agentCards.mjs +5 -30
- package/bin/lib/agentFiles.mjs +1 -1
- package/bin/lib/agentFootprint.mjs +5 -5
- package/bin/lib/agentPlan.mjs +16 -27
- package/bin/lib/cardThread.mjs +1 -1
- package/bin/lib/claudeStream.mjs +18 -7
- package/bin/lib/claudeSubagentProgress.mjs +62 -0
- package/bin/lib/claudeSubagents.mjs +77 -0
- package/bin/lib/codexRelay.mjs +6 -22
- package/bin/lib/codexSubagentEvents.mjs +34 -0
- package/bin/lib/codexSubagents.mjs +232 -0
- package/bin/lib/hooks/orchestrator.mjs +22 -0
- package/bin/lib/orchestrator.mjs +295 -0
- package/bin/lib/orchestratorClient.mjs +30 -0
- package/bin/lib/orchestratorHooks.mjs +15 -0
- package/bin/lib/orchestratorPrompt.mjs +19 -0
- package/bin/lib/orchestratorProtocol.mjs +15 -0
- package/bin/lib/promptTransport.mjs +34 -0
- package/bin/lib/runTurn.mjs +87 -14
- package/bin/lib/runtimeClaude.mjs +3 -1
- package/bin/lib/runtimeCodex.mjs +32 -11
- package/bin/lib/runtimeEvents.mjs +12 -1
- package/bin/lib/trace.mjs +48 -10
- package/bin/lib/turnDiagnostics.mjs +13 -5
- package/bin/lib/work.mjs +2 -1
- package/bin/lib/workAgentTurnExecution.mjs +84 -7
- package/bin/lib/workAgentTurnOutcome.mjs +8 -8
- package/bin/lib/workAgentTurns.mjs +2 -0
- package/bin/lib/workChildren.mjs +4 -0
- package/bin/lib/workLiveMessages.mjs +7 -1
- package/bin/lib/workSessionTurns.mjs +3 -3
- package/package.json +2 -2
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Move complete prompts off argv's OS byte limits. Keep each adapter's
|
|
2
|
+
* assembled framing verbatim; stdin carries the very string argv held.
|
|
3
|
+
* Claude's separate system prompt uses its native file flag. */
|
|
4
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
export function promptTransport(runtime, original, stdinText = null) {
|
|
9
|
+
const args = [...original];
|
|
10
|
+
let text = null;
|
|
11
|
+
let dir = null;
|
|
12
|
+
try {
|
|
13
|
+
if (runtime === 'codex') {
|
|
14
|
+
text = stdinText ?? args[args.length - 1];
|
|
15
|
+
args[args.length - 1] = '-';
|
|
16
|
+
} else if (runtime === 'claude') {
|
|
17
|
+
if (!args.includes('--input-format')) {
|
|
18
|
+
const at = args.indexOf('-p');
|
|
19
|
+
text = args.splice(at + 1, 1)[0];
|
|
20
|
+
}
|
|
21
|
+
const at = args.indexOf('--append-system-prompt');
|
|
22
|
+
if (at !== -1) {
|
|
23
|
+
dir = mkdtempSync(join(tmpdir(), 'flowviant-system-'));
|
|
24
|
+
const file = join(dir, 'system.txt');
|
|
25
|
+
writeFileSync(file, args[at + 1], { mode: 0o600 });
|
|
26
|
+
args.splice(at, 2, '--append-system-prompt-file', file);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { args, text, cleanup: () => { if (dir) rmSync(dir, { recursive: true, force: true }); } };
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
package/bin/lib/runTurn.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* (runtimeEvents.mjs).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { promptTransport } from './promptTransport.mjs';
|
|
13
14
|
import { runIsolatedResearch } from './codexResearch.mjs';
|
|
14
15
|
import { spawn } from 'node:child_process';
|
|
15
16
|
import { runtimeById } from './runtimes.mjs';
|
|
@@ -24,6 +25,11 @@ import { codexTurnModel } from './turnModel.mjs';
|
|
|
24
25
|
import { printableText } from './printable.mjs';
|
|
25
26
|
import { startTurnDiagnostics } from './turnDiagnostics.mjs';
|
|
26
27
|
import { createClaudeInput } from './claudeInput.mjs';
|
|
28
|
+
import { createClaudeSubagents } from './claudeSubagents.mjs';
|
|
29
|
+
import { createCodexSubagentReader } from './codexSubagents.mjs';
|
|
30
|
+
import { measuredRuntimeVersion } from './runtimeDetection.mjs';
|
|
31
|
+
import { createClaudeSubagentProgress } from './claudeSubagentProgress.mjs';
|
|
32
|
+
import { codexSpawnResult } from './codexSubagentEvents.mjs';
|
|
27
33
|
|
|
28
34
|
/** How long a Codex turn's answer may wait on its rollout's model line. */
|
|
29
35
|
const CODEX_MODEL_READ_MS = 1_500;
|
|
@@ -84,10 +90,13 @@ function runPlainTurn(opts) {
|
|
|
84
90
|
* Both answers stay pending instead, as `process.exit` left them.
|
|
85
91
|
*/
|
|
86
92
|
if (workLanesStopped()) return never();
|
|
87
|
-
return new Promise((resolveTurn) => {
|
|
93
|
+
return new Promise((resolveTurn, rejectTurn) => {
|
|
88
94
|
const resolve = (v) => {
|
|
89
95
|
if (!workLanesStopped()) resolveTurn(v);
|
|
90
96
|
};
|
|
97
|
+
const reject = (error) => {
|
|
98
|
+
if (!workLanesStopped()) rejectTurn(error);
|
|
99
|
+
};
|
|
91
100
|
const rt = runtimeById(runtime);
|
|
92
101
|
// AN UNKNOWN RUNTIME IS REFUSED, NEVER RUN AS CLAUDE (2026-09-26). Asked
|
|
93
102
|
// before anything is spawned. This is the belt, not the relay: the words
|
|
@@ -167,8 +176,13 @@ function runPlainTurn(opts) {
|
|
|
167
176
|
// up by the SAME name (`claudePermFor`), so the two cannot disagree about
|
|
168
177
|
// which posture a turn is running under. There is no precedence chain left
|
|
169
178
|
// to get wrong: the caller named one profile (turnProfile.mjs).
|
|
179
|
+
const stdinText = rt.stdin?.({ prompt, system, profile: turnProfile.adapterProfile }) ?? null;
|
|
170
180
|
const args = rt.args({
|
|
171
|
-
|
|
181
|
+
cardTurn: opts.cardTurn === true,
|
|
182
|
+
orchestration: opts.orchestration === true,
|
|
183
|
+
codexVersion: measuredRuntimeVersion('codex')?.version,
|
|
184
|
+
|
|
185
|
+
streamInput: stdinText !== null || (rt.id === 'claude' && Boolean(opts.liveMessages)),
|
|
172
186
|
writableDirs: opts.writableDirs,
|
|
173
187
|
personal,
|
|
174
188
|
researchIsolation: opts.researchIsolation,
|
|
@@ -254,10 +268,15 @@ function runPlainTurn(opts) {
|
|
|
254
268
|
* so no throw on the way leaves that file behind: the sweep takes only a
|
|
255
269
|
* dead process's leftovers, and this one names the daemon's own pid.
|
|
256
270
|
*/
|
|
271
|
+
let transport;
|
|
272
|
+
try { transport = promptTransport(rt.id, args, stdinText); }
|
|
273
|
+
catch (error) { rt.releaseArgs?.(args); throw error; }
|
|
274
|
+
args.splice(0, args.length, ...transport.args);
|
|
257
275
|
let argsReleased = false;
|
|
258
276
|
const releaseArgs = () => {
|
|
259
277
|
if (argsReleased) return;
|
|
260
278
|
argsReleased = true;
|
|
279
|
+
transport.cleanup();
|
|
261
280
|
try {
|
|
262
281
|
rt.releaseArgs?.(args);
|
|
263
282
|
} catch {
|
|
@@ -277,10 +296,11 @@ function runPlainTurn(opts) {
|
|
|
277
296
|
browserHome: browser?.dir,
|
|
278
297
|
});
|
|
279
298
|
finishDiagnostics = startTurnDiagnostics({ runtime, bin: rt.bin, env: childEnv,
|
|
280
|
-
cwd, args,
|
|
299
|
+
cwd, args, stdinBytes: transport.text === null ? 0 : Buffer.byteLength(transport.text),
|
|
300
|
+
profile: turnProfile.name, planId: opts.diagnosticPlanId });
|
|
281
301
|
child = spawn(rt.bin, args, {
|
|
282
302
|
cwd,
|
|
283
|
-
stdio: [rt.id === 'claude' && opts.liveMessages ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
303
|
+
stdio: [transport.text !== null || (rt.id === 'claude' && opts.liveMessages) ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
284
304
|
/**
|
|
285
305
|
* ITS OWN PROCESS GROUP, so what the agent starts stays attributable.
|
|
286
306
|
*
|
|
@@ -322,11 +342,12 @@ function runPlainTurn(opts) {
|
|
|
322
342
|
child.once('close', browser.cleanup);
|
|
323
343
|
child.once('error', browser.cleanup);
|
|
324
344
|
}
|
|
325
|
-
onSpawn?.(child);
|
|
345
|
+
if (child.pid) onSpawn?.(child);
|
|
326
346
|
// When this turn began, for the Codex rollout read: a `turn_context` older
|
|
327
347
|
// than this is a previous turn's (turnModel.mjs).
|
|
328
348
|
const spawnedAt = Date.now();
|
|
329
349
|
let out = '';
|
|
350
|
+
let stdinError = null;
|
|
330
351
|
const input = rt.id === 'claude' && opts.liveMessages ? createClaudeInput(child, { onTaken: opts.liveMessages.onTaken }) : null;
|
|
331
352
|
const pfx = label ? `${label} ` : '';
|
|
332
353
|
/**
|
|
@@ -372,7 +393,25 @@ function runPlainTurn(opts) {
|
|
|
372
393
|
recordClaudeRateLimit(info);
|
|
373
394
|
onRateLimit?.(info);
|
|
374
395
|
};
|
|
396
|
+
const agentStates = new Map();
|
|
397
|
+
const reportSubagent = (update) => {
|
|
398
|
+
if (!update?.id || (!agentStates.has(update.id) && agentStates.size >= 32)) return;
|
|
399
|
+
agentStates.set(update.id, update.status ?? agentStates.get(update.id) ?? 'running');
|
|
400
|
+
opts.onSubagent?.(update);
|
|
401
|
+
};
|
|
402
|
+
const subagents = createClaudeSubagents(reportSubagent);
|
|
403
|
+
const claudeProgress = rt.id === 'claude' && opts.onSubagent ? createClaudeSubagentProgress({
|
|
404
|
+
cwd, since: spawnedAt, env: { ...process.env, ...(mcpEnv ?? {}) },
|
|
405
|
+
onLine: line => handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText,
|
|
406
|
+
answerFromResult: true, subagents }),
|
|
407
|
+
}) : null;
|
|
408
|
+
const codexAgents = rt.id === 'codex' && opts.onSubagent ? createCodexSubagentReader({
|
|
409
|
+
since: spawnedAt, env: { ...process.env, ...(mcpEnv ?? {}) },
|
|
410
|
+
onSubagent: reportSubagent, onActivity, onToolEvent,
|
|
411
|
+
onSpawnResult: opts.onSpawnResult,
|
|
412
|
+
}) : null;
|
|
375
413
|
let codexThread = rt.id === 'codex' && typeof resumeThreadId === 'string' ? resumeThreadId : null;
|
|
414
|
+
if (codexThread) codexAgents?.thread(codexThread);
|
|
376
415
|
/**
|
|
377
416
|
* THE MODEL THIS SPAWN RAN ON, ONCE (2026-09-29, turnModel.mjs). Claude
|
|
378
417
|
* names it in its init event and again on every reply; the first valid
|
|
@@ -396,6 +435,7 @@ function runPlainTurn(opts) {
|
|
|
396
435
|
*/
|
|
397
436
|
const harvest = initHarvestFor(rt, args);
|
|
398
437
|
const onInitAll = (i) => {
|
|
438
|
+
claudeProgress?.session(i?.sessionId);
|
|
399
439
|
if (harvest.skills) recordSkills(i?.skills);
|
|
400
440
|
if (harvest.mcp) recordMcpServers(i?.mcpServers, i?.tools);
|
|
401
441
|
onInit?.(i);
|
|
@@ -405,7 +445,8 @@ function runPlainTurn(opts) {
|
|
|
405
445
|
const onLine = (line) => {
|
|
406
446
|
try {
|
|
407
447
|
const event = JSON.parse(line);
|
|
408
|
-
|
|
448
|
+
const parentEvent = event.parent_tool_use_id == null && !event.isSidechain;
|
|
449
|
+
if (input && parentEvent) {
|
|
409
450
|
input.event(event);
|
|
410
451
|
if (event.type === 'system' && event.subtype === 'init' && event.session_id) opts.liveMessages.onInit(event.session_id);
|
|
411
452
|
// A message written just before result may become a second native
|
|
@@ -413,26 +454,30 @@ function runPlainTurn(opts) {
|
|
|
413
454
|
// structured objects joined together; every result still reports spend.
|
|
414
455
|
if (event.type === 'result') out = '';
|
|
415
456
|
}
|
|
416
|
-
if ((event.type === 'result' && event.is_error === true) ||
|
|
417
|
-
event.type === 'error' || event.type === 'turn.failed') {
|
|
457
|
+
if (parentEvent && ((event.type === 'result' && event.is_error === true) ||
|
|
458
|
+
event.type === 'error' || event.type === 'turn.failed')) {
|
|
418
459
|
cliFailed = true;
|
|
419
460
|
opts.onCliFailure?.();
|
|
420
461
|
}
|
|
421
462
|
} catch { /* ordinary non-JSON CLI output */ }
|
|
422
463
|
if (!rt.parse)
|
|
423
|
-
return handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText, answerFromResult, onInit: onInitAll, onUsage, onRateLimit: onRate, onModel: onRanOn });
|
|
464
|
+
return handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText, answerFromResult, onInit: onInitAll, onUsage, onRateLimit: onRate, onModel: onRanOn, subagents });
|
|
424
465
|
const ev = rt.parse(line, cwd);
|
|
425
466
|
if (!ev) return;
|
|
426
467
|
// The conversation id, when the runtime announces one (codex's
|
|
427
468
|
// thread.started). Purely additive: callers that pass no onThreadId —
|
|
428
469
|
// every dispatch path — see zero behavior change.
|
|
429
470
|
if (ev.threadId) {
|
|
430
|
-
if (rt.id === 'codex') codexThread = ev.threadId;
|
|
471
|
+
if (rt.id === 'codex') { codexThread = ev.threadId; codexAgents?.thread(ev.threadId); }
|
|
431
472
|
onThreadId?.(ev.threadId);
|
|
432
473
|
}
|
|
433
474
|
// The runtime's own token count (codex's `turn.completed`) — the same
|
|
434
475
|
// `onUsage` Claude's result event feeds, so a caller charges a turn
|
|
435
476
|
// identically whichever CLI ran it.
|
|
477
|
+
for (const update of ev.subagents ?? []) codexAgents?.update(update);
|
|
478
|
+
if (rt.id === 'codex') {
|
|
479
|
+
try { const item = JSON.parse(line)?.item; const result = codexSpawnResult(item); if (result) opts.onSpawnResult?.(result); } catch { /* non-JSON */ }
|
|
480
|
+
}
|
|
436
481
|
if (ev.toolEvent) onToolEvent?.(...ev.toolEvent);
|
|
437
482
|
if (ev.usage) onUsage?.(ev.usage);
|
|
438
483
|
// One agent MESSAGE, alone — see parseCodexLine's `answer`. Last call
|
|
@@ -475,11 +520,13 @@ function runPlainTurn(opts) {
|
|
|
475
520
|
* legitimately, and the wiki/plan-check/consult turns still ask for Claude
|
|
476
521
|
* by default. On such a machine the first wiki sweep would have killed the
|
|
477
522
|
* whole daemon, taking every in-flight build with it, because one
|
|
478
|
-
* background job could not find one binary.
|
|
479
|
-
*
|
|
523
|
+
* background job could not find one binary. Other spawn errors reject
|
|
524
|
+
* this turn with the native reason; the lane records a failed report.
|
|
480
525
|
*/
|
|
481
526
|
const onSpawnError = (e) => {
|
|
482
527
|
input?.close();
|
|
528
|
+
void codexAgents?.stop();
|
|
529
|
+
void claudeProgress?.stop();
|
|
483
530
|
if (opts.liveMessages) void opts.liveMessages.stop();
|
|
484
531
|
finishDiagnostics({ pid: child.pid, spawnError: typeof e.code === 'string' ? e.code : 'SPAWN_ERROR' });
|
|
485
532
|
releaseArgs();
|
|
@@ -489,7 +536,7 @@ function runPlainTurn(opts) {
|
|
|
489
536
|
return;
|
|
490
537
|
}
|
|
491
538
|
console.error(e);
|
|
492
|
-
|
|
539
|
+
reject(e);
|
|
493
540
|
};
|
|
494
541
|
child.on('error', onSpawnError);
|
|
495
542
|
child.on('close', (code, signal) => {
|
|
@@ -497,9 +544,24 @@ function runPlainTurn(opts) {
|
|
|
497
544
|
releaseArgs();
|
|
498
545
|
opts.onExit?.(code, signal);
|
|
499
546
|
if (buf.trim()) onLine(buf);
|
|
500
|
-
finishDiagnostics({ code, signal, pid: child.pid, output: out, cliFailed });
|
|
547
|
+
finishDiagnostics({ code, signal, pid: child.pid, output: out, cliFailed, inputError: stdinError?.code });
|
|
501
548
|
const handBack = async () => {
|
|
502
549
|
if (opts.liveMessages) await opts.liveMessages.stop();
|
|
550
|
+
// Native transcript reads are readouts too: a stuck filesystem must
|
|
551
|
+
// not hold the card's answer. Readers stop scheduling before draining.
|
|
552
|
+
let progressTimer;
|
|
553
|
+
try {
|
|
554
|
+
await Promise.race([
|
|
555
|
+
Promise.all([codexAgents?.stop(), claudeProgress?.stop()]),
|
|
556
|
+
new Promise(resolve => { progressTimer = setTimeout(resolve, CODEX_MODEL_READ_MS); }),
|
|
557
|
+
]);
|
|
558
|
+
} finally { clearTimeout(progressTimer); }
|
|
559
|
+
for (const [id, status] of agentStates)
|
|
560
|
+
if (status === 'running' || status === 'waiting') reportSubagent({ id, status: 'stopped' });
|
|
561
|
+
if (stdinError) {
|
|
562
|
+
if (!workLanesStopped()) rejectTurn(stdinError);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
503
565
|
resolve(out);
|
|
504
566
|
// After the answer is handed back, never before it: see `onRate`.
|
|
505
567
|
if (codexThread) {
|
|
@@ -530,6 +592,17 @@ function runPlainTurn(opts) {
|
|
|
530
592
|
}
|
|
531
593
|
handBack();
|
|
532
594
|
});
|
|
595
|
+
if (transport.text !== null) {
|
|
596
|
+
// end() buffers with backpressure and closes at EOF; Codex reads the
|
|
597
|
+
// entire prompt before starting. An early close must not accept output
|
|
598
|
+
// from a turn whose complete authorized input never reached the CLI.
|
|
599
|
+
child.stdin.on('error', (error) => {
|
|
600
|
+
stdinError = Object.assign(new Error(`${rt.label} could not receive the complete turn input (${error.code || 'PIPE_ERROR'}).`), { code: error.code || 'PIPE_ERROR' });
|
|
601
|
+
opts.onCliFailure?.();
|
|
602
|
+
child.kill('SIGTERM');
|
|
603
|
+
});
|
|
604
|
+
child.stdin.end(transport.text, 'utf8');
|
|
605
|
+
}
|
|
533
606
|
if (input) {
|
|
534
607
|
const initial = opts.liveMessages.initialPrompt(prompt, resumeThreadId, resume, opts.liveContinuation === true);
|
|
535
608
|
input.send(opts.liveMessages.initialId || opts.liveMessages.turnId, initial);
|
|
@@ -16,6 +16,7 @@ import { join } from 'node:path';
|
|
|
16
16
|
import { MODEL, USER_AGENT } from './config.mjs';
|
|
17
17
|
import { NO_SETTING_SOURCES, personalClaudeSettings, releaseTurnSettings, withPersonalSettings } from './claudePersonal.mjs';
|
|
18
18
|
import { withMachineMcp } from './claudePosture.mjs';
|
|
19
|
+
import { orchestratorHooks } from './orchestratorHooks.mjs';
|
|
19
20
|
|
|
20
21
|
/** The profiles an agent lane mounts the machine's MCP servers on (2026-10-11):
|
|
21
22
|
* the ones whose turn profile says `projectMcp` and that Claude declares. */
|
|
@@ -101,7 +102,7 @@ export const CLAUDE_RUNTIME = {
|
|
|
101
102
|
* strongest form of it available anywhere: `--append-system-prompt` sits
|
|
102
103
|
* above the conversation rather than inside it.
|
|
103
104
|
*/
|
|
104
|
-
args({ writableDirs = [], prompt, system, model, effort, resume, perm, profile = 'build', mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir, vaultDir, agentTools, personalSettings, machineMcp, streamInput = false }) {
|
|
105
|
+
args({ writableDirs = [], prompt, system, model, effort, resume, perm, profile = 'build', mcp = [], resultSchemaArgs = [], adoptResumeId, resumeThreadId, knowledgeDir, vaultDir, agentTools, personalSettings, machineMcp, streamInput = false, orchestration = false }) {
|
|
105
106
|
// THE CARTOGRAPHER'S FENCE NAMES ITS VAULT (2026-10-10): its one write rule
|
|
106
107
|
// is the vault's absolute path (claudePosture.mjs `wikiPermFor`), so a turn
|
|
107
108
|
// with no vault, or one no rule can name, has no list — refused here in
|
|
@@ -169,6 +170,7 @@ export const CLAUDE_RUNTIME = {
|
|
|
169
170
|
* agent renders the CURRENT base snapshot, not the first turn's cache.
|
|
170
171
|
*/
|
|
171
172
|
const fenced = profile !== 'build';
|
|
173
|
+
if (orchestration && !fenced) a.push('--settings', JSON.stringify({ hooks: orchestratorHooks(), disableAllHooks: false }));
|
|
172
174
|
if (fenced) a.push(...NO_SETTING_SOURCES);
|
|
173
175
|
// A FENCED POSTURE NAMES ITS OWN MODE (2026-10-10). Its lists fence only
|
|
174
176
|
// under the CLI's `default` mode, where a tool the list does not name is
|
package/bin/lib/runtimeCodex.mjs
CHANGED
|
@@ -16,6 +16,8 @@ import { ARTIFACT_DIR, FENCE_TMP_DIR } from './artifacts.mjs';
|
|
|
16
16
|
import { codexPersonalTurn } from './codexPersonal.mjs';
|
|
17
17
|
import { researchFence } from './codexResearch.mjs';
|
|
18
18
|
import { projectReadBoundary, projectReadFence } from './codexProjectRead.mjs';
|
|
19
|
+
import { compareVersions } from './versionOrder.mjs';
|
|
20
|
+
import { codexOrchestratorArgs } from './orchestratorHooks.mjs';
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* FLOWVIANT'S OWN TOOLS NEED NO APPROVAL FROM A CHAT NOBODY CAN ANSWER
|
|
@@ -78,9 +80,18 @@ export const NO_REACH_OUT = Object.freeze([
|
|
|
78
80
|
*/
|
|
79
81
|
export const NO_WEB = Object.freeze(['-c', 'tools.web_search=false', '-c', 'web_search="disabled"']);
|
|
80
82
|
|
|
81
|
-
/**
|
|
83
|
+
/** Fenced lanes and non-card turns never delegate. */
|
|
82
84
|
export const NO_SUBAGENTS = Object.freeze(['-c', 'features.multi_agent=false', '-c', 'features.goals=false']);
|
|
83
85
|
|
|
86
|
+
/** rust-v0.160.0 core/src/agent/child_config.rs applies the invoking turn's
|
|
87
|
+
* approval policy, permission profile and cwd AFTER role overrides, for V1
|
|
88
|
+
* and V2. Older/unmeasured binaries have no inheritance evidence here. */
|
|
89
|
+
export const CODEX_SUBAGENTS_MIN = '0.160.0';
|
|
90
|
+
export function codexSubagentsSafe(version) {
|
|
91
|
+
const order = compareVersions(String(version ?? '').replace(/^codex-cli\s+/, ''), CODEX_SUBAGENTS_MIN);
|
|
92
|
+
return order === 0 || order === 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
84
95
|
/**
|
|
85
96
|
* HERMETIC. Without these a user's ~/.codex/config.toml, a project `.rules`
|
|
86
97
|
* execpolicy file, or their own MCP servers can widen a posture we are
|
|
@@ -158,6 +169,13 @@ const codexWikiFence = (vaultDir) => [
|
|
|
158
169
|
...fencedTail(),
|
|
159
170
|
];
|
|
160
171
|
|
|
172
|
+
function codexPrompt({ system, prompt, profile = 'build' }) {
|
|
173
|
+
const readers = profile === 'consult' || profile === 'plan'
|
|
174
|
+
? '\nUse exec_command with shell="/bin/bash" and login=false for project discovery. Project files, local Git history and the project knowledge directory are readable; host files are outside this turn’s read boundary.'
|
|
175
|
+
: '';
|
|
176
|
+
return `${system}${readers}\n\n---\n\n${prompt}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
161
179
|
export const CODEX_RUNTIME = {
|
|
162
180
|
id: 'codex',
|
|
163
181
|
label: 'Codex',
|
|
@@ -227,13 +245,12 @@ export const CODEX_RUNTIME = {
|
|
|
227
245
|
* can diff. A write-up reads a COPY with no `.git` and never resumes
|
|
228
246
|
* (codexResearch.mjs), so its fence passes it.
|
|
229
247
|
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* that parses today and stops parsing on some future clap upgrade.
|
|
248
|
+
* The prompt positional stays last; runTurn uses `-` and sends the complete
|
|
249
|
+
* framed text through stdin. A whole batch can exceed Linux's single-argument
|
|
250
|
+
* limit even when its argv plus environment is far below ARG_MAX. Both exec
|
|
251
|
+
* and exec resume support `-` (verified on Codex 0.160.0, 2026-10-16).
|
|
235
252
|
*/
|
|
236
|
-
args({ writableDirs = [], prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, cwd, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools, personal = null, researchIsolation, knowledgeDir, projectReads }) {
|
|
253
|
+
args({ streamInput = false, writableDirs = [], prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, cwd, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools, personal = null, researchIsolation, knowledgeDir, projectReads, cardTurn = false, codexVersion, orchestration = false }) {
|
|
237
254
|
// Adoption resumes a conversation in ITS OWN CLI's store (claude forks,
|
|
238
255
|
// agy moves) — codex has no adoptable store wired yet. Reaching here
|
|
239
256
|
// with an adopt id is a wiring mistake upstream, and it fails loudly on
|
|
@@ -279,6 +296,10 @@ export const CODEX_RUNTIME = {
|
|
|
279
296
|
// before it started. `-c sandbox_mode=` means the same on both.
|
|
280
297
|
if (writableDirs.length) a.push('-c', `sandbox_workspace_write.writable_roots=${JSON.stringify(writableDirs)}`);
|
|
281
298
|
a.push('-c', `sandbox_mode="${SAFE ? 'workspace-write' : 'danger-full-access'}"`);
|
|
299
|
+
if (cardTurn) a.push('-c', 'approval_policy="never"');
|
|
300
|
+
a.push(...(cardTurn && profile === 'build' && codexSubagentsSafe(codexVersion)
|
|
301
|
+
? ['-c', 'features.multi_agent=true', '-c', 'features.goals=false']
|
|
302
|
+
: NO_SUBAGENTS));
|
|
282
303
|
}
|
|
283
304
|
|
|
284
305
|
/**
|
|
@@ -294,12 +315,12 @@ export const CODEX_RUNTIME = {
|
|
|
294
315
|
if (a.includes('--ignore-user-config')) a.push(...(personal?.args ?? []));
|
|
295
316
|
|
|
296
317
|
a.push(...mcp, ...resultSchemaArgs);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
: '';
|
|
300
|
-
a.push(`${system}${readers}\n\n---\n\n${prompt}`);
|
|
318
|
+
if (orchestration && !fenced && codexSubagentsSafe(codexVersion)) a.push(...codexOrchestratorArgs());
|
|
319
|
+
a.push(streamInput ? '-' : codexPrompt({ system, prompt, profile }));
|
|
301
320
|
return a;
|
|
302
321
|
},
|
|
322
|
+
/** One-shot UTF-8 input, closed at EOF; distinct from Claude's live JSONL. */
|
|
323
|
+
stdin: codexPrompt,
|
|
303
324
|
/**
|
|
304
325
|
* THE PERSON'S LOGIN FOR A FENCED TURN, read once per spawn by runTurn from
|
|
305
326
|
* the turn's own environment (its CODEX_HOME — an agent's own home when the
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* own stream loop and calls `humanizeClaudeTool`/`toolEventOf` from it.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { codexCollabUpdates } from './codexSubagentEvents.mjs';
|
|
18
|
+
|
|
17
19
|
/** Truncate for a one-line activity label. */
|
|
18
20
|
const oneLine = (s, n = 140) =>
|
|
19
21
|
String(s ?? '').replace(/\s+/g, ' ').trim().slice(0, n);
|
|
@@ -139,6 +141,11 @@ export function countLines(s) {
|
|
|
139
141
|
* window minus the cap to straddle out of it — no real credential is. */
|
|
140
142
|
const SCRUB_WINDOW = 8_192;
|
|
141
143
|
|
|
144
|
+
/** The same window/scrub/cap order for native agent metadata and tools. */
|
|
145
|
+
export function scrubNativeText(value, cap, scrub) {
|
|
146
|
+
return scrub(String(value ?? '').slice(0, SCRUB_WINDOW)).slice(0, cap);
|
|
147
|
+
}
|
|
148
|
+
|
|
142
149
|
/**
|
|
143
150
|
* Tool-call → ONE STRUCTURED EVENT for the transcript's tool cards — the
|
|
144
151
|
* relay's durable form, where `humanizeClaudeTool` above is its one-line live
|
|
@@ -165,7 +172,7 @@ const SCRUB_WINDOW = 8_192;
|
|
|
165
172
|
export function toolEventOf(name, input = {}, cwd = '', scrub = (s) => s) {
|
|
166
173
|
// Window → scrub → cap. The window bounds what a multi-MB input costs; the
|
|
167
174
|
// cap is applied AFTER the scrub so a replacement cannot overflow it.
|
|
168
|
-
const clean = (v, cap) =>
|
|
175
|
+
const clean = (v, cap) => scrubNativeText(v, cap, scrub);
|
|
169
176
|
const rel = (p) => clean(shortPath(p, cwd), 300);
|
|
170
177
|
// The first k lines of a side, from a scrubbed bounded prefix — never a
|
|
171
178
|
// full split of the raw string.
|
|
@@ -197,6 +204,7 @@ export function toolEventOf(name, input = {}, cwd = '', scrub = (s) => s) {
|
|
|
197
204
|
return { t: 'glob', q: clean(input.pattern, 200) };
|
|
198
205
|
case 'Bash':
|
|
199
206
|
return { t: 'bash', c: clean(input.command, 200) };
|
|
207
|
+
case 'Agent':
|
|
200
208
|
case 'Task':
|
|
201
209
|
return { t: 'task', q: clean(input.description, 200) };
|
|
202
210
|
case 'TodoWrite': {
|
|
@@ -313,6 +321,9 @@ export function parseCodexLine(line, cwd) {
|
|
|
313
321
|
} catch {
|
|
314
322
|
return null; // not every line is JSON (warnings go to stderr, but be safe)
|
|
315
323
|
}
|
|
324
|
+
if (['item.started', 'item.updated', 'item.completed'].includes(ev.type) &&
|
|
325
|
+
(ev.item?.type ?? ev.item?.item_type) === 'collab_tool_call')
|
|
326
|
+
return { subagents: codexCollabUpdates(ev.item), activity: null, text: '' };
|
|
316
327
|
switch (ev.type) {
|
|
317
328
|
// The conversation's own id, announced before any item (`ThreadStarted` on
|
|
318
329
|
// the shipped 0.147 binary, like the measured events below). Surfaced so a
|
package/bin/lib/trace.mjs
CHANGED
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
* tail of a trace, never the settle behind it.
|
|
60
60
|
*/
|
|
61
61
|
|
|
62
|
-
import { THINK_MARKER } from './runtimeEvents.mjs';
|
|
62
|
+
import { THINK_MARKER, scrubNativeText } from './runtimeEvents.mjs';
|
|
63
63
|
import { withoutTurnDeclaration } from './turnDeclaration.mjs';
|
|
64
64
|
|
|
65
65
|
/** One batch every two seconds — the discipline the tab's narrator keeps, for
|
|
@@ -107,7 +107,7 @@ function newRunId() {
|
|
|
107
107
|
* with no such route, a body the server will never accept). Resolving falsy
|
|
108
108
|
* keeps them queued for the next flush.
|
|
109
109
|
*/
|
|
110
|
-
export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run = newRunId() }) {
|
|
110
|
+
export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run = newRunId(), orchestrationVersion }) {
|
|
111
111
|
/** Entries not yet delivered. `base` is the ABSOLUTE index of queue[0] in
|
|
112
112
|
* THIS RUN's stream, so `base + queue.length` is everything this relay has
|
|
113
113
|
* ever accepted — sent, queued or shed. Absolute for the TURN is the
|
|
@@ -119,7 +119,9 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
119
119
|
* empties the queue and a batch boundary is not a change in the stream. Read
|
|
120
120
|
* by the bare-marker collapse in `prose()` and nothing else. */
|
|
121
121
|
let lastEntry = null;
|
|
122
|
-
|
|
122
|
+
const agentReadings = new Map();
|
|
123
|
+
let dirty = orchestrationVersion === 2;
|
|
124
|
+
let markerPending = dirty;
|
|
123
125
|
let timer = null;
|
|
124
126
|
let stopped = false;
|
|
125
127
|
/** The await chain. Every flush appends to it, so batches leave in order
|
|
@@ -149,7 +151,7 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
149
151
|
|
|
150
152
|
const drain = async (deadlineMs) => {
|
|
151
153
|
const until = deadlineMs > 0 ? Date.now() + deadlineMs : 0;
|
|
152
|
-
while (queue.length) {
|
|
154
|
+
while (queue.length || markerPending) {
|
|
153
155
|
if (until && Date.now() > until) {
|
|
154
156
|
dirty = true;
|
|
155
157
|
return;
|
|
@@ -159,7 +161,9 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
159
161
|
dirty = false;
|
|
160
162
|
let ok = false;
|
|
161
163
|
try {
|
|
162
|
-
ok = (await post({ agentId, turnId, run, seq, entries
|
|
164
|
+
ok = (await post({ agentId, turnId, run, seq, entries,
|
|
165
|
+
...(orchestrationVersion === 2 ? { orchestrationVersion } : {}),
|
|
166
|
+
})) !== false;
|
|
163
167
|
} catch {
|
|
164
168
|
ok = false;
|
|
165
169
|
}
|
|
@@ -169,6 +173,7 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
169
173
|
dirty = true;
|
|
170
174
|
return;
|
|
171
175
|
}
|
|
176
|
+
markerPending = false;
|
|
172
177
|
// Remove only what of the batch is STILL at the head: a push during the
|
|
173
178
|
// await may have shed some of it already (advancing `base`), and
|
|
174
179
|
// splicing the whole batch length would then drop entries that were
|
|
@@ -188,7 +193,11 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
188
193
|
return done;
|
|
189
194
|
};
|
|
190
195
|
|
|
196
|
+
// The first report stamps even a silent or immediately failed CLI invocation.
|
|
197
|
+
schedule();
|
|
198
|
+
const scoped = (id) => id ? `${run}:${scrubNativeText(id, 96, scrub)}` : undefined;
|
|
191
199
|
return {
|
|
200
|
+
agentId: scoped,
|
|
192
201
|
/**
|
|
193
202
|
* A prose line from the stream. `kind` is the daemon's activity vocabulary
|
|
194
203
|
* (runtimeEvents.mjs); anything that is not thinking or the model speaking is a
|
|
@@ -207,7 +216,7 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
207
216
|
* CLI's own stdout, and a secret in a thought must not ride further than it
|
|
208
217
|
* did when a thought was 300 characters.
|
|
209
218
|
*/
|
|
210
|
-
prose(kind, text) {
|
|
219
|
+
prose(kind, text, agentId) {
|
|
211
220
|
const readable = kind === 'think' ? String(text ?? '') : withoutTurnDeclaration(text);
|
|
212
221
|
const t = scrub(readable)
|
|
213
222
|
// One line ending, whatever the CLI printed.
|
|
@@ -239,10 +248,10 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
239
248
|
* never takes a seq, exactly like the empty line above it. The "N earlier
|
|
240
249
|
* steps are not shown" count stays a count of steps that existed.
|
|
241
250
|
*/
|
|
242
|
-
if (k === 'think' && t === THINK_MARKER && lastEntry?.k === 'think' && lastEntry.t === t) {
|
|
251
|
+
if (k === 'think' && t === THINK_MARKER && lastEntry?.k === 'think' && lastEntry.t === t && lastEntry.agentId === scoped(agentId)) {
|
|
243
252
|
return readable;
|
|
244
253
|
}
|
|
245
|
-
push({ k, t });
|
|
254
|
+
push({ k, t, ...(agentId ? { agentId: scoped(agentId) } : {}) });
|
|
246
255
|
// The activity pulse uses this same filtering; its original compact
|
|
247
256
|
// label stands unless protocol text was removed from the full message.
|
|
248
257
|
return readable;
|
|
@@ -250,13 +259,42 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
250
259
|
/** One structured tool event, exactly as `toolEventOf` built it. A tool this
|
|
251
260
|
* builder does not know returns null there and nothing is pushed here — a
|
|
252
261
|
* card is never invented. */
|
|
253
|
-
tool(e) {
|
|
254
|
-
if (e && typeof e === 'object') push({ k: 'tool', e });
|
|
262
|
+
tool(e, agentId) {
|
|
263
|
+
if (e && typeof e === 'object') push({ k: 'tool', e, ...(agentId ? { agentId: scoped(agentId) } : {}) });
|
|
264
|
+
},
|
|
265
|
+
agent(update) {
|
|
266
|
+
if (!update?.id) return;
|
|
267
|
+
const clean = (v, n) => scrubNativeText(v, n, scrub);
|
|
268
|
+
const fields = {};
|
|
269
|
+
for (const [key, cap] of Object.entries({ kind: 80, title: 200, now: 400, brief: 4_000 }))
|
|
270
|
+
if (typeof update[key] === 'string') fields[key] = clean(update[key], cap);
|
|
271
|
+
if (typeof fields.now === 'string') fields.now = fields.now.replace(/\s+/g, ' ').trim();
|
|
272
|
+
if (['running', 'waiting', 'asked', 'stopped', 'done'].includes(update.status)) fields.status = update.status;
|
|
273
|
+
const previous = agentReadings.get(update.id) ?? {};
|
|
274
|
+
if (agentReadings.has(update.id) && Object.entries(fields).every(([k, v]) => previous[k] === v)) return;
|
|
275
|
+
if (!agentReadings.has(update.id) && agentReadings.size >= 32) return;
|
|
276
|
+
const reading = { ...previous, ...fields };
|
|
277
|
+
agentReadings.set(update.id, reading);
|
|
278
|
+
// A later update repairs metadata whose first report was shed while
|
|
279
|
+
// offline; a delta alone cannot recover the child's prompt or title.
|
|
280
|
+
push({ k: 'agent', id: scoped(update.id), ...reading });
|
|
281
|
+
},
|
|
282
|
+
/** Routing is declared by the orchestrator; no server interpretation. */
|
|
283
|
+
handoff(agentId, from, text) {
|
|
284
|
+
push({ k: 'handoff', agentId: scoped(agentId), from: scrub(String(from)).slice(0, 160), t: scrub(String(text)).slice(0, TRACE_PROSE_CAP) });
|
|
285
|
+
},
|
|
286
|
+
doing(agentId, text) {
|
|
287
|
+
push({ k: 'doing', agentId: scoped(agentId), t: scrub(String(text)).slice(0, TRACE_PROSE_CAP) });
|
|
255
288
|
},
|
|
256
289
|
flush,
|
|
257
290
|
/** No more entries, no more timers. The queue survives so a final flush can
|
|
258
291
|
* still deliver it. */
|
|
259
292
|
stop() {
|
|
293
|
+
if (!stopped && orchestrationVersion === 2) {
|
|
294
|
+
// Even a quiet child retains its identity after another child's noisy
|
|
295
|
+
// stream overflowed the relay. These snapshots remain sequence-safe.
|
|
296
|
+
for (const [id, reading] of agentReadings) push({ k: 'agent', id: scoped(id), ...reading });
|
|
297
|
+
}
|
|
260
298
|
stopped = true;
|
|
261
299
|
if (timer) {
|
|
262
300
|
clearTimeout(timer);
|
|
@@ -10,6 +10,7 @@ import { AUTH_ENV_VARS, claudeAuthContext, claudeCredentialPath } from './claude
|
|
|
10
10
|
import { scrub } from './uplinkScrub.mjs';
|
|
11
11
|
import { printableText } from './printable.mjs';
|
|
12
12
|
import { measuredRuntimeVersion } from './runtimeDetection.mjs';
|
|
13
|
+
import { VERSION } from './config.mjs';
|
|
13
14
|
|
|
14
15
|
let active = 0;
|
|
15
16
|
const authNames = (env) => AUTH_ENV_VARS.filter((name) =>
|
|
@@ -40,7 +41,7 @@ export function startTurnDiagnostics(options) {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
function createTurnDiagnostics({ runtime, bin, env, cwd = process.cwd(), profile,
|
|
43
|
-
planId = null, args = [], version, write = (row) => console.info(`[cli-turn] ${JSON.stringify(row)}`) }) {
|
|
44
|
+
planId = null, args = [], stdinBytes = 0, version, write = (row) => console.info(`[cli-turn] ${JSON.stringify(row)}`) }) {
|
|
44
45
|
const startedAt = Date.now();
|
|
45
46
|
const turnId = randomUUID();
|
|
46
47
|
const executable = executableFor(bin, env, cwd);
|
|
@@ -58,7 +59,14 @@ function createTurnDiagnostics({ runtime, bin, env, cwd = process.cwd(), profile
|
|
|
58
59
|
} catch { /* CLI settings unavailable */ }
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
+
// Only byte counts, including each terminating NUL. No values or hashes.
|
|
63
|
+
const argSizes = [bin, ...args].map(value => Buffer.byteLength(value) + 1);
|
|
64
|
+
const envSizes = Object.entries(env).filter(([, value]) => value !== undefined)
|
|
65
|
+
.map(([key, value]) => Buffer.byteLength(`${key}=${value}`) + 1);
|
|
66
|
+
const sum = values => values.reduce((a, b) => a + b, 0);
|
|
67
|
+
const launch = { argvBytes: sum(argSizes), maxArgBytes: Math.max(0, ...argSizes),
|
|
68
|
+
envBytes: sum(envSizes), maxEnvEntryBytes: Math.max(0, ...envSizes), stdinBytes };
|
|
69
|
+
const base = { turnId, runtime, profile, planId, cwd, daemonPid: process.pid, daemonVersion: VERSION, launch,
|
|
62
70
|
uid: process.getuid?.() ?? null, platform: process.platform,
|
|
63
71
|
nodeVersion: process.version, executable, version: version ?? null,
|
|
64
72
|
runtimeMeasurement: measuredRuntimeVersion(runtime) };
|
|
@@ -79,15 +87,15 @@ function createTurnDiagnostics({ runtime, bin, env, cwd = process.cwd(), profile
|
|
|
79
87
|
active++;
|
|
80
88
|
emit({ ...base, phase: 'start', at: new Date(startedAt).toISOString(), activeTurns: active, auth: auth() });
|
|
81
89
|
let finished = false;
|
|
82
|
-
return ({ code = null, signal = null, pid = null, output = '', spawnError = null, cliFailed = false } = {}) => {
|
|
90
|
+
return ({ code = null, signal = null, pid = null, output = '', spawnError = null, inputError = null, cliFailed = false } = {}) => {
|
|
83
91
|
if (finished) return;
|
|
84
92
|
finished = true;
|
|
85
93
|
active--;
|
|
86
94
|
const failed = cliFailed || (code !== null && code !== 0);
|
|
87
95
|
emit({ ...base, phase: 'end', at: new Date().toISOString(), durationMs: Date.now() - startedAt,
|
|
88
|
-
pid, code, signal, spawnError, activeTurns: active, auth: auth(),
|
|
96
|
+
pid, code, signal, spawnError, inputError, activeTurns: active, auth: auth(),
|
|
89
97
|
failureKind: failed && /OAuth session expired and could not be refreshed/i.test(output)
|
|
90
98
|
? 'oauth_refresh' : failed && /Failed to authenticate/i.test(output) ? 'authentication'
|
|
91
|
-
: spawnError ? 'spawn' : code !== null && code !== 0 ? 'cli_exit' : null });
|
|
99
|
+
: spawnError ? 'spawn' : inputError ? 'input' : code !== null && code !== 0 ? 'cli_exit' : null });
|
|
92
100
|
};
|
|
93
101
|
}
|
package/bin/lib/work.mjs
CHANGED
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
* turnLock.mjs tab pid locks; workLiveMessages.mjs — live input; workDelivery.mjs — queue
|
|
55
55
|
* (and, split earlier: workDiffs, workPreviews, workPullRequests, workShip,
|
|
56
56
|
* workAgentPlans, workAgentCheck, workAgentPrecheck, workAgentReview,
|
|
57
|
-
* workAgentTurns, workAgentRecovery, workAgentMerges,
|
|
57
|
+
* workAgentTurns, workAgentRecovery, workAgentMerges, workMergeBatch,
|
|
58
|
+
* workProcesses; and out of workAgentTurns,
|
|
58
59
|
* SOLID F036: workAgentTurnReports, workAgentTurnExecution, workAgentTurnBegun,
|
|
59
60
|
* workAgentTurnOutcome; repositoryPlaces + agentRepositories: multi-repo places; the live brief: workAgentBrief, workAgentBriefRunner)
|
|
60
61
|
*/
|