flowviant 0.102.2 → 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.
Files changed (141) hide show
  1. package/bin/cli.mjs +87 -474
  2. package/bin/lib/admission.mjs +19 -3
  3. package/bin/lib/agentPlan.mjs +2 -2
  4. package/bin/lib/agentTaskKinds.mjs +93 -0
  5. package/bin/lib/agySessions.mjs +158 -0
  6. package/bin/lib/artifactPolicy.mjs +44 -0
  7. package/bin/lib/artifactPreview.mjs +52 -0
  8. package/bin/lib/artifactReporter.mjs +163 -0
  9. package/bin/lib/artifacts.mjs +47 -227
  10. package/bin/lib/authReject.mjs +16 -4
  11. package/bin/lib/authproxy.mjs +47 -304
  12. package/bin/lib/boxIdentity.mjs +245 -0
  13. package/bin/lib/childEnv.mjs +11 -9
  14. package/bin/lib/claude.mjs +14 -851
  15. package/bin/lib/claudeAuth.mjs +3 -3
  16. package/bin/lib/claudePosture.mjs +367 -0
  17. package/bin/lib/claudeSessions.mjs +411 -0
  18. package/bin/lib/claudeStream.mjs +207 -0
  19. package/bin/lib/cliLimit.mjs +34 -0
  20. package/bin/lib/commandSpecs.mjs +173 -0
  21. package/bin/lib/config.mjs +11 -42
  22. package/bin/lib/credentialDate.mjs +22 -0
  23. package/bin/lib/credentials.mjs +21 -26
  24. package/bin/lib/daemonLogging.mjs +55 -0
  25. package/bin/lib/daemonState.mjs +64 -0
  26. package/bin/lib/deploy.mjs +148 -647
  27. package/bin/lib/deployCheckout.mjs +265 -0
  28. package/bin/lib/deployConfig.mjs +142 -0
  29. package/bin/lib/deployRunner.mjs +311 -0
  30. package/bin/lib/deployWire.mjs +41 -0
  31. package/bin/lib/desktopContract.mjs +56 -105
  32. package/bin/lib/env.mjs +49 -484
  33. package/bin/lib/fleet.mjs +184 -2396
  34. package/bin/lib/fleetJobs.mjs +120 -0
  35. package/bin/lib/fleetPost.mjs +40 -0
  36. package/bin/lib/fleetReports.mjs +392 -0
  37. package/bin/lib/fleetRoster.mjs +470 -0
  38. package/bin/lib/fleetStartup.mjs +196 -0
  39. package/bin/lib/fleetWake.mjs +45 -0
  40. package/bin/lib/fleetWire.mjs +46 -0
  41. package/bin/lib/git.mjs +34 -3
  42. package/bin/lib/gitMergeIdentity.mjs +51 -0
  43. package/bin/lib/grant.mjs +5 -3
  44. package/bin/lib/help.mjs +3 -125
  45. package/bin/lib/holder.mjs +482 -0
  46. package/bin/lib/instance.mjs +124 -46
  47. package/bin/lib/knowledge.mjs +16 -799
  48. package/bin/lib/knowledgeFetch.mjs +38 -0
  49. package/bin/lib/knowledgeFileSync.mjs +112 -0
  50. package/bin/lib/knowledgeFiles.mjs +487 -0
  51. package/bin/lib/knowledgeLibrary.mjs +249 -0
  52. package/bin/lib/landed.mjs +2 -1
  53. package/bin/lib/listeners.mjs +99 -141
  54. package/bin/lib/localSessions.mjs +30 -555
  55. package/bin/lib/login.mjs +3 -2
  56. package/bin/lib/loginPath.mjs +3 -5
  57. package/bin/lib/machineDisconnect.mjs +203 -0
  58. package/bin/lib/machineEnv.mjs +22 -0
  59. package/bin/lib/machineLimits.mjs +61 -0
  60. package/bin/lib/machineListing.mjs +338 -0
  61. package/bin/lib/machines.mjs +78 -455
  62. package/bin/lib/machinesCommand.mjs +214 -0
  63. package/bin/lib/mcp-cli.mjs +2 -1
  64. package/bin/lib/patchRevert.mjs +60 -0
  65. package/bin/lib/prWorkflow.mjs +157 -0
  66. package/bin/lib/preflight.mjs +2 -1
  67. package/bin/lib/pressurePolicy.mjs +104 -0
  68. package/bin/lib/preview.mjs +2 -272
  69. package/bin/lib/previewFramePolicy.mjs +112 -0
  70. package/bin/lib/previewGatePolicy.mjs +236 -0
  71. package/bin/lib/previewRegistry.mjs +235 -0
  72. package/bin/lib/printable.mjs +2 -2
  73. package/bin/lib/procRegistry.mjs +28 -18
  74. package/bin/lib/procScan.mjs +146 -0
  75. package/bin/lib/processes.mjs +29 -37
  76. package/bin/lib/projectToolRuntimes.mjs +138 -0
  77. package/bin/lib/projectTools.mjs +13 -88
  78. package/bin/lib/prompts.mjs +28 -42
  79. package/bin/lib/repoPath.mjs +26 -0
  80. package/bin/lib/resources.mjs +7 -69
  81. package/bin/lib/resumeLost.mjs +107 -0
  82. package/bin/lib/runTurn.mjs +324 -0
  83. package/bin/lib/runtimeAntigravity.mjs +230 -0
  84. package/bin/lib/runtimeCapabilities.mjs +334 -0
  85. package/bin/lib/runtimeClaude.mjs +126 -0
  86. package/bin/lib/runtimeCodex.mjs +217 -0
  87. package/bin/lib/runtimeDetection.mjs +121 -0
  88. package/bin/lib/runtimeEvents.mjs +482 -0
  89. package/bin/lib/runtimeOfferings.mjs +4 -1
  90. package/bin/lib/runtimes.mjs +42 -1402
  91. package/bin/lib/safeFileName.mjs +74 -0
  92. package/bin/lib/shipMerge.mjs +33 -2
  93. package/bin/lib/standDownExit.mjs +181 -0
  94. package/bin/lib/standDownGate.mjs +48 -0
  95. package/bin/lib/startProject.mjs +279 -0
  96. package/bin/lib/trace.mjs +3 -3
  97. package/bin/lib/turnLock.mjs +68 -0
  98. package/bin/lib/turnProfile.mjs +77 -0
  99. package/bin/lib/uninstall.mjs +55 -45
  100. package/bin/lib/uninstallCopies.mjs +112 -0
  101. package/bin/lib/update.mjs +30 -17
  102. package/bin/lib/uplinkScrub.mjs +265 -0
  103. package/bin/lib/vault.mjs +22 -151
  104. package/bin/lib/vaultDiff.mjs +183 -0
  105. package/bin/lib/versionOrder.mjs +42 -0
  106. package/bin/lib/views.mjs +35 -12
  107. package/bin/lib/wikiRunner.mjs +630 -0
  108. package/bin/lib/work.mjs +188 -3037
  109. package/bin/lib/workAdoptCarry.mjs +94 -0
  110. package/bin/lib/workAgentCheck.mjs +278 -0
  111. package/bin/lib/workAgentMerges.mjs +54 -142
  112. package/bin/lib/workAgentPlans.mjs +14 -12
  113. package/bin/lib/workAgentPrecheck.mjs +393 -0
  114. package/bin/lib/workAgentPublish.mjs +177 -0
  115. package/bin/lib/workAgentReview.mjs +8 -666
  116. package/bin/lib/workAgentTurnBegun.mjs +109 -0
  117. package/bin/lib/workAgentTurnExecution.mjs +604 -0
  118. package/bin/lib/workAgentTurnOutcome.mjs +207 -0
  119. package/bin/lib/workAgentTurnReports.mjs +189 -0
  120. package/bin/lib/workAgentTurns.mjs +92 -897
  121. package/bin/lib/workAttachments.mjs +93 -0
  122. package/bin/lib/workBrain.mjs +53 -0
  123. package/bin/lib/workChildren.mjs +110 -0
  124. package/bin/lib/workCommandAudit.mjs +62 -0
  125. package/bin/lib/workDiffs.mjs +2 -1
  126. package/bin/lib/workNarration.mjs +152 -0
  127. package/bin/lib/workPlaceReports.mjs +426 -0
  128. package/bin/lib/workPlaces.mjs +228 -0
  129. package/bin/lib/workPreviews.mjs +31 -9
  130. package/bin/lib/workProcesses.mjs +4 -5
  131. package/bin/lib/workPullRequests.mjs +52 -126
  132. package/bin/lib/workReportQueue.mjs +254 -0
  133. package/bin/lib/workRetire.mjs +185 -0
  134. package/bin/lib/workSessionGroups.mjs +166 -0
  135. package/bin/lib/workSessionRuntime.mjs +164 -0
  136. package/bin/lib/workSessionTokens.mjs +68 -0
  137. package/bin/lib/workSessionTurns.mjs +892 -0
  138. package/bin/lib/workShip.mjs +9 -29
  139. package/bin/lib/workToolLog.mjs +76 -0
  140. package/package.json +3 -2
  141. 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
+ }