flowviant 0.115.0 → 0.117.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 +31 -12
- package/bin/cli.mjs +13 -17
- package/bin/lib/admission.mjs +7 -14
- package/bin/lib/agentInternalWait.mjs +83 -0
- package/bin/lib/agentPlan.mjs +56 -24
- package/bin/lib/agentPublish.mjs +12 -13
- package/bin/lib/agentTaskKinds.mjs +4 -7
- package/bin/lib/agySessions.mjs +3 -3
- package/bin/lib/artifactPreview.mjs +92 -18
- package/bin/lib/artifactReporter.mjs +16 -8
- package/bin/lib/artifacts.mjs +64 -60
- package/bin/lib/authproxy.mjs +2 -1
- package/bin/lib/childEnv.mjs +9 -4
- package/bin/lib/claude.mjs +10 -20
- package/bin/lib/claudePersonal.mjs +71 -6
- package/bin/lib/claudePosture.mjs +119 -42
- package/bin/lib/claudeSessions.mjs +7 -7
- package/bin/lib/claudeStream.mjs +7 -11
- package/bin/lib/cliLimit.mjs +2 -2
- package/bin/lib/codexHome.mjs +17 -0
- package/bin/lib/codexPersonal.mjs +44 -3
- package/bin/lib/codexRelay.mjs +21 -5
- package/bin/lib/codexResearch.mjs +180 -0
- package/bin/lib/config.mjs +37 -24
- package/bin/lib/conversationIds.mjs +51 -0
- package/bin/lib/credentials.mjs +2 -7
- package/bin/lib/daemonLocks.mjs +88 -0
- package/bin/lib/daemonState.mjs +5 -5
- package/bin/lib/deploy.mjs +6 -9
- package/bin/lib/deployCheckout.mjs +26 -22
- package/bin/lib/deployRunner.mjs +54 -18
- package/bin/lib/deployWire.mjs +32 -23
- package/bin/lib/desktopContract.mjs +3 -5
- package/bin/lib/env.mjs +104 -35
- package/bin/lib/fleet.mjs +7 -9
- package/bin/lib/fleetPost.mjs +172 -25
- package/bin/lib/fleetReports.mjs +130 -170
- package/bin/lib/fleetStartup.mjs +6 -6
- package/bin/lib/fleetWire.mjs +42 -0
- package/bin/lib/git.mjs +111 -61
- package/bin/lib/gitMergeIdentity.mjs +2 -8
- package/bin/lib/grant.mjs +23 -7
- package/bin/lib/holder.mjs +108 -81
- package/bin/lib/hooks/readGuard.mjs +41 -7
- package/bin/lib/install.mjs +80 -33
- package/bin/lib/instance.mjs +36 -83
- package/bin/lib/intakeDraft.mjs +4 -1
- package/bin/lib/knowledgeFileSync.mjs +31 -0
- package/bin/lib/knowledgeFiles.mjs +28 -83
- package/bin/lib/landed.mjs +22 -29
- package/bin/lib/launchCommand.mjs +91 -0
- package/bin/lib/listeners.mjs +0 -5
- package/bin/lib/localSessions.mjs +52 -39
- package/bin/lib/login.mjs +22 -12
- package/bin/lib/loginPath.mjs +22 -17
- package/bin/lib/machineListing.mjs +6 -3
- package/bin/lib/mcp-cli.mjs +9 -10
- package/bin/lib/noWindowEnv.mjs +23 -3
- package/bin/lib/ownedDirs.mjs +99 -0
- package/bin/lib/pinnedCapture.mjs +383 -0
- package/bin/lib/prWorkflow.mjs +94 -17
- package/bin/lib/preflight.mjs +8 -25
- package/bin/lib/preview.mjs +19 -25
- package/bin/lib/previewGatePolicy.mjs +34 -9
- package/bin/lib/previewRegistry.mjs +4 -11
- package/bin/lib/printable.mjs +70 -7
- package/bin/lib/procRegistry.mjs +33 -13
- package/bin/lib/procScan.mjs +2 -3
- package/bin/lib/procStat.mjs +76 -0
- package/bin/lib/processGroup.mjs +48 -0
- package/bin/lib/processes.mjs +10 -24
- package/bin/lib/projectToolRuntimes.mjs +101 -16
- package/bin/lib/projectTools.mjs +50 -32
- package/bin/lib/prompts.mjs +41 -19
- package/bin/lib/repoPath.mjs +19 -0
- package/bin/lib/repoState.mjs +17 -19
- package/bin/lib/runTurn.mjs +268 -210
- package/bin/lib/runtimeAntigravity.mjs +16 -5
- package/bin/lib/runtimeCapabilities.mjs +140 -20
- package/bin/lib/runtimeClaude.mjs +55 -5
- package/bin/lib/runtimeCodex.mjs +245 -205
- package/bin/lib/runtimeDetection.mjs +2 -2
- package/bin/lib/runtimeEvents.mjs +16 -10
- package/bin/lib/runtimeLimits.mjs +5 -6
- package/bin/lib/runtimeOfferings.mjs +24 -11
- package/bin/lib/runtimes.mjs +10 -0
- package/bin/lib/shipSweep.mjs +5 -3
- package/bin/lib/shot.mjs +91 -17
- package/bin/lib/sleep.mjs +11 -0
- package/bin/lib/startProject.mjs +5 -5
- package/bin/lib/statePaths.mjs +40 -0
- package/bin/lib/tty.mjs +25 -15
- package/bin/lib/turnLock.mjs +7 -14
- package/bin/lib/turnProfile.mjs +1 -1
- package/bin/lib/ui.mjs +0 -3
- package/bin/lib/uninstall.mjs +30 -26
- package/bin/lib/update.mjs +79 -27
- package/bin/lib/uplinkScrub.mjs +296 -31
- package/bin/lib/vault.mjs +78 -10
- package/bin/lib/vaultDiff.mjs +5 -2
- package/bin/lib/views.mjs +37 -12
- package/bin/lib/wikiRunner.mjs +40 -61
- package/bin/lib/work.mjs +17 -7
- package/bin/lib/workAdoptCarry.mjs +2 -4
- package/bin/lib/workAgentCheck.mjs +104 -57
- package/bin/lib/workAgentMerges.mjs +62 -57
- package/bin/lib/workAgentPlans.mjs +99 -145
- package/bin/lib/workAgentPrecheck.mjs +71 -138
- package/bin/lib/workAgentPublish.mjs +6 -8
- package/bin/lib/workAgentTurnBegun.mjs +15 -11
- package/bin/lib/workAgentTurnExecution.mjs +46 -53
- package/bin/lib/workAgentTurnReports.mjs +9 -37
- package/bin/lib/workAgentTurns.mjs +4 -3
- package/bin/lib/workAttachments.mjs +66 -11
- package/bin/lib/workBrain.mjs +9 -4
- package/bin/lib/workCappedTurn.mjs +109 -0
- package/bin/lib/workChildren.mjs +5 -5
- package/bin/lib/workCommandAudit.mjs +11 -2
- package/bin/lib/workDiffs.mjs +52 -6
- package/bin/lib/workIntake.mjs +35 -60
- package/bin/lib/workPlaceReports.mjs +7 -4
- package/bin/lib/workPlaces.mjs +96 -30
- package/bin/lib/workPreviews.mjs +47 -112
- package/bin/lib/workProcesses.mjs +8 -35
- package/bin/lib/workPullRequests.mjs +37 -56
- package/bin/lib/workReportQueue.mjs +100 -148
- package/bin/lib/workRetire.mjs +2 -1
- package/bin/lib/workSessionRuntime.mjs +38 -37
- package/bin/lib/workSessionTurns.mjs +111 -189
- package/bin/lib/workShip.mjs +43 -23
- package/bin/lib/workTurnShape.mjs +163 -0
- package/npm-shrinkwrap.json +59 -0
- package/package.json +3 -2
- package/bin/lib/fleetJobs.mjs +0 -120
- package/bin/lib/patchRevert.mjs +0 -60
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
/** Agent plan claims, read-only planning turns, and proposal reports. */
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { git, isSafePathSegment } from './git.mjs';
|
|
3
|
+
import { FLEET_URL, DAEMON_INSTANCE } from './config.mjs';
|
|
4
|
+
import { gitOrNull, isSafePathSegment } from './git.mjs';
|
|
6
5
|
import { parseProposal } from './agentPlan.mjs';
|
|
7
6
|
import { c, note } from './ui.mjs';
|
|
8
7
|
import { runTurn as defaultRunTurn } from './runTurn.mjs';
|
|
9
8
|
import { SYSTEM_PLAN, AGENT_PLAN_KICKOFF } from './prompts.mjs';
|
|
10
9
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
11
|
-
import {
|
|
12
|
-
import { pickRuntimeFor as defaultPickRuntimeFor } from './runtimeDetection.mjs';
|
|
13
|
-
import {
|
|
10
|
+
import { canRun, runtimeById, runtimeLabel } from './runtimes.mjs';
|
|
11
|
+
import { detectRuntimes as defaultDetectRuntimes, pickRuntimeFor as defaultPickRuntimeFor } from './runtimeDetection.mjs';
|
|
12
|
+
import { brainFor } from './workBrain.mjs';
|
|
13
|
+
import { runCappedTurn } from './workCappedTurn.mjs';
|
|
14
14
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
15
|
-
import {
|
|
15
|
+
import { claimFleetLease, fleetHeaders } from './fleetPost.mjs';
|
|
16
|
+
import { REPO_PLACE, worktreePath } from './workPlaces.mjs';
|
|
16
17
|
import { addUsage } from './turnUsage.mjs';
|
|
17
18
|
|
|
18
19
|
export function createWorkAgentPlans({
|
|
@@ -24,10 +25,11 @@ export function createWorkAgentPlans({
|
|
|
24
25
|
admit,
|
|
25
26
|
placeLocks,
|
|
26
27
|
workChildren,
|
|
27
|
-
// The real CLI driver and picker; injectable
|
|
28
|
+
// The real CLI driver, detector and picker; injectable so the lane can be driven
|
|
28
29
|
// in a test without spending a model turn (the wiki runner's shape).
|
|
29
30
|
runTurn = defaultRunTurn,
|
|
30
31
|
pickRuntimeFor = defaultPickRuntimeFor,
|
|
32
|
+
detectRuntimes = defaultDetectRuntimes,
|
|
31
33
|
}) {
|
|
32
34
|
const AGENT_PLAN_DONE_URL = fleetEndpoint('agent-plan-done', FLEET_URL);
|
|
33
35
|
const AGENT_PLAN_ACTIVITY_URL = fleetEndpoint('agent-plan-activity', FLEET_URL);
|
|
@@ -41,8 +43,11 @@ export function createWorkAgentPlans({
|
|
|
41
43
|
// creates a worktree, a branch or a card.
|
|
42
44
|
//
|
|
43
45
|
// READ-ONLY IN THE CHECKOUT. `profile: 'consult'` selects `consultPermFor` (the
|
|
44
|
-
// fenced Read/Glob rules plus a few
|
|
45
|
-
//
|
|
46
|
+
// fenced Read/Glob rules plus a few shell readers: `ls`/`cat`/`head`, which
|
|
47
|
+
// the CLI path-validates itself, and the `git` readers, which it does not —
|
|
48
|
+
// their path fence is the read guard's refusal (hooks/readGuard.mjs), of
|
|
49
|
+
// `--no-index` and of any git argument aimed off the checkout — and no
|
|
50
|
+
// Write, no Edit, no mkdir, no rm) and NO MCP is passed at all, so this
|
|
46
51
|
// turn has no control plane to reach even if the repository it reads tries
|
|
47
52
|
// to steer it. The proposal comes back as the turn's final message rather
|
|
48
53
|
// than through a tool, which is exactly what lets that permission set be
|
|
@@ -66,8 +71,8 @@ export function createWorkAgentPlans({
|
|
|
66
71
|
* on a PRESS instead of an agent.
|
|
67
72
|
*
|
|
68
73
|
* This turn was already streaming and the daemon was already reading it:
|
|
69
|
-
*
|
|
70
|
-
* it behind `[plan]` on the operator's terminal. Every one of those lines
|
|
74
|
+
* runTurn's event reader humanizes every read, grep, command and thought
|
|
75
|
+
* and prints it behind `[plan]` on the operator's terminal. Every one of those lines
|
|
71
76
|
* was then thrown away, so a Deploy press said "planning" on the board and
|
|
72
77
|
* nothing else for as long as it took — which on an overnight queue is the
|
|
73
78
|
* whole night. Forwarding the CLI's own tail costs nothing that is not
|
|
@@ -88,11 +93,7 @@ export function createWorkAgentPlans({
|
|
|
88
93
|
try {
|
|
89
94
|
await fetch(AGENT_PLAN_ACTIVITY_URL, {
|
|
90
95
|
method: 'POST',
|
|
91
|
-
headers:
|
|
92
|
-
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
93
|
-
'User-Agent': USER_AGENT,
|
|
94
|
-
'Content-Type': 'application/json',
|
|
95
|
-
},
|
|
96
|
+
headers: fleetHeaders(),
|
|
96
97
|
signal: AbortSignal.timeout(15_000),
|
|
97
98
|
body: JSON.stringify({ planId, line: text }),
|
|
98
99
|
});
|
|
@@ -129,24 +130,8 @@ export function createWorkAgentPlans({
|
|
|
129
130
|
*/
|
|
130
131
|
const PLAN_TURN_TIMEOUT_MS = 15 * 60_000;
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const res = await fetch(AGENT_PLAN_CLAIM_URL, {
|
|
135
|
-
method: 'POST',
|
|
136
|
-
headers: {
|
|
137
|
-
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
138
|
-
'User-Agent': USER_AGENT,
|
|
139
|
-
'Content-Type': 'application/json',
|
|
140
|
-
},
|
|
141
|
-
signal: AbortSignal.timeout(15_000),
|
|
142
|
-
body: JSON.stringify({ id, instance: DAEMON_INSTANCE }),
|
|
143
|
-
});
|
|
144
|
-
const j = await res.json().catch(() => null);
|
|
145
|
-
return j?.data?.claimed === true;
|
|
146
|
-
} catch {
|
|
147
|
-
return false; // the peer may hold it; doing nothing is the safe answer
|
|
148
|
-
}
|
|
149
|
-
};
|
|
133
|
+
// The peer may hold it; doing nothing is the safe answer.
|
|
134
|
+
const claimAgentPlan = (id) => claimFleetLease(AGENT_PLAN_CLAIM_URL, { id });
|
|
150
135
|
|
|
151
136
|
/**
|
|
152
137
|
* What a live agent has already TOUCHED, measured here rather than sent.
|
|
@@ -162,7 +147,7 @@ export function createWorkAgentPlans({
|
|
|
162
147
|
*/
|
|
163
148
|
const agentChangedFiles = (placeId) => {
|
|
164
149
|
if (!placeId || !isSafePathSegment(placeId)) return [];
|
|
165
|
-
const wt =
|
|
150
|
+
const wt = worktreePath(baseDir, placeId);
|
|
166
151
|
if (!existsSync(wt)) return [];
|
|
167
152
|
const out = new Set();
|
|
168
153
|
for (const args of [
|
|
@@ -183,16 +168,12 @@ export function createWorkAgentPlans({
|
|
|
183
168
|
* `planning` until its expiry, having spent nothing and explained
|
|
184
169
|
* nothing, on what would be an ordinary second Deploy.
|
|
185
170
|
*
|
|
186
|
-
* An agent whose files cannot be read contributes none
|
|
187
|
-
*
|
|
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.
|
|
188
174
|
*/
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
r = git(args, wt);
|
|
192
|
-
} catch {
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
if (typeof r !== 'string') continue;
|
|
175
|
+
const r = gitOrNull(args, wt);
|
|
176
|
+
if (r == null) continue;
|
|
196
177
|
for (const line of r.split('\n')) {
|
|
197
178
|
const f = line.trim();
|
|
198
179
|
if (f) out.add(f);
|
|
@@ -224,15 +205,35 @@ export function createWorkAgentPlans({
|
|
|
224
205
|
}
|
|
225
206
|
|
|
226
207
|
/**
|
|
227
|
-
* WHICH CLI PLANS.
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* which hold a conversation across many turns and whose value is that held
|
|
233
|
-
* context. A planner runs once and reads; it takes what the machine has.
|
|
208
|
+
* WHICH CLI PLANS. The person's pick comes first; a named CLI must be
|
|
209
|
+
* installed and able to run the read-only profile, or the press says why.
|
|
210
|
+
* A model or effort pin without a named CLI is a Claude pick. With no pick
|
|
211
|
+
* or pin, `pickRuntimeFor('consult')` is the fallback — Claude when it is
|
|
212
|
+
* here, otherwise whatever can express the profile.
|
|
234
213
|
*/
|
|
235
|
-
|
|
214
|
+
let rt;
|
|
215
|
+
const pickedRuntime = typeof job.runtime === 'string' && job.runtime.length > 0
|
|
216
|
+
? job.runtime
|
|
217
|
+
: job.model != null || job.effort != null ? 'claude' : null;
|
|
218
|
+
if (pickedRuntime) {
|
|
219
|
+
rt = pickedRuntime;
|
|
220
|
+
const runtime = runtimeById(rt);
|
|
221
|
+
if (!runtime) {
|
|
222
|
+
await postAgentPlan({ id, error: `${rt} is not a CLI this machine knows` });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const label = runtime.label;
|
|
226
|
+
if (!detectRuntimes().find((r) => r.id === rt)?.installed) {
|
|
227
|
+
await postAgentPlan({ id, error: `${label} is not installed on this machine` });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (!canRun(runtime, 'consult')) {
|
|
231
|
+
await postAgentPlan({ id, error: `${label} can't run a read-only turn on this machine` });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
rt = pickRuntimeFor('consult');
|
|
236
|
+
}
|
|
236
237
|
if (!rt) {
|
|
237
238
|
await postAgentPlan({ id, error: 'no CLI on this machine can run a read-only turn' });
|
|
238
239
|
return;
|
|
@@ -283,23 +284,6 @@ export function createWorkAgentPlans({
|
|
|
283
284
|
}));
|
|
284
285
|
|
|
285
286
|
let out = '';
|
|
286
|
-
/** REMOVED IN A `finally`. `workChildren` is what `workBusy()` counts, and
|
|
287
|
-
* a leaked entry keeps the daemon permanently "busy" — which blocks every
|
|
288
|
-
* auto-update from that moment on, silently, until a restart. */
|
|
289
|
-
let planChild = null;
|
|
290
|
-
let planTimer = null;
|
|
291
|
-
/**
|
|
292
|
-
* THE PLANNER'S OWN CONVERSATION, so its transcript can be removed.
|
|
293
|
-
*
|
|
294
|
-
* `claude -p` in the CHECKOUT writes `~/.claude/projects/<checkout>/<id>.jsonl`,
|
|
295
|
-
* and the checkout is exactly where the operator runs their own terminal
|
|
296
|
-
* Claude. Left behind, every Deploy press became the newest ENDED session
|
|
297
|
-
* there: it displaced the operator's real conversation from the `+` adopt
|
|
298
|
-
* menu (one row per directory), offered to fork a read-only planner into a
|
|
299
|
-
* build tab, and was what their own `claude --continue` resumed. The
|
|
300
|
-
* pre-review and the skills probe delete theirs for the same reason.
|
|
301
|
-
*/
|
|
302
|
-
let planSession = null;
|
|
303
287
|
/** The cap fired: the CLI was still running when this machine stopped it. */
|
|
304
288
|
let wedged = false;
|
|
305
289
|
/**
|
|
@@ -320,88 +304,58 @@ export function createWorkAgentPlans({
|
|
|
320
304
|
try {
|
|
321
305
|
await inPlace(REPO_PLACE, false, async () => {
|
|
322
306
|
/**
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
307
|
+
* ONE CAPPED CONSULT TURN (workCappedTurn.mjs): the child registered
|
|
308
|
+
* in `workChildren` and removed on every exit, the reservation
|
|
309
|
+
* released at the spawn, the cap ARMED AT THE SPAWN — time spent
|
|
310
|
+
* waiting for the checkout is not a wedged CLI, and a press that never
|
|
311
|
+
* got to run is what the server's own clock is for — and the
|
|
312
|
+
* planner's transcript removed from the CHECKOUT, where it would
|
|
313
|
+
* otherwise become the newest ENDED session: displacing the
|
|
314
|
+
* operator's real conversation from the `+` adopt menu, offering to
|
|
315
|
+
* fork a read-only planner into a build tab, and being what their own
|
|
316
|
+
* `claude --continue` resumed.
|
|
328
317
|
*/
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
usage = addUsage(usage, { ...u, runtime: rt });
|
|
318
|
+
({ out, wedged } = await runCappedTurn({
|
|
319
|
+
runTurn,
|
|
320
|
+
capMs: PLAN_TURN_TIMEOUT_MS,
|
|
321
|
+
workChildren,
|
|
322
|
+
releaseSlot,
|
|
323
|
+
transcriptCwd: repoRoot,
|
|
324
|
+
turnArgs: {
|
|
325
|
+
prompt: AGENT_PLAN_KICKOFF({
|
|
326
|
+
tasks,
|
|
327
|
+
liveAgents,
|
|
328
|
+
agentCap: Number.isInteger(job.agentCap) && job.agentCap > 0 ? job.agentCap : 3,
|
|
329
|
+
}),
|
|
330
|
+
system: SYSTEM_PLAN,
|
|
331
|
+
// READ-ONLY, and no MCP: `mcpArgs` is omitted entirely rather than
|
|
332
|
+
// passed empty, so there is no control plane on this turn at all.
|
|
333
|
+
profile: 'consult',
|
|
334
|
+
cwd: repoRoot,
|
|
335
|
+
runtime: rt,
|
|
336
|
+
...brainFor(job, rt),
|
|
337
|
+
answerFromResult: true,
|
|
338
|
+
label: c.cyan('[plan]'),
|
|
339
|
+
// The CLI's own tail, forwarded to the press. Same rule as an agent
|
|
340
|
+
// turn's: throttled, overwritten rather than appended, and never
|
|
341
|
+
// awaited by the turn.
|
|
342
|
+
onActivity: (a) => {
|
|
343
|
+
if (a?.label) narrate(String(a.label));
|
|
344
|
+
},
|
|
345
|
+
// Tagged with the CLI that counted it; one turn, so the sum is the
|
|
346
|
+
// one report (turnUsage.mjs keeps it null-safe).
|
|
347
|
+
onUsage: (u) => {
|
|
348
|
+
usage = addUsage(usage, { ...u, runtime: rt });
|
|
349
|
+
},
|
|
362
350
|
},
|
|
363
|
-
onSpawn: (
|
|
364
|
-
|
|
365
|
-
// No id: a Deploy press is not a task, and the snapshot's per-task
|
|
366
|
-
// rows must not invent one. It still COUNTS against the machine's
|
|
367
|
-
// ceiling — see liveTurnCount.
|
|
368
|
-
workChildren.set(ch, null);
|
|
369
|
-
releaseSlot();
|
|
370
|
-
say(`${RUNTIMES[rt]?.label ?? rt} started on this machine`);
|
|
371
|
-
/**
|
|
372
|
-
* ARMED AT THE SPAWN, not at the claim: time spent waiting for the
|
|
373
|
-
* checkout is not a wedged CLI, and a press that never got to run
|
|
374
|
-
* is what the server's own clock is for.
|
|
375
|
-
*
|
|
376
|
-
* The CHILD, never its group — the rule teardown keeps. A
|
|
377
|
-
* read-only planning turn starts no dev server, so there is
|
|
378
|
-
* nothing behind it worth signalling and everything to lose by
|
|
379
|
-
* signalling somebody else's.
|
|
380
|
-
*/
|
|
381
|
-
planTimer = setTimeout(() => {
|
|
382
|
-
wedged = true;
|
|
383
|
-
try {
|
|
384
|
-
ch.kill('SIGKILL');
|
|
385
|
-
} catch {
|
|
386
|
-
/* already gone */
|
|
387
|
-
}
|
|
388
|
-
stopWaiting('');
|
|
389
|
-
}, PLAN_TURN_TIMEOUT_MS);
|
|
390
|
-
planTimer.unref?.();
|
|
351
|
+
onSpawn: () => {
|
|
352
|
+
say(`${runtimeLabel(rt)} started on this machine`);
|
|
391
353
|
},
|
|
392
|
-
});
|
|
393
|
-
out = await Promise.race([turn, capped]);
|
|
354
|
+
}));
|
|
394
355
|
});
|
|
395
356
|
} catch (e) {
|
|
396
357
|
await postAgentPlan({ id, error: envScrub(String(e?.message || e)).slice(0, 500), ...spent() });
|
|
397
358
|
return;
|
|
398
|
-
} finally {
|
|
399
|
-
if (planTimer) clearTimeout(planTimer);
|
|
400
|
-
if (planChild) workChildren.delete(planChild);
|
|
401
|
-
// On EVERY exit, after the kill and on a delay — the transcript is the
|
|
402
|
-
// child's file, and removing it while the child is still dying races a
|
|
403
|
-
// recreate. The pre-review's own shape.
|
|
404
|
-
if (planSession) setTimeout(() => removeProbeTranscript(repoRoot, planSession), 750).unref?.();
|
|
405
359
|
}
|
|
406
360
|
|
|
407
361
|
if (wedged) {
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* is parsed, scrubbed and re-sent), the project check for SECURITY ones. The
|
|
8
8
|
* review entry beat sequences the two and owns neither.
|
|
9
9
|
*/
|
|
10
|
-
import { FLEET_URL
|
|
11
|
-
import {
|
|
10
|
+
import { FLEET_URL } from './config.mjs';
|
|
11
|
+
import { gitOrNull } from './git.mjs';
|
|
12
12
|
import { parsePrecheck } from './agentPlan.mjs';
|
|
13
13
|
import { readStash } from './agentCards.mjs';
|
|
14
14
|
import { c, note } from './ui.mjs';
|
|
@@ -16,10 +16,11 @@ import { runTurn } from './runTurn.mjs';
|
|
|
16
16
|
import { SYSTEM_PRECHECK, AGENT_PRECHECK_KICKOFF } from './prompts.mjs';
|
|
17
17
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
18
18
|
import { pickRuntimeFor } from './runtimeDetection.mjs';
|
|
19
|
-
import {
|
|
19
|
+
import { runCappedTurn } from './workCappedTurn.mjs';
|
|
20
20
|
import { taskIdsFromMessage } from './worktreeDiff.mjs';
|
|
21
21
|
import { limitLine } from './cliLimit.mjs';
|
|
22
|
-
import { fleetEndpoint } from './fleetWire.mjs';
|
|
22
|
+
import { fleetEndpoint, isConsideredRefusal } from './fleetWire.mjs';
|
|
23
|
+
import { postFleetJson } from './fleetPost.mjs';
|
|
23
24
|
import { agentBrowserHome } from './noWindowEnv.mjs';
|
|
24
25
|
|
|
25
26
|
export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workChildren }) {
|
|
@@ -38,7 +39,8 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
38
39
|
// meets the card it answers from the very context that produced the work. The
|
|
39
40
|
// reviewer stands IN the agent's worktree because it needs the code and the
|
|
40
41
|
// diff, under the `consult` profile (`consultPermFor` — the fenced Read/Glob rules plus
|
|
41
|
-
// a few
|
|
42
|
+
// a few shell readers; the `git` ones are fenced by the read guard's refusal,
|
|
43
|
+
// hooks/readGuard.mjs, not by the CLI) with NO MCP passed at all, so there is no
|
|
42
44
|
// control plane on this turn even if the repository it reads tries to steer
|
|
43
45
|
// it.
|
|
44
46
|
//
|
|
@@ -94,20 +96,8 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
94
96
|
*/
|
|
95
97
|
const postPre = async (body) => {
|
|
96
98
|
try {
|
|
97
|
-
const res = await
|
|
98
|
-
|
|
99
|
-
headers: {
|
|
100
|
-
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
101
|
-
'User-Agent': USER_AGENT,
|
|
102
|
-
'Content-Type': 'application/json',
|
|
103
|
-
},
|
|
104
|
-
signal: AbortSignal.timeout(30_000),
|
|
105
|
-
body: JSON.stringify(body),
|
|
106
|
-
});
|
|
107
|
-
return (
|
|
108
|
-
res.ok ||
|
|
109
|
-
(res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429)
|
|
110
|
-
);
|
|
99
|
+
const res = await postFleetJson(AGENT_PRECHECK_URL, body, { timeoutMs: 30_000 });
|
|
100
|
+
return res.ok || isConsideredRefusal(res.status);
|
|
111
101
|
} catch {
|
|
112
102
|
return false; // a blip — worth one more attempt at a model call's answer
|
|
113
103
|
}
|
|
@@ -127,13 +117,8 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
127
117
|
* review-entry beat it is standing in.
|
|
128
118
|
*/
|
|
129
119
|
const branchLog = (wt) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
out = git(['log', '--format=%s%n%b%n--', `${baseRef()}..HEAD`, '--not', baseRef()], wt);
|
|
133
|
-
} catch {
|
|
134
|
-
return { text: '', taskIds: [] };
|
|
135
|
-
}
|
|
136
|
-
if (typeof out !== 'string') return { text: '', taskIds: [] };
|
|
120
|
+
const out = gitOrNull(['log', '--format=%s%n%b%n--', `${baseRef()}..HEAD`, '--not', baseRef()], wt);
|
|
121
|
+
if (out == null) return { text: '', taskIds: [] };
|
|
137
122
|
// LINEAR, never backtracking (audit 2026-09-24) — this was
|
|
138
123
|
// `out.matchAll(/^\s*Flowviant-Task:\s*(\S+)\s*$/gm)`, whose `\s*$` is
|
|
139
124
|
// quadratic against a long trailing run of whitespace on one line
|
|
@@ -178,12 +163,7 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
178
163
|
// `checkFingerprint` shape, so a commit landing after this voids it rather
|
|
179
164
|
// than letting an old reading label a branch it never saw. Optional: an
|
|
180
165
|
// unreadable head costs the staleness comparison, not the precheck.
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
headSha = (git(['rev-parse', 'HEAD'], wt) || '').trim() || null;
|
|
184
|
-
} catch {
|
|
185
|
-
headSha = null;
|
|
186
|
-
}
|
|
166
|
+
const headSha = gitOrNull(['rev-parse', 'HEAD'], wt);
|
|
187
167
|
|
|
188
168
|
const stash = readStash(sessionMetaPath(wt, 'flowviant-agent-cards', agentId));
|
|
189
169
|
const log = branchLog(wt);
|
|
@@ -193,8 +173,6 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
193
173
|
const missingSpecs = log.taskIds.filter((id) => !held.has(id)).length;
|
|
194
174
|
|
|
195
175
|
let out = '';
|
|
196
|
-
let child = null;
|
|
197
|
-
let timer = null;
|
|
198
176
|
/** The cap fired: the CLI was still running when this machine stopped it. */
|
|
199
177
|
let wedged = false;
|
|
200
178
|
/**
|
|
@@ -202,8 +180,8 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
202
180
|
* other lane that charges an AGENT (2026-09-19).
|
|
203
181
|
*
|
|
204
182
|
* It rides the agent and never a card link: a pre-review belongs to no card
|
|
205
|
-
* (the same reason `workChildren`
|
|
206
|
-
* counters it adds to are the container's alone. That is also why an
|
|
183
|
+
* (the same reason its `workChildren` entry carries a null task id), so the
|
|
184
|
+
* four counters it adds to are the container's alone. That is also why an
|
|
207
185
|
* agent's links can never sum to its total, and the schema says so.
|
|
208
186
|
*
|
|
209
187
|
* A WEDGED OR UNPARSEABLE READING POSTS NOTHING AT ALL, so it charges
|
|
@@ -212,116 +190,71 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
212
190
|
* settle to carry a number would be a post whose only content is a bill.
|
|
213
191
|
*/
|
|
214
192
|
let usage = null;
|
|
215
|
-
/**
|
|
216
|
-
* THE CONVERSATION THIS READING SPEAKS UNDER — held for exactly one reason:
|
|
217
|
-
* to DELETE the transcript it leaves behind (review, 2026-09-17).
|
|
218
|
-
*
|
|
219
|
-
* This is the first thing in the daemon to run a second `claude -p` inside
|
|
220
|
-
* an AGENT's worktree, and the agent's own resume is `--continue`, which is
|
|
221
|
-
* CWD-KEYED — the invariant `runAgentTurn` states in words ("ONE AGENT IS
|
|
222
|
-
* ONE DIRECTORY, so the CLI's own cwd-keyed resume is exactly right here").
|
|
223
|
-
* Leaving this turn's `~/.claude/projects/<munged-cwd>/<id>.jsonl` in place
|
|
224
|
-
* makes the read-only stranger the newest conversation in that directory,
|
|
225
|
-
* so the agent's NEXT turn — a send-back's re-queued card, a merge-resolve,
|
|
226
|
-
* a human's typed answer — resumes "you are a SECOND reviewer… YOU ARE
|
|
227
|
-
* READ-ONLY" instead of its own four-turn context, under build permissions.
|
|
228
|
-
* That is the 0.69.0 Workbench cross-resume and codex's `resume --last`,
|
|
229
|
-
* arriving a third time by a third route.
|
|
230
|
-
*
|
|
231
|
-
* `removeProbeTranscript` is exported for precisely this and already serves
|
|
232
|
-
* the skills probe and the dev-command resolver.
|
|
233
|
-
*/
|
|
234
|
-
let preSession = null;
|
|
235
193
|
try {
|
|
236
194
|
/**
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
195
|
+
* ONE CAPPED CONSULT TURN (workCappedTurn.mjs), the planner's and
|
|
196
|
+
* intake's shape: the child registered and removed on every exit, the
|
|
197
|
+
* reservation released at the spawn, the cap armed at the spawn and
|
|
198
|
+
* resolving the wait itself — this promise is inside the place's writer
|
|
199
|
+
* lock — and THIS READING'S TRANSCRIPT REMOVED FROM THE WORKTREE
|
|
200
|
+
* (review, 2026-09-17).
|
|
201
|
+
*
|
|
202
|
+
* This is the first thing in the daemon to run a second `claude -p`
|
|
203
|
+
* inside an AGENT's worktree, and the agent's own resume is
|
|
204
|
+
* `--continue`, which is CWD-KEYED — the invariant `runAgentTurn` states
|
|
205
|
+
* in words ("ONE AGENT IS ONE DIRECTORY, so the CLI's own cwd-keyed
|
|
206
|
+
* resume is exactly right here"). Leaving this turn's
|
|
207
|
+
* `~/.claude/projects/<munged-cwd>/<id>.jsonl` in place makes the
|
|
208
|
+
* read-only stranger the newest conversation in that directory, so the
|
|
209
|
+
* agent's NEXT turn — a send-back's re-queued card, a merge-resolve, a
|
|
210
|
+
* human's typed answer — resumes "you are a SECOND reviewer… YOU ARE
|
|
211
|
+
* READ-ONLY" instead of its own four-turn context, under build
|
|
212
|
+
* permissions. That is the 0.69.0 Workbench cross-resume and codex's
|
|
213
|
+
* `resume --last`, arriving a third time by a third route.
|
|
242
214
|
*/
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// The CLI's own count for this reading. Set, never accumulated: one
|
|
279
|
-
// turn is one `result` event, and the adding-up is the server's.
|
|
280
|
-
// Tagged with the CLI, so a Claude pre-review of a Codex agent is
|
|
281
|
-
// charged as Claude's and not folded into the agent's own figure.
|
|
282
|
-
onUsage: (u) => {
|
|
283
|
-
usage = { ...u, runtime: rt };
|
|
215
|
+
({ out, wedged } = await runCappedTurn({
|
|
216
|
+
runTurn,
|
|
217
|
+
capMs: PRECHECK_TIMEOUT_MS,
|
|
218
|
+
workChildren,
|
|
219
|
+
releaseSlot,
|
|
220
|
+
transcriptCwd: wt,
|
|
221
|
+
turnArgs: {
|
|
222
|
+
prompt: AGENT_PRECHECK_KICKOFF({
|
|
223
|
+
agentName,
|
|
224
|
+
cards: stash.map((s) => s.prompt).join('\n---\n'),
|
|
225
|
+
missingSpecs,
|
|
226
|
+
commits: log.text,
|
|
227
|
+
diffCommand: `git diff ${baseRef()}...HEAD`,
|
|
228
|
+
}),
|
|
229
|
+
system: SYSTEM_PRECHECK,
|
|
230
|
+
// READ-ONLY, and no MCP: `mcpArgs` is omitted entirely rather than
|
|
231
|
+
// passed empty, so there is no control plane on this turn at all.
|
|
232
|
+
profile: 'consult',
|
|
233
|
+
cwd: wt,
|
|
234
|
+
runtime: rt,
|
|
235
|
+
// The agent's own browser profile, in the worktree this reader stands
|
|
236
|
+
// in; no display either way (noWindowEnv.mjs, 2026-09-29).
|
|
237
|
+
browserHome: agentBrowserHome(sessionMetaPath, wt, agentId),
|
|
238
|
+
// NO `resume`, AND THAT IS THE FEATURE. A resumed turn would be the
|
|
239
|
+
// agent grading its own homework out of its own context; this is a
|
|
240
|
+
// stranger reading a diff.
|
|
241
|
+
answerFromResult: true,
|
|
242
|
+
label: c.cyan('[pre-review]'),
|
|
243
|
+
// The CLI's own count for this reading. Set, never accumulated: one
|
|
244
|
+
// turn is one `result` event, and the adding-up is the server's.
|
|
245
|
+
// Tagged with the CLI, so a Claude pre-review of a Codex agent is
|
|
246
|
+
// charged as Claude's and not folded into the agent's own figure.
|
|
247
|
+
onUsage: (u) => {
|
|
248
|
+
usage = { ...u, runtime: rt };
|
|
249
|
+
},
|
|
284
250
|
},
|
|
285
|
-
|
|
286
|
-
child = ch;
|
|
287
|
-
// No task id: a pre-review belongs to no card, and the machine
|
|
288
|
-
// snapshot's per-task rows must not invent one. It still COUNTS
|
|
289
|
-
// against the machine's ceiling — see liveTurnCount.
|
|
290
|
-
workChildren.set(ch, null);
|
|
291
|
-
releaseSlot();
|
|
292
|
-
// ARMED AT THE SPAWN, not at entry: time spent getting here is not a
|
|
293
|
-
// wedged CLI. The CHILD and never its group — a read-only turn starts
|
|
294
|
-
// no server, so there is nothing behind it worth signalling and
|
|
295
|
-
// everything to lose by signalling somebody else's.
|
|
296
|
-
timer = setTimeout(() => {
|
|
297
|
-
wedged = true;
|
|
298
|
-
try {
|
|
299
|
-
ch.kill('SIGKILL');
|
|
300
|
-
} catch {
|
|
301
|
-
/* already gone */
|
|
302
|
-
}
|
|
303
|
-
stopWaiting('');
|
|
304
|
-
}, PRECHECK_TIMEOUT_MS);
|
|
305
|
-
timer.unref?.();
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
out = await Promise.race([turn, capped]);
|
|
251
|
+
}));
|
|
309
252
|
} catch {
|
|
310
253
|
return; // a label may never fail the beat it rides
|
|
311
254
|
} finally {
|
|
312
|
-
|
|
313
|
-
|
|
255
|
+
// The reservation this lane took itself, released on every exit — the
|
|
256
|
+
// turn's own release covers a spawn; this covers everything else.
|
|
314
257
|
releaseSlot();
|
|
315
|
-
/**
|
|
316
|
-
* DELETE THIS READING'S TRANSCRIPT, on EVERY exit — settled, wedged and
|
|
317
|
-
* killed, or thrown — because every one of them leaves the file behind
|
|
318
|
-
* and the agent's `--continue` reads the newest one in the directory.
|
|
319
|
-
*
|
|
320
|
-
* AFTER the kill and ON A DELAY, the skills probe's own shape: the
|
|
321
|
-
* transcript is the CHILD's file, so removing it while the child is still
|
|
322
|
-
* dying races a recreate. Unref'd — it must not hold the process open.
|
|
323
|
-
*/
|
|
324
|
-
if (preSession) setTimeout(() => removeProbeTranscript(wt, preSession), 750).unref?.();
|
|
325
258
|
}
|
|
326
259
|
|
|
327
260
|
// A WEDGED READING POSTS NOTHING. There is no row to settle and nobody
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* begun-guard, the sweep keeps a published branch current, the merge lane
|
|
11
11
|
* retires the ref — all through the maps and two functions returned here.
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
13
|
+
import { gitOrNull, sessionBranchRef } from './git.mjs';
|
|
14
14
|
import { publishPushArgs, publishFetchArgs, publishErrorText } from './agentPublish.mjs';
|
|
15
15
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
16
16
|
|
|
@@ -67,13 +67,11 @@ export function createWorkAgentPublish({ repoRoot, gitNet }) {
|
|
|
67
67
|
* — which is a perfectly ordinary state (the begun-guard's whole subject) and
|
|
68
68
|
* means there is nothing to publish, never that a push failed. */
|
|
69
69
|
const agentBranchSha = (place) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
70
|
+
// The one branch-name definition (git.mjs): an unsafe place names no
|
|
71
|
+
// branch, which is the same "nothing to publish" as an absent one.
|
|
72
|
+
const ref = sessionBranchRef(place);
|
|
73
|
+
if (!ref) return null;
|
|
74
|
+
return gitOrNull(['rev-parse', '--verify', '--quiet', ref], repoRoot);
|
|
77
75
|
};
|
|
78
76
|
/**
|
|
79
77
|
* PUSH ONE AGENT'S BRANCH TO THE NAME THE SERVER GAVE IT.
|