flowviant 0.117.0 → 0.124.0
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 +35 -0
- package/bin/cli.mjs +37 -0
- package/bin/lib/agentAsk.mjs +123 -0
- package/bin/lib/agentBrief.mjs +36 -0
- package/bin/lib/agentFootprint.mjs +76 -0
- package/bin/lib/agentInternalWait.mjs +2 -2
- package/bin/lib/agentPlan.mjs +78 -7
- package/bin/lib/agentPrerequisiteContract.mjs +43 -0
- package/bin/lib/agentTaskKinds.mjs +1 -1
- package/bin/lib/artifactContracts.mjs +91 -20
- package/bin/lib/artifactIteration.mjs +182 -0
- package/bin/lib/artifactRecovery.mjs +36 -0
- package/bin/lib/artifactReporter.mjs +183 -19
- package/bin/lib/artifacts.mjs +6 -3
- package/bin/lib/claudeAuth.mjs +15 -5
- package/bin/lib/claudePosture.mjs +110 -14
- package/bin/lib/claudeStream.mjs +20 -2
- package/bin/lib/codexProjectRead.mjs +63 -0
- package/bin/lib/codexResearch.mjs +16 -3
- package/bin/lib/commandSpecs.mjs +6 -0
- package/bin/lib/daemonLocks.mjs +5 -3
- package/bin/lib/desktopContract.mjs +3 -0
- package/bin/lib/discoveryContract.mjs +19 -0
- package/bin/lib/fleet.mjs +46 -4
- package/bin/lib/fleetReports.mjs +44 -3
- package/bin/lib/hooks/mcpAllow.mjs +60 -0
- package/bin/lib/hooks/readGuard.mjs +13 -4
- package/bin/lib/imageArtifact.mjs +74 -0
- package/bin/lib/maintenance.mjs +47 -0
- package/bin/lib/maintenanceInventory.mjs +39 -0
- package/bin/lib/mcpInstall.mjs +163 -0
- package/bin/lib/mcpInventory.mjs +119 -0
- package/bin/lib/planDependencies.mjs +70 -0
- package/bin/lib/postureTools.mjs +85 -0
- package/bin/lib/projectToolRuntimes.mjs +227 -3
- package/bin/lib/projectTools.mjs +61 -4
- package/bin/lib/prompts.mjs +164 -41
- package/bin/lib/repositoryBaseline.mjs +68 -0
- package/bin/lib/runTurn.mjs +35 -16
- package/bin/lib/runtimeCapabilities.mjs +77 -3
- package/bin/lib/runtimeClaude.mjs +27 -9
- package/bin/lib/runtimeCodex.mjs +27 -59
- package/bin/lib/runtimeDetection.mjs +6 -0
- package/bin/lib/runtimeEvents.mjs +12 -1
- package/bin/lib/runtimeOfferings.mjs +75 -3
- package/bin/lib/turnDiagnostics.mjs +93 -0
- package/bin/lib/turnProfile.mjs +15 -3
- package/bin/lib/work.mjs +28 -7
- package/bin/lib/workAgentBrief.mjs +92 -0
- package/bin/lib/workAgentBriefRunner.mjs +59 -0
- package/bin/lib/workAgentPlans.mjs +21 -70
- package/bin/lib/workAgentPrecheck.mjs +5 -0
- package/bin/lib/workAgentTurnExecution.mjs +93 -22
- package/bin/lib/workAgentTurnOutcome.mjs +11 -2
- package/bin/lib/workAgentTurns.mjs +60 -4
- package/bin/lib/workArtifactReconcile.mjs +79 -0
- package/bin/lib/workCappedTurn.mjs +10 -0
- package/bin/lib/workCommandAudit.mjs +8 -1
- package/bin/lib/workPlaces.mjs +18 -2
- package/bin/lib/workSessionTurns.mjs +148 -13
- package/bin/lib/workToolLog.mjs +70 -3
- package/bin/lib/workTurnShape.mjs +6 -4
- package/npm-shrinkwrap.json +12 -2
- package/package.json +7 -3
package/bin/lib/turnProfile.mjs
CHANGED
|
@@ -26,6 +26,14 @@
|
|
|
26
26
|
* Claude Code's and, since 0.115.0, Codex's; image (0.114.0)
|
|
27
27
|
* is Codex's.
|
|
28
28
|
*
|
|
29
|
+
* `projectMcp` (2026-10-10, the owner): whether an agent's turn under this
|
|
30
|
+
* profile mounts MCP servers — the project's reviewed base `.mcp.json` copy
|
|
31
|
+
* and, since 2026-10-11 ("the server that is running flowviant's claude HAS
|
|
32
|
+
* these MCPs yet the agents are not using them"), this machine's own CLI
|
|
33
|
+
* servers and claude.ai connectors. Every card's posture: the code card's
|
|
34
|
+
* build, and the write-up's, mockup/3D-model/deck's and image's fences. The
|
|
35
|
+
* consult, plan, wiki and plan-mode turns are not a card's and mount none.
|
|
36
|
+
*
|
|
29
37
|
* Claude's permission list for each name lives beside the lists themselves
|
|
30
38
|
* (`claudePermFor`, claudePosture.mjs); this module owns the vocabulary and the
|
|
31
39
|
* refusals, and imports the card-kind table and the runtime registry (whose
|
|
@@ -44,10 +52,12 @@ const declaredBy = (name) =>
|
|
|
44
52
|
.join(' or ') || 'no CLI this daemon knows';
|
|
45
53
|
const article = (word) => (/^[aeiou]/i.test(word) ? 'an' : 'a');
|
|
46
54
|
|
|
47
|
-
const plain = (adapterProfile) => Object.freeze({ adapterProfile, onlyOn: null, strictMcp: false });
|
|
55
|
+
const plain = (adapterProfile) => Object.freeze({ adapterProfile, onlyOn: null, strictMcp: false, projectMcp: false });
|
|
56
|
+
/** The kind postures that mount MCP servers (2026-10-10; image 2026-10-11). */
|
|
57
|
+
const KIND_PROJECT_MCP = new Set(['design', 'research', 'image']);
|
|
48
58
|
|
|
49
59
|
export const TURN_PROFILES = Object.freeze({
|
|
50
|
-
build: plain('build'),
|
|
60
|
+
build: Object.freeze({ ...plain('build'), projectMcp: true }),
|
|
51
61
|
wiki: plain('wiki'),
|
|
52
62
|
consult: plain('consult'),
|
|
53
63
|
plan: plain('plan'),
|
|
@@ -58,6 +68,7 @@ export const TURN_PROFILES = Object.freeze({
|
|
|
58
68
|
// wearing the word "plan".
|
|
59
69
|
onlyOn: (rt) => (rt.id === 'claude' ? null : `plan mode runs on Claude Code only — not '${rt.label}'`),
|
|
60
70
|
strictMcp: true,
|
|
71
|
+
projectMcp: false,
|
|
61
72
|
}),
|
|
62
73
|
// The kind postures: fenced to "write only the artifacts directory", and a
|
|
63
74
|
// runtime that does not declare one would run its build branch instead.
|
|
@@ -73,6 +84,7 @@ export const TURN_PROFILES = Object.freeze({
|
|
|
73
84
|
? null
|
|
74
85
|
: `${article(name)} ${name} card runs on ${declaredBy(name)} only — not '${rt.label}'`,
|
|
75
86
|
strictMcp: false,
|
|
87
|
+
projectMcp: KIND_PROJECT_MCP.has(name),
|
|
76
88
|
}),
|
|
77
89
|
])
|
|
78
90
|
),
|
|
@@ -83,7 +95,7 @@ export const TURN_PROFILES = Object.freeze({
|
|
|
83
95
|
export const RETIRED_POSTURE_KEYS = Object.freeze(['wikiPerm', 'readOnly', 'planPerm', 'planMode', 'posture']);
|
|
84
96
|
|
|
85
97
|
/**
|
|
86
|
-
* `{ name, adapterProfile, onlyOn, strictMcp }` for a turn's options, or
|
|
98
|
+
* `{ name, adapterProfile, onlyOn, strictMcp, projectMcp }` for a turn's options, or
|
|
87
99
|
* `{ error }` in words. Absent `profile` is build.
|
|
88
100
|
*/
|
|
89
101
|
export function resolveTurnProfile(opts = {}) {
|
package/bin/lib/work.mjs
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
* workChildren.mjs the live CLI children, teardown, the admission
|
|
34
34
|
* workAgentPublish.mjs pushing and fetching an agent's branch
|
|
35
35
|
* workPlaceReports.mjs the worktree readout: settle, burst, sweep
|
|
36
|
+
* workArtifactReconcile.mjs an idle agent's stored delivery against its worktree
|
|
36
37
|
* workReportQueue.mjs every settle, held and retried until delivered
|
|
37
38
|
* workSessionTokens.mjs the per-session work credential
|
|
38
39
|
* workNarration.mjs the tab's live line and the deferral sentence
|
|
@@ -54,8 +55,8 @@
|
|
|
54
55
|
* (and, split earlier: workDiffs, workPreviews, workPullRequests, workShip,
|
|
55
56
|
* workAgentPlans, workAgentCheck, workAgentPrecheck, workAgentReview,
|
|
56
57
|
* workAgentTurns, workAgentMerges, workProcesses; and out of workAgentTurns,
|
|
57
|
-
* SOLID F036: workAgentTurnReports, workAgentTurnExecution,
|
|
58
|
-
*
|
|
58
|
+
* SOLID F036: workAgentTurnReports, workAgentTurnExecution, workAgentTurnBegun,
|
|
59
|
+
* workAgentTurnOutcome; the live brief: workAgentBrief, workAgentBriefRunner)
|
|
59
60
|
*/
|
|
60
61
|
|
|
61
62
|
import { createWorkProcesses } from './workProcesses.mjs';
|
|
@@ -92,6 +93,7 @@ import { warn } from './ui.mjs';
|
|
|
92
93
|
import { FLOWVIANT_OWN_PATHS } from './knowledgeLibrary.mjs';
|
|
93
94
|
import { buildArtifactUpload, changedSince, snapshotArtifacts } from './artifacts.mjs';
|
|
94
95
|
import { createArtifactReporter } from './artifactReporter.mjs';
|
|
96
|
+
import { createWorkArtifactReconcile } from './workArtifactReconcile.mjs';
|
|
95
97
|
import { renderDesignPreview } from './artifactPreview.mjs';
|
|
96
98
|
import { scrub as envScrub, secretIn as envSecretIn } from './uplinkScrub.mjs';
|
|
97
99
|
|
|
@@ -156,6 +158,13 @@ export function createWorkManager({
|
|
|
156
158
|
listChanged: changedSince,
|
|
157
159
|
buildUpload: buildArtifactUpload,
|
|
158
160
|
renderPreview: renderDesignPreview,
|
|
161
|
+
// What each place held, per owner, beside its worktree's git dir: it dies
|
|
162
|
+
// with the worktree, so a recreated one proves no discard (artifactIteration.mjs).
|
|
163
|
+
// `sessionMetaPath` and `worktreeBornAt` are declared below; both are called only once the manager is built.
|
|
164
|
+
seenPath: (dir, owner) =>
|
|
165
|
+
/^[A-Za-z0-9_-]{1,64}$/.test(String(owner)) ? sessionMetaPath(dir, 'flowviant-artifacts-seen', owner) : null,
|
|
166
|
+
// …and when that worktree was cut, so rows stored before it are never its own.
|
|
167
|
+
placeBornAt: (dir) => worktreeBornAt(dir),
|
|
159
168
|
});
|
|
160
169
|
/**
|
|
161
170
|
* Snapshot a place's artifact directory before a CLI spawns in it — and make
|
|
@@ -192,7 +201,7 @@ export function createWorkManager({
|
|
|
192
201
|
const gitNet = (args, ms) => gitNetIn(args, repoRoot, ms);
|
|
193
202
|
|
|
194
203
|
// ── THE SESSION'S OWN HALF — built first, because the lanes below are handed it.
|
|
195
|
-
const { placeOf, notePlace, learnPlaces, placeDir, branchAt, placeWtFor, sessionMetaPath } =
|
|
204
|
+
const { placeOf, notePlace, learnPlaces, placeDir, branchAt, placeWtFor, sessionMetaPath, worktreeBornAt } =
|
|
196
205
|
createWorkPlaces({ repoRoot, baseDir, baseRef, onRepoChanged });
|
|
197
206
|
const { sessionGroups, noteSessionGroup, pruneSessionGroups, sessionProcesses } =
|
|
198
207
|
createWorkSessionGroups({ repoRoot });
|
|
@@ -200,6 +209,15 @@ export function createWorkManager({
|
|
|
200
209
|
createWorkChildren({ extraLiveTurns });
|
|
201
210
|
const { agentPublished, agentRemoteAt, publishAgentBranch, fetchPublishedBranch } =
|
|
202
211
|
createWorkAgentPublish({ repoRoot, gitNet });
|
|
212
|
+
const { reconcileIdleArtifacts } = createWorkArtifactReconcile({
|
|
213
|
+
artifacts,
|
|
214
|
+
placeLocks,
|
|
215
|
+
inPlace,
|
|
216
|
+
placeDir,
|
|
217
|
+
beforeArtifacts,
|
|
218
|
+
getArtifactsAccepted,
|
|
219
|
+
log: (line) => warn(line),
|
|
220
|
+
});
|
|
203
221
|
const { reportSessionWorktree, reportPlaceWorktrees, burstListeners, reportWorktrees } =
|
|
204
222
|
createWorkPlaceReports({
|
|
205
223
|
repoRoot,
|
|
@@ -237,8 +255,9 @@ export function createWorkManager({
|
|
|
237
255
|
const { fetchAttachments, fetchAgentFiles } = createWorkAttachments();
|
|
238
256
|
const { sessionRuntime, agyRegistryLookup } = createWorkSessionRuntime({ sessionMetaPath });
|
|
239
257
|
const { carryDirtyState } = createAdoptCarry({ sessionMetaPath });
|
|
240
|
-
const { processWorkTurns, workAnswering } = createWorkSessionTurns({
|
|
258
|
+
const { processWorkTurns, interruptWorkTurns, workAnswering } = createWorkSessionTurns({
|
|
241
259
|
repoRoot,
|
|
260
|
+
baseRef,
|
|
242
261
|
baseDir,
|
|
243
262
|
getMcpUrl,
|
|
244
263
|
getArtifactsAccepted,
|
|
@@ -349,7 +368,7 @@ export function createWorkManager({
|
|
|
349
368
|
});
|
|
350
369
|
const { runReviewEntry } = createWorkAgentReview({ runCheck, runPrecheck });
|
|
351
370
|
|
|
352
|
-
const { processAgentTurnJobs, settleAgentTurns, agentTurns, agentChildren, agentReported } = createWorkAgentTurns({
|
|
371
|
+
const { processAgentTurnJobs, interruptAgentTurns, settleAgentTurns, agentTurns, agentChildren, agentReported } = createWorkAgentTurns({
|
|
353
372
|
REJECT_RETRY_MS,
|
|
354
373
|
baseRef,
|
|
355
374
|
inPlace,
|
|
@@ -441,7 +460,7 @@ export function createWorkManager({
|
|
|
441
460
|
return {
|
|
442
461
|
flushWorkReports,
|
|
443
462
|
learnPlaces,
|
|
444
|
-
processWorkTurns,
|
|
463
|
+
processWorkTurns, interruptWorkTurns,
|
|
445
464
|
processShipJobs,
|
|
446
465
|
processDiffJobs,
|
|
447
466
|
processKillJobs,
|
|
@@ -453,8 +472,10 @@ export function createWorkManager({
|
|
|
453
472
|
shutdownPreviews,
|
|
454
473
|
retireWorkSessions,
|
|
455
474
|
reportWorktrees,
|
|
475
|
+
reconcileIdleArtifacts,
|
|
456
476
|
shutdownWork,
|
|
457
477
|
workBusy,
|
|
478
|
+
runningAgentCount: () => agentTurns.size,
|
|
458
479
|
// The machine's own admission answer, and what it is counting. Handed to
|
|
459
480
|
// the loop so the lanes it owns — the wiki cartographer (wikiRunner.mjs) — ask the
|
|
460
481
|
// same question, and so the machine snapshot can charge memory to the work
|
|
@@ -464,7 +485,7 @@ export function createWorkManager({
|
|
|
464
485
|
liveTurnCount,
|
|
465
486
|
processAgentPlanJobs,
|
|
466
487
|
processIntakeJobs,
|
|
467
|
-
processAgentTurnJobs,
|
|
488
|
+
processAgentTurnJobs, interruptAgentTurns,
|
|
468
489
|
// Only the roster's stand-down calls this — a displacement, a removal or a
|
|
469
490
|
// commanded stop (holder.mjs, `standDownDisplaced`); see its comment.
|
|
470
491
|
// Exported rather than hooked into `shutdownWork` because the signal
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** A readout alongside ONE running CLI, never another worker on its branch.
|
|
2
|
+
* No events since the last snapshot means no model call. A stopped child,
|
|
3
|
+
* pause, park or lost holder prevents a call or a late result from publishing. */
|
|
4
|
+
import { AGENT_BRIEF_SYSTEM, parseAgentBrief, briefModelFor } from './agentBrief.mjs';
|
|
5
|
+
|
|
6
|
+
export const AGENT_BRIEF_INTERVAL_MS = 3 * 60_000;
|
|
7
|
+
export const AGENT_BRIEF_FIRST_MS = 20_000;
|
|
8
|
+
export const AGENT_BRIEF_INPUT_MAX = 24_000;
|
|
9
|
+
export function makeAgentBrief({ runtime, model, context, summarize, post, scrub = (s) => s, models = () => [], now = Date.now,
|
|
10
|
+
setTimer = setTimeout, clearTimer = clearTimeout }) {
|
|
11
|
+
let running = false;
|
|
12
|
+
let stopped = false;
|
|
13
|
+
let timer = null;
|
|
14
|
+
let sequence = 0;
|
|
15
|
+
let consumed = 0;
|
|
16
|
+
let entries = [];
|
|
17
|
+
let flight = null;
|
|
18
|
+
let controller = null;
|
|
19
|
+
let held = null;
|
|
20
|
+
const append = (kind, data) => {
|
|
21
|
+
if (stopped || data == null) return;
|
|
22
|
+
// Scrub before trimming so a cut never turns a secret into an unrecognized prefix.
|
|
23
|
+
let text;
|
|
24
|
+
try {
|
|
25
|
+
text = (typeof data === 'string' ? scrub(data) : JSON.stringify(data, (_key, value) => {
|
|
26
|
+
// Redact decoded strings BEFORE JSON escapes quotes and newlines;
|
|
27
|
+
// otherwise a multiline secret no longer matches its learned value.
|
|
28
|
+
if (typeof value === 'string') return scrub(value);
|
|
29
|
+
if (value && typeof value === 'object' && !Array.isArray(value))
|
|
30
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [scrub(key), item]));
|
|
31
|
+
return value;
|
|
32
|
+
})).slice(0, 4_000);
|
|
33
|
+
} catch { return; } // an optional readout must never fail the working CLI
|
|
34
|
+
if (!text.trim()) return;
|
|
35
|
+
entries.push({ seq: ++sequence, kind, text });
|
|
36
|
+
while (entries.length > 100 || entries.reduce((sum, e) => sum + e.text.length, 0) > AGENT_BRIEF_INPUT_MAX) entries.shift();
|
|
37
|
+
};
|
|
38
|
+
const schedule = (ms = AGENT_BRIEF_INTERVAL_MS) => {
|
|
39
|
+
if (timer || stopped || !running) return;
|
|
40
|
+
timer = setTimer(() => { timer = null; void tick(); }, ms);
|
|
41
|
+
timer?.unref?.();
|
|
42
|
+
};
|
|
43
|
+
const refresh = async () => {
|
|
44
|
+
if (!running || stopped || (!held && sequence === consumed)) return;
|
|
45
|
+
// Retry a paid-for reading without paying for another. The API rechecks
|
|
46
|
+
// holder and turn in the write, and duplicate/older snapshots cannot win.
|
|
47
|
+
if (held) { if (await post(held)) held = null; return; }
|
|
48
|
+
const brain = briefModelFor(runtime, typeof model === 'function' ? model() : model, models());
|
|
49
|
+
if (!brain) return;
|
|
50
|
+
controller = new AbortController();
|
|
51
|
+
const state = await context(controller.signal);
|
|
52
|
+
if (state?.unsupported) { stopped = true; if (timer) clearTimer(timer); timer = null; return; }
|
|
53
|
+
if (stopped || !running || !state?.active || controller.signal.aborted) return;
|
|
54
|
+
// Card changes and daemon restarts do not reset the agent's three-minute
|
|
55
|
+
// cadence. Both dates came from the server, not this machine's clock.
|
|
56
|
+
if (state.brief && Date.parse(state.sourceAt) - Date.parse(state.brief.updatedAt) < AGENT_BRIEF_INTERVAL_MS) return;
|
|
57
|
+
const through = sequence;
|
|
58
|
+
const events = entries.filter((e) => e.seq > consumed);
|
|
59
|
+
if (!events.length) return;
|
|
60
|
+
const prompt = JSON.stringify({ previous: state.brief ?? null, taskId: state.taskId ?? null,
|
|
61
|
+
omittedEvents: Math.max(0, events[0].seq - consumed - 1), events: events.map(({ kind, text }) => ({ kind, text })) });
|
|
62
|
+
const out = await summarize({ runtime, ...brain, system: AGENT_BRIEF_SYSTEM, prompt, signal: controller.signal });
|
|
63
|
+
if (stopped || !running || controller.signal.aborted) return;
|
|
64
|
+
const content = parseAgentBrief(typeof out === 'string' ? out : out?.out, scrub);
|
|
65
|
+
if (!content) return;
|
|
66
|
+
consumed = through;
|
|
67
|
+
entries = entries.filter((e) => e.seq > through);
|
|
68
|
+
held = { ...content, sourceAt: state.sourceAt ?? new Date(now()).toISOString(), ...(out?.usage ? { usage: out.usage } : {}) };
|
|
69
|
+
if (await post(held)) held = null;
|
|
70
|
+
};
|
|
71
|
+
const tick = () => {
|
|
72
|
+
if (flight) return flight;
|
|
73
|
+
flight = refresh().catch(() => {}).finally(() => { flight = null; controller = null; schedule(); });
|
|
74
|
+
return flight;
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
append, tick,
|
|
78
|
+
running(active) {
|
|
79
|
+
running = active;
|
|
80
|
+
if (active) schedule(AGENT_BRIEF_FIRST_MS);
|
|
81
|
+
else { if (timer) clearTimer(timer); timer = null; controller?.abort(); }
|
|
82
|
+
},
|
|
83
|
+
async stop() {
|
|
84
|
+
stopped = true;
|
|
85
|
+
running = false;
|
|
86
|
+
if (timer) clearTimer(timer);
|
|
87
|
+
timer = null;
|
|
88
|
+
controller?.abort();
|
|
89
|
+
await flight;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** A separate, short-lived CLI conversation using the same vendor's login.
|
|
2
|
+
* It stands in an empty scratch directory, never in the working agent's
|
|
3
|
+
* cwd-keyed resume. No project tools, no MCP, no repository instructions. */
|
|
4
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { runTurn } from './runTurn.mjs';
|
|
8
|
+
import { runCappedTurn } from './workCappedTurn.mjs';
|
|
9
|
+
import { makeAgentBrief } from './workAgentBrief.mjs';
|
|
10
|
+
import { knownCodexModels } from './runtimeOfferings.mjs';
|
|
11
|
+
import { FLEET_URL, DAEMON_INSTANCE } from './config.mjs';
|
|
12
|
+
import { fleetRequest } from './fleetPost.mjs';
|
|
13
|
+
import { fleetEndpoint, isConsideredRefusal } from './fleetWire.mjs';
|
|
14
|
+
import { scrub } from './uplinkScrub.mjs';
|
|
15
|
+
|
|
16
|
+
export function createAgentBriefRunner({ admit, workChildren, run = runTurn, request = fleetRequest }) {
|
|
17
|
+
return ({ agentId, turnId, runtime, model }) => {
|
|
18
|
+
const identity = { agentId, turnId, instance: DAEMON_INSTANCE };
|
|
19
|
+
return makeAgentBrief({ runtime, model, scrub, models: knownCodexModels,
|
|
20
|
+
context: async (signal) => {
|
|
21
|
+
// Node >=20, including versions before AbortSignal.any was added.
|
|
22
|
+
const abort = new AbortController();
|
|
23
|
+
const timeout = AbortSignal.timeout(15_000);
|
|
24
|
+
const cancel = () => abort.abort();
|
|
25
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
26
|
+
timeout.addEventListener('abort', cancel, { once: true });
|
|
27
|
+
if (signal.aborted) cancel();
|
|
28
|
+
try {
|
|
29
|
+
const r = await request(fleetEndpoint('agent-brief-context', FLEET_URL), { body: identity, signal: abort.signal });
|
|
30
|
+
return r.ok ? r.json?.data : isConsideredRefusal(r.status) ? { unsupported: true } : null;
|
|
31
|
+
} finally {
|
|
32
|
+
signal.removeEventListener('abort', cancel);
|
|
33
|
+
timeout.removeEventListener('abort', cancel);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
post: async (content) => {
|
|
37
|
+
const r = await request(fleetEndpoint('agent-brief', FLEET_URL), { body: { ...identity, ...content }, timeoutMs: 15_000 });
|
|
38
|
+
return r.ok || isConsideredRefusal(r.status);
|
|
39
|
+
},
|
|
40
|
+
summarize: async ({ signal, ...turnArgs }) => {
|
|
41
|
+
if (signal.aborted || admit('churn')) return '';
|
|
42
|
+
const releaseSlot = admit.reserve();
|
|
43
|
+
let cwd = null;
|
|
44
|
+
let usage = null;
|
|
45
|
+
try {
|
|
46
|
+
cwd = mkdtempSync(join(tmpdir(), 'flowviant-brief-'));
|
|
47
|
+
const result = await runCappedTurn({ runTurn: run, workChildren, releaseSlot, signal,
|
|
48
|
+
capMs: 60_000, transcriptCwd: cwd,
|
|
49
|
+
turnArgs: { ...turnArgs, cwd, profile: 'consult', answerFromResult: true, label: '[brief]',
|
|
50
|
+
onUsage: (u) => { usage = { ...u, runtime: turnArgs.runtime }; } } });
|
|
51
|
+
return result.wedged ? '' : { out: result.out, usage };
|
|
52
|
+
} finally {
|
|
53
|
+
releaseSlot();
|
|
54
|
+
if (cwd) rmSync(cwd, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/** Agent plan claims, read-only planning turns, and proposal reports. */
|
|
2
|
-
import {
|
|
2
|
+
import { discoveryGuide } from './discoveryContract.mjs';
|
|
3
|
+
import { compareRepositoryBaselines, measureRepositoryBaseline, repositoryBaselineOf } from './repositoryBaseline.mjs';
|
|
4
|
+
import { knowledgeDirFor } from './knowledgeFiles.mjs';
|
|
5
|
+
import { createToolLog } from './workToolLog.mjs';
|
|
3
6
|
import { FLEET_URL, DAEMON_INSTANCE } from './config.mjs';
|
|
4
|
-
import {
|
|
7
|
+
import { measureAgentFootprint, liveAgentContext } from './agentFootprint.mjs';
|
|
5
8
|
import { parseProposal } from './agentPlan.mjs';
|
|
6
9
|
import { c, note } from './ui.mjs';
|
|
7
10
|
import { runTurn as defaultRunTurn } from './runTurn.mjs';
|
|
8
|
-
import { SYSTEM_PLAN, AGENT_PLAN_KICKOFF } from './prompts.mjs';
|
|
11
|
+
import { SYSTEM_PLAN, AGENT_PLAN_KICKOFF, withProjectContext, REPOSITORY_BASELINE_PARAGRAPH } from './prompts.mjs';
|
|
9
12
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
10
13
|
import { canRun, runtimeById, runtimeLabel } from './runtimes.mjs';
|
|
11
14
|
import { detectRuntimes as defaultDetectRuntimes, pickRuntimeFor as defaultPickRuntimeFor } from './runtimeDetection.mjs';
|
|
@@ -13,7 +16,7 @@ import { brainFor } from './workBrain.mjs';
|
|
|
13
16
|
import { runCappedTurn } from './workCappedTurn.mjs';
|
|
14
17
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
15
18
|
import { claimFleetLease, fleetHeaders } from './fleetPost.mjs';
|
|
16
|
-
import { REPO_PLACE
|
|
19
|
+
import { REPO_PLACE } from './workPlaces.mjs';
|
|
17
20
|
import { addUsage } from './turnUsage.mjs';
|
|
18
21
|
|
|
19
22
|
export function createWorkAgentPlans({
|
|
@@ -133,56 +136,6 @@ export function createWorkAgentPlans({
|
|
|
133
136
|
// The peer may hold it; doing nothing is the safe answer.
|
|
134
137
|
const claimAgentPlan = (id) => claimFleetLease(AGENT_PLAN_CLAIM_URL, { id });
|
|
135
138
|
|
|
136
|
-
/**
|
|
137
|
-
* What a live agent has already TOUCHED, measured here rather than sent.
|
|
138
|
-
*
|
|
139
|
-
* The server could have shipped a copy of this, and deliberately does not:
|
|
140
|
-
* these are directories on THIS machine, and a server-side copy would be a
|
|
141
|
-
* second, staler source of truth for a fact the daemon is standing on top of.
|
|
142
|
-
*
|
|
143
|
-
* Uncommitted work AND commits against base, because a planner asking "will
|
|
144
|
-
* these collide" cares about both — a file this agent has already rewritten
|
|
145
|
-
* and merged into its own branch collides exactly as hard as one it is
|
|
146
|
-
* editing now.
|
|
147
|
-
*/
|
|
148
|
-
const agentChangedFiles = (placeId) => {
|
|
149
|
-
if (!placeId || !isSafePathSegment(placeId)) return [];
|
|
150
|
-
const wt = worktreePath(baseDir, placeId);
|
|
151
|
-
if (!existsSync(wt)) return [];
|
|
152
|
-
const out = new Set();
|
|
153
|
-
for (const args of [
|
|
154
|
-
['diff', '--name-only', 'HEAD'],
|
|
155
|
-
['diff', '--name-only', `${baseRef()}...HEAD`],
|
|
156
|
-
['ls-files', '--others', '--exclude-standard'],
|
|
157
|
-
]) {
|
|
158
|
-
/**
|
|
159
|
-
* `git()` THROWS; it does not return a non-string.
|
|
160
|
-
*
|
|
161
|
-
* The guard below was `if (typeof r !== 'string') continue`, which is a
|
|
162
|
-
* value `execFileSync` can never produce — so every one of these calls
|
|
163
|
-
* was effectively unguarded. And this runs over the LIVE AGENTS a plan
|
|
164
|
-
* job carries, which include agents still in `planning`: they have no
|
|
165
|
-
* worktree yet, so `git diff` in a directory that does not exist exits
|
|
166
|
-
* non-zero and throws straight out of `runAgentPlan` — after the press
|
|
167
|
-
* has been claimed and before the try/finally below. The press then sat
|
|
168
|
-
* `planning` until its expiry, having spent nothing and explained
|
|
169
|
-
* nothing, on what would be an ordinary second Deploy.
|
|
170
|
-
*
|
|
171
|
-
* An agent whose files cannot be read contributes none — `gitOrNull`
|
|
172
|
-
* (git.mjs) reads a failure as null. That is the honest answer and it
|
|
173
|
-
* is what the planner should be told.
|
|
174
|
-
*/
|
|
175
|
-
const r = gitOrNull(args, wt);
|
|
176
|
-
if (r == null) continue;
|
|
177
|
-
for (const line of r.split('\n')) {
|
|
178
|
-
const f = line.trim();
|
|
179
|
-
if (f) out.add(f);
|
|
180
|
-
if (out.size >= 60) return [...out];
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return [...out];
|
|
184
|
-
};
|
|
185
|
-
|
|
186
139
|
/** See `runAgentTurn` — the admission reservation, released the moment the
|
|
187
140
|
* planner's CLI exists. */
|
|
188
141
|
const runAgentPlan = async (job, releaseSlot = () => {}) => {
|
|
@@ -268,20 +221,6 @@ export function createWorkAgentPlans({
|
|
|
268
221
|
lastPlanBeat = Date.now();
|
|
269
222
|
void postAgentPlanActivity(id, line);
|
|
270
223
|
};
|
|
271
|
-
const liveAgents = (Array.isArray(job.liveAgents) ? job.liveAgents : []).map((a) => ({
|
|
272
|
-
id: String(a?.id ?? ''),
|
|
273
|
-
name: String(a?.name ?? ''),
|
|
274
|
-
status: String(a?.status ?? ''),
|
|
275
|
-
// Belt: `agentChangedFiles` is defensive internally, but it resolves a
|
|
276
|
-
// place first and this whole block sits outside the try below.
|
|
277
|
-
changedFiles: (() => {
|
|
278
|
-
try {
|
|
279
|
-
return agentChangedFiles(a?.placeId);
|
|
280
|
-
} catch {
|
|
281
|
-
return [];
|
|
282
|
-
}
|
|
283
|
-
})(),
|
|
284
|
-
}));
|
|
285
224
|
|
|
286
225
|
let out = '';
|
|
287
226
|
/** The cap fired: the CLI was still running when this machine stopped it. */
|
|
@@ -294,7 +233,8 @@ export function createWorkAgentPlans({
|
|
|
294
233
|
* a report the retry posts again adds nothing. Null until the CLI reports.
|
|
295
234
|
*/
|
|
296
235
|
let usage = null;
|
|
297
|
-
const
|
|
236
|
+
const discovery = createToolLog(repoRoot);
|
|
237
|
+
const spent = () => ({ ...(usage ? { usage } : {}), tools: discovery.toolLog });
|
|
298
238
|
// Said BEFORE the lock is asked for, because a reader only waits when a
|
|
299
239
|
// writer holds the checkout or is queued ahead of it — which is exactly
|
|
300
240
|
// this condition, read one line before we join the queue.
|
|
@@ -315,6 +255,12 @@ export function createWorkAgentPlans({
|
|
|
315
255
|
* fork a read-only planner into a build tab, and being what their own
|
|
316
256
|
* `claude --continue` resumed.
|
|
317
257
|
*/
|
|
258
|
+
const liveAgents = (Array.isArray(job.liveAgents) ? job.liveAgents : []).map(a =>
|
|
259
|
+
liveAgentContext(a, measureAgentFootprint({ placeId: a?.placeId, baseDir, baseRef })));
|
|
260
|
+
const knowledgeDir = knowledgeDirFor(repoRoot);
|
|
261
|
+
discovery.toolLog.baseline = measureRepositoryBaseline({ cwd: repoRoot, baseRef, knowledgeDir });
|
|
262
|
+
discovery.toolLog.captureBaseline = repositoryBaselineOf(job.captureBaseline);
|
|
263
|
+
discovery.toolLog.reconciliation = compareRepositoryBaselines(discovery.toolLog.baseline, discovery.toolLog.captureBaseline);
|
|
318
264
|
({ out, wedged } = await runCappedTurn({
|
|
319
265
|
runTurn,
|
|
320
266
|
capMs: PLAN_TURN_TIMEOUT_MS,
|
|
@@ -322,12 +268,14 @@ export function createWorkAgentPlans({
|
|
|
322
268
|
releaseSlot,
|
|
323
269
|
transcriptCwd: repoRoot,
|
|
324
270
|
turnArgs: {
|
|
271
|
+
diagnosticPlanId: id,
|
|
325
272
|
prompt: AGENT_PLAN_KICKOFF({
|
|
326
273
|
tasks,
|
|
327
274
|
liveAgents,
|
|
328
275
|
agentCap: Number.isInteger(job.agentCap) && job.agentCap > 0 ? job.agentCap : 3,
|
|
329
276
|
}),
|
|
330
|
-
system: SYSTEM_PLAN,
|
|
277
|
+
system: withProjectContext(SYSTEM_PLAN, { knowledgeDir }) + '\n\n' + discoveryGuide(rt, 'planning') + '\n\n' + REPOSITORY_BASELINE_PARAGRAPH(discovery.toolLog.baseline, discovery.toolLog.captureBaseline, discovery.toolLog.reconciliation),
|
|
278
|
+
knowledgeDir,
|
|
331
279
|
// READ-ONLY, and no MCP: `mcpArgs` is omitted entirely rather than
|
|
332
280
|
// passed empty, so there is no control plane on this turn at all.
|
|
333
281
|
profile: 'consult',
|
|
@@ -342,6 +290,7 @@ export function createWorkAgentPlans({
|
|
|
342
290
|
onActivity: (a) => {
|
|
343
291
|
if (a?.label) narrate(String(a.label));
|
|
344
292
|
},
|
|
293
|
+
onToolEvent: discovery.pushToolEvent,
|
|
345
294
|
// Tagged with the CLI that counted it; one turn, so the sum is the
|
|
346
295
|
// one report (turnUsage.mjs keeps it null-safe).
|
|
347
296
|
onUsage: (u) => {
|
|
@@ -354,11 +303,13 @@ export function createWorkAgentPlans({
|
|
|
354
303
|
}));
|
|
355
304
|
});
|
|
356
305
|
} catch (e) {
|
|
306
|
+
discovery.toolLog.state = 'partial';
|
|
357
307
|
await postAgentPlan({ id, error: envScrub(String(e?.message || e)).slice(0, 500), ...spent() });
|
|
358
308
|
return;
|
|
359
309
|
}
|
|
360
310
|
|
|
361
311
|
if (wedged) {
|
|
312
|
+
discovery.toolLog.state = 'partial';
|
|
362
313
|
// NEVER LEAVE A CLAIMED PRESS UNREPORTED — the belt the merge lane wears
|
|
363
314
|
// beneath this one. The machine's own words, because the machine is the
|
|
364
315
|
// only side that knows: the server would have expired this press in
|
|
@@ -314,10 +314,15 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
314
314
|
agentId,
|
|
315
315
|
...(headSha ? { headSha } : {}),
|
|
316
316
|
...(usage ? { usage } : {}),
|
|
317
|
+
// Named fields, never a spread of what the parser returned: a field it
|
|
318
|
+
// keeps must be passed on here as well. `criteria` rides beside the verdict
|
|
319
|
+
// and note (2026-10-10), and an older server strips it and draws the one
|
|
320
|
+
// row it always drew.
|
|
317
321
|
cards: result.cards.map((cd) => ({
|
|
318
322
|
taskId: cd.taskId,
|
|
319
323
|
verdict: cd.verdict,
|
|
320
324
|
...(cd.note ? { note: cd.note } : {}),
|
|
325
|
+
...(cd.criteria ? { criteria: cd.criteria } : {}),
|
|
321
326
|
})),
|
|
322
327
|
...(result.overall ? { overall: result.overall } : {}),
|
|
323
328
|
};
|