flowviant 0.103.0 → 0.104.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/bin/cli.mjs +87 -474
- package/bin/lib/admission.mjs +19 -3
- package/bin/lib/agentPlan.mjs +2 -2
- package/bin/lib/agentTaskKinds.mjs +93 -0
- package/bin/lib/agySessions.mjs +158 -0
- package/bin/lib/artifactPolicy.mjs +44 -0
- package/bin/lib/artifactPreview.mjs +52 -0
- package/bin/lib/artifactReporter.mjs +163 -0
- package/bin/lib/artifacts.mjs +47 -227
- package/bin/lib/authReject.mjs +16 -4
- package/bin/lib/authproxy.mjs +47 -304
- package/bin/lib/boxIdentity.mjs +245 -0
- package/bin/lib/childEnv.mjs +11 -9
- package/bin/lib/claude.mjs +14 -851
- package/bin/lib/claudeAuth.mjs +3 -3
- package/bin/lib/claudePosture.mjs +367 -0
- package/bin/lib/claudeSessions.mjs +411 -0
- package/bin/lib/claudeStream.mjs +207 -0
- package/bin/lib/cliLimit.mjs +34 -0
- package/bin/lib/commandSpecs.mjs +173 -0
- package/bin/lib/config.mjs +11 -42
- package/bin/lib/credentialDate.mjs +22 -0
- package/bin/lib/credentials.mjs +21 -26
- package/bin/lib/daemonLogging.mjs +55 -0
- package/bin/lib/daemonState.mjs +64 -0
- package/bin/lib/deploy.mjs +148 -647
- package/bin/lib/deployCheckout.mjs +265 -0
- package/bin/lib/deployConfig.mjs +142 -0
- package/bin/lib/deployRunner.mjs +311 -0
- package/bin/lib/deployWire.mjs +41 -0
- package/bin/lib/desktopContract.mjs +56 -105
- package/bin/lib/env.mjs +49 -484
- package/bin/lib/fleet.mjs +183 -2383
- package/bin/lib/fleetJobs.mjs +120 -0
- package/bin/lib/fleetPost.mjs +40 -0
- package/bin/lib/fleetReports.mjs +392 -0
- package/bin/lib/fleetRoster.mjs +470 -0
- package/bin/lib/fleetStartup.mjs +196 -0
- package/bin/lib/fleetWake.mjs +45 -0
- package/bin/lib/fleetWire.mjs +46 -0
- package/bin/lib/git.mjs +2 -2
- package/bin/lib/gitMergeIdentity.mjs +51 -0
- package/bin/lib/grant.mjs +5 -3
- package/bin/lib/help.mjs +3 -125
- package/bin/lib/holder.mjs +482 -0
- package/bin/lib/instance.mjs +124 -46
- package/bin/lib/knowledge.mjs +16 -799
- package/bin/lib/knowledgeFetch.mjs +38 -0
- package/bin/lib/knowledgeFileSync.mjs +112 -0
- package/bin/lib/knowledgeFiles.mjs +487 -0
- package/bin/lib/knowledgeLibrary.mjs +249 -0
- package/bin/lib/landed.mjs +2 -1
- package/bin/lib/listeners.mjs +99 -141
- package/bin/lib/localSessions.mjs +30 -555
- package/bin/lib/login.mjs +3 -2
- package/bin/lib/loginPath.mjs +3 -5
- package/bin/lib/machineDisconnect.mjs +203 -0
- package/bin/lib/machineEnv.mjs +22 -0
- package/bin/lib/machineLimits.mjs +61 -0
- package/bin/lib/machineListing.mjs +338 -0
- package/bin/lib/machines.mjs +78 -455
- package/bin/lib/machinesCommand.mjs +214 -0
- package/bin/lib/mcp-cli.mjs +2 -1
- package/bin/lib/patchRevert.mjs +60 -0
- package/bin/lib/prWorkflow.mjs +157 -0
- package/bin/lib/preflight.mjs +2 -1
- package/bin/lib/pressurePolicy.mjs +104 -0
- package/bin/lib/preview.mjs +2 -272
- package/bin/lib/previewFramePolicy.mjs +112 -0
- package/bin/lib/previewGatePolicy.mjs +236 -0
- package/bin/lib/previewRegistry.mjs +235 -0
- package/bin/lib/printable.mjs +2 -2
- package/bin/lib/procRegistry.mjs +28 -18
- package/bin/lib/procScan.mjs +146 -0
- package/bin/lib/processes.mjs +29 -37
- package/bin/lib/projectToolRuntimes.mjs +138 -0
- package/bin/lib/projectTools.mjs +13 -88
- package/bin/lib/prompts.mjs +28 -42
- package/bin/lib/repoPath.mjs +26 -0
- package/bin/lib/resources.mjs +7 -69
- package/bin/lib/resumeLost.mjs +107 -0
- package/bin/lib/runTurn.mjs +324 -0
- package/bin/lib/runtimeAntigravity.mjs +230 -0
- package/bin/lib/runtimeCapabilities.mjs +334 -0
- package/bin/lib/runtimeClaude.mjs +126 -0
- package/bin/lib/runtimeCodex.mjs +217 -0
- package/bin/lib/runtimeDetection.mjs +121 -0
- package/bin/lib/runtimeEvents.mjs +482 -0
- package/bin/lib/runtimeOfferings.mjs +4 -1
- package/bin/lib/runtimes.mjs +42 -1402
- package/bin/lib/safeFileName.mjs +74 -0
- package/bin/lib/standDownExit.mjs +181 -0
- package/bin/lib/standDownGate.mjs +48 -0
- package/bin/lib/startProject.mjs +279 -0
- package/bin/lib/trace.mjs +3 -3
- package/bin/lib/turnLock.mjs +68 -0
- package/bin/lib/turnProfile.mjs +77 -0
- package/bin/lib/uninstall.mjs +55 -45
- package/bin/lib/uninstallCopies.mjs +112 -0
- package/bin/lib/update.mjs +30 -17
- package/bin/lib/uplinkScrub.mjs +265 -0
- package/bin/lib/vault.mjs +22 -151
- package/bin/lib/vaultDiff.mjs +183 -0
- package/bin/lib/versionOrder.mjs +42 -0
- package/bin/lib/views.mjs +17 -17
- package/bin/lib/wikiRunner.mjs +630 -0
- package/bin/lib/work.mjs +188 -3034
- package/bin/lib/workAdoptCarry.mjs +94 -0
- package/bin/lib/workAgentCheck.mjs +278 -0
- package/bin/lib/workAgentMerges.mjs +54 -142
- package/bin/lib/workAgentPlans.mjs +14 -12
- package/bin/lib/workAgentPrecheck.mjs +393 -0
- package/bin/lib/workAgentPublish.mjs +177 -0
- package/bin/lib/workAgentReview.mjs +8 -666
- package/bin/lib/workAgentTurnBegun.mjs +109 -0
- package/bin/lib/workAgentTurnExecution.mjs +604 -0
- package/bin/lib/workAgentTurnOutcome.mjs +207 -0
- package/bin/lib/workAgentTurnReports.mjs +189 -0
- package/bin/lib/workAgentTurns.mjs +91 -898
- package/bin/lib/workAttachments.mjs +93 -0
- package/bin/lib/workBrain.mjs +53 -0
- package/bin/lib/workChildren.mjs +110 -0
- package/bin/lib/workCommandAudit.mjs +62 -0
- package/bin/lib/workDiffs.mjs +2 -1
- package/bin/lib/workNarration.mjs +152 -0
- package/bin/lib/workPlaceReports.mjs +426 -0
- package/bin/lib/workPlaces.mjs +228 -0
- package/bin/lib/workPreviews.mjs +31 -9
- package/bin/lib/workProcesses.mjs +4 -5
- package/bin/lib/workPullRequests.mjs +52 -126
- package/bin/lib/workReportQueue.mjs +254 -0
- package/bin/lib/workRetire.mjs +185 -0
- package/bin/lib/workSessionGroups.mjs +166 -0
- package/bin/lib/workSessionRuntime.mjs +164 -0
- package/bin/lib/workSessionTokens.mjs +68 -0
- package/bin/lib/workSessionTurns.mjs +892 -0
- package/bin/lib/workShip.mjs +9 -29
- package/bin/lib/workToolLog.mjs +76 -0
- package/package.json +3 -2
- package/bin/lib/patch.mjs +0 -363
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE AGENT TURN, RUN: the worktree, the CLI, and what it left behind.
|
|
3
|
+
*
|
|
4
|
+
* Split out of workAgentTurns.mjs (SOLID F036, 2026-09-26). The lane there
|
|
5
|
+
* decides WHICH turns run and makes sure each one is settled exactly once; this
|
|
6
|
+
* runs one, inside the place lock the lane already holds — the begun-guard
|
|
7
|
+
* (workAgentTurnBegun.mjs), the worktree and its branch facts, the kind's
|
|
8
|
+
* refusals, the prompt, the CLI and its resume, the trace and pulse relays,
|
|
9
|
+
* the markers and the re-measurement after it exits — then hands the
|
|
10
|
+
* measurements to the one settlement decision (workAgentTurnOutcome.mjs) and
|
|
11
|
+
* posts the body that decision chose.
|
|
12
|
+
*
|
|
13
|
+
* `postAgentTurn` arrives PER TURN, from the lane, rather than from the report
|
|
14
|
+
* module directly: the lane wraps it to learn that a settle was sent, which is
|
|
15
|
+
* what lets its catch settle a turn that threw — and never one that already
|
|
16
|
+
* answered. `measured` is the same bargain the other way: the branch facts and
|
|
17
|
+
* the spend, written here as they are learned, read by that catch.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { PROJECT_ID } from './config.mjs';
|
|
21
|
+
import { setRuntimeLimit } from './daemonState.mjs';
|
|
22
|
+
import { git } from './git.mjs';
|
|
23
|
+
import { stashCard } from './agentCards.mjs';
|
|
24
|
+
import { c } from './ui.mjs';
|
|
25
|
+
import { runTurn } from './runTurn.mjs';
|
|
26
|
+
import { SYSTEM_AGENT_FOR, AGENT_TASK_KICKOFF, AGENT_TASK_SPEC, AGENT_HUMAN_KICKOFF, withProjectContext } from './prompts.mjs';
|
|
27
|
+
import { AGENT_TASK_KINDS, agentTaskKindOf, unknownAgentTaskKind } from './agentTaskKinds.mjs';
|
|
28
|
+
import { knowledgeDirFor } from './knowledgeFiles.mjs';
|
|
29
|
+
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
30
|
+
import { canRun, RUNTIMES } from './runtimes.mjs';
|
|
31
|
+
import { recordSkills, recordMcpServers } from './runtimeCapabilities.mjs';
|
|
32
|
+
import { toolEventOf, CLAUDE_TOOL_PROSE_KINDS } from './runtimeEvents.mjs';
|
|
33
|
+
import { makeTraceRelay } from './trace.mjs';
|
|
34
|
+
import { readBaseTools, prepareAgentTools } from './projectTools.mjs';
|
|
35
|
+
import { runTurnResumingOnce } from './resumeLost.mjs';
|
|
36
|
+
import { CODEX_THREAD_RE } from './workSessionRuntime.mjs';
|
|
37
|
+
import { brainFor } from './workBrain.mjs';
|
|
38
|
+
import { begunTurnRefusal } from './workAgentTurnBegun.mjs';
|
|
39
|
+
import { agentTurnSettlement } from './workAgentTurnOutcome.mjs';
|
|
40
|
+
|
|
41
|
+
export function createAgentTurnExecution({
|
|
42
|
+
baseRef,
|
|
43
|
+
baseDir,
|
|
44
|
+
repoRoot,
|
|
45
|
+
fetchPublishedBranch,
|
|
46
|
+
placeWtFor,
|
|
47
|
+
sessionMetaPath,
|
|
48
|
+
beforeArtifacts,
|
|
49
|
+
getArtifactsAccepted,
|
|
50
|
+
noteSessionGroup,
|
|
51
|
+
reportSessionWorktree,
|
|
52
|
+
artifacts,
|
|
53
|
+
runReviewEntry,
|
|
54
|
+
workChildren,
|
|
55
|
+
agentChildren,
|
|
56
|
+
postAgentTrace,
|
|
57
|
+
postAgentActivity,
|
|
58
|
+
postAgentParked,
|
|
59
|
+
}) {
|
|
60
|
+
/** How long the final flush may hold the settle. Bounded because the settle
|
|
61
|
+
* is the turn's contract and the trace is a readout: a wedged uplink costs
|
|
62
|
+
* the tail of a trace, never the answer behind it. */
|
|
63
|
+
const TRACE_FINAL_FLUSH_MS = 8_000;
|
|
64
|
+
|
|
65
|
+
const lastAgentBeat = new Map(); // agentId -> last activity POST, ms
|
|
66
|
+
|
|
67
|
+
/** Every sha that landed on this branch between two points. Measured, never
|
|
68
|
+
* asserted — this is the whole reason an agent is not asked for its own. */
|
|
69
|
+
const commitsBetween = (wt, from) => {
|
|
70
|
+
if (!from) return [];
|
|
71
|
+
/**
|
|
72
|
+
* `--not <base>` and `--no-merges`, because `from..HEAD` alone reports
|
|
73
|
+
* BASE'S commits as this turn's receipts the moment anything folds base in
|
|
74
|
+
* — which the stale path does on purpose before a merge. A receipt naming
|
|
75
|
+
* somebody else's commit is worse than a missing one.
|
|
76
|
+
*/
|
|
77
|
+
/**
|
|
78
|
+
* IT CANNOT THROW, and that guard is the whole point of it being here.
|
|
79
|
+
*
|
|
80
|
+
* `git()` throws on a non-zero exit, and `baseRef()` is built from the
|
|
81
|
+
* project's Base branch setting — free text an owner types, never verified
|
|
82
|
+
* against the remote. Point it at a branch with no tracking ref (`develop`
|
|
83
|
+
* on a repo whose remote branch is `dev`) and this exits "fatal: ambiguous
|
|
84
|
+
* argument". The throw escaped `runAgentTurn` AFTER the CLI had already run
|
|
85
|
+
* the card, so `postAgentTurn` was never reached, the server never settled
|
|
86
|
+
* the turn, and the next poll handed back the identical turn — the same
|
|
87
|
+
* card re-run every poll for six hours, on the operator's shared account,
|
|
88
|
+
* piling commits onto the review branch, silently.
|
|
89
|
+
*
|
|
90
|
+
* An unreadable range means we cannot MEASURE the receipts, which is a
|
|
91
|
+
* smaller failure than not settling: the turn still reports, and ship-time
|
|
92
|
+
* reconciliation books whatever no card claimed. Missing beats fabricated
|
|
93
|
+
* and both beat a stall.
|
|
94
|
+
*/
|
|
95
|
+
let out;
|
|
96
|
+
try {
|
|
97
|
+
out = git(['log', '--format=%H', '--no-merges', `${from}..HEAD`, '--not', baseRef()], wt);
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
return typeof out === 'string'
|
|
102
|
+
? out.split('\n').map((x) => x.trim()).filter(Boolean).slice(0, 50)
|
|
103
|
+
: [];
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The turn, run inside its place's writer lock (the lane takes it). `turnId`,
|
|
108
|
+
* `agentId` and `place` are the lane's own readings of the job, handed down
|
|
109
|
+
* so the two can never disagree about which directory this is.
|
|
110
|
+
*
|
|
111
|
+
* `releaseSlot` hands back the admission reservation the lane took on this
|
|
112
|
+
* turn's behalf, at the moment the CLI actually exists. Idempotent.
|
|
113
|
+
*/
|
|
114
|
+
const runAgentTurnInPlace = async (job, { turnId, agentId, place, releaseSlot, postAgentTurn, measured }) => {
|
|
115
|
+
// WORK THAT HAS BEGUN LIVES ON EXACTLY ONE BOX — read BEFORE anything is
|
|
116
|
+
// cut, because `placeWtFor` would cut a rival branch of the same name. The
|
|
117
|
+
// rule and its three-state branch measurement: workAgentTurnBegun.mjs.
|
|
118
|
+
if (job.begun) {
|
|
119
|
+
const refusal = begunTurnRefusal(job, place, { baseDir, repoRoot, fetchPublishedBranch });
|
|
120
|
+
if (refusal) {
|
|
121
|
+
await postAgentTurn({ turnId, outcome: 'nothing', answer: refusal });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const dir = placeWtFor(place);
|
|
126
|
+
if (!dir) {
|
|
127
|
+
// No worktree and none could be cut. `nothing`, WITH git's own words:
|
|
128
|
+
// without them the agent sat in Stuck with no question and no reason
|
|
129
|
+
// (the owner, 2026-09-25: "it got stuck without saying why").
|
|
130
|
+
const why = placeWtFor.lastError ?? null;
|
|
131
|
+
await postAgentTurn({ turnId, outcome: 'nothing', ...(why ? { answer: `This machine could not start the turn: ${why}` } : {}) });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const wt = dir.wt;
|
|
135
|
+
/**
|
|
136
|
+
* NEITHER OF THESE MAY THROW. `git()` throws on a non-zero exit, and both
|
|
137
|
+
* of these exit non-zero in states a real worktree reaches: `symbolic-ref`
|
|
138
|
+
* on a DETACHED HEAD (an agent that ran `git checkout <sha>`, a rebase
|
|
139
|
+
* left half-done), and `rev-parse HEAD` on a branch with no commits yet.
|
|
140
|
+
* An escape here happens BEFORE any settle, so the turn stays pending
|
|
141
|
+
* until the six-hour expiry while the agent sits in Working with nothing
|
|
142
|
+
* behind it — the wedge this whole lane exists to avoid.
|
|
143
|
+
*
|
|
144
|
+
* `runAgentMerge` learned this two commits ago and says so in its own
|
|
145
|
+
* comment; this is the same call, in the same file, left unguarded. Both
|
|
146
|
+
* facts are OPTIONAL to a turn — they describe the branch, they do not
|
|
147
|
+
* gate the work — so failing to read them costs a null, not the turn.
|
|
148
|
+
*/
|
|
149
|
+
const readGit = (args) => {
|
|
150
|
+
try {
|
|
151
|
+
return (git(args, wt) || '').trim() || null;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
// WHERE THE BRANCH WAS BEFORE THIS TURN, so the commits reported are the
|
|
157
|
+
// ones this turn actually made.
|
|
158
|
+
const before = readGit(['rev-parse', 'HEAD']);
|
|
159
|
+
const branch = readGit(['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
160
|
+
// …and the same three facts for the lane's catch: a turn that throws past
|
|
161
|
+
// here is still settled with what was measured (workAgentTurns.mjs).
|
|
162
|
+
measured.wt = wt;
|
|
163
|
+
measured.before = before;
|
|
164
|
+
measured.branch = branch;
|
|
165
|
+
|
|
166
|
+
const rt = job.runtime || 'claude';
|
|
167
|
+
/**
|
|
168
|
+
* WHAT THIS CARD HANDS BACK, AND THE POSTURE THAT FOLLOWS (0.97.0).
|
|
169
|
+
*
|
|
170
|
+
* `code` (the default — absent on the job IS code) is the build turn,
|
|
171
|
+
* byte-for-byte what every agent turn was. `design` and `research` run
|
|
172
|
+
* their own contract (SYSTEM_AGENT_DESIGN / _RESEARCH) under their own
|
|
173
|
+
* posture, which can write ONLY `.flowviant/artifacts/` (claudePosture.mjs).
|
|
174
|
+
*
|
|
175
|
+
* A RUNTIME THAT CANNOT EXPRESS THE POSTURE IS REFUSED, NOT IMPROVISED.
|
|
176
|
+
* Codex and antigravity declare neither (runtimeCodex.mjs,
|
|
177
|
+
* runtimeAntigravity.mjs), and the only
|
|
178
|
+
* alternative to refusing is running the card as a BUILD turn with
|
|
179
|
+
* permissions skipped — an agent writing code for an ask that was a
|
|
180
|
+
* mockup, reporting success. `nothing` puts the agent in Stuck saying so.
|
|
181
|
+
*
|
|
182
|
+
* THE JOB'S OWN KIND FIRST (2026-09-23). A `human` turn — a send-back
|
|
183
|
+
* from the review deck, an answer — usually names NO card: the queue has
|
|
184
|
+
* emptied, or the card it is about was just re-queued behind it. Read off
|
|
185
|
+
* `job.task` alone, a send-back on a design agent ran as a BUILD turn
|
|
186
|
+
* under the code contract with permissions skipped — the one turn in the
|
|
187
|
+
* owner's loop ("iterate in the review column, the agent opened") that
|
|
188
|
+
* could edit code on an ask that was a mockup. So the server projects the
|
|
189
|
+
* kind of the card the turn is ABOUT onto the job itself (`taskKind`,
|
|
190
|
+
* sent only for design and research; the same `DAEMON_TASK_KIND_MIN`
|
|
191
|
+
* floor as the card's own key, no new one), and the card's key stays the
|
|
192
|
+
* fallback for a server that sends only that. Absent on both is code.
|
|
193
|
+
*/
|
|
194
|
+
/**
|
|
195
|
+
* A KIND THIS DAEMON DOES NOT KNOW IS REFUSED, NEVER BUILT (2026-09-23).
|
|
196
|
+
* `agentTaskKindOf` reads anything unrecognised as code — right for a
|
|
197
|
+
* printed label, wrong here, where code means the build posture with
|
|
198
|
+
* permissions skipped. A server that learned a fourth kind ahead of
|
|
199
|
+
* this daemon is exactly the case the version floor cannot cover (the
|
|
200
|
+
* handout does not re-check it), and the answer is an update, said in
|
|
201
|
+
* the word the server used. `nothing` puts the agent in Stuck with it.
|
|
202
|
+
*/
|
|
203
|
+
const strangeKind = unknownAgentTaskKind(job.taskKind ?? job.task?.taskKind);
|
|
204
|
+
if (strangeKind !== null) {
|
|
205
|
+
await postAgentTurn({
|
|
206
|
+
turnId,
|
|
207
|
+
outcome: 'nothing',
|
|
208
|
+
answer: `this daemon does not know the card kind '${strangeKind}' — update it`,
|
|
209
|
+
branch,
|
|
210
|
+
worktree: wt,
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const taskKind = agentTaskKindOf(job.taskKind ?? job.task?.taskKind);
|
|
215
|
+
// What this kind IS — its posture, its artifact, its refusal — is one
|
|
216
|
+
// entry in agentTaskKinds.mjs; nothing below branches on the kind name.
|
|
217
|
+
const kind = AGENT_TASK_KINDS[taskKind];
|
|
218
|
+
const posture = kind.posture;
|
|
219
|
+
if (!canRun(RUNTIMES[rt], posture)) {
|
|
220
|
+
await postAgentTurn({
|
|
221
|
+
turnId,
|
|
222
|
+
outcome: 'nothing',
|
|
223
|
+
answer: kind.offRuntime(rt),
|
|
224
|
+
branch,
|
|
225
|
+
worktree: wt,
|
|
226
|
+
});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ONE AGENT IS ONE DIRECTORY, so the CLI's own cwd-keyed resume is exactly
|
|
231
|
+
// right here — the ambiguity that forced per-tab session pinning in the
|
|
232
|
+
// Workbench (many tabs, one place) cannot arise. The marker is what
|
|
233
|
+
// distinguishes the first turn from every later one across restarts.
|
|
234
|
+
//
|
|
235
|
+
// IT MEANS "CLAUDE HAS SPOKEN HERE", and since 2026-09-24 only a Claude
|
|
236
|
+
// turn writes it: an agent can be switched between CLIs, and a marker a
|
|
237
|
+
// codex turn left behind would send the first Claude turn to
|
|
238
|
+
// `--continue` a directory holding no Claude conversation at all.
|
|
239
|
+
const ranMarker = sessionMetaPath(wt, 'flowviant-agent-ran');
|
|
240
|
+
/**
|
|
241
|
+
* CODEX RESUMES BY ITS OWN THREAD ID, PINNED PER AGENT (2026-09-24).
|
|
242
|
+
*
|
|
243
|
+
* Resume was Claude-only, because codex's `resume --last` is
|
|
244
|
+
* MACHINE-GLOBAL and would cross-resume whichever conversation spoke
|
|
245
|
+
* last anywhere on the box. The cost was that every codex card started
|
|
246
|
+
* a stranger: no memory of the card before it, and — worse — an answer
|
|
247
|
+
* to the agent's own question arrived at a codex that never asked it.
|
|
248
|
+
* The Workbench solved the same problem in 0.69.0 by pinning the id
|
|
249
|
+
* `thread.started` announces; this is that, keyed by the AGENT rather
|
|
250
|
+
* than a tab. The id is shape-checked before it rides argv.
|
|
251
|
+
*/
|
|
252
|
+
const codexThreadMarker =
|
|
253
|
+
rt === 'codex' ? sessionMetaPath(wt, 'flowviant-agent-codex-thread', agentId) : null;
|
|
254
|
+
let codexResumeId = null;
|
|
255
|
+
if (codexThreadMarker && existsSync(codexThreadMarker)) {
|
|
256
|
+
try {
|
|
257
|
+
const v = readFileSync(codexThreadMarker, 'utf8').trim();
|
|
258
|
+
if (CODEX_THREAD_RE.test(v)) codexResumeId = v;
|
|
259
|
+
} catch {
|
|
260
|
+
/* unreadable marker — run fresh */
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
let seenThreadId = null;
|
|
264
|
+
const resume =
|
|
265
|
+
rt === 'codex'
|
|
266
|
+
? Boolean(codexResumeId)
|
|
267
|
+
: rt === 'claude' && Boolean(ranMarker && existsSync(ranMarker));
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* WRITE THE CARD DOWN BEFORE HANDING IT OVER — the material the AI
|
|
271
|
+
* pre-review reads at review entry (see agentCards.mjs).
|
|
272
|
+
*
|
|
273
|
+
* HERE rather than at review entry because here is the ONLY moment this
|
|
274
|
+
* machine holds the card at all: the server feeds an agent one card per
|
|
275
|
+
* prompt and keeps no copy on this disk, so by the time the queue empties
|
|
276
|
+
* card one exists locally as nothing but its own commit messages — which
|
|
277
|
+
* are a claim about the work, not the specification it was judged against.
|
|
278
|
+
*
|
|
279
|
+
* BEFORE the CLI runs rather than after, so a turn that crashes still
|
|
280
|
+
* leaves the spec behind: the card really was given to the agent, the
|
|
281
|
+
* commits it made are on the branch, and a reviewer is entitled to read
|
|
282
|
+
* what was asked for either way.
|
|
283
|
+
*
|
|
284
|
+
* ONE SPEC BUILDER (`AGENT_TASK_SPEC`) shared with the kickoff below, so
|
|
285
|
+
* what the reviewer reads is byte-identical to what the agent read.
|
|
286
|
+
* Failure is swallowed inside `stashCard`: a note about a turn may never
|
|
287
|
+
* cost the turn.
|
|
288
|
+
*/
|
|
289
|
+
if (job.kind === 'task' && job.task) {
|
|
290
|
+
stashCard(
|
|
291
|
+
sessionMetaPath(wt, 'flowviant-agent-cards', agentId),
|
|
292
|
+
job.task.id,
|
|
293
|
+
AGENT_TASK_SPEC(job.task)
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* THE WHOLE STREAM, not just its latest line — see trace.mjs.
|
|
299
|
+
*
|
|
300
|
+
* The pulse below is untouched and still sent: it carries staleness (how
|
|
301
|
+
* long the machine has been quiet), which an append-only list of steps
|
|
302
|
+
* cannot say, because a list that stopped growing looks exactly like a
|
|
303
|
+
* list that is finished.
|
|
304
|
+
*/
|
|
305
|
+
const trace = makeTraceRelay({
|
|
306
|
+
agentId,
|
|
307
|
+
turnId,
|
|
308
|
+
post: postAgentTrace,
|
|
309
|
+
scrub: envScrub,
|
|
310
|
+
});
|
|
311
|
+
/**
|
|
312
|
+
* Prose the structured event will carry anyway, dropped so a read does
|
|
313
|
+
* not render twice — but ONLY on a runtime whose stream reaches
|
|
314
|
+
* `onToolEvent` at all. Codex and agy have their own parsers and never
|
|
315
|
+
* call it, so dropping their tool prose would blank their agents' traces.
|
|
316
|
+
* `parse: null` is exactly the claudeStream.mjs stream path. See
|
|
317
|
+
* CLAUDE_TOOL_PROSE_KINDS.
|
|
318
|
+
*/
|
|
319
|
+
const doubledKinds = RUNTIMES[rt]?.parse ? null : CLAUDE_TOOL_PROSE_KINDS;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* WHICH BRAIN THIS CONTAINER WAS PINNED TO — the same `brainFor` the tab
|
|
323
|
+
* lane runs, on the same two job keys, because an agent turn and a
|
|
324
|
+
* session turn differ in who is watching and in nothing else that a model
|
|
325
|
+
* name touches. Every guard lives in `brainFor`: a second copy here would
|
|
326
|
+
* be a second answer to "is this a model we can spell", and the two would
|
|
327
|
+
* drift the first time one of them learned a new effort.
|
|
328
|
+
*
|
|
329
|
+
* Absent stays genuinely absent — an agent nobody pinned produces the
|
|
330
|
+
* byte-identical argv it produced yesterday, on the machine's own
|
|
331
|
+
* default. That is also what an OLDER server yields, since it sends
|
|
332
|
+
* neither key.
|
|
333
|
+
*/
|
|
334
|
+
const brain = brainFor(job, rt);
|
|
335
|
+
|
|
336
|
+
let out = '';
|
|
337
|
+
let child = null;
|
|
338
|
+
/**
|
|
339
|
+
* WHAT THIS TURN SPENT, AS THE CLI COUNTED IT (2026-09-19).
|
|
340
|
+
*
|
|
341
|
+
* Held across the whole turn so every settle below can carry it — a turn
|
|
342
|
+
* that hit a limit, produced no parseable outcome or delivered properly
|
|
343
|
+
* all spent real tokens, and a readout that only charged the happy path
|
|
344
|
+
* would under-report the runs somebody opens the number to understand.
|
|
345
|
+
*
|
|
346
|
+
* The three settles it is spread into are the ones that follow a CLI
|
|
347
|
+
* actually running. The pre-spawn refusals above (a bad place, a missing
|
|
348
|
+
* card, the begun-guard) and the teardown sweep below deliberately do NOT
|
|
349
|
+
* take it: nothing ran there, and `usage` stays null anyway, which is
|
|
350
|
+
* what makes `...(usage ? … : {})` the whole guard.
|
|
351
|
+
*/
|
|
352
|
+
let usage = null;
|
|
353
|
+
/**
|
|
354
|
+
* THE AGENT'S LAST MESSAGE, ALONE — where the final JSON object lives.
|
|
355
|
+
* Codex's `out` is every message plus stderr plus error text, and
|
|
356
|
+
* `parseTurnResult` takes the first object that fits, so an early
|
|
357
|
+
* message quoting the format could be read as the outcome. Claude never
|
|
358
|
+
* calls this (its `out` is already the result under `answerFromResult`).
|
|
359
|
+
*/
|
|
360
|
+
let lastAnswer = null;
|
|
361
|
+
/** The agent's artifact directory as it stood before this turn — the
|
|
362
|
+
* tab lane's rule, in the agent's own worktree (2026-09-22). */
|
|
363
|
+
const artifactsBefore = beforeArtifacts(wt);
|
|
364
|
+
let projectTools = null;
|
|
365
|
+
try {
|
|
366
|
+
// A turn reads the base ref afresh. Changes the agent has made to its
|
|
367
|
+
// own worktree cannot silently change the next turn's tools.
|
|
368
|
+
projectTools = prepareAgentTools(readBaseTools(repoRoot, baseRef()), rt, process.env, wt);
|
|
369
|
+
const agentTurnArgs = {
|
|
370
|
+
prompt:
|
|
371
|
+
job.kind === 'task' && job.task
|
|
372
|
+
? AGENT_TASK_KICKOFF({
|
|
373
|
+
agentName: job.agentName,
|
|
374
|
+
task: job.task,
|
|
375
|
+
position: job.position ?? 1,
|
|
376
|
+
total: job.total ?? 1,
|
|
377
|
+
})
|
|
378
|
+
: AGENT_HUMAN_KICKOFF({
|
|
379
|
+
agentName: job.agentName,
|
|
380
|
+
message: job.body ?? '',
|
|
381
|
+
askedByName: job.askedByName,
|
|
382
|
+
task: job.task,
|
|
383
|
+
position: job.position ?? 1,
|
|
384
|
+
total: job.total ?? 1,
|
|
385
|
+
}),
|
|
386
|
+
// The knowledge paragraph, when this box holds a library — the same
|
|
387
|
+
// composer the tabs use, so an agent and a tab can never be told two
|
|
388
|
+
// different things about the same directory.
|
|
389
|
+
system: withProjectContext(SYSTEM_AGENT_FOR(taskKind), {
|
|
390
|
+
knowledgeDir: knowledgeDirFor(repoRoot),
|
|
391
|
+
// ARTIFACTS (0.94.0), while the server can show one — an agent's
|
|
392
|
+
// land on its page, under the facts row.
|
|
393
|
+
artifacts: getArtifactsAccepted(),
|
|
394
|
+
}) + (projectTools.instructions ? `\n\n${projectTools.instructions}` : ''),
|
|
395
|
+
agentTools: projectTools,
|
|
396
|
+
...(projectTools.codexHome ? { mcpEnv: { CODEX_HOME: projectTools.codexHome } } : {}),
|
|
397
|
+
...(posture === 'build' && rt === 'claude'
|
|
398
|
+
? { mcpArgs: ['--strict-mcp-config', '--mcp-config', projectTools.mcpPath] }
|
|
399
|
+
: posture === 'build' && rt === 'codex'
|
|
400
|
+
? { mcpArgs: projectTools.codexArgs }
|
|
401
|
+
: {}),
|
|
402
|
+
knowledgeDir: knowledgeDirFor(repoRoot),
|
|
403
|
+
// The kind's posture, as the one turn profile (turnProfile.mjs):
|
|
404
|
+
// build for code, the fenced design/research postures otherwise.
|
|
405
|
+
profile: posture,
|
|
406
|
+
cwd: wt,
|
|
407
|
+
runtime: rt,
|
|
408
|
+
resume,
|
|
409
|
+
resumeThreadId: codexResumeId || undefined,
|
|
410
|
+
onThreadId: (id) => {
|
|
411
|
+
seenThreadId = String(id ?? '').trim() || seenThreadId;
|
|
412
|
+
},
|
|
413
|
+
onAnswer: (t) => {
|
|
414
|
+
lastAnswer = t;
|
|
415
|
+
},
|
|
416
|
+
// Present only when the container named one — see brainFor.
|
|
417
|
+
...brain,
|
|
418
|
+
streamJson: true,
|
|
419
|
+
answerFromResult: true,
|
|
420
|
+
label: c.cyan('[agent]'),
|
|
421
|
+
// The CLI's own tail, relayed. Throttled by the same rule the tab's
|
|
422
|
+
// narrator keeps: overwritten, never appended, and ignored by the
|
|
423
|
+
// board past ~90 seconds.
|
|
424
|
+
onActivity: (a) => {
|
|
425
|
+
const line = a?.label;
|
|
426
|
+
if (!line) return;
|
|
427
|
+
// THE TRACE TAKES EVERY LINE; the pulse takes one every two
|
|
428
|
+
// seconds. Two channels, one stream, and the drop-sampler stays a
|
|
429
|
+
// drop-sampler — buffering the pulse would make a stale line look
|
|
430
|
+
// fresh, which is the one thing it exists to answer.
|
|
431
|
+
//
|
|
432
|
+
// …AND THE TRACE TAKES THE WHOLE LINE. `label` is a 160-char
|
|
433
|
+
// console readout; `full` is what the CLI actually said, when the
|
|
434
|
+
// parser had more than the label could hold (see runtimeEvents.mjs).
|
|
435
|
+
// The fallback is not a degradation — it is what every activity
|
|
436
|
+
// without a fuller form carries, and what an entire pre-0.87.0
|
|
437
|
+
// daemon carried for all of them. The PULSE below is deliberately
|
|
438
|
+
// still `line`: it is one overwritten line on a board, and a
|
|
439
|
+
// paragraph there would be a paragraph nobody can read.
|
|
440
|
+
if (!doubledKinds || !doubledKinds.has(a.kind)) trace.prose(a.kind, a.full ?? line);
|
|
441
|
+
const now = Date.now();
|
|
442
|
+
if (now - (lastAgentBeat.get(agentId) ?? 0) < 2_000) return;
|
|
443
|
+
lastAgentBeat.set(agentId, now);
|
|
444
|
+
void postAgentActivity(agentId, envScrub(String(line)).slice(0, 400));
|
|
445
|
+
},
|
|
446
|
+
// The structured twin of the line above — the same `tool_use` the
|
|
447
|
+
// Workbench's tool cards are built from, scrubbed at collection by
|
|
448
|
+
// the builder itself (bounded window BEFORE its caps; see
|
|
449
|
+
// toolEventOf). A tool it does not know pushes nothing.
|
|
450
|
+
onToolEvent: (name, input) => {
|
|
451
|
+
trace.tool(toolEventOf(name, input, wt, envScrub));
|
|
452
|
+
},
|
|
453
|
+
// The CLI's own per-turn token counts, off the `result` event. One
|
|
454
|
+
// result per turn, so this is a set and not an accumulate — the
|
|
455
|
+
// adding-up happens SERVER-side, behind the settle's idempotent win,
|
|
456
|
+
// because a retried settle must not charge the same turn twice.
|
|
457
|
+
// …tagged with WHICH CLI counted it (2026-09-24), so the server can
|
|
458
|
+
// split an agent's spend per CLI rather than labelling a codex
|
|
459
|
+
// agent's total with whatever the pre-review spent.
|
|
460
|
+
onUsage: (u) => {
|
|
461
|
+
usage = { ...u, runtime: rt };
|
|
462
|
+
},
|
|
463
|
+
// WHAT THE CLI SAYS IT CAN BE ASKED FOR, AND WHICH CONNECTORS NEED A
|
|
464
|
+
// LOGIN — the same free fact off the same init event the tab lane
|
|
465
|
+
// records (2026-09-23). Without it a box that only ever runs agents
|
|
466
|
+
// taught the app its skills and connectors ONCE, from the startup
|
|
467
|
+
// probe, and never again: a connector signed into at the box kept
|
|
468
|
+
// reading "needs authorization" for the life of the process.
|
|
469
|
+
onInit: (i) => {
|
|
470
|
+
recordSkills(i.skills);
|
|
471
|
+
recordMcpServers(i.mcpServers);
|
|
472
|
+
},
|
|
473
|
+
onSpawn: (ch) => {
|
|
474
|
+
child = ch;
|
|
475
|
+
// The AGENT it serves — what the machine snapshot charges this
|
|
476
|
+
// child's memory to.
|
|
477
|
+
workChildren.set(ch, agentId);
|
|
478
|
+
// …and the registry now counts what the reservation was standing
|
|
479
|
+
// in for.
|
|
480
|
+
releaseSlot();
|
|
481
|
+
// Under the PLACE, the id every reader asks with — the worktree
|
|
482
|
+
// sweep and `killTargetOk` both key on `a-<agentId>`. Recorded under
|
|
483
|
+
// the bare agent id it was never read, reported `processes: []`
|
|
484
|
+
// over a running watcher, and left one dead entry per turn.
|
|
485
|
+
noteSessionGroup(place, ch.pid);
|
|
486
|
+
// Keyed by PLACE, because the retire sweep iterates directory names
|
|
487
|
+
// and a place id IS one. It is what lets a hard stop actually reach
|
|
488
|
+
// the CLI — see `retireWorkSessions`.
|
|
489
|
+
agentChildren.set(place, ch);
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
/**
|
|
493
|
+
* A RESUME THAT CAME BACK EMPTY, OR WITH ONLY THE CLI SAYING THE
|
|
494
|
+
* CONVERSATION IS GONE, RUNS ONCE MORE FRESH — the tab lane's rule,
|
|
495
|
+
* for the tab lane's reason: a pinned id the CLI has since pruned
|
|
496
|
+
* (or a `--continue` over a directory Claude never spoke in, which an
|
|
497
|
+
* agent switched between CLIs can reach) would otherwise settle
|
|
498
|
+
* `nothing` on every turn forever. Same worktree, never a reset. The
|
|
499
|
+
* init evidence the rule needs is measured by the sequence itself
|
|
500
|
+
* (resumeLost.mjs, SOLID F001): a successful answer that merely
|
|
501
|
+
* MENTIONS a missing session is never run twice.
|
|
502
|
+
*/
|
|
503
|
+
out = await runTurnResumingOnce(runTurn, agentTurnArgs, {
|
|
504
|
+
resume,
|
|
505
|
+
runtime: rt,
|
|
506
|
+
beforeFresh: () => {
|
|
507
|
+
seenThreadId = null;
|
|
508
|
+
lastAnswer = null;
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
} finally {
|
|
512
|
+
// What the CLI counted, for the lane's catch if anything below throws.
|
|
513
|
+
measured.usage = usage;
|
|
514
|
+
projectTools?.cleanup();
|
|
515
|
+
/**
|
|
516
|
+
* THE TAIL, BEFORE THE SETTLE — so the last thing the agent did is on
|
|
517
|
+
* the record by the time the board is told the turn is over.
|
|
518
|
+
*
|
|
519
|
+
* The server deliberately does NOT require a pending turn to accept a
|
|
520
|
+
* trace batch (a late tail is still that turn's record), so a race here
|
|
521
|
+
* is survivable rather than lossy; flushing first simply means it
|
|
522
|
+
* almost never happens. Bounded, and the settle is what matters: an
|
|
523
|
+
* uplink that will not answer costs the tail and nothing else.
|
|
524
|
+
*/
|
|
525
|
+
trace.stop();
|
|
526
|
+
await trace.flush(TRACE_FINAL_FLUSH_MS);
|
|
527
|
+
if (child) workChildren.delete(child);
|
|
528
|
+
if (agentChildren.get(place) === child) agentChildren.delete(place);
|
|
529
|
+
if (ranMarker && rt === 'claude') {
|
|
530
|
+
try {
|
|
531
|
+
writeFileSync(ranMarker, '1');
|
|
532
|
+
} catch {
|
|
533
|
+
/* a missing marker only costs one un-resumed turn */
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// The thread THIS turn spoke under, pinned so the next card, answer
|
|
537
|
+
// or merge-resolve resumes it. After the turn, like the tab lane: an
|
|
538
|
+
// id learned mid-turn is only true once the turn that learned it ends.
|
|
539
|
+
if (codexThreadMarker && seenThreadId && CODEX_THREAD_RE.test(seenThreadId)) {
|
|
540
|
+
try {
|
|
541
|
+
writeFileSync(codexThreadMarker, seenThreadId);
|
|
542
|
+
} catch {
|
|
543
|
+
/* best-effort — the next turn runs fresh */
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* AN ACTION THAT CHANGES WHAT THE MACHINE WOULD MEASURE MUST CAUSE A
|
|
548
|
+
* NEW MEASUREMENT — the rule every session settle keeps, and the one
|
|
549
|
+
* lane that did not. An agent's branch diff, head sha and trailered
|
|
550
|
+
* commits only refreshed on the ≤60s sweep, so review opened right
|
|
551
|
+
* after a turn described the branch as it was BEFORE the work.
|
|
552
|
+
*
|
|
553
|
+
* Here rather than after the settle POST because the CLI has exited,
|
|
554
|
+
* so the tree is final — and because a queue that just emptied runs
|
|
555
|
+
* the project's check next, which may hold this function for ten
|
|
556
|
+
* minutes. Fire-and-forget on an endpoint that already exists, so no
|
|
557
|
+
* floor: it must never delay the settle behind it.
|
|
558
|
+
*/
|
|
559
|
+
void reportSessionWorktree(place).catch(() => {});
|
|
560
|
+
// …and what it drew to SHOW the person (2026-09-22): uploaded against
|
|
561
|
+
// the AGENT, so it lands on the agent's page whoever is looking.
|
|
562
|
+
// Fire-and-forget for the same reason — never delay the settle.
|
|
563
|
+
void artifacts
|
|
564
|
+
.report({ placeDir: wt, before: artifactsBefore, agentId, turnId })
|
|
565
|
+
.catch(() => {});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
const commits = commitsBetween(wt, before);
|
|
570
|
+
// WHAT THE TURN REPORTS is one decision, off what was measured here
|
|
571
|
+
// (workAgentTurnOutcome.mjs). The side effects it implies stay on this
|
|
572
|
+
// side: the machine's limit state, the park, and the review-entry beat.
|
|
573
|
+
const { res, limit, body, final } = agentTurnSettlement({
|
|
574
|
+
turnId,
|
|
575
|
+
job,
|
|
576
|
+
kind,
|
|
577
|
+
out,
|
|
578
|
+
lastAnswer,
|
|
579
|
+
usage,
|
|
580
|
+
commits,
|
|
581
|
+
artifactsBefore,
|
|
582
|
+
branch,
|
|
583
|
+
wt,
|
|
584
|
+
});
|
|
585
|
+
if (res) setRuntimeLimit(PROJECT_ID, rt, null);
|
|
586
|
+
if (limit) {
|
|
587
|
+
setRuntimeLimit(PROJECT_ID, rt, limit);
|
|
588
|
+
// Every agent ON THIS CLI parks, because that account is shared: one
|
|
589
|
+
// hitting the limit means all of them have. Named since 2026-09-24 —
|
|
590
|
+
// a machine can hold a Claude login and a Codex login, and a Codex
|
|
591
|
+
// limit is no reason to stop the Claude agents. The turn itself is
|
|
592
|
+
// reported as `nothing` — it did not deliver and it did not ask.
|
|
593
|
+
await postAgentParked(limit, rt);
|
|
594
|
+
}
|
|
595
|
+
const reply = await postAgentTurn(body);
|
|
596
|
+
// The queue just emptied. Run the project's own check and the AI
|
|
597
|
+
// pre-review HERE, in the worktree we are already standing in and still
|
|
598
|
+
// hold the lock on — see `runReviewEntry`. Only after the settle that may
|
|
599
|
+
// say delivered: the three `nothing` settles never opened it.
|
|
600
|
+
if (final && reply?.review === true) await runReviewEntry(agentId, wt, job.agentName);
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
return { runAgentTurnInPlace, commitsBetween };
|
|
604
|
+
}
|