flowviant 0.125.6 → 0.125.9
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 +84 -6
- package/bin/cli.mjs +7 -1
- package/bin/lib/activityFeed.mjs +69 -0
- package/bin/lib/agentMergeReports.mjs +46 -0
- package/bin/lib/agentMergeResolution.mjs +44 -0
- package/bin/lib/agentPlan.mjs +6 -2
- package/bin/lib/claudeInput.mjs +61 -1
- package/bin/lib/claudePosture.mjs +21 -0
- package/bin/lib/claudeStream.mjs +54 -4
- package/bin/lib/codexContract.mjs +52 -0
- package/bin/lib/codexResearch.mjs +33 -5
- package/bin/lib/commandSpecs.mjs +8 -5
- package/bin/lib/config.mjs +22 -1
- package/bin/lib/daemonJournal.mjs +102 -0
- package/bin/lib/daemonLogging.mjs +22 -18
- package/bin/lib/fleet.mjs +16 -6
- package/bin/lib/fleetReports.mjs +12 -5
- package/bin/lib/hooks/orchestrator.mjs +3 -1
- package/bin/lib/logReplay.mjs +133 -0
- package/bin/lib/mcpInventory.mjs +2 -1
- package/bin/lib/mcpReasons.mjs +6 -0
- package/bin/lib/nativeAgentTools.mjs +20 -3
- package/bin/lib/observedLines.mjs +63 -0
- package/bin/lib/orchestrator.mjs +49 -7
- package/bin/lib/orchestratorNative.mjs +133 -3
- package/bin/lib/orchestratorPrompt.mjs +50 -4
- package/bin/lib/postureTools.mjs +14 -6
- package/bin/lib/printable.mjs +1 -1
- package/bin/lib/processGroup.mjs +41 -0
- package/bin/lib/projectTools.mjs +3 -2
- package/bin/lib/prompts.mjs +47 -18
- package/bin/lib/reportJournal.mjs +61 -0
- package/bin/lib/repositoryMerge.mjs +1 -1
- package/bin/lib/researchHandoff.mjs +144 -0
- package/bin/lib/researchHandoffText.mjs +3 -0
- package/bin/lib/researchKnowledge.mjs +60 -0
- package/bin/lib/researchRestoration.mjs +38 -0
- package/bin/lib/resumeLost.mjs +21 -1
- package/bin/lib/runTurn.mjs +98 -44
- package/bin/lib/runtimeCapabilities.mjs +7 -2
- package/bin/lib/runtimeClaude.mjs +30 -8
- package/bin/lib/runtimeCodex.mjs +54 -22
- package/bin/lib/runtimeEvents.mjs +17 -7
- package/bin/lib/runtimeOfferings.mjs +3 -1
- package/bin/lib/startProject.mjs +10 -0
- package/bin/lib/turnDiagnostics.mjs +2 -1
- package/bin/lib/turnOutput.mjs +63 -0
- package/bin/lib/turnRecord.mjs +11 -0
- package/bin/lib/turnSubagents.mjs +13 -3
- package/bin/lib/ui.mjs +2 -2
- package/bin/lib/update.mjs +18 -4
- package/bin/lib/uplinkScrub.mjs +49 -0
- package/bin/lib/views.mjs +49 -58
- package/bin/lib/wikiRunner.mjs +2 -2
- package/bin/lib/work.mjs +5 -5
- package/bin/lib/workActivity.mjs +65 -0
- package/bin/lib/workAgentBriefRunner.mjs +2 -2
- package/bin/lib/workAgentMerges.mjs +57 -52
- package/bin/lib/workAgentPlans.mjs +76 -27
- package/bin/lib/workAgentPrecheck.mjs +1 -1
- package/bin/lib/workAgentTurnExecution.mjs +62 -8
- package/bin/lib/workAgentTurnReports.mjs +33 -3
- package/bin/lib/workAgentTurnShape.mjs +6 -4
- package/bin/lib/workAgentTurns.mjs +12 -8
- package/bin/lib/workAttachments.mjs +27 -5
- package/bin/lib/workIntake.mjs +8 -2
- package/bin/lib/workMergeBatch.mjs +7 -1
- package/bin/lib/workNarration.mjs +32 -0
- package/bin/lib/workPlanRecovery.mjs +50 -0
- package/bin/lib/workReportQueue.mjs +18 -1
- package/bin/lib/workSessionTurns.mjs +64 -22
- package/bin/lib/worktreeDiff.mjs +29 -5
- package/package.json +1 -1
|
@@ -29,12 +29,13 @@ const oneLine = (s, n = 140) =>
|
|
|
29
29
|
* terminal console and a one-line pulse that is overwritten every two seconds,
|
|
30
30
|
* and neither can show a paragraph. The turn TRACE is a third consumer with the
|
|
31
31
|
* opposite need: it is scrollback, and clipping a sentence at 140 characters
|
|
32
|
-
* there is the product summarizing its own agent.
|
|
32
|
+
* there is the product summarizing its own agent. Errors also use `full` in
|
|
33
|
+
* the native daemon console (2026-10-19), preserving their words and lines.
|
|
33
34
|
*
|
|
34
35
|
* So a prose activity may ALSO carry `full`: the text EXACTLY as the CLI
|
|
35
|
-
* emitted it, uncollapsed and unclipped. Nothing derives it
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* emitted it, uncollapsed and unclipped. Nothing derives it. The trace and
|
|
37
|
+
* the console's errors read it; routine console activity and the pulse still
|
|
38
|
+
* read `label`. Absent means there was no fuller text than the
|
|
38
39
|
* label (a tool line, a bare thinking marker), and the trace falls back to the
|
|
39
40
|
* label — which is what every pre-0.87.0 daemon does for everything.
|
|
40
41
|
*
|
|
@@ -367,7 +368,14 @@ function codexEventOf(ev, cwd) {
|
|
|
367
368
|
exitCode: item.exit_code, isError: item.status === 'failed',
|
|
368
369
|
output: item.aggregated_output ?? '',
|
|
369
370
|
}] : null;
|
|
370
|
-
|
|
371
|
+
const mcp = (item.item_type ?? item.type) === 'mcp_tool_call';
|
|
372
|
+
const toolEvents = mcp ? [
|
|
373
|
+
...(item.arguments !== undefined ? [[`${item.server ?? 'mcp'}.${item.tool ?? ''}`, item.arguments, { id: item.id, runtime: 'codex' }]] : []),
|
|
374
|
+
[null, {}, { id: item.id, result: true, runtime: 'codex', isError: item.status === 'failed',
|
|
375
|
+
output: item.error?.message ?? (item.result === undefined ? '' : JSON.stringify(item.result)),
|
|
376
|
+
detail: item.error?.message ?? item.result ?? '' }],
|
|
377
|
+
] : [];
|
|
378
|
+
return { activity, text, ...(toolEvent ? { toolEvent } : {}), ...(toolEvents.length ? { toolEvents } : {}), ...(isMessage ? { answer: String(item.text ?? item.message ?? '') } : {}) };
|
|
371
379
|
}
|
|
372
380
|
// THE TURN'S OWN TOKEN COUNT (2026-09-24). `codex exec --json` closes a
|
|
373
381
|
// turn with `turn.completed` carrying `usage` — the codex twin of Claude's
|
|
@@ -500,18 +508,19 @@ export function parseAgyLine(line, cwd) {
|
|
|
500
508
|
const ti = su.tool_info;
|
|
501
509
|
if (!ti) return null;
|
|
502
510
|
const err = ti.error?.message;
|
|
511
|
+
const toolEvent = ti.name && ti.parameters !== undefined ? [ti.name, ti.parameters, { runtime: 'antigravity', ...(ti.id ? { id: ti.id } : {}) }] : null;
|
|
503
512
|
// Same rule as codex's error lines: the label is the console's, `full` is
|
|
504
513
|
// the trace's, and a tool error's message is exactly the kind of sentence a
|
|
505
514
|
// 140-char clamp cuts the cause out of.
|
|
506
515
|
if (err)
|
|
507
516
|
return {
|
|
508
517
|
activity: { kind: 'error', label: oneLine(err), full: String(err) },
|
|
509
|
-
text: '',
|
|
518
|
+
...(toolEvent ? { toolEvent } : {}), text: '',
|
|
510
519
|
};
|
|
511
520
|
// Each tool is reported twice — once ACTIVE, once DONE — so only the
|
|
512
521
|
// terminal state emits, otherwise every action appears in the thread twice.
|
|
513
522
|
if (su.state && su.state !== 'DONE') return null;
|
|
514
|
-
return { activity: humanizeAgyTool(ti.name, ti.parameters ?? {}, cwd), text: '' };
|
|
523
|
+
return { activity: humanizeAgyTool(ti.name, ti.parameters ?? {}, cwd), ...(toolEvent ? { toolEvent } : {}), text: '' };
|
|
515
524
|
}
|
|
516
525
|
if (ev.event === 'result') {
|
|
517
526
|
const r = ev.result ?? {};
|
|
@@ -521,6 +530,7 @@ export function parseAgyLine(line, cwd) {
|
|
|
521
530
|
activity: r.error
|
|
522
531
|
? { kind: 'error', label: oneLine(r.error), full: String(r.error) }
|
|
523
532
|
: null,
|
|
533
|
+
...(typeof r.response === 'string' && r.response ? { detail: { source: 'answer', text: r.response } } : {}),
|
|
524
534
|
text: `${r.response ?? ''}${r.error ? `\n${r.error}` : ''}\n`,
|
|
525
535
|
};
|
|
526
536
|
}
|
|
@@ -96,7 +96,9 @@ function commandFound(command, env) {
|
|
|
96
96
|
export function codexServerRows(servers, env = process.env) {
|
|
97
97
|
const rows = [];
|
|
98
98
|
for (const s of servers) {
|
|
99
|
-
|
|
99
|
+
// `mcp list` reads the person's own config; the daemon's `flowviant`
|
|
100
|
+
// rides `-c` and is never in it, so one of that name is theirs.
|
|
101
|
+
if (!s || typeof s.name !== 'string' || !(nameOk(s.name) || s.name === 'flowviant')) continue;
|
|
100
102
|
const t = s.transport && typeof s.transport === 'object' ? s.transport : {};
|
|
101
103
|
const auth = typeof s.auth_status === 'string' ? s.auth_status : '';
|
|
102
104
|
let status = 'configured';
|
package/bin/lib/startProject.mjs
CHANGED
|
@@ -208,6 +208,16 @@ export async function resolveStartProject({ dirGiven }) {
|
|
|
208
208
|
`or connect this repo to its own project with \`${terminalCommand('login')}\`.`
|
|
209
209
|
);
|
|
210
210
|
process.exit(1);
|
|
211
|
+
} else if (interactive && !CREDENTIAL.choices?.length) {
|
|
212
|
+
// NOTHING STORED AND A PERSON AT THE TERMINAL IS A FIRST CONNECTION
|
|
213
|
+
// (2026-10-19). The app's connect command ends in a bare `flowviant`
|
|
214
|
+
// and the README promises "approve the code in Flowviant → connected",
|
|
215
|
+
// yet this branch printed "no credential found" and exited 1 — so the
|
|
216
|
+
// one command every first connection runs failed after a successful
|
|
217
|
+
// install. Log in here, as the picker's connect row does; headless
|
|
218
|
+
// still refuses in the words below, because nobody can approve a code.
|
|
219
|
+
console.log('No project is connected on this machine yet — connecting one now.');
|
|
220
|
+
await reexecAfterLogin();
|
|
211
221
|
} else {
|
|
212
222
|
console.error(
|
|
213
223
|
'error: no credential found. Easiest:\n' +
|
|
@@ -12,6 +12,7 @@ import { scrub } from './uplinkScrub.mjs';
|
|
|
12
12
|
import { printableText } from './printable.mjs';
|
|
13
13
|
import { measuredRuntimeVersion } from './runtimeDetection.mjs';
|
|
14
14
|
import { VERSION } from './config.mjs';
|
|
15
|
+
import { diagnostic } from './daemonLogging.mjs';
|
|
15
16
|
|
|
16
17
|
let active = 0;
|
|
17
18
|
const authNames = (env) => AUTH_ENV_VARS.filter((name) =>
|
|
@@ -42,7 +43,7 @@ export function startTurnDiagnostics(options) {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
function createTurnDiagnostics({ runtime, bin, env, cwd = process.cwd(), profile,
|
|
45
|
-
planId = null, args = [], stdinBytes = 0, version, write = (row) =>
|
|
46
|
+
planId = null, args = [], stdinBytes = 0, version, write = (row) => diagnostic(`[cli-turn] ${JSON.stringify(row)}`) }) {
|
|
46
47
|
const startedAt = Date.now();
|
|
47
48
|
const turnId = randomUUID();
|
|
48
49
|
const executable = executableFor(bin, env, cwd);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** The human output contract for one CLI invocation. Each sink write is a
|
|
2
|
+
* complete record; no turn leaves a prefix or an unfinished line in the
|
|
3
|
+
* shared stream. `work` is local attribution, never a new machine event.
|
|
4
|
+
* Callers supply { kind, name, id, turnId }; the writer also receives the
|
|
5
|
+
* complete sanitized record as its third argument for subsequent local sinks.
|
|
6
|
+
* Its fourth argument is retention: true / failed false / unconfigured null.
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { basename } from 'node:path';
|
|
10
|
+
import { format } from 'node:util';
|
|
11
|
+
import { printableLine, printableText } from './printable.mjs';
|
|
12
|
+
import { formatTurnRecord } from './turnRecord.mjs';
|
|
13
|
+
import { retainTurnRecord } from './daemonLogging.mjs';
|
|
14
|
+
import { createLocalDetailScrubber } from './uplinkScrub.mjs';
|
|
15
|
+
import { closeActivityTurn, presentTurnRecord } from './activityFeed.mjs';
|
|
16
|
+
|
|
17
|
+
const nameOf = (value) => printableLine(printableText(value), 120);
|
|
18
|
+
const idOf = (value) => typeof value === 'string' && value ? printableLine(printableText(value), Infinity) : null;
|
|
19
|
+
|
|
20
|
+
export function createTurnOutput({ work = {}, label, cwd, runtime = 'claude', mcpEnv }, {
|
|
21
|
+
now = Date.now,
|
|
22
|
+
write = (text, source) => (source === 'error' ? process.stderr : process.stdout).write(text),
|
|
23
|
+
} = {}) {
|
|
24
|
+
// A lane without a server turn (wiki/pre-review/brief) gets a local
|
|
25
|
+
// invocation id. It is never presented as the CLI's native session id.
|
|
26
|
+
const redact = createLocalDetailScrubber(mcpEnv);
|
|
27
|
+
const retain = (text, meta = {}) => retainTurnRecord({ ...identity, ...meta, kind: redact.text(work.kind || (label ? identity.kind : runtime)), name: redact.text(work.name || identity.name),
|
|
28
|
+
text: printableText(meta.source === 'stderr' ? redact.stderr(text) : redact.text(text)) });
|
|
29
|
+
const runtimeName = nameOf(runtime) || 'CLI';
|
|
30
|
+
const turnId = idOf(work.turnId) || randomUUID();
|
|
31
|
+
const identity = Object.freeze({
|
|
32
|
+
kind: nameOf(work.kind) || nameOf(printableText(label)) || runtimeName,
|
|
33
|
+
name: nameOf(work.name) || nameOf(cwd ? basename(cwd) : '') || runtimeName,
|
|
34
|
+
workId: idOf(work.id) || turnId,
|
|
35
|
+
turnId,
|
|
36
|
+
});
|
|
37
|
+
const emit = (raw, { observedAt = now(), agentId, source = 'activity', full, kind: activityKind } = {}) => {
|
|
38
|
+
const text = printableText(String(raw));
|
|
39
|
+
if (!text) return;
|
|
40
|
+
const child = idOf(agentId);
|
|
41
|
+
const record = Object.freeze({ ...identity, observedAt, source, text, ...(activityKind ? { activityKind } : {}), ...(child ? { agentId: child } : {}) });
|
|
42
|
+
const retained = retain(typeof full === 'string' ? full : String(raw), { observedAt, source, ...(child ? { agentId: child } : {}) });
|
|
43
|
+
presentTurnRecord(record, formatTurnRecord(record, { label }), retained, write);
|
|
44
|
+
};
|
|
45
|
+
const tool = (name, input, meta = {}, observedAt = now()) => {
|
|
46
|
+
try {
|
|
47
|
+
const heading = meta.result ? `tool result ${meta.id || ''}${meta.isError ? ' (failed)' : ''}${meta.exitCode !== undefined ? ` exit=${meta.exitCode}` : ''}` : `tool call ${name || 'tool'}${meta.id ? ` id=${meta.id}` : ''}`;
|
|
48
|
+
// Scrub strings before JSON escaping can hide multiline secret values.
|
|
49
|
+
const value = meta.result ? meta.detail ?? meta.output ?? '' : input;
|
|
50
|
+
const data = typeof value === 'string' ? value : JSON.stringify(value, (_key, field) => typeof field === 'string' ? redact.text(field) : field, 2);
|
|
51
|
+
if (meta.result && (meta.isError === true || typeof meta.exitCode === 'number' && meta.exitCode !== 0))
|
|
52
|
+
emit(data || heading, { observedAt, agentId: meta.agentId, source: 'error' });
|
|
53
|
+
return retain(heading + '\n' + data, { observedAt, source: meta.result ? 'tool_result' : 'tool_call', ...(meta.agentId ? { agentId: idOf(meta.agentId) } : {}) });
|
|
54
|
+
} catch { return retain('[tool detail unavailable: could not serialize the supplied data]', { observedAt, source: 'tool_call' }); }
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
identity, emit, tool,
|
|
58
|
+
event: (text, event = 'lifecycle') => emit(text, { source: event }),
|
|
59
|
+
close: () => closeActivityTurn(turnId),
|
|
60
|
+
detail: (text, meta = {}) => retain(text, { observedAt: now(), source: 'activity', ...meta }),
|
|
61
|
+
error: (error) => emit(format(error), { source: 'error' }),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** One complete attributable human record. Shared by live output and replay. */
|
|
2
|
+
import { c, stamp } from './ui.mjs';
|
|
3
|
+
import { printableText } from './printable.mjs';
|
|
4
|
+
|
|
5
|
+
export function formatTurnRecord(record, { label, color = true } = {}) {
|
|
6
|
+
const { observedAt, kind, name, workId, turnId, agentId, text } = record;
|
|
7
|
+
const lane = `[${kind}]`;
|
|
8
|
+
const prefix = `${color ? stamp(observedAt) : printableText(stamp(observedAt))} ${label || (color ? c.cyan(lane) : lane)} ${JSON.stringify(name)} work=${JSON.stringify(workId)} turn=${JSON.stringify(turnId)}${agentId ? ` child=${JSON.stringify(agentId)}` : ''} `;
|
|
9
|
+
const body = text.endsWith('\n') ? text.slice(0, -1) : text;
|
|
10
|
+
return body.split('\n').map(line => prefix + line).join('\n') + '\n';
|
|
11
|
+
}
|
|
@@ -23,6 +23,7 @@ import { handleStreamLine } from './claudeStream.mjs';
|
|
|
23
23
|
import { createClaudeSubagents } from './claudeSubagents.mjs';
|
|
24
24
|
import { createClaudeSubagentProgress } from './claudeSubagentProgress.mjs';
|
|
25
25
|
import { createCodexSubagentReader } from './codexSubagents.mjs';
|
|
26
|
+
import { humanizeClaudeTool } from './runtimeEvents.mjs';
|
|
26
27
|
import { SUBAGENT_CAP } from './subagentBounds.mjs';
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -30,7 +31,7 @@ import { SUBAGENT_CAP } from './subagentBounds.mjs';
|
|
|
30
31
|
* sinks, which a Claude child's transcript rows feed as its stream does.
|
|
31
32
|
*/
|
|
32
33
|
export function createTurnSubagents({ runtime, cwd, since, env, owned, onSubagent, onSpawnResult, resumeThreadId,
|
|
33
|
-
sinks: { emit, onActivity, onToolEvent, appendText } }) {
|
|
34
|
+
sinks: { emit, onActivity, onToolEvent, appendText, onDetail } }) {
|
|
34
35
|
// An orchestrator bounds its own records and settles them at close
|
|
35
36
|
// (orchestrator.mjs). Counted here by stream id, refused retries used up
|
|
36
37
|
// the 32 and later children's updates were dropped; the exit sweep
|
|
@@ -48,11 +49,20 @@ export function createTurnSubagents({ runtime, cwd, since, env, owned, onSubagen
|
|
|
48
49
|
const claudeProgress = runtime === 'claude' && onSubagent ? createClaudeSubagentProgress({
|
|
49
50
|
cwd, since, env,
|
|
50
51
|
onLine: line => handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText,
|
|
51
|
-
answerFromResult: true, subagents }),
|
|
52
|
+
answerFromResult: true, subagents, onDetail }),
|
|
52
53
|
}) : null;
|
|
53
54
|
const codexAgents = runtime === 'codex' && onSubagent ? createCodexSubagentReader({
|
|
54
55
|
since, env,
|
|
55
|
-
onSubagent: reportSubagent,
|
|
56
|
+
onSubagent: reportSubagent,
|
|
57
|
+
onActivity: (activity) => {
|
|
58
|
+
emit(activity.kind === 'error' ? activity.full ?? activity.label : activity.label + '\n', activity);
|
|
59
|
+
onActivity?.(activity);
|
|
60
|
+
},
|
|
61
|
+
onToolEvent: (name, input, meta) => {
|
|
62
|
+
const activity = humanizeClaudeTool(name, input, cwd);
|
|
63
|
+
if (activity && !meta?.result) emit(activity.label + '\n', { ...activity, ...meta });
|
|
64
|
+
onToolEvent?.(name, input, meta);
|
|
65
|
+
},
|
|
56
66
|
onSpawnResult,
|
|
57
67
|
}) : null;
|
|
58
68
|
const resumed = runtime === 'codex' && typeof resumeThreadId === 'string' ? resumeThreadId : null;
|
package/bin/lib/ui.mjs
CHANGED
|
@@ -9,8 +9,8 @@ export const c = {
|
|
|
9
9
|
blue: wrap(34), magenta: wrap(35), cyan: wrap(36), gray: wrap(90),
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
const stamp = () => {
|
|
13
|
-
const d = new Date();
|
|
12
|
+
export const stamp = (observedAt = Date.now()) => {
|
|
13
|
+
const d = new Date(observedAt);
|
|
14
14
|
const p = (n) => String(n).padStart(2, '0');
|
|
15
15
|
return c.gray(`${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`);
|
|
16
16
|
};
|
package/bin/lib/update.mjs
CHANGED
|
@@ -39,7 +39,7 @@ import { platform, arch } from 'node:process';
|
|
|
39
39
|
import { pipeline } from 'node:stream/promises';
|
|
40
40
|
import { Readable } from 'node:stream';
|
|
41
41
|
import { relaunchSelf, runningCompiledBinary, runningViaNpx, selfArgv, terminalCommand } from './launchCommand.mjs';
|
|
42
|
-
import { VERSION } from './config.mjs';
|
|
42
|
+
import { DAEMON_INSTANCE, VERSION } from './config.mjs';
|
|
43
43
|
import { compareVersions, versionBelow } from './versionOrder.mjs';
|
|
44
44
|
import { note, ok, warn } from './ui.mjs';
|
|
45
45
|
|
|
@@ -194,6 +194,9 @@ export function reexec(teardown, { viaNpx = false, target = null } = {}, deps =
|
|
|
194
194
|
// as a rival and the new daemon SIGTERMed this proxy. instance.mjs adopts
|
|
195
195
|
// the lock from this pid when it is really an ancestor of the successor.
|
|
196
196
|
FLOWVIANT_REEXEC_FROM: String(process.pid),
|
|
197
|
+
// WHO THE SUCCESSOR IS, to the server: this process's own instance, so
|
|
198
|
+
// the idle tabs' session leases are already its own (config.mjs).
|
|
199
|
+
FLOWVIANT_DAEMON_INSTANCE: DAEMON_INSTANCE,
|
|
197
200
|
// WHAT WE RESTARTED IN ORDER TO BECOME. The successor compares its own
|
|
198
201
|
// VERSION against this: if it came back still short, the update did not
|
|
199
202
|
// take (a registry serving a stale `latest`, an npx cache that refused to
|
|
@@ -223,9 +226,20 @@ export function updateRestartFailed(target) {
|
|
|
223
226
|
return nowVsAttempted === -1 && targetVsAttempted !== null && targetVsAttempted <= 0;
|
|
224
227
|
}
|
|
225
228
|
|
|
226
|
-
/**
|
|
227
|
-
|
|
228
|
-
|
|
229
|
+
/**
|
|
230
|
+
* HOW LONG A GLOBAL INSTALL MAY TAKE (2026-10-19). The install is synchronous
|
|
231
|
+
* — it holds the event loop for as long as npm runs — and it had no bound, so
|
|
232
|
+
* a registry or a lock that hung npm hung the whole daemon: no poll, no wake,
|
|
233
|
+
* no turn, on a machine that read as connected. Five minutes is far past a
|
|
234
|
+
* real install; past it npm is killed and the failure is the usual one,
|
|
235
|
+
* retried in fifteen minutes.
|
|
236
|
+
*/
|
|
237
|
+
export const INSTALL_TIMEOUT_MS = 5 * 60_000;
|
|
238
|
+
|
|
239
|
+
/** Install @latest globally. Throws on failure (EACCES without sudo, offline,
|
|
240
|
+
* past `INSTALL_TIMEOUT_MS`…). `exec` and `timeoutMs` are the test seams. */
|
|
241
|
+
export function installLatest({ exec = execFileSync, timeoutMs = INSTALL_TIMEOUT_MS } = {}) {
|
|
242
|
+
exec('npm', ['install', '-g', 'flowviant@latest'], { ...npmOutsideCheckout(), stdio: 'inherit', timeout: timeoutMs, killSignal: 'SIGKILL' });
|
|
229
243
|
}
|
|
230
244
|
|
|
231
245
|
/**
|
package/bin/lib/uplinkScrub.mjs
CHANGED
|
@@ -528,3 +528,52 @@ export function secretIn(bytes) {
|
|
|
528
528
|
}
|
|
529
529
|
return null;
|
|
530
530
|
}
|
|
531
|
+
|
|
532
|
+
/** Local diagnostic retention has the same learned values and token shapes.
|
|
533
|
+
* A supplied per-turn MCP/login credential is known directly, even before an
|
|
534
|
+
* env rescan. stderr arrives as complete physical lines, so keep PEM masking
|
|
535
|
+
* state and mask the lines of learned multiline secrets too. This does not
|
|
536
|
+
* change uplink scrubbing or the CLI's foreground output.
|
|
537
|
+
*/
|
|
538
|
+
export function createLocalDetailScrubber(extraEnv = {}) {
|
|
539
|
+
const extras = Object.entries(extraEnv).filter(([name, value]) =>
|
|
540
|
+
/(?:TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|AUTH|CAPABILITY)(?:_|$)/i.test(name) && typeof value === 'string' && value);
|
|
541
|
+
const text = raw => {
|
|
542
|
+
let out = scrub(String(raw ?? ''));
|
|
543
|
+
const known = [...values.map(v => [v.name, v.value]), ...extras].sort((a, b) => b[1].length - a[1].length);
|
|
544
|
+
for (const [name, value] of known) {
|
|
545
|
+
const pieces = value.includes('\n') ? [value, ...value.split(/\r?\n/).filter(Boolean)] : [value];
|
|
546
|
+
for (const piece of pieces) out = out.split(piece).join(`[REDACTED:${name}]`);
|
|
547
|
+
}
|
|
548
|
+
return out;
|
|
549
|
+
};
|
|
550
|
+
let privateKey = false;
|
|
551
|
+
return {
|
|
552
|
+
text,
|
|
553
|
+
stderr(raw) {
|
|
554
|
+
const value = String(raw), trailing = value.endsWith('\n');
|
|
555
|
+
const lines = value.split('\n');
|
|
556
|
+
if (trailing) lines.pop();
|
|
557
|
+
return lines.map(line => {
|
|
558
|
+
PEM_BEGIN_RE.lastIndex = 0;
|
|
559
|
+
const begin = PEM_BEGIN_RE.exec(line);
|
|
560
|
+
PEM_END_RE.lastIndex = begin ? begin.index + begin[0].length : 0;
|
|
561
|
+
const end = PEM_END_RE.exec(line);
|
|
562
|
+
if (privateKey || begin) {
|
|
563
|
+
const prefix = privateKey ? '' : line.slice(0, begin.index);
|
|
564
|
+
privateKey = !end;
|
|
565
|
+
return text(prefix + '[REDACTED:PRIVATE_KEY]' + (end ? line.slice(end.index + end[0].length) : ''));
|
|
566
|
+
}
|
|
567
|
+
return text(line);
|
|
568
|
+
}).join('\n') + (trailing ? '\n' : '');
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Durable handoff fields share the portable server filter (parity checked). */
|
|
574
|
+
export function filterResearchText(text) {
|
|
575
|
+
return text
|
|
576
|
+
.replace(/(\bBearer\s+)[A-Za-z0-9._~+\/-]+=*/gi, '$1[REDACTED:credential]')
|
|
577
|
+
.replace(/(["']?(?:[A-Za-z0-9_]*(?:api[_-]?key|token|secret|password|passwd|credential)|authorization)["']?\s*[:=]\s*)(?:"[^"\n]*"|'[^'\n]*'|[^\s,;\n}]+)/gi, '$1[REDACTED:credential]')
|
|
578
|
+
.replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, '$1[REDACTED:credential]@');
|
|
579
|
+
}
|
package/bin/lib/views.mjs
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { spawn } from 'node:child_process';
|
|
11
|
-
import {
|
|
12
|
-
import { StringDecoder } from 'node:string_decoder';
|
|
11
|
+
import { statSync } from 'node:fs';
|
|
13
12
|
import { APP_URL, FLEET_URL, USER_AGENT, VERSION } from './config.mjs';
|
|
14
13
|
import { detectRepoRoot, listStoredProjects, projectLabel, resolveStoredCredential } from './credentials.mjs';
|
|
15
14
|
import { desktopStatusRemote, installChannel } from './desktopContract.mjs';
|
|
@@ -17,9 +16,11 @@ import { daemonLogPath } from './daemonState.mjs';
|
|
|
17
16
|
import { git as runGit } from './git.mjs';
|
|
18
17
|
import { daemonRunningFor } from './instance.mjs';
|
|
19
18
|
import { agoFrom } from './machineListing.mjs';
|
|
20
|
-
import { printable, printableText } from './printable.mjs';
|
|
19
|
+
import { printable, printableLine, printableText } from './printable.mjs';
|
|
21
20
|
import { detectRuntimes } from './runtimeDetection.mjs';
|
|
22
21
|
import { c } from './ui.mjs';
|
|
22
|
+
import { createLogReader } from './logReplay.mjs';
|
|
23
|
+
import { scrub } from './uplinkScrub.mjs';
|
|
23
24
|
import { versionBelow } from './versionOrder.mjs';
|
|
24
25
|
|
|
25
26
|
// "Behind" in these views is versionBelow (versionOrder.mjs): an unreadable
|
|
@@ -101,67 +102,57 @@ export function resolveProjectForView(argv = process.argv, cwd = process.cwd())
|
|
|
101
102
|
};
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
/**
|
|
105
|
-
* The last `count` lines of a file, read from its end — PRINTABLE (2026-10-10,
|
|
106
|
-
* the audit's F133). A daemon before this release logged member-written card
|
|
107
|
-
* titles raw, so a replay could carry an OSC 52 into whichever terminal runs
|
|
108
|
-
* `flowviant logs`. `printableText` keeps the lines' shape; a log written on a
|
|
109
|
-
* TTY loses its colours with everything else.
|
|
110
|
-
*/
|
|
105
|
+
/** Compatibility readout over the same retained-history reader as logs -f. */
|
|
111
106
|
export function tailLines(path, count) {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const fd = openSync(path, 'r');
|
|
116
|
-
try { readSync(fd, buf, 0, want, size - want); } finally { closeSync(fd); }
|
|
117
|
-
const lines = printableText(buf.toString('utf8')).split('\n');
|
|
118
|
-
if (lines.at(-1) === '') lines.pop();
|
|
119
|
-
if (want < size) lines.shift(); // the first line is cut mid-way
|
|
120
|
-
return { text: lines.slice(-count).join('\n'), size };
|
|
107
|
+
const reader = createLogReader(path);
|
|
108
|
+
try { return { text: reader.snapshot(count).text, size: statSync(path).size }; }
|
|
109
|
+
finally { reader.close(); }
|
|
121
110
|
}
|
|
122
111
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const path = daemonLogPath(which.entry.projectId);
|
|
112
|
+
export async function runLogs(argv = process.argv, {
|
|
113
|
+
resolveProject = resolveProjectForView, pathFor = daemonLogPath,
|
|
114
|
+
write = text => process.stdout.write(text), say = text => console.error(text),
|
|
115
|
+
signal, intervalMs = 500,
|
|
116
|
+
} = {}) {
|
|
117
|
+
const atTurn = argv.indexOf('--turn');
|
|
118
|
+
const turnId = atTurn >= 0 ? argv[atTurn + 1] : argv.find(a => a.startsWith('--turn='))?.slice(7) ?? null;
|
|
119
|
+
if (turnId !== null && (!turnId || turnId.startsWith('-'))) {
|
|
120
|
+
say('error: --turn needs the exact turn id printed in an activity line');
|
|
121
|
+
process.exitCode = 1; return;
|
|
122
|
+
}
|
|
123
|
+
const which = resolveProject(argv);
|
|
124
|
+
if (!which.entry) { say(which.message); process.exitCode = 1; return; }
|
|
125
|
+
const path = pathFor(which.entry.projectId);
|
|
138
126
|
const at = argv.indexOf('-n');
|
|
139
|
-
const count = at >= 0 && Number(argv[at + 1]) > 0 ? Math.floor(Number(argv[at + 1])) : 80;
|
|
127
|
+
const count = at >= 0 && Number.isFinite(Number(argv[at + 1])) && Number(argv[at + 1]) > 0 ? Math.max(1, Math.floor(Number(argv[at + 1]))) : 80;
|
|
140
128
|
const follow = argv.includes('-f') || argv.includes('--follow');
|
|
141
|
-
|
|
142
|
-
|
|
129
|
+
const reader = createLogReader(path, { turnId });
|
|
130
|
+
try {
|
|
131
|
+
const snapshot = reader.snapshot(count);
|
|
132
|
+
if (snapshot.text) write(snapshot.text + '\n');
|
|
133
|
+
if (!follow && snapshot.pending) say('An unfinished trailing log record was not replayed.');
|
|
134
|
+
if (snapshot.omitted) say(`Showing the last ${count} retained lines; earlier retained lines were omitted.`);
|
|
135
|
+
if (turnId !== null) {
|
|
136
|
+
if (!snapshot.matched) say(`No retained detail for turn ${printableLine(scrub(turnId), 200)}. It may have expired or predate local detail recording.${follow ? ' Waiting for new detail.' : ''}`);
|
|
137
|
+
else say('Local detail retains at most two 2 MiB files; older detail may have expired.');
|
|
138
|
+
} else if (!snapshot.found) {
|
|
139
|
+
say(`No log yet for ${projectLabel(which.entry)}. It starts when the daemon first runs: flowviant in its repo.`);
|
|
140
|
+
}
|
|
143
141
|
if (!follow) return;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (size === offset) continue;
|
|
159
|
-
const buf = Buffer.alloc(size - offset);
|
|
160
|
-
const fd = openSync(path, 'r');
|
|
161
|
-
try { readSync(fd, buf, 0, buf.length, offset); } finally { closeSync(fd); }
|
|
162
|
-
process.stdout.write(followed(buf));
|
|
163
|
-
offset = size;
|
|
164
|
-
}
|
|
142
|
+
while (!signal?.aborted) {
|
|
143
|
+
await new Promise(resolve => {
|
|
144
|
+
const done = () => { clearTimeout(timer); signal?.removeEventListener('abort', done); resolve(); };
|
|
145
|
+
const timer = setTimeout(done, intervalMs);
|
|
146
|
+
signal?.addEventListener('abort', done, { once: true });
|
|
147
|
+
});
|
|
148
|
+
if (signal?.aborted) break;
|
|
149
|
+
const next = reader.poll();
|
|
150
|
+
if (next.text) write(next.text);
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
say(`Could not read the local log (${printableText(String(error?.code || 'read failed'))}).`);
|
|
154
|
+
process.exitCode = 1;
|
|
155
|
+
} finally { reader.close(); }
|
|
165
156
|
}
|
|
166
157
|
|
|
167
158
|
/** The command that opens a URL in this computer's browser. Inside WSL that is
|
package/bin/lib/wikiRunner.mjs
CHANGED
|
@@ -442,7 +442,7 @@ export function createWikiRunner({
|
|
|
442
442
|
profile: 'wiki',
|
|
443
443
|
vaultDir,
|
|
444
444
|
runtime: wikiRt,
|
|
445
|
-
|
|
445
|
+
work: { kind: 'wiki', id: vaultDir, name: 'Wiki sweep' },
|
|
446
446
|
// No skills or connectors are harvested off a wiki turn: it is
|
|
447
447
|
// fenced (no settings source, strict MCP), so its init names less
|
|
448
448
|
// than the machine has (runTurn.mjs, `initHarvestFor`).
|
|
@@ -492,7 +492,7 @@ export function createWikiRunner({
|
|
|
492
492
|
profile: 'wiki',
|
|
493
493
|
vaultDir,
|
|
494
494
|
runtime: wikiRt,
|
|
495
|
-
|
|
495
|
+
work: { kind: 'wiki', id: vaultDir, name: task.title },
|
|
496
496
|
onActivity,
|
|
497
497
|
onUsage: onUsage(wikiRt),
|
|
498
498
|
onSpawn: (ch) => {
|
package/bin/lib/work.mjs
CHANGED
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
* whole — `workBusy` and the returned surface. A pure rule or constant
|
|
28
28
|
* (REPO_PLACE, the resume-id guards, brainFor, the turn lock) is IMPORTED by
|
|
29
29
|
* every lane that reads it, never carried through here.
|
|
30
|
-
*
|
|
31
30
|
* workPlaces.mjs where a session works, its worktree, its markers
|
|
32
31
|
* workSessionGroups.mjs which process groups each tab started
|
|
33
32
|
* workChildren.mjs the live CLI children, teardown, the admission
|
|
@@ -36,7 +35,7 @@
|
|
|
36
35
|
* workArtifactReconcile.mjs an idle agent's stored delivery against its worktree
|
|
37
36
|
* workReportQueue.mjs every settle, held and retried until delivered
|
|
38
37
|
* workSessionTokens.mjs the per-session work credential
|
|
39
|
-
* workNarration.mjs the tab's live line
|
|
38
|
+
* workNarration.mjs the tab's live line; workActivity.mjs — local outcomes and destinations
|
|
40
39
|
* workAttachments.mjs files the human attached, brought to disk (tabs and agents)
|
|
41
40
|
* workSessionRuntime.mjs which CLI a tab speaks, which conversation resumes
|
|
42
41
|
* workAdoptCarry.mjs adoption's dirty carry
|
|
@@ -48,6 +47,7 @@
|
|
|
48
47
|
* workCommandAudit.mjs one turn's `$ …` audit, batched to the server
|
|
49
48
|
* workBrain.mjs the model and effort a turn may put on argv (pure)
|
|
50
49
|
* workIntake.mjs one incoming ticket drafted into a card, read-only (2026-09-28)
|
|
50
|
+
* workPlanRecovery.mjs read-only diagnosis and attempt cancellation
|
|
51
51
|
* workCappedTurn.mjs one capped read-only consult turn: its child, its slot,
|
|
52
52
|
* its cap and its transcript — the planner, the pre-review
|
|
53
53
|
* and intake all run one (2026-10-10)
|
|
@@ -250,7 +250,7 @@ export function createWorkManager({
|
|
|
250
250
|
flushWorkReports,
|
|
251
251
|
sweepMergedSessionBranch,
|
|
252
252
|
} = createWorkReportQueue({
|
|
253
|
-
repoRoot,
|
|
253
|
+
repoRoot, journalDir: baseDir,
|
|
254
254
|
baseRef,
|
|
255
255
|
workAttempts,
|
|
256
256
|
artifacts,
|
|
@@ -261,7 +261,7 @@ export function createWorkManager({
|
|
|
261
261
|
});
|
|
262
262
|
const { workTokens, mintWorkToken } = createWorkSessionTokens({ getLeaseTtl });
|
|
263
263
|
const { makeNarrator, sayTurnDeferred, lastDeferSaid } = createWorkNarration();
|
|
264
|
-
const {
|
|
264
|
+
const { fetchTabFiles, fetchAgentFiles } = createWorkAttachments();
|
|
265
265
|
const { sessionRuntime, agyRegistryLookup } = createWorkSessionRuntime({ sessionMetaPath });
|
|
266
266
|
const { carryDirtyState } = createAdoptCarry({ sessionMetaPath });
|
|
267
267
|
const { processWorkTurns, interruptWorkTurns, workAnswering } = createWorkSessionTurns({
|
|
@@ -287,7 +287,7 @@ export function createWorkManager({
|
|
|
287
287
|
sessionRuntime,
|
|
288
288
|
agyRegistryLookup,
|
|
289
289
|
carryDirtyState,
|
|
290
|
-
|
|
290
|
+
fetchTabFiles,
|
|
291
291
|
makeNarrator,
|
|
292
292
|
sayTurnDeferred,
|
|
293
293
|
lastDeferSaid,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Local lifecycle presentation. Inputs are the lane's validated settlement,
|
|
2
|
+
* never a phrase guessed from narration or a successful process exit.
|
|
3
|
+
*/
|
|
4
|
+
import { APP_URL, PROJECT_ID } from './config.mjs';
|
|
5
|
+
import { createTurnOutput } from './turnOutput.mjs';
|
|
6
|
+
|
|
7
|
+
export const agentWork = job => ({ kind: 'agent', id: job.agentId,
|
|
8
|
+
name: job.task?.title || job.agentName || job.agentId, turnId: job.id });
|
|
9
|
+
export const sessionWork = job => ({ kind: 'tab', id: job.sessionId,
|
|
10
|
+
name: job.sessionName || job.sessionId, turnId: job.id });
|
|
11
|
+
export const planWork = job => ({ kind: 'plan', id: job.id,
|
|
12
|
+
name: (Array.isArray(job.tasks) ? job.tasks : []).map(task => task?.title).filter(Boolean).join(' · ') || 'Planning', turnId: job.id });
|
|
13
|
+
|
|
14
|
+
export function workDestination(work, { projectId = PROJECT_ID, appUrl = APP_URL, taskId } = {}) {
|
|
15
|
+
const safe = id => typeof id === 'string' && id && id !== '.' && id !== '..';
|
|
16
|
+
if (!safe(projectId) || !safe(work.id)) return null;
|
|
17
|
+
const base = `${appUrl}/projects/${encodeURIComponent(projectId)}`;
|
|
18
|
+
if (work.kind === 'agent') return `${base}/v2/agents/${encodeURIComponent(work.id)}${safe(taskId) ? `?${new URLSearchParams({ task: taskId })}` : ''}`;
|
|
19
|
+
if (work.kind === 'tab') return `${base}/v2/terminal?${new URLSearchParams({ tab: work.id })}`;
|
|
20
|
+
return `${base}/board`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function reportWorkOutcome(work, body, destination = workDestination(work)) {
|
|
24
|
+
const output = createTurnOutput({ work });
|
|
25
|
+
let text;
|
|
26
|
+
if (body.interrupted) text = 'Interrupted';
|
|
27
|
+
else if (body.outcome === 'question') text = `Needs input in Flowviant\n${body.answer || ''}`;
|
|
28
|
+
else if (body.outcome === 'delivered') text = 'Delivered for review';
|
|
29
|
+
else if (body.outcome === 'replied') text = 'Replied in the session';
|
|
30
|
+
else if (body.outcome === 'drafted') text = 'Ticket draft ready for review';
|
|
31
|
+
else if (body.outcome === 'proposed') text = 'Plan ready for review';
|
|
32
|
+
else if (body.outcome === 'merged') text = 'Merged on the base branch';
|
|
33
|
+
else text = `Failed\n${body.answer || 'The turn returned no outcome.'}`;
|
|
34
|
+
if (destination) text += `\n${body.outcome === 'question' ? 'Answer in Flowviant: ' : ''}${destination}`;
|
|
35
|
+
output.event(text, body.outcome === 'question' ? 'question' : 'outcome');
|
|
36
|
+
output.close();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Recovery settlements describe the retained operation, rather than a new
|
|
40
|
+
* review decision. The caller emits this once, before retrying its receipt. */
|
|
41
|
+
export function reportPlanOutcome(job, body) {
|
|
42
|
+
const work = planWork(job);
|
|
43
|
+
if (body.question) return reportWorkOutcome(work, { outcome: 'question',
|
|
44
|
+
answer: [body.question.question, body.question.context].filter(Boolean).join('\n\n') });
|
|
45
|
+
if (job.recovery?.start && body.proposal) {
|
|
46
|
+
const output = createTurnOutput({ work });
|
|
47
|
+
output.event('Start diagnosis complete; continuing the approved Start', 'outcome');
|
|
48
|
+
output.close();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
reportWorkOutcome(work, { outcome: body.proposal ? 'proposed' : 'nothing', answer: body.error });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function reportAgentOutcome(job, body) {
|
|
55
|
+
const work = agentWork(job);
|
|
56
|
+
if (job.kind === 'merge_resolve' && body.outcome === 'delivered') {
|
|
57
|
+
const output = createTurnOutput({ work });
|
|
58
|
+
output.event(body.mergeResolved === true
|
|
59
|
+
? 'Merge repair verified; continuing the approved Merge'
|
|
60
|
+
: 'Merge repair returned; verification is still needed', 'outcome');
|
|
61
|
+
output.close();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
reportWorkOutcome(work, body, workDestination(work, { taskId: job.task?.id }));
|
|
65
|
+
}
|
|
@@ -14,7 +14,7 @@ import { fleetEndpoint, isConsideredRefusal } from './fleetWire.mjs';
|
|
|
14
14
|
import { scrub } from './uplinkScrub.mjs';
|
|
15
15
|
|
|
16
16
|
export function createAgentBriefRunner({ admit, workChildren, run = runTurn, request = fleetRequest }) {
|
|
17
|
-
return ({ agentId, turnId, runtime, model }) => {
|
|
17
|
+
return ({ agentId, turnId, workName, runtime, model }) => {
|
|
18
18
|
const identity = { agentId, turnId, instance: DAEMON_INSTANCE };
|
|
19
19
|
return makeAgentBrief({ runtime, model, scrub, models: knownCodexModels,
|
|
20
20
|
context: async (signal) => {
|
|
@@ -46,7 +46,7 @@ export function createAgentBriefRunner({ admit, workChildren, run = runTurn, req
|
|
|
46
46
|
cwd = mkdtempSync(join(tmpdir(), 'flowviant-brief-'));
|
|
47
47
|
const result = await runCappedTurn({ runTurn: run, workChildren, releaseSlot, signal,
|
|
48
48
|
capMs: 60_000, transcriptCwd: cwd,
|
|
49
|
-
turnArgs: { ...turnArgs, cwd, profile: 'consult', answerFromResult: true,
|
|
49
|
+
turnArgs: { ...turnArgs, cwd, profile: 'consult', answerFromResult: true, work: { kind: 'brief', id: agentId, name: workName || 'Work brief' },
|
|
50
50
|
onUsage: (u) => { usage = { ...u, runtime: turnArgs.runtime }; } } });
|
|
51
51
|
return result.wedged ? '' : { out: result.out, usage };
|
|
52
52
|
} finally {
|