flowviant 0.125.29 → 0.125.31
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 +364 -0
- package/bin/cli.mjs +23 -0
- package/bin/lib/agentPlan.mjs +8 -0
- package/bin/lib/agentQueueContext.mjs +6 -0
- package/bin/lib/agentSplit.mjs +90 -0
- package/bin/lib/architectureContract.mjs +57 -0
- package/bin/lib/architectureReport.mjs +38 -0
- package/bin/lib/artifactContracts.mjs +21 -2
- package/bin/lib/claudeInput.mjs +1 -1
- package/bin/lib/claudeSubagents.mjs +46 -6
- package/bin/lib/claudeWorkflows.mjs +94 -0
- package/bin/lib/cliLimit.mjs +5 -0
- package/bin/lib/codexInput.mjs +191 -0
- package/bin/lib/codexSubagentEvents.mjs +3 -1
- package/bin/lib/codexSubagents.mjs +34 -9
- package/bin/lib/conversationOutput.mjs +14 -0
- package/bin/lib/crossCliChild.mjs +327 -0
- package/bin/lib/crossCliLaunch.mjs +92 -0
- package/bin/lib/crossCliPosture.mjs +55 -0
- package/bin/lib/crossCliResearch.mjs +19 -0
- package/bin/lib/environmentProcess.mjs +44 -0
- package/bin/lib/fixtures/workflow-ultracode.jsonl +22 -0
- package/bin/lib/fleet.mjs +2 -0
- package/bin/lib/linuxDesktopBridge.mjs +62 -0
- package/bin/lib/managedBrowser.mjs +50 -10
- package/bin/lib/mergeBatch.mjs +1 -1
- package/bin/lib/nativeAgentTools.mjs +12 -5
- package/bin/lib/noWindowEnv.mjs +4 -2
- package/bin/lib/orchestrator.mjs +24 -7
- package/bin/lib/orchestratorClient.mjs +8 -2
- package/bin/lib/orchestratorPrompt.mjs +13 -5
- package/bin/lib/orchestratorProtocol.mjs +18 -0
- package/bin/lib/outsideCli.mjs +124 -0
- package/bin/lib/projectToolRuntimes.mjs +4 -2
- package/bin/lib/projectTools.mjs +2 -2
- package/bin/lib/prompts.mjs +7 -5
- package/bin/lib/repositoryPlaces.mjs +1 -1
- package/bin/lib/repositorySetup.mjs +15 -24
- package/bin/lib/reviewBridge.mjs +110 -0
- package/bin/lib/reviewDatabase.mjs +112 -0
- package/bin/lib/reviewDesktop.mjs +153 -0
- package/bin/lib/reviewEnvironment.mjs +286 -0
- package/bin/lib/reviewEnvironmentConfig.mjs +104 -0
- package/bin/lib/reviewEnvironmentMain.mjs +38 -0
- package/bin/lib/reviewImage.mjs +58 -0
- package/bin/lib/reviewMcpClient.mjs +69 -0
- package/bin/lib/reviewProcess.mjs +51 -0
- package/bin/lib/reviewTargets.mjs +50 -0
- package/bin/lib/robloxReview.mjs +234 -0
- package/bin/lib/robloxStudioBridge.mjs +68 -0
- package/bin/lib/runTurn.mjs +30 -8
- package/bin/lib/runtimeClaude.mjs +7 -3
- package/bin/lib/runtimeCodex.mjs +12 -6
- package/bin/lib/runtimeLimits.mjs +59 -1
- package/bin/lib/runtimeReview.mjs +185 -0
- package/bin/lib/splitGate.mjs +115 -0
- package/bin/lib/subagentBounds.mjs +26 -0
- package/bin/lib/subagentLedger.mjs +110 -0
- package/bin/lib/trace.mjs +134 -6
- package/bin/lib/turnDeclaration.mjs +4 -4
- package/bin/lib/turnGaps.mjs +6 -2
- package/bin/lib/turnSubagents.mjs +23 -9
- package/bin/lib/uiReview.mjs +5 -2
- package/bin/lib/wikiDraft.mjs +9 -0
- package/bin/lib/work.mjs +22 -1
- package/bin/lib/workAgentCheck.mjs +24 -1
- package/bin/lib/workAgentResume.mjs +11 -2
- package/bin/lib/workAgentTools.mjs +6 -4
- package/bin/lib/workAgentTurnExecution.mjs +77 -9
- package/bin/lib/workAgentTurnOutcome.mjs +11 -0
- package/bin/lib/workAgentTurnReports.mjs +5 -2
- package/package.json +1 -1
package/bin/lib/trace.mjs
CHANGED
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
import { THINK_MARKER, scrubNativeText } from './runtimeEvents.mjs';
|
|
63
63
|
import { withoutTurnDeclaration } from './turnDeclaration.mjs';
|
|
64
64
|
import { MAX_MCP_SERVERS, MCP_NAME_MAX, MCP_STATUSES } from './mcpReportBounds.mjs';
|
|
65
|
-
import {
|
|
65
|
+
import { WORKFLOW_AGENT_CAP, WORKFLOW_CAP, OUTSIDE_CAP, OUTSIDE_COMMAND_CAP, OUTSIDE_STATUSES, SETTLED_SUBAGENT_STATUSES, SUBAGENT_CAP, SUBAGENT_CLIS, SUBAGENT_FIELD_CAPS,
|
|
66
|
+
SUBAGENT_KEY_MAX, SUBAGENT_RESULT_CAP, SUBAGENT_RUNS_CAP, SUBAGENT_STATUSES } from './subagentBounds.mjs';
|
|
66
67
|
|
|
67
68
|
/** One batch every two seconds — the discipline the tab's narrator keeps, for
|
|
68
69
|
* the same reason: a turn emits hundreds of entries and nobody is reading them
|
|
@@ -111,7 +112,18 @@ function newRunId() {
|
|
|
111
112
|
* with no such route, a body the server will never accept). Resolving falsy
|
|
112
113
|
* keeps them queued for the next flush.
|
|
113
114
|
*/
|
|
114
|
-
|
|
115
|
+
/** One native window a brief or result is scrubbed over before its cap. */
|
|
116
|
+
const NATIVE_WINDOW = 65_536;
|
|
117
|
+
const EFFORT_RE = /^[a-z]{1,24}$/;
|
|
118
|
+
const RUN_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* `cli` is the runtime whose children this turn records. `runsOf(key)` is
|
|
122
|
+
* the agent's subagent ledger (workAgentResume.mjs): the turns a child of
|
|
123
|
+
* that stable key appeared in, this one included; absent, a child's runs are
|
|
124
|
+
* unknown and carry no field.
|
|
125
|
+
*/
|
|
126
|
+
export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run = newRunId(), orchestrationVersion, cli, runsOf }) {
|
|
115
127
|
/** Entries not yet delivered. `base` is the ABSOLUTE index of queue[0] in
|
|
116
128
|
* THIS RUN's stream, so `base + queue.length` is everything this relay has
|
|
117
129
|
* ever accepted — sent, queued or shed. Absolute for the TURN is the
|
|
@@ -124,6 +136,19 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
124
136
|
* by the bare-marker collapse in `prose()` and nothing else. */
|
|
125
137
|
let lastEntry = null;
|
|
126
138
|
const agentReadings = new Map();
|
|
139
|
+
/** Each child's own steps, sent or shed: its record's `steps`, against
|
|
140
|
+
* which the server says "N earlier not kept" past its own budget. */
|
|
141
|
+
const childSteps = new Map();
|
|
142
|
+
/** Children past the 32, counted and never recorded (bounded: a count). */
|
|
143
|
+
const unrecorded = new Set();
|
|
144
|
+
let notRecorded = 0;
|
|
145
|
+
/** The lead's own entries this run, sent or shed: where a child or an
|
|
146
|
+
* Outside row sits in the lead's thread (its `position`). */
|
|
147
|
+
let leadCount = 0;
|
|
148
|
+
/** The lead's shell-outs to another CLI ("Outside", D23), by call id. */
|
|
149
|
+
const outsideRows = new Map();
|
|
150
|
+
const workflowRows = new Map(), workflowPositions = new Map();
|
|
151
|
+
let extrasPending = false;
|
|
127
152
|
const mcpSaid = new Set();
|
|
128
153
|
let dirty = orchestrationVersion === 2;
|
|
129
154
|
let markerPending = dirty;
|
|
@@ -145,6 +170,8 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
145
170
|
const push = (entry) => {
|
|
146
171
|
if (stopped || !entry) return;
|
|
147
172
|
lastEntry = entry;
|
|
173
|
+
if (entry.agentId) childSteps.set(entry.agentId, (childSteps.get(entry.agentId) ?? 0) + 1);
|
|
174
|
+
else if (entry.k !== 'agent') leadCount += 1;
|
|
148
175
|
queue.push(entry);
|
|
149
176
|
while (queue.length > TRACE_BUFFER) {
|
|
150
177
|
queue.shift();
|
|
@@ -156,7 +183,7 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
156
183
|
|
|
157
184
|
const drain = async (deadlineMs) => {
|
|
158
185
|
const until = deadlineMs > 0 ? Date.now() + deadlineMs : 0;
|
|
159
|
-
while (queue.length || markerPending) {
|
|
186
|
+
while (queue.length || markerPending || extrasPending) {
|
|
160
187
|
if (until && Date.now() > until) {
|
|
161
188
|
dirty = true;
|
|
162
189
|
return;
|
|
@@ -164,10 +191,17 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
164
191
|
const entries = queue.slice(0, TRACE_BATCH);
|
|
165
192
|
const seq = base;
|
|
166
193
|
dirty = false;
|
|
194
|
+
// The turn's counts and Outside rows ride beside the stream, whole: they
|
|
195
|
+
// take no seq, so an older server ignoring them sheds no step.
|
|
196
|
+
const extras = extrasPending;
|
|
197
|
+
extrasPending = false;
|
|
167
198
|
let ok = false;
|
|
168
199
|
try {
|
|
169
200
|
ok = (await post({ agentId, turnId, run, seq, entries,
|
|
170
201
|
...(orchestrationVersion === 2 ? { orchestrationVersion } : {}),
|
|
202
|
+
...(extras && notRecorded > 0 ? { notRecorded } : {}),
|
|
203
|
+
...(extras && outsideRows.size ? { outside: [...outsideRows.values()] } : {}),
|
|
204
|
+
...(extras && workflowRows.size ? { workflows: [...workflowRows.values()] } : {}),
|
|
171
205
|
})) !== false;
|
|
172
206
|
} catch {
|
|
173
207
|
ok = false;
|
|
@@ -176,6 +210,7 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
176
210
|
// Held, at the SAME seq: a retry the server has already seen is
|
|
177
211
|
// trimmed against its high-water mark rather than doubled.
|
|
178
212
|
dirty = true;
|
|
213
|
+
extrasPending ||= extras;
|
|
179
214
|
return;
|
|
180
215
|
}
|
|
181
216
|
markerPending = false;
|
|
@@ -201,6 +236,30 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
201
236
|
// The first report stamps even a silent or immediately failed CLI invocation.
|
|
202
237
|
schedule();
|
|
203
238
|
const scoped = (id) => id ? `${run}:${scrubNativeText(id, 96, scrub)}` : undefined;
|
|
239
|
+
/** A child's S4.1 detail, each field present only when its source said it. */
|
|
240
|
+
const cleanKey = v => scrubNativeText(v, SUBAGENT_KEY_MAX, scrub);
|
|
241
|
+
const detailOf = (update) => {
|
|
242
|
+
const out = {};
|
|
243
|
+
const childCli = update.cli ?? cli;
|
|
244
|
+
if (SUBAGENT_CLIS.includes(childCli)) out.cli = childCli;
|
|
245
|
+
if (typeof update.key === 'string' && update.key && update.key.length <= SUBAGENT_KEY_MAX) out.key = scrubNativeText(update.key, SUBAGENT_KEY_MAX, scrub);
|
|
246
|
+
if (update.parent === 'lead') out.parent = 'lead';
|
|
247
|
+
else if (typeof update.parent === 'string' && update.parent && update.parent !== update.id) out.parent = scoped(update.parent);
|
|
248
|
+
if (typeof update.model === 'string' && update.model) out.model = scrubNativeText(update.model, 80, scrub);
|
|
249
|
+
if (typeof update.effort === 'string' && EFFORT_RE.test(update.effort)) out.effort = update.effort;
|
|
250
|
+
// A subagent on another CLI stopped by its CLI's plan limit (E7 S7.5), cleared when it runs again.
|
|
251
|
+
if (typeof update.limited === 'boolean') out.limited = update.limited;
|
|
252
|
+
if (typeof update.parentKey === 'string') out.parentKey = cleanKey(update.parentKey);
|
|
253
|
+
if (typeof update.phase === 'string') out.phase = scrubNativeText(update.phase, 200, scrub);
|
|
254
|
+
for (const k of ['tokens', 'toolCalls']) if (Number.isSafeInteger(update[k]) && update[k] >= 0) out[k] = update[k];
|
|
255
|
+
for (const k of ['startedAt', 'endedAt']) if (typeof update[k] === 'string' && Number.isFinite(Date.parse(update[k]))) out[k] = update[k];
|
|
256
|
+
if (typeof update.result === 'string' && update.result.trim()) {
|
|
257
|
+
const full = scrubNativeText(update.result, NATIVE_WINDOW, scrub, NATIVE_WINDOW);
|
|
258
|
+
out.result = full.slice(0, SUBAGENT_RESULT_CAP);
|
|
259
|
+
out.resultStatus = update.resultStatus === 'truncated' || update.result.length > NATIVE_WINDOW || full.length > SUBAGENT_RESULT_CAP ? 'truncated' : 'complete';
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
};
|
|
204
263
|
return {
|
|
205
264
|
agentId: scoped,
|
|
206
265
|
/**
|
|
@@ -283,11 +342,44 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
283
342
|
}
|
|
284
343
|
if (typeof fields.now === 'string') fields.now = fields.now.replace(/\s+/g, ' ').trim();
|
|
285
344
|
if (SUBAGENT_STATUSES.includes(update.status)) fields.status = update.status;
|
|
345
|
+
Object.assign(fields, detailOf(update));
|
|
286
346
|
const previous = agentReadings.get(update.id) ?? {};
|
|
287
|
-
if (agentReadings.has(update.id) &&
|
|
288
|
-
|
|
347
|
+
if (!agentReadings.has(update.id) && fields.kind !== 'workflow' && [...agentReadings.values()].filter(a => a.kind !== 'workflow').length >= SUBAGENT_CAP) {
|
|
348
|
+
// A 33rd child runs; its record does not (D25): the turn says how many.
|
|
349
|
+
if (!unrecorded.has(update.id)) {
|
|
350
|
+
notRecorded += 1;
|
|
351
|
+
if (unrecorded.size < 4_096) unrecorded.add(update.id);
|
|
352
|
+
extrasPending = true; dirty = true; schedule();
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (!agentReadings.has(update.id)) {
|
|
357
|
+
// Where the lead stood when the child was first seen, and when.
|
|
358
|
+
fields.position = leadCount;
|
|
359
|
+
fields.startedAt ??= new Date().toISOString();
|
|
360
|
+
}
|
|
361
|
+
const key = fields.key ?? previous.key, title = fields.title ?? previous.title;
|
|
362
|
+
// The ledger learns a child's key (and the names a Codex follow-up may
|
|
363
|
+
// use for it) and answers the turns it appeared in, this one included.
|
|
364
|
+
if (key && (key !== previous.key || title !== previous.title) && typeof runsOf === 'function') {
|
|
365
|
+
let runs = null;
|
|
366
|
+
try { runs = runsOf(key, { title }); } catch { /* the ledger is a readout */ }
|
|
367
|
+
if (Array.isArray(runs)) fields.runs = runs.filter(r => typeof r === 'string' && RUN_ID_RE.test(r)).slice(-SUBAGENT_RUNS_CAP);
|
|
368
|
+
}
|
|
369
|
+
const status = fields.status ?? previous.status;
|
|
370
|
+
if (SETTLED_SUBAGENT_STATUSES.includes(status)) { if (!previous.endedAt) fields.endedAt ??= new Date().toISOString(); }
|
|
371
|
+
const same = (k, v) => k === 'endedAt' ? Boolean(previous.endedAt) : k === 'runs' ? JSON.stringify(previous.runs) === JSON.stringify(v) : previous[k] === v;
|
|
372
|
+
if (agentReadings.has(update.id) && Object.entries(fields).every(([k, v]) => same(k, v))) return;
|
|
289
373
|
const reading = { ...previous, ...fields };
|
|
374
|
+
// A child running again (a follow-up) has not ended.
|
|
375
|
+
if (!SETTLED_SUBAGENT_STATUSES.includes(status)) delete reading.endedAt;
|
|
290
376
|
agentReadings.set(update.id, reading);
|
|
377
|
+
if (reading.kind === 'workflow') {
|
|
378
|
+
const siblings = [...agentReadings].filter(([, a]) => a.kind === 'workflow' && a.parentKey === reading.parentKey)
|
|
379
|
+
.sort((a, b) => (b[1].startedAt ?? '').localeCompare(a[1].startedAt ?? ''));
|
|
380
|
+
for (const [id] of siblings.slice(WORKFLOW_AGENT_CAP)) agentReadings.delete(id);
|
|
381
|
+
if (!agentReadings.has(update.id)) return;
|
|
382
|
+
}
|
|
291
383
|
// A later update repairs metadata whose first report was shed while
|
|
292
384
|
// offline; a delta alone cannot recover the child's prompt or title.
|
|
293
385
|
push({ k: 'agent', id: scoped(update.id), ...reading });
|
|
@@ -319,6 +411,38 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
319
411
|
}
|
|
320
412
|
if (fresh.length) push({ k: 'note', t: scrub(`The CLI reported MCP servers that are not connected: ${fresh.join(', ')}.`).slice(0, TRACE_PROSE_CAP) });
|
|
321
413
|
},
|
|
414
|
+
/**
|
|
415
|
+
* ONE SHELL COMMAND THE LEAD RAN THAT STARTS ANOTHER CLI (D23): its row
|
|
416
|
+
* by call id, updated at its result. Shown and never blocked, never a
|
|
417
|
+
* subagent and never counted toward the 32. `row` is outsideCli.mjs's.
|
|
418
|
+
*/
|
|
419
|
+
outside(id, row) {
|
|
420
|
+
if (stopped || typeof id !== 'string' || !id || !row) return;
|
|
421
|
+
const previous = outsideRows.get(id);
|
|
422
|
+
if (!previous && outsideRows.size >= OUTSIDE_CAP) return;
|
|
423
|
+
const next = { ...(previous ?? {
|
|
424
|
+
id: scrubNativeText(id, 96, scrub), position: leadCount, startedAt: new Date().toISOString(),
|
|
425
|
+
}) };
|
|
426
|
+
if (typeof row.cli === 'string') next.cli = row.cli;
|
|
427
|
+
if (typeof row.model === 'string' && row.model) next.model = scrubNativeText(row.model, 80, scrub);
|
|
428
|
+
if (typeof row.command === 'string') next.command = scrubNativeText(row.command, OUTSIDE_COMMAND_CAP, scrub);
|
|
429
|
+
if (OUTSIDE_STATUSES.includes(row.status)) next.status = row.status;
|
|
430
|
+
if (next.status && next.status !== 'running' && !next.endedAt) next.endedAt = new Date().toISOString();
|
|
431
|
+
if (previous && JSON.stringify(previous) === JSON.stringify(next)) return;
|
|
432
|
+
outsideRows.set(id, next);
|
|
433
|
+
extrasPending = true; dirty = true; schedule();
|
|
434
|
+
},
|
|
435
|
+
workflow(row) {
|
|
436
|
+
if (stopped || !row) return;
|
|
437
|
+
if (row.launcherId && workflowPositions.size < WORKFLOW_CAP && !workflowPositions.has(row.launcherId)) workflowPositions.set(row.launcherId, leadCount);
|
|
438
|
+
if (!row.id || (!workflowRows.has(row.id) && workflowRows.size >= WORKFLOW_CAP)) return;
|
|
439
|
+
const { launcherId, ...next } = row;
|
|
440
|
+
next.position = workflowRows.get(row.id)?.position ?? workflowPositions.get(launcherId) ?? leadCount;
|
|
441
|
+
for (const k of ['id', 'key', 'name', 'description']) if (typeof next[k] === 'string') next[k] = scrubNativeText(next[k], k === 'description' ? 600 : 200, scrub);
|
|
442
|
+
next.phases = (next.phases ?? []).slice(0, 64).map(p => ({ ...p, title: scrubNativeText(p.title, 200, scrub) }));
|
|
443
|
+
workflowRows.set(row.id, next);
|
|
444
|
+
extrasPending = true; dirty = true; schedule();
|
|
445
|
+
},
|
|
322
446
|
flush,
|
|
323
447
|
/** No more entries, no more timers. The queue survives so a final flush can
|
|
324
448
|
* still deliver it. */
|
|
@@ -326,8 +450,12 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
326
450
|
if (!stopped && orchestrationVersion === 2) {
|
|
327
451
|
// Even a quiet child retains its identity after another child's noisy
|
|
328
452
|
// stream overflowed the relay. These snapshots remain sequence-safe.
|
|
329
|
-
for (const [id, reading] of agentReadings)
|
|
453
|
+
for (const [id, reading] of agentReadings) {
|
|
454
|
+
const steps = childSteps.get(scoped(id));
|
|
455
|
+
push({ k: 'agent', id: scoped(id), ...reading, ...(steps ? { steps } : {}) });
|
|
456
|
+
}
|
|
330
457
|
}
|
|
458
|
+
if (!stopped && (notRecorded > 0 || outsideRows.size || workflowRows.size)) extrasPending = true;
|
|
331
459
|
stopped = true;
|
|
332
460
|
if (timer) {
|
|
333
461
|
clearTimeout(timer);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* inside prose or another language's fence stay as the CLI wrote them. */
|
|
5
5
|
import { jsonObjectClosers } from './modelJson.mjs';
|
|
6
6
|
|
|
7
|
-
const statuses = new Set(['delivered', 'blocked', 'prerequisite', 'nothing']);
|
|
7
|
+
const statuses = new Set(['delivered', 'blocked', 'prerequisite', 'split', 'nothing']);
|
|
8
8
|
function declaration(body) {
|
|
9
9
|
const raw = body.trim();
|
|
10
10
|
if (!raw.startsWith('{')) return false;
|
|
@@ -13,13 +13,13 @@ function declaration(body) {
|
|
|
13
13
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
14
14
|
&& ((Object.hasOwn(value, 'summary') && Object.hasOwn(value, 'progress'))
|
|
15
15
|
|| (typeof value.status === 'string'
|
|
16
|
-
&& (statuses.has(value.status) || ['summary', 'progress', 'question'].some(key => Object.hasOwn(value, key)))));
|
|
16
|
+
&& (statuses.has(value.status) || ['summary', 'progress', 'question', 'split'].some(key => Object.hasOwn(value, key)))));
|
|
17
17
|
} catch {
|
|
18
18
|
// A truncated/invalid declaration must not fall through as an answer.
|
|
19
19
|
// Require a protocol key at the root's opening and the declared status;
|
|
20
20
|
// arbitrary malformed JSON remains useful diagnostic output.
|
|
21
|
-
return /^\{\s*"(?:status|summary|progress|question)"\s*:/.test(raw)
|
|
22
|
-
&& /"status"\s*:\s*"(?:delivered|blocked|prerequisite|nothing)"/.test(raw);
|
|
21
|
+
return /^\{\s*"(?:status|summary|progress|question|split)"\s*:/.test(raw)
|
|
22
|
+
&& /"status"\s*:\s*"(?:delivered|blocked|prerequisite|split|nothing)"/.test(raw);
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
package/bin/lib/turnGaps.mjs
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* listed. A malformed entry costs that entry alone.
|
|
17
17
|
*/
|
|
18
18
|
export const GAP_STATUSES = ['not built', 'partly', 'unverified', 'unowned'];
|
|
19
|
+
export const GAP_DISPOSITIONS = ['required', 'owned', 'optional', 'decision'];
|
|
19
20
|
export const MAX_GAPS = 40;
|
|
20
21
|
export const MAX_GAP_ITEM = 300;
|
|
21
22
|
export const MAX_GAP_WHY = 800;
|
|
@@ -32,7 +33,10 @@ export function parseGaps(raw, scrub) {
|
|
|
32
33
|
const status = typeof g.status === 'string' ? g.status.trim().toLowerCase() : '';
|
|
33
34
|
if (!GAP_STATUSES.includes(status)) continue;
|
|
34
35
|
const why = typeof g.why === 'string' && g.why.trim() ? cut(scrub(g.why.trim()), MAX_GAP_WHY) : null;
|
|
35
|
-
|
|
36
|
+
const disposition = GAP_DISPOSITIONS.includes(g.disposition) ? g.disposition : null;
|
|
37
|
+
const ownerTaskId = typeof g.ownerTaskId === 'string' && /^[a-zA-Z0-9_-]{1,100}$/.test(g.ownerTaskId) ? g.ownerTaskId : null;
|
|
38
|
+
out.push({ item: cut(scrub(g.item.trim()), MAX_GAP_ITEM), status, ...(why ? { why } : {}),
|
|
39
|
+
...(disposition ? { disposition } : {}), ...(ownerTaskId ? { ownerTaskId } : {}) });
|
|
36
40
|
}
|
|
37
41
|
return out;
|
|
38
42
|
}
|
|
@@ -41,7 +45,7 @@ export function parseGaps(raw, scrub) {
|
|
|
41
45
|
export function gapsMarkdown(gaps) {
|
|
42
46
|
if (!Array.isArray(gaps)) return null;
|
|
43
47
|
if (!gaps.length) return '**Gaps against the request:** none reported.';
|
|
44
|
-
return ['**Gaps against the request**', ...gaps.map((g) => `- **${g.status}** — ${oneLine(g.item)}${g.why ? `: ${oneLine(g.why)}` : ''}`)].join('\n');
|
|
48
|
+
return ['**Gaps against the request**', ...gaps.map((g) => `- **${g.status}** — ${oneLine(g.item)}${g.why ? `: ${oneLine(g.why)}` : ''}${g.disposition ? ` (${g.disposition}${g.ownerTaskId ? `: card ${g.ownerTaskId}` : ''})` : ''}`)].join('\n');
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
const oneLine = (s) => s.replace(/\s*\n\s*/g, ' ');
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
* bounds and settles its own records, so only an unowned turn is counted
|
|
18
18
|
* here; and native transcript reads are readouts, so the drain at close is
|
|
19
19
|
* bounded and never holds the card's answer.
|
|
20
|
+
*
|
|
21
|
+
* EVERY UPDATE NAMES ITS CLI AND, ONCE KNOWN, ITS KEY (2026-11-06, S4.1):
|
|
22
|
+
* the identity the CLI itself keeps for the child across turns — Claude's
|
|
23
|
+
* native agent id, Codex's child thread id — so a child resumed in a later
|
|
24
|
+
* turn is the same record there (workAgentResume.mjs keeps which turns).
|
|
20
25
|
*/
|
|
21
26
|
|
|
22
27
|
import { handleStreamLine } from './claudeStream.mjs';
|
|
@@ -24,35 +29,44 @@ import { createClaudeSubagents } from './claudeSubagents.mjs';
|
|
|
24
29
|
import { createClaudeSubagentProgress } from './claudeSubagentProgress.mjs';
|
|
25
30
|
import { createCodexSubagentReader } from './codexSubagents.mjs';
|
|
26
31
|
import { humanizeClaudeTool } from './runtimeEvents.mjs';
|
|
27
|
-
import { SUBAGENT_CAP } from './subagentBounds.mjs';
|
|
32
|
+
import { SUBAGENT_CAP, SUBAGENT_KEY_MAX } from './subagentBounds.mjs';
|
|
33
|
+
|
|
34
|
+
/** The child's stable identity, `<cli>:<native id>`, or nothing yet. */
|
|
35
|
+
export function subagentKey(runtime, update) {
|
|
36
|
+
const native = runtime === 'codex' ? update.id : runtime === 'claude' ? update.nativeId : null;
|
|
37
|
+
const key = typeof native === 'string' && native ? `${runtime}:${native}` : null;
|
|
38
|
+
return key && key.length <= SUBAGENT_KEY_MAX ? key : null;
|
|
39
|
+
}
|
|
28
40
|
|
|
29
41
|
/**
|
|
30
42
|
* `sinks` are the turn's own: its console, activity, tool-event and text
|
|
31
43
|
* sinks, which a Claude child's transcript rows feed as its stream does.
|
|
32
44
|
*/
|
|
33
|
-
export function createTurnSubagents({ runtime, cwd, since, env, owned, onSubagent, onSpawnResult, resumeThreadId,
|
|
45
|
+
export function createTurnSubagents({ runtime, cwd, since, env, owned, onSubagent, onWorkflow, onSpawnResult, resumeThreadId, knownSubagents,
|
|
34
46
|
sinks: { emit, onActivity, onToolEvent, appendText, onDetail } }) {
|
|
35
47
|
// An orchestrator bounds its own records and settles them at close
|
|
36
48
|
// (orchestrator.mjs). Counted here by stream id, refused retries used up
|
|
37
49
|
// the 32 and later children's updates were dropped; the exit sweep
|
|
38
50
|
// reported 'stopped' off stream state the controller had already settled.
|
|
39
51
|
const agentStates = new Map();
|
|
40
|
-
const reportSubagent = (
|
|
41
|
-
if (!
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
const reportSubagent = (raw) => {
|
|
53
|
+
if (!raw?.id) return;
|
|
54
|
+
const key = subagentKey(runtime, raw);
|
|
55
|
+
const update = { ...raw, cli: runtime, ...(key ? { key } : {}) };
|
|
56
|
+
// Past the cap a child is still handed on: the trace relay counts it as
|
|
57
|
+
// not recorded (its `notRecorded`), and the sweep does not follow it.
|
|
58
|
+
if (!owned && raw.kind !== 'workflow' && (agentStates.has(update.id) || agentStates.size < SUBAGENT_CAP))
|
|
44
59
|
agentStates.set(update.id, update.status ?? agentStates.get(update.id) ?? 'running');
|
|
45
|
-
}
|
|
46
60
|
onSubagent?.(update);
|
|
47
61
|
};
|
|
48
|
-
const subagents = createClaudeSubagents(reportSubagent);
|
|
62
|
+
const subagents = createClaudeSubagents(reportSubagent, onWorkflow);
|
|
49
63
|
const claudeProgress = runtime === 'claude' && onSubagent ? createClaudeSubagentProgress({
|
|
50
64
|
cwd, since, env,
|
|
51
65
|
onLine: line => handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText,
|
|
52
66
|
answerFromResult: true, subagents, onDetail }),
|
|
53
67
|
}) : null;
|
|
54
68
|
const codexAgents = runtime === 'codex' && onSubagent ? createCodexSubagentReader({
|
|
55
|
-
since, env,
|
|
69
|
+
since, env, knownTargets: knownSubagents ?? null,
|
|
56
70
|
onSubagent: reportSubagent,
|
|
57
71
|
onActivity: (activity) => {
|
|
58
72
|
emit(activity.kind === 'error' ? activity.full ?? activity.label : activity.label + '\n', activity);
|
package/bin/lib/uiReview.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { procStat } from './procStat.mjs';
|
|
|
12
12
|
import { killGroup } from './processGroup.mjs';
|
|
13
13
|
import { unattendedEnv } from './noWindowEnv.mjs';
|
|
14
14
|
import { childEnv } from './childEnv.mjs';
|
|
15
|
+
import { environmentOverlay, readWorktreeEnvironment } from './reviewEnvironment.mjs';
|
|
15
16
|
import { gitRaw } from './git.mjs';
|
|
16
17
|
import { serveMcpStdio } from './mcpStdio.mjs';
|
|
17
18
|
import { scrub, learnExtraSecrets, forgetExtraSecrets } from './uplinkScrub.mjs';
|
|
@@ -172,8 +173,10 @@ export async function createUiReview({ wt, signal, capture = captureLocalReview,
|
|
|
172
173
|
record.folder = `${ARTIFACT_DIR}/${name}`;
|
|
173
174
|
record.artifact = `${record.folder}/report.json`;
|
|
174
175
|
scratch = mkdtempSync(join(tmpdir(), 'flowviant-ui-preview-'));
|
|
175
|
-
const
|
|
176
|
-
|
|
176
|
+
const environment = readWorktreeEnvironment(wt);
|
|
177
|
+
const env = unattendedEnv({ ...childEnv({ cwd }), ...(environment ? environmentOverlay(environment) : {}) }, { browserHome: scratch });
|
|
178
|
+
// Only explicit scoped bindings join the minimal preview environment;
|
|
179
|
+
// no inherited personal home, SDK credentials or browser sessions.
|
|
177
180
|
env.HOME = scratch; env.USERPROFILE = scratch; env.XDG_CONFIG_HOME = scratch;
|
|
178
181
|
env.XDG_DATA_HOME = scratch; env.XDG_CACHE_HOME = scratch;
|
|
179
182
|
let output = '';
|
package/bin/lib/wikiDraft.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { ensureRealSubdir } from './ownedDirs.mjs';
|
|
|
8
8
|
import { isSafePath, assertWikiDraftFits } from './vaultDiff.mjs';
|
|
9
9
|
|
|
10
10
|
import { wikiScalars as scalars, protectedWikiPage } from './wikiOwnership.mjs';
|
|
11
|
+
import { SYSTEM_ARCHITECTURE_PATH, readSystemArchitecture } from './architectureReport.mjs';
|
|
11
12
|
|
|
12
13
|
/** Never follow links into an imported vault. A partial read cannot be a draft
|
|
13
14
|
* basis: fail before the CLI starts rather than infer missing pages as deleted. */
|
|
@@ -60,6 +61,14 @@ export function beginWikiDraft(vaultDir) {
|
|
|
60
61
|
discard: () => rmSync(dir, { recursive: true, force: true }),
|
|
61
62
|
publish({ sha, repoFullName }) {
|
|
62
63
|
const candidate = pages(dir, '', new Map(), true);
|
|
64
|
+
const architecture = candidate.get(SYSTEM_ARCHITECTURE_PATH);
|
|
65
|
+
const oldArchitecture = before.get(SYSTEM_ARCHITECTURE_PATH);
|
|
66
|
+
if (!protectedWikiPage(SYSTEM_ARCHITECTURE_PATH, oldArchitecture ?? architecture ?? '')) {
|
|
67
|
+
const parsed = readSystemArchitecture(architecture ?? '');
|
|
68
|
+
if (parsed.status === 'invalid' || (oldArchitecture && readSystemArchitecture(oldArchitecture).status === 'available' && parsed.status !== 'available')) {
|
|
69
|
+
throw new Error('Architecture report is incomplete or invalid; last good wiki retained');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
63
72
|
const changes = new Map();
|
|
64
73
|
for (const [path, text] of candidate) {
|
|
65
74
|
const old = before.get(path);
|
package/bin/lib/work.mjs
CHANGED
|
@@ -67,6 +67,8 @@ import { placeTurnLockedByLivePid } from './turnLock.mjs';
|
|
|
67
67
|
import { createWorkLocalCleanup } from './workLocalCleanup.mjs';
|
|
68
68
|
import { createManagedLocalCleanup } from './managedLocalCleanup.mjs';
|
|
69
69
|
import { trackedRepositorySetup } from './repositorySetup.mjs';
|
|
70
|
+
import { sweepWorktreeEnvironments } from './reviewEnvironment.mjs';
|
|
71
|
+
import { workLanesStopped } from './standDownGate.mjs';
|
|
70
72
|
import { createRepositoryPlaces } from './repositoryPlaces.mjs';
|
|
71
73
|
import { createAgentRepositories } from './agentRepositories.mjs';
|
|
72
74
|
import { createWorkProcesses } from './workProcesses.mjs';
|
|
@@ -395,6 +397,7 @@ export function createWorkManager({
|
|
|
395
397
|
// The check and the pre-review are built with only what each one uses; the
|
|
396
398
|
// review entry beat is handed the two and owns only their order.
|
|
397
399
|
const { runCheck } = createWorkAgentCheck({
|
|
400
|
+
baseRef,
|
|
398
401
|
repoRoot,
|
|
399
402
|
postBestEffort,
|
|
400
403
|
workChildren,
|
|
@@ -461,6 +464,23 @@ export function createWorkManager({
|
|
|
461
464
|
sweepMergedSessionBranch,
|
|
462
465
|
});
|
|
463
466
|
|
|
467
|
+
let environmentSweep = null, lastEnvironmentSweep = 0;
|
|
468
|
+
const reconcileEnvironments = () => {
|
|
469
|
+
if (environmentSweep || workLanesStopped() || Date.now() - lastEnvironmentSweep < 60_000) return;
|
|
470
|
+
lastEnvironmentSweep = Date.now();
|
|
471
|
+
environmentSweep = (async () => {
|
|
472
|
+
const roots = [...new Set(repositories?.list().map(r => r.repoRoot).filter(Boolean) ?? [repoRoot])];
|
|
473
|
+
for (const root of roots) {
|
|
474
|
+
if (workLanesStopped()) break;
|
|
475
|
+
const result = await sweepWorktreeEnvironments(root, {
|
|
476
|
+
onSpawn: ch => { workChildren.set(ch, null); groupKillChildren.add(ch); },
|
|
477
|
+
onClose: ch => { workChildren.delete(ch); groupKillChildren.delete(ch); },
|
|
478
|
+
});
|
|
479
|
+
for (const failure of result.errors) warn(envScrub(`Environment ${failure.id}: ${failure.error}`));
|
|
480
|
+
}
|
|
481
|
+
})().catch(error => warn(envScrub(error.message))).finally(() => { environmentSweep = null; });
|
|
482
|
+
};
|
|
483
|
+
|
|
464
484
|
const { processKillJobs } = createWorkProcesses({
|
|
465
485
|
placeDir,
|
|
466
486
|
reportPlaceWorktrees,
|
|
@@ -498,7 +518,7 @@ export function createWorkManager({
|
|
|
498
518
|
workChildren.size > 0 ||
|
|
499
519
|
workAnswering.size > 0 ||
|
|
500
520
|
pendingWorkReports.size > 0 ||
|
|
501
|
-
pendingShipReports.size > 0 || cleanupBusy();
|
|
521
|
+
pendingShipReports.size > 0 || cleanupBusy() || environmentSweep !== null;
|
|
502
522
|
|
|
503
523
|
return {
|
|
504
524
|
flushWorkReports,
|
|
@@ -514,6 +534,7 @@ export function createWorkManager({
|
|
|
514
534
|
retirePreviews,
|
|
515
535
|
shutdownPreviews,
|
|
516
536
|
retireWorkSessions,
|
|
537
|
+
reconcileEnvironments,
|
|
517
538
|
reportWorktrees,
|
|
518
539
|
reconcileIdleArtifacts,
|
|
519
540
|
reconcileLocalCleanup,
|
|
@@ -21,6 +21,9 @@ import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
|
21
21
|
import { agentBrowserHome, openBrowserHome, unattendedEnv } from './noWindowEnv.mjs';
|
|
22
22
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
23
23
|
import { workLanesStopped } from './standDownGate.mjs';
|
|
24
|
+
import { setupRepository } from './repositorySetup.mjs';
|
|
25
|
+
import { readEnvironmentRecipe } from './reviewEnvironmentConfig.mjs';
|
|
26
|
+
import { holdWorktreeEnvironment } from './reviewEnvironment.mjs';
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
29
|
* CLEAR THE ARTIFACTS DIRECTORY OF ANYTHING THAT IS NOT AN ARTIFACT, BEFORE
|
|
@@ -135,6 +138,7 @@ export function readConfiguredCheck(root) {
|
|
|
135
138
|
|
|
136
139
|
export function createWorkAgentCheck({
|
|
137
140
|
repoRoot,
|
|
141
|
+
baseRef = () => 'HEAD',
|
|
138
142
|
postBestEffort,
|
|
139
143
|
workChildren,
|
|
140
144
|
groupKillChildren,
|
|
@@ -184,6 +188,24 @@ export function createWorkAgentCheck({
|
|
|
184
188
|
await postCheck({ agentId, status: 'none', ...(headSha ? { headSha } : {}) });
|
|
185
189
|
return { status: 'none' };
|
|
186
190
|
}
|
|
191
|
+
let environment = null, releaseEnvironment = () => {};
|
|
192
|
+
try {
|
|
193
|
+
const root = repository?.repoRoot ?? repoRoot;
|
|
194
|
+
const snapshot = readEnvironmentRecipe(root, repository?.baseRef ?? baseRef());
|
|
195
|
+
if (snapshot) {
|
|
196
|
+
// A combined merge has a different worktree/database from its input
|
|
197
|
+
// agents. Migrate its CURRENT integrated source before its checks.
|
|
198
|
+
environment = await setupRepository(root, wt, { snapshot,
|
|
199
|
+
onSpawn: ch => { workChildren.set(ch, agentId); groupKillChildren.add(ch); },
|
|
200
|
+
onClose: ch => { workChildren.delete(ch); groupKillChildren.delete(ch); } });
|
|
201
|
+
releaseEnvironment = holdWorktreeEnvironment(environment);
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
const text = envScrub(error.message);
|
|
205
|
+
await postCheck({ agentId, status: 'failed', output: text, ...(headSha ? { headSha } : {}) });
|
|
206
|
+
return { status: 'failed', text };
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
187
209
|
const cleared = clearNonArtifacts(wt);
|
|
188
210
|
if (cleared.length)
|
|
189
211
|
warn(
|
|
@@ -241,7 +263,7 @@ export function createWorkAgentCheck({
|
|
|
241
263
|
shell: true,
|
|
242
264
|
detached: true,
|
|
243
265
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
244
|
-
env: checkEnv(process.env, browser.dir),
|
|
266
|
+
env: checkEnv(environment?.env ?? process.env, browser.dir),
|
|
245
267
|
});
|
|
246
268
|
/**
|
|
247
269
|
* …AND IT IS TRACKED, so a stop or a takeover takes it with them.
|
|
@@ -330,6 +352,7 @@ export function createWorkAgentCheck({
|
|
|
330
352
|
...(headSha ? { headSha } : {}),
|
|
331
353
|
});
|
|
332
354
|
return out;
|
|
355
|
+
} finally { releaseEnvironment(); }
|
|
333
356
|
};
|
|
334
357
|
|
|
335
358
|
return { runCheck };
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { existsSync, writeFileSync } from 'node:fs';
|
|
26
26
|
import { CLAUDE_SESSION_RE, CODEX_THREAD_RE, RESUME_MARKERS } from './conversationIds.mjs';
|
|
27
27
|
import { pinId, readPinnedId } from './resumeMarker.mjs';
|
|
28
|
+
import { subagentLedger } from './subagentLedger.mjs';
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* One agent turn's resume. `sessionMetaPath` is the lane's (a path in the
|
|
@@ -32,9 +33,11 @@ import { pinId, readPinnedId } from './resumeMarker.mjs';
|
|
|
32
33
|
* `resumeThreadId`, the kept `codexHome`, and what the run tells it as the
|
|
33
34
|
* CLI speaks: `sawSession` (Claude's init), `sawThread` (Codex's
|
|
34
35
|
* `thread.started`), `forgetThread` (a lost resume retried fresh), `threadNow`
|
|
35
|
-
* and, once the turn is over, `persist`.
|
|
36
|
+
* and, once the turn is over, `persist`. `subagents` is the agent's subagent
|
|
37
|
+
* ledger for this turn (subagentLedger.mjs): which turns each child appeared
|
|
38
|
+
* in, written with the markers.
|
|
36
39
|
*/
|
|
37
|
-
export function agentTurnResume({ rt, wt, agentId, sessionMetaPath }) {
|
|
40
|
+
export function agentTurnResume({ rt, wt, agentId, sessionMetaPath, turnId }) {
|
|
38
41
|
// ONE AGENT IS ONE DIRECTORY, so the CLI's own cwd-keyed resume is exactly
|
|
39
42
|
// right here — the ambiguity that forced per-tab session pinning in the
|
|
40
43
|
// Workbench (many tabs, one place) cannot arise. The marker is what
|
|
@@ -86,8 +89,13 @@ export function agentTurnResume({ rt, wt, agentId, sessionMetaPath }) {
|
|
|
86
89
|
const codexAgentHome =
|
|
87
90
|
rt === 'codex' ? sessionMetaPath(wt, 'flowviant-agent-codex-home', agentId) : null;
|
|
88
91
|
|
|
92
|
+
// Its children's turns, kept beside the markers (a child resumes only where
|
|
93
|
+
// its conversation does), per agent and per CLI: keys never cross CLIs.
|
|
94
|
+
const subagents = subagentLedger(sessionMetaPath(wt, `flowviant-agent-subagents-${rt}`, agentId), turnId);
|
|
95
|
+
|
|
89
96
|
return {
|
|
90
97
|
resume,
|
|
98
|
+
subagents,
|
|
91
99
|
resumeThreadId: codexResumeId || claudeResumeId || undefined,
|
|
92
100
|
codexHome: codexAgentHome,
|
|
93
101
|
/** Claude's session id from its init, pinned at once (a Claude turn only). */
|
|
@@ -116,6 +124,7 @@ export function agentTurnResume({ rt, wt, agentId, sessionMetaPath }) {
|
|
|
116
124
|
// id learned mid-turn is only true once the turn that learned it ends.
|
|
117
125
|
// Best-effort — the next turn runs fresh.
|
|
118
126
|
pinId(codexThreadMarker, CODEX_THREAD_RE, seenThreadId);
|
|
127
|
+
subagents.persist();
|
|
119
128
|
},
|
|
120
129
|
};
|
|
121
130
|
}
|
|
@@ -28,6 +28,7 @@ import { machineMcpServers } from './claudeMachineMcp.mjs';
|
|
|
28
28
|
import { ORCHESTRATOR_SERVER } from './orchestratorProtocol.mjs';
|
|
29
29
|
import { VIDEO_SERVER } from './videoRender.mjs';
|
|
30
30
|
import { BROWSER_SERVER } from './managedBrowser.mjs';
|
|
31
|
+
import { RUNTIME_REVIEW_SERVER } from './runtimeReview.mjs';
|
|
31
32
|
|
|
32
33
|
/** The servers the daemon itself mounts on a card turn, by the names the
|
|
33
34
|
* agent calls them: a multi-repository card's add_repository, an
|
|
@@ -35,7 +36,7 @@ import { BROWSER_SERVER } from './managedBrowser.mjs';
|
|
|
35
36
|
* render_video (2026-10-26), and a code turn's managed browser (2026-10-30).
|
|
36
37
|
* Never a project server's, whatever the base
|
|
37
38
|
* `.mcp.json` names. */
|
|
38
|
-
export const CARD_TURN_SERVERS = Object.freeze({ repositories: 'card_repositories', orchestrator: ORCHESTRATOR_SERVER, uiReview: 'card_ui_review', video: VIDEO_SERVER, browser: BROWSER_SERVER });
|
|
39
|
+
export const CARD_TURN_SERVERS = Object.freeze({ repositories: 'card_repositories', orchestrator: ORCHESTRATOR_SERVER, uiReview: 'card_ui_review', video: VIDEO_SERVER, browser: BROWSER_SERVER, runtime: RUNTIME_REVIEW_SERVER });
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* The card turn's tools, prepared, and what it is told of them.
|
|
@@ -48,13 +49,13 @@ export const CARD_TURN_SERVERS = Object.freeze({ repositories: 'card_repositorie
|
|
|
48
49
|
* from here), the note, the mount's part of the CLI's `mcpEnv` and whether it
|
|
49
50
|
* needs one, and the keys the runner takes the mount as.
|
|
50
51
|
*/
|
|
51
|
-
export function prepareCardTools({ rt, posture, wt, repoRoot, baseRef, sources = [], repositoryTool = null, orchestratorConfig = null, uiReviewConfig = null, videoConfig = null, browserConfig = null, orchestrated = false, codexHome = null, env = process.env, machineServers = machineMcpServers }) {
|
|
52
|
+
export function prepareCardTools({ rt, posture, wt, repoRoot, baseRef, sources = [], repositoryTool = null, orchestratorConfig = null, uiReviewConfig = null, videoConfig = null, browserConfig = null, runtimeConfig = null, orchestrated = false, codexHome = null, env = process.env, machineServers = machineMcpServers }) {
|
|
52
53
|
// A turn reads the base ref afresh. Changes the agent has made to its
|
|
53
54
|
// own worktree cannot silently change the next turn's tools.
|
|
54
55
|
const source = sources.find((r) => r.wt === wt);
|
|
55
|
-
const snapshot = readBaseTools(source?.repoRoot ?? repoRoot, source?.baseRef ?? baseRef());
|
|
56
|
+
const snapshot = readBaseTools(source?.repoRoot ?? repoRoot, source?.baseRef ?? baseRef(), env);
|
|
56
57
|
for (const other of sources.filter((r) => r !== source)) {
|
|
57
|
-
const base = readBaseTools(other.repoRoot, other.baseRef);
|
|
58
|
+
const base = readBaseTools(other.repoRoot, other.baseRef, env);
|
|
58
59
|
snapshot.instructions.push(...[...base.instructions, ...base.skillFiles].map((file) => ({ ...file, path: `repositories/${other.folder}/${file.path}` })));
|
|
59
60
|
}
|
|
60
61
|
const projectTools = prepareAgentTools(snapshot, rt, env, wt, { codexHome, fenced: posture !== 'build', turnServers: {
|
|
@@ -62,6 +63,7 @@ export function prepareCardTools({ rt, posture, wt, repoRoot, baseRef, sources =
|
|
|
62
63
|
...(orchestratorConfig ? { [CARD_TURN_SERVERS.orchestrator]: orchestratorConfig } : {}),
|
|
63
64
|
...(posture === 'build' && uiReviewConfig ? { [CARD_TURN_SERVERS.uiReview]: uiReviewConfig } : {}),
|
|
64
65
|
...(posture === 'build' && browserConfig ? { [CARD_TURN_SERVERS.browser]: browserConfig } : {}),
|
|
66
|
+
...(posture === 'build' && runtimeConfig ? { [CARD_TURN_SERVERS.runtime]: runtimeConfig } : {}),
|
|
65
67
|
// A video card's render tool rides its fenced posture (videoRender.mjs).
|
|
66
68
|
...(videoConfig ? { [CARD_TURN_SERVERS.video]: videoConfig } : {}),
|
|
67
69
|
} });
|