flowviant 0.116.0 → 0.124.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +66 -12
  2. package/bin/cli.mjs +50 -17
  3. package/bin/lib/admission.mjs +7 -14
  4. package/bin/lib/agentAsk.mjs +123 -0
  5. package/bin/lib/agentBrief.mjs +36 -0
  6. package/bin/lib/agentFootprint.mjs +76 -0
  7. package/bin/lib/agentInternalWait.mjs +83 -0
  8. package/bin/lib/agentPlan.mjs +134 -31
  9. package/bin/lib/agentPrerequisiteContract.mjs +43 -0
  10. package/bin/lib/agentPublish.mjs +12 -13
  11. package/bin/lib/agentTaskKinds.mjs +5 -8
  12. package/bin/lib/agySessions.mjs +3 -3
  13. package/bin/lib/artifactContracts.mjs +91 -20
  14. package/bin/lib/artifactIteration.mjs +182 -0
  15. package/bin/lib/artifactPreview.mjs +92 -18
  16. package/bin/lib/artifactRecovery.mjs +36 -0
  17. package/bin/lib/artifactReporter.mjs +194 -22
  18. package/bin/lib/artifacts.mjs +70 -63
  19. package/bin/lib/authproxy.mjs +2 -1
  20. package/bin/lib/childEnv.mjs +9 -4
  21. package/bin/lib/claude.mjs +10 -20
  22. package/bin/lib/claudeAuth.mjs +15 -5
  23. package/bin/lib/claudePersonal.mjs +71 -6
  24. package/bin/lib/claudePosture.mjs +227 -54
  25. package/bin/lib/claudeSessions.mjs +7 -7
  26. package/bin/lib/claudeStream.mjs +27 -13
  27. package/bin/lib/cliLimit.mjs +2 -2
  28. package/bin/lib/codexHome.mjs +17 -0
  29. package/bin/lib/codexPersonal.mjs +44 -3
  30. package/bin/lib/codexProjectRead.mjs +63 -0
  31. package/bin/lib/codexRelay.mjs +21 -5
  32. package/bin/lib/codexResearch.mjs +193 -0
  33. package/bin/lib/commandSpecs.mjs +6 -0
  34. package/bin/lib/config.mjs +37 -24
  35. package/bin/lib/conversationIds.mjs +51 -0
  36. package/bin/lib/credentials.mjs +2 -7
  37. package/bin/lib/daemonLocks.mjs +90 -0
  38. package/bin/lib/daemonState.mjs +5 -5
  39. package/bin/lib/deploy.mjs +6 -9
  40. package/bin/lib/deployCheckout.mjs +26 -22
  41. package/bin/lib/deployRunner.mjs +54 -18
  42. package/bin/lib/deployWire.mjs +32 -23
  43. package/bin/lib/desktopContract.mjs +6 -5
  44. package/bin/lib/discoveryContract.mjs +19 -0
  45. package/bin/lib/env.mjs +104 -35
  46. package/bin/lib/fleet.mjs +53 -13
  47. package/bin/lib/fleetPost.mjs +172 -25
  48. package/bin/lib/fleetReports.mjs +172 -171
  49. package/bin/lib/fleetStartup.mjs +6 -6
  50. package/bin/lib/fleetWire.mjs +42 -0
  51. package/bin/lib/git.mjs +111 -61
  52. package/bin/lib/gitMergeIdentity.mjs +2 -8
  53. package/bin/lib/grant.mjs +23 -7
  54. package/bin/lib/holder.mjs +108 -81
  55. package/bin/lib/hooks/mcpAllow.mjs +60 -0
  56. package/bin/lib/hooks/readGuard.mjs +54 -11
  57. package/bin/lib/imageArtifact.mjs +74 -0
  58. package/bin/lib/install.mjs +80 -33
  59. package/bin/lib/instance.mjs +36 -83
  60. package/bin/lib/intakeDraft.mjs +4 -1
  61. package/bin/lib/knowledgeFileSync.mjs +31 -0
  62. package/bin/lib/knowledgeFiles.mjs +28 -83
  63. package/bin/lib/landed.mjs +22 -29
  64. package/bin/lib/launchCommand.mjs +91 -0
  65. package/bin/lib/listeners.mjs +0 -5
  66. package/bin/lib/localSessions.mjs +52 -39
  67. package/bin/lib/login.mjs +22 -12
  68. package/bin/lib/loginPath.mjs +22 -17
  69. package/bin/lib/machineListing.mjs +6 -3
  70. package/bin/lib/maintenance.mjs +47 -0
  71. package/bin/lib/maintenanceInventory.mjs +39 -0
  72. package/bin/lib/mcp-cli.mjs +9 -10
  73. package/bin/lib/mcpInstall.mjs +163 -0
  74. package/bin/lib/mcpInventory.mjs +119 -0
  75. package/bin/lib/noWindowEnv.mjs +23 -3
  76. package/bin/lib/ownedDirs.mjs +99 -0
  77. package/bin/lib/pinnedCapture.mjs +383 -0
  78. package/bin/lib/planDependencies.mjs +70 -0
  79. package/bin/lib/postureTools.mjs +85 -0
  80. package/bin/lib/prWorkflow.mjs +94 -17
  81. package/bin/lib/preflight.mjs +8 -25
  82. package/bin/lib/preview.mjs +19 -25
  83. package/bin/lib/previewGatePolicy.mjs +34 -9
  84. package/bin/lib/previewRegistry.mjs +4 -11
  85. package/bin/lib/printable.mjs +70 -7
  86. package/bin/lib/procRegistry.mjs +33 -13
  87. package/bin/lib/procScan.mjs +2 -3
  88. package/bin/lib/procStat.mjs +76 -0
  89. package/bin/lib/processGroup.mjs +48 -0
  90. package/bin/lib/processes.mjs +10 -24
  91. package/bin/lib/projectToolRuntimes.mjs +328 -19
  92. package/bin/lib/projectTools.mjs +109 -34
  93. package/bin/lib/prompts.mjs +205 -60
  94. package/bin/lib/repoPath.mjs +19 -0
  95. package/bin/lib/repoState.mjs +17 -19
  96. package/bin/lib/repositoryBaseline.mjs +68 -0
  97. package/bin/lib/runTurn.mjs +286 -209
  98. package/bin/lib/runtimeAntigravity.mjs +16 -5
  99. package/bin/lib/runtimeCapabilities.mjs +215 -21
  100. package/bin/lib/runtimeClaude.mjs +78 -10
  101. package/bin/lib/runtimeCodex.mjs +217 -209
  102. package/bin/lib/runtimeDetection.mjs +8 -2
  103. package/bin/lib/runtimeEvents.mjs +28 -11
  104. package/bin/lib/runtimeLimits.mjs +5 -6
  105. package/bin/lib/runtimeOfferings.mjs +99 -14
  106. package/bin/lib/runtimes.mjs +10 -0
  107. package/bin/lib/shipSweep.mjs +5 -3
  108. package/bin/lib/shot.mjs +91 -17
  109. package/bin/lib/sleep.mjs +11 -0
  110. package/bin/lib/startProject.mjs +5 -5
  111. package/bin/lib/statePaths.mjs +40 -0
  112. package/bin/lib/tty.mjs +25 -15
  113. package/bin/lib/turnDiagnostics.mjs +93 -0
  114. package/bin/lib/turnLock.mjs +7 -14
  115. package/bin/lib/turnProfile.mjs +16 -4
  116. package/bin/lib/ui.mjs +0 -3
  117. package/bin/lib/uninstall.mjs +30 -26
  118. package/bin/lib/update.mjs +79 -27
  119. package/bin/lib/uplinkScrub.mjs +296 -31
  120. package/bin/lib/vault.mjs +78 -10
  121. package/bin/lib/vaultDiff.mjs +5 -2
  122. package/bin/lib/views.mjs +37 -12
  123. package/bin/lib/wikiRunner.mjs +40 -61
  124. package/bin/lib/work.mjs +44 -13
  125. package/bin/lib/workAdoptCarry.mjs +2 -4
  126. package/bin/lib/workAgentBrief.mjs +92 -0
  127. package/bin/lib/workAgentBriefRunner.mjs +59 -0
  128. package/bin/lib/workAgentCheck.mjs +104 -57
  129. package/bin/lib/workAgentMerges.mjs +62 -57
  130. package/bin/lib/workAgentPlans.mjs +82 -199
  131. package/bin/lib/workAgentPrecheck.mjs +76 -138
  132. package/bin/lib/workAgentPublish.mjs +6 -8
  133. package/bin/lib/workAgentTurnBegun.mjs +15 -11
  134. package/bin/lib/workAgentTurnExecution.mjs +131 -67
  135. package/bin/lib/workAgentTurnOutcome.mjs +11 -2
  136. package/bin/lib/workAgentTurnReports.mjs +9 -37
  137. package/bin/lib/workAgentTurns.mjs +64 -7
  138. package/bin/lib/workArtifactReconcile.mjs +79 -0
  139. package/bin/lib/workAttachments.mjs +66 -11
  140. package/bin/lib/workBrain.mjs +9 -4
  141. package/bin/lib/workCappedTurn.mjs +119 -0
  142. package/bin/lib/workChildren.mjs +5 -5
  143. package/bin/lib/workCommandAudit.mjs +18 -2
  144. package/bin/lib/workDiffs.mjs +52 -6
  145. package/bin/lib/workIntake.mjs +35 -60
  146. package/bin/lib/workPlaceReports.mjs +7 -4
  147. package/bin/lib/workPlaces.mjs +113 -31
  148. package/bin/lib/workPreviews.mjs +47 -112
  149. package/bin/lib/workProcesses.mjs +8 -35
  150. package/bin/lib/workPullRequests.mjs +37 -56
  151. package/bin/lib/workReportQueue.mjs +100 -148
  152. package/bin/lib/workRetire.mjs +2 -1
  153. package/bin/lib/workSessionRuntime.mjs +38 -37
  154. package/bin/lib/workSessionTurns.mjs +256 -199
  155. package/bin/lib/workShip.mjs +43 -23
  156. package/bin/lib/workToolLog.mjs +70 -3
  157. package/bin/lib/workTurnShape.mjs +165 -0
  158. package/npm-shrinkwrap.json +69 -0
  159. package/package.json +9 -4
  160. package/bin/lib/fleetJobs.mjs +0 -120
  161. package/bin/lib/patchRevert.mjs +0 -60
@@ -19,20 +19,23 @@
19
19
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
20
20
  import { PROJECT_ID } from './config.mjs';
21
21
  import { setRuntimeLimit } from './daemonState.mjs';
22
- import { git } from './git.mjs';
22
+ import { gitOrNull } from './git.mjs';
23
23
  import { stashCard } from './agentCards.mjs';
24
24
  import { c } from './ui.mjs';
25
25
  import { runTurn } from './runTurn.mjs';
26
+ import { killGroup } from './processGroup.mjs';
26
27
  import { SYSTEM_AGENT_FOR, AGENT_TASK_KICKOFF, AGENT_TASK_SPEC, AGENT_HUMAN_KICKOFF, withProjectContext } from './prompts.mjs';
27
28
  import { AGENT_TASK_KINDS, agentTaskKindOf, unknownAgentTaskKind } from './agentTaskKinds.mjs';
28
29
  import { knowledgeDirFor } from './knowledgeFiles.mjs';
29
30
  import { scrub as envScrub } from './uplinkScrub.mjs';
30
- import { canRun, RUNTIMES } from './runtimes.mjs';
31
- import { recordSkills, recordMcpServers } from './runtimeCapabilities.mjs';
31
+ import { canRun, runtimeById } from './runtimes.mjs';
32
32
  import { toolEventOf, CLAUDE_TOOL_PROSE_KINDS } from './runtimeEvents.mjs';
33
33
  import { makeTraceRelay } from './trace.mjs';
34
- import { readBaseTools, prepareAgentTools } from './projectTools.mjs';
34
+ import { mcpJsonServerNamesAbove, readBaseTools, prepareAgentTools } from './projectTools.mjs';
35
+ import { TURN_PROFILES } from './turnProfile.mjs';
36
+ import { postureToolsNote } from './postureTools.mjs';
35
37
  import { runTurnResumingOnce } from './resumeLost.mjs';
38
+ import { INTERNAL_WAIT_CONTRACT, runAgentWithInternalWaits } from './agentInternalWait.mjs';
36
39
  import { CODEX_THREAD_RE } from './workSessionRuntime.mjs';
37
40
  import { brainFor } from './workBrain.mjs';
38
41
  import { begunTurnRefusal } from './workAgentTurnBegun.mjs';
@@ -41,6 +44,7 @@ import { ensureArtifactDir, ensureFenceScratch } from './artifacts.mjs';
41
44
  import { agentBrowserHome } from './noWindowEnv.mjs';
42
45
 
43
46
  export function createAgentTurnExecution({
47
+ makeBrief,
44
48
  baseRef,
45
49
  baseDir,
46
50
  repoRoot,
@@ -95,15 +99,8 @@ export function createAgentTurnExecution({
95
99
  * reconciliation books whatever no card claimed. Missing beats fabricated
96
100
  * and both beat a stall.
97
101
  */
98
- let out;
99
- try {
100
- out = git(['log', '--format=%H', '--no-merges', `${from}..HEAD`, '--not', baseRef()], wt);
101
- } catch {
102
- return [];
103
- }
104
- return typeof out === 'string'
105
- ? out.split('\n').map((x) => x.trim()).filter(Boolean).slice(0, 50)
106
- : [];
102
+ const out = gitOrNull(['log', '--format=%H', '--no-merges', `${from}..HEAD`, '--not', baseRef()], wt);
103
+ return out?.split('\n').map((x) => x.trim()).filter(Boolean).slice(0, 50) ?? [];
107
104
  };
108
105
 
109
106
  /**
@@ -114,7 +111,9 @@ export function createAgentTurnExecution({
114
111
  * `releaseSlot` hands back the admission reservation the lane took on this
115
112
  * turn's behalf, at the moment the CLI actually exists. Idempotent.
116
113
  */
117
- const runAgentTurnInPlace = async (job, { turnId, agentId, place, releaseSlot, postAgentTurn, measured }) => {
114
+ /** `run` is the lane's record of this turn (`interrupted`, set by
115
+ * `interruptAgentTurns`); absent, nothing can interrupt it. */
116
+ const runAgentTurnInPlace = async (job, { turnId, agentId, place, releaseSlot, postAgentTurn, measured, run = { interrupted: false } }) => {
118
117
  // WORK THAT HAS BEGUN LIVES ON EXACTLY ONE BOX — read BEFORE anything is
119
118
  // cut, because `placeWtFor` would cut a rival branch of the same name. The
120
119
  // rule and its three-state branch measurement: workAgentTurnBegun.mjs.
@@ -147,19 +146,13 @@ export function createAgentTurnExecution({
147
146
  * `runAgentMerge` learned this two commits ago and says so in its own
148
147
  * comment; this is the same call, in the same file, left unguarded. Both
149
148
  * facts are OPTIONAL to a turn — they describe the branch, they do not
150
- * gate the work — so failing to read them costs a null, not the turn.
149
+ * gate the work — so failing to read them costs a null, not the turn:
150
+ * git.mjs's `gitOrNull`, the one spelling of that rule.
151
151
  */
152
- const readGit = (args) => {
153
- try {
154
- return (git(args, wt) || '').trim() || null;
155
- } catch {
156
- return null;
157
- }
158
- };
159
152
  // WHERE THE BRANCH WAS BEFORE THIS TURN, so the commits reported are the
160
153
  // ones this turn actually made.
161
- const before = readGit(['rev-parse', 'HEAD']);
162
- const branch = readGit(['symbolic-ref', '--quiet', '--short', 'HEAD']);
154
+ const before = gitOrNull(['rev-parse', 'HEAD'], wt);
155
+ const branch = gitOrNull(['symbolic-ref', '--quiet', '--short', 'HEAD'], wt);
163
156
  // …and the same three facts for the lane's catch: a turn that throws past
164
157
  // here is still settled with what was measured (workAgentTurns.mjs).
165
158
  measured.wt = wt;
@@ -176,9 +169,9 @@ export function createAgentTurnExecution({
176
169
  * posture, which can write ONLY `.flowviant/artifacts/` (claudePosture.mjs).
177
170
  *
178
171
  * A RUNTIME THAT CANNOT EXPRESS THE POSTURE IS REFUSED, NOT IMPROVISED.
179
- * Antigravity declares none of them and Codex declares `design` (since
180
- * 0.115.0) and `image` but not `research` (runtimeCodex.mjs,
181
- * runtimeAntigravity.mjs), and the only
172
+ * Antigravity declares none of them; Codex declares `design` (since
173
+ * 0.115.0), `image` and `research` (0.117.0, over an isolated copy —
174
+ * codexResearch.mjs) (runtimeCodex.mjs, runtimeAntigravity.mjs), and the only
182
175
  * alternative to refusing is running the card as a BUILD turn with
183
176
  * permissions skipped — an agent writing code for an ask that was a
184
177
  * mockup, reporting success. `nothing` puts the agent in Stuck saying so.
@@ -220,7 +213,7 @@ export function createAgentTurnExecution({
220
213
  // entry in agentTaskKinds.mjs; nothing below branches on the kind name.
221
214
  const kind = AGENT_TASK_KINDS[taskKind];
222
215
  const posture = kind.posture;
223
- if (!canRun(RUNTIMES[rt], posture)) {
216
+ if (!canRun(runtimeById(rt), posture)) {
224
217
  await postAgentTurn({
225
218
  turnId,
226
219
  outcome: 'nothing',
@@ -388,12 +381,12 @@ export function createAgentTurnExecution({
388
381
  /**
389
382
  * Prose the structured event will carry anyway, dropped so a read does
390
383
  * not render twice — but ONLY on a runtime whose stream reaches
391
- * `onToolEvent` at all. Codex and agy have their own parsers and never
392
- * call it, so dropping their tool prose would blank their agents' traces.
384
+ * `onToolEvent` here. Codex uses completed activity; agy has its own
385
+ * parser. Dropping their tool prose would blank their agents' traces.
393
386
  * `parse: null` is exactly the claudeStream.mjs stream path. See
394
387
  * CLAUDE_TOOL_PROSE_KINDS.
395
388
  */
396
- const doubledKinds = RUNTIMES[rt]?.parse ? null : CLAUDE_TOOL_PROSE_KINDS;
389
+ const doubledKinds = runtimeById(rt)?.parse ? null : CLAUDE_TOOL_PROSE_KINDS;
397
390
 
398
391
  /**
399
392
  * WHICH BRAIN THIS CONTAINER WAS PINNED TO — the same `brainFor` the tab
@@ -421,6 +414,9 @@ export function createAgentTurnExecution({
421
414
  * ever, which holds the self-update and the tray's install for ever.
422
415
  */
423
416
  const spawnedChildren = new Set();
417
+ /** The CLI, unless a person already asked to stop this turn (Interrupt
418
+ * now): then nothing starts — no fresh retry, no continuation. */
419
+ const runCli = (args) => (run.interrupted ? Promise.resolve('') : runTurn(args));
424
420
  /**
425
421
  * WHAT THIS TURN SPENT, AS THE CLI COUNTED IT (2026-09-19).
426
422
  *
@@ -439,6 +435,8 @@ export function createAgentTurnExecution({
439
435
  /** THE MODEL THE TURN RAN ON, as its CLI named it (turnModel.mjs) — rides
440
436
  * every settle `usage` rides, for the same reason; null sends no key. */
441
437
  let model = null;
438
+ const brief = makeBrief?.({ agentId, turnId, runtime: rt, model: () => model ?? job.model });
439
+ brief?.append('prompt', job.task ? { title: job.task.title, message: job.body, discussion: job.cardThread, brief: job.task.brief, criteria: job.task.criteria } : job.body);
442
440
  /**
443
441
  * THE AGENT'S LAST MESSAGE, ALONE — where the final JSON object lives.
444
442
  * Codex's `out` is every message plus stderr plus error text, and
@@ -450,11 +448,32 @@ export function createAgentTurnExecution({
450
448
  /** The agent's artifact directory as it stood before this turn — the
451
449
  * tab lane's rule, in the agent's own worktree (2026-09-22). */
452
450
  const artifactsBefore = beforeArtifacts(wt);
451
+ const artifactInstructions = getArtifactsAccepted()
452
+ ? await artifacts.prepare?.({ placeDir: wt, before: artifactsBefore, agentId }) ?? '' : '';
453
453
  let projectTools = null;
454
454
  try {
455
455
  // A turn reads the base ref afresh. Changes the agent has made to its
456
456
  // own worktree cannot silently change the next turn's tools.
457
457
  projectTools = prepareAgentTools(readBaseTools(repoRoot, baseRef()), rt, process.env, wt, { codexHome: codexAgentHome });
458
+ const mountsProjectMcp = TURN_PROFILES[posture]?.projectMcp === true && (rt === 'claude' || rt === 'codex');
459
+ // A fenced posture on a runner that ignores its own file there (Codex's
460
+ // `--ignore-user-config`) takes the servers as `fencedMount` instead.
461
+ const fencedMount = mountsProjectMcp && posture !== 'build' ? projectTools.fencedMount : null;
462
+ // THE MACHINE'S OWN SERVERS ON CLAUDE (2026-10-11, the owner): the turn
463
+ // is not strict, and every `.mcp.json` name from the worktree up is
464
+ // disabled so only the base copy speaks for the project. Null (an
465
+ // unboundable walk) mounts strictly: the base copy alone.
466
+ const claudeDisabled = mountsProjectMcp && rt === 'claude' ? mcpJsonServerNamesAbove(wt) : null;
467
+ // What this card's posture withholds, said to the agent — with the
468
+ // servers it did and did not mount, and why — so it relays the fence
469
+ // instead of inventing a setting (postureTools.mjs). Empty for a code card.
470
+ const toolsNote = postureToolsNote({
471
+ posture,
472
+ runtime: rt,
473
+ mounted: !mountsProjectMcp ? [] : fencedMount ? fencedMount.mounted : projectTools.mcpServers,
474
+ machine: mountsProjectMcp && (rt === 'codex' || claudeDisabled != null),
475
+ unmounted: [...projectTools.mcpUnmounted.map((u) => ({ ...u, source: 'project' })), ...(fencedMount ? fencedMount.unmounted : [])],
476
+ });
458
477
  const agentTurnArgs = {
459
478
  prompt:
460
479
  job.kind === 'task' && job.task
@@ -464,6 +483,8 @@ export function createAgentTurnExecution({
464
483
  // The card's discussion (0.106.0), printed inside THE CARD by
465
484
  // the same builder the stash above wrote down — and the files
466
485
  // on it (0.112.0), after it, the same way.
486
+ resumeBody: job.body,
487
+ investigationContext: job.investigationContext,
467
488
  cardThread: job.cardThread,
468
489
  cardFiles: files.card,
469
490
  position: job.position ?? 1,
@@ -471,7 +492,10 @@ export function createAgentTurnExecution({
471
492
  })
472
493
  : AGENT_HUMAN_KICKOFF({
473
494
  agentName: job.agentName,
495
+ // INTERRUPT NOW: the previous run was stopped to send this.
496
+ interrupted: job.interrupted === true,
474
497
  message: job.body ?? '',
498
+ investigationContext: job.investigationContext,
475
499
  askedByName: job.askedByName,
476
500
  task: job.task,
477
501
  // The discussion on the one card this message is about, in a
@@ -489,24 +513,28 @@ export function createAgentTurnExecution({
489
513
  // different things about the same directory.
490
514
  // Spoken to the CLI that runs it (0.115.0): a mockup on Codex is
491
515
  // told it is Codex, never Claude.
492
- system: withProjectContext(SYSTEM_AGENT_FOR(taskKind, RUNTIMES[rt]?.label), {
516
+ system: withProjectContext(SYSTEM_AGENT_FOR(taskKind, runtimeById(rt)?.label, { prerequisitesAccepted: job.prerequisitesAccepted === true }), {
493
517
  knowledgeDir: knowledgeDirFor(repoRoot),
494
518
  // ARTIFACTS (0.94.0), while the server can show one — an agent's
495
519
  // land on its page, under the facts row.
496
520
  artifacts: getArtifactsAccepted(),
497
- }) + (projectTools.instructions ? `\n\n${projectTools.instructions}` : ''),
521
+ }) + (toolsNote ? `\n\n${toolsNote}` : '') + (artifactInstructions ? `\n\n${artifactInstructions}` : '') + `\n\n${INTERNAL_WAIT_CONTRACT}` + (projectTools.instructions ? `\n\n${projectTools.instructions}` : ''),
498
522
  agentTools: projectTools,
499
- ...(projectTools.codexHome ? { mcpEnv: { CODEX_HOME: projectTools.codexHome } } : {}),
523
+ ...((projectTools.codexHome || fencedMount) ? { mcpEnv: { ...(projectTools.codexHome ? { CODEX_HOME: projectTools.codexHome } : {}), ...(fencedMount?.env ?? {}) } } : {}),
500
524
  // NOBODY IS AT THIS SCREEN (2026-09-29): no display, BROWSER=none, and
501
525
  // a browser profile of the agent's own, kept beside its Codex home —
502
526
  // so a Chrome it starts to look at its page never reaches the
503
527
  // person's (noWindowEnv.mjs).
504
528
  browserHome: agentBrowserHome(sessionMetaPath, wt, agentId),
505
- ...(posture === 'build' && rt === 'claude'
506
- ? { mcpArgs: ['--strict-mcp-config', '--mcp-config', projectTools.mcpPath] }
507
- : posture === 'build' && rt === 'codex'
508
- ? { mcpArgs: projectTools.codexArgs }
509
- : {}),
529
+ // Reviewed project servers and the machine's configured servers, on
530
+ // initial cards, answers and review iterations alike.
531
+ ...(mountsProjectMcp && rt === 'claude'
532
+ ? {
533
+ mcpArgs: [...(claudeDisabled ? [] : ['--strict-mcp-config']), '--mcp-config', projectTools.mcpPath],
534
+ projectMcpServers: projectTools.mcpServers,
535
+ ...(claudeDisabled ? { machineMcp: claudeDisabled } : {}),
536
+ }
537
+ : fencedMount ? { mcpArgs: fencedMount.args } : {}),
510
538
  knowledgeDir: knowledgeDirFor(repoRoot),
511
539
  // The kind's posture, as the one turn profile (turnProfile.mjs):
512
540
  // build for code, the fenced design/research postures otherwise.
@@ -523,13 +551,13 @@ export function createAgentTurnExecution({
523
551
  },
524
552
  // Present only when the container named one — see brainFor.
525
553
  ...brain,
526
- streamJson: true,
527
554
  answerFromResult: true,
528
555
  label: c.cyan('[agent]'),
529
556
  // The CLI's own tail, relayed. Throttled by the same rule the tab's
530
557
  // narrator keeps: overwritten, never appended, and ignored by the
531
558
  // board past ~90 seconds.
532
559
  onActivity: (a) => {
560
+ brief?.append(a?.kind ?? 'activity', a?.full ?? a?.label);
533
561
  const line = a?.label;
534
562
  if (!line) return;
535
563
  // THE TRACE TAKES EVERY LINE; the pulse takes one every two
@@ -555,12 +583,19 @@ export function createAgentTurnExecution({
555
583
  // Workbench's tool cards are built from, scrubbed at collection by
556
584
  // the builder itself (bounded window BEFORE its caps; see
557
585
  // toolEventOf). A tool it does not know pushes nothing.
558
- onToolEvent: (name, input) => {
586
+ onToolEvent: (name, input, meta) => {
587
+ brief?.append(meta?.result ? 'tool result' : 'tool call', meta?.result
588
+ ? { id: meta.id, exitCode: meta.exitCode, isError: meta.isError, output: meta.output }
589
+ : { name, input });
590
+ // Codex request/result pairs feed capture/planner evidence. This
591
+ // append-only agent trace already relays its completed activity.
592
+ if (meta?.runtime === 'codex') return;
559
593
  trace.tool(toolEventOf(name, input, wt, envScrub));
560
594
  },
561
595
  // The CLI's own per-turn token counts, off the `result` event. One
562
- // result per turn, so this is a set and not an accumulate — the
563
- // adding-up happens SERVER-side, behind the settle's idempotent win,
596
+ // result per CLI invocation. Internal-wait recovery combines those
597
+ // counts before this callback; project accounting still happens
598
+ // SERVER-side, behind the settle's idempotent win,
564
599
  // because a retried settle must not charge the same turn twice.
565
600
  // …tagged with WHICH CLI counted it (2026-09-24), so the server can
566
601
  // split an agent's spend per CLI rather than labelling a codex
@@ -571,17 +606,14 @@ export function createAgentTurnExecution({
571
606
  onModel: (m) => {
572
607
  model = m;
573
608
  },
574
- // WHAT THE CLI SAYS IT CAN BE ASKED FOR, AND WHICH CONNECTORS NEED A
575
- // LOGIN — the same free fact off the same init event the tab lane
576
- // records (2026-09-23). Without it a box that only ever runs agents
577
- // taught the app its skills and connectors ONCE, from the startup
578
- // probe, and never again: a connector signed into at the box kept
579
- // reading "needs authorization" for the life of the process.
580
- onInit: (i) => {
581
- recordSkills(i.skills);
582
- recordMcpServers(i.mcpServers);
583
- },
609
+ // No skills or connectors are harvested here (2026-10-10): every
610
+ // agent turn narrows them — `--setting-sources user` and the reviewed
611
+ // plugin, `--strict-mcp-config` — so its init is not the machine's
612
+ // (runTurn.mjs, `initHarvestFor`). An agent-only box learns from the
613
+ // startup probe and its Terminal tabs.
584
614
  onSpawn: (ch) => {
615
+ brief?.running(true);
616
+ ch.once('close', () => { if (child === ch) brief?.running(false); });
585
617
  child = ch;
586
618
  spawnedChildren.add(ch);
587
619
  // The AGENT it serves — what the machine snapshot charges this
@@ -599,6 +631,8 @@ export function createAgentTurnExecution({
599
631
  // and a place id IS one. It is what lets a hard stop actually reach
600
632
  // the CLI — see `retireWorkSessions`.
601
633
  agentChildren.set(place, ch);
634
+ // Asked to stop between the check and the spawn.
635
+ if (run.interrupted) killGroup(ch);
602
636
  },
603
637
  };
604
638
  /**
@@ -612,16 +646,26 @@ export function createAgentTurnExecution({
612
646
  * (resumeLost.mjs, SOLID F001): a successful answer that merely
613
647
  * MENTIONS a missing session is never run twice.
614
648
  */
615
- out = await runTurnResumingOnce(runTurn, agentTurnArgs, {
616
- resume,
617
- runtime: rt,
618
- beforeFresh: () => {
619
- seenThreadId = null;
649
+ out = await runAgentWithInternalWaits(
650
+ (args, continuing) => continuing ? runCli(args) : runTurnResumingOnce(runCli, args, {
651
+ // A Codex write-up always runs fresh over its copy (codexResearch
652
+ // .mjs), so there is no lost conversation to retry it for.
653
+ resume: rt === 'codex' && posture === 'research' ? false : args.resume,
654
+ runtime: rt,
655
+ beforeFresh: () => {
656
+ seenThreadId = null;
657
+ lastAnswer = null;
658
+ model = null;
659
+ },
660
+ }),
661
+ { ...agentTurnArgs, onContinuation: (n) => {
620
662
  lastAnswer = null;
621
- model = null;
622
- },
623
- });
663
+ trace.prose('text', `The machine resumed this card after an internal wait (continuation ${n}).`);
664
+ void postAgentActivity(agentId, 'resuming after an internal wait');
665
+ } }
666
+ );
624
667
  } finally {
668
+ await brief?.stop();
625
669
  // What the CLI counted, for the lane's catch if anything below throws.
626
670
  measured.usage = usage;
627
671
  measured.model = model;
@@ -671,16 +715,36 @@ export function createAgentTurnExecution({
671
715
  * floor: it must never delay the settle behind it.
672
716
  */
673
717
  void reportSessionWorktree(place).catch(() => {});
674
- // …and what it drew to SHOW the person (2026-09-22): uploaded against
675
- // the AGENT, so it lands on the agent's page whoever is looking.
676
- // Fire-and-forget for the same reason — never delay the settle.
677
- void artifacts
678
- .report({ placeDir: wt, before: artifactsBefore, agentId, turnId })
679
- .catch(() => {});
718
+ // A file card delivers uploaded bytes, not an in-memory retry. Otherwise
719
+ // its dependent card can start while Review has nothing to show.
720
+ await artifacts.report({ placeDir: wt, before: artifactsBefore, agentId, turnId, requireStored: kind.artifact != null });
680
721
  }
681
722
 
682
723
 
683
724
  const commits = commitsBetween(wt, before);
725
+ /**
726
+ * A PERSON INTERRUPTED IT (Interrupt now). Whatever the CLI printed was
727
+ * cut off, so there is no outcome to read: the turn reports `interrupted`
728
+ * with what it measured — the commits it made, its spend, the branch —
729
+ * and leaves the worktree exactly as the stopped run left it. The message
730
+ * sent to interrupt it runs next, here, resuming this conversation. No
731
+ * limit, no park and no review entry: none of that happened.
732
+ */
733
+ if (run.interrupted) {
734
+ measured.ranCli = spawnedChildren.size > 0;
735
+ await postAgentTurn({
736
+ turnId,
737
+ outcome: 'nothing',
738
+ interrupted: true,
739
+ ran: spawnedChildren.size > 0,
740
+ commits,
741
+ ...(usage ? { usage } : {}),
742
+ ...(model ? { model } : {}),
743
+ ...(branch ? { branch } : {}),
744
+ worktree: wt,
745
+ });
746
+ return;
747
+ }
684
748
  // WHAT THE TURN REPORTS is one decision, off what was measured here
685
749
  // (workAgentTurnOutcome.mjs). The side effects it implies stay on this
686
750
  // side: the machine's limit state, the park, and the review-entry beat.
@@ -25,6 +25,7 @@ import { limitLine } from './cliLimit.mjs';
25
25
  import { parseTurnResult } from './agentPlan.mjs';
26
26
  import { changedArtifacts, scanArtifacts } from './artifacts.mjs';
27
27
  import { scrub as envScrub } from './uplinkScrub.mjs';
28
+ import { usableImageArtifact } from './imageArtifact.mjs';
28
29
 
29
30
  export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage, model = null, commits, artifactsBefore, branch, wt }) {
30
31
  const res = (lastAnswer && parseTurnResult(lastAnswer)) || parseTurnResult(out);
@@ -110,7 +111,8 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
110
111
  */
111
112
  if (res.outcome === 'delivered' && kind.artifact) {
112
113
  const want = kind.artifact.match;
113
- const has = (l) => l.some((e) => want.test(e.name));
114
+ const has = (l) => l.some((e) => want.test(e.name)
115
+ && (kind.posture !== 'image' || usableImageArtifact(e)));
114
116
  const standing = scanArtifacts(wt);
115
117
  const wrote = has(changedArtifacts(artifactsBefore, standing));
116
118
  const present = wrote || ((job.kind !== 'task' || job.redo === true) && has(standing));
@@ -126,11 +128,18 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
126
128
  });
127
129
  }
128
130
  }
131
+ const prerequisite = res.prerequisite;
132
+ const cannotRequest = prerequisite && job.prerequisitesAccepted !== true
133
+ ? 'This server does not support prerequisite-card requests yet. This work has not been delivered.'
134
+ : prerequisite?.taskKind === 'image' && (job.runtime ?? 'claude') !== 'codex'
135
+ ? 'This agent runs on a CLI that cannot generate images. Image cards require Codex’s own image generation; this agent cannot run the requested texture work.'
136
+ : null;
129
137
  return report(
130
138
  {
131
139
  turnId,
140
+ ...(prerequisite && !cannotRequest ? { prerequisite } : {}),
132
141
  outcome: res.outcome,
133
- answer: envScrub(res.answer ?? '').slice(0, 8000),
142
+ answer: cannotRequest ?? envScrub(res.answer ?? '').slice(0, 8000),
134
143
  ...(commits.length ? { commits } : {}),
135
144
  // SCRUBBED like the answer beside it: a raised card lands in the
136
145
  // project doc every member reads, and a brief quoting the `.env` the
@@ -14,8 +14,9 @@
14
14
  * Daemon→server reports, every one of them: no version floor, and a permanent
15
15
  * 4xx is the server saying it will never take the body (see each POST).
16
16
  */
17
- import { FLEET_URL, FLEET_TOKEN, USER_AGENT } from './config.mjs';
18
- import { fleetEndpoint } from './fleetWire.mjs';
17
+ import { FLEET_URL } from './config.mjs';
18
+ import { fleetEndpoint, isConsideredRefusal } from './fleetWire.mjs';
19
+ import { fleetHeaders, postFleetJson } from './fleetPost.mjs';
19
20
 
20
21
  export function createAgentTurnReports({ REJECT_RETRY_MS }) {
21
22
  const AGENT_TURN_DONE_URL = fleetEndpoint('agent-turn-done', FLEET_URL);
@@ -53,16 +54,7 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
53
54
  const turnId = String(body.turnId);
54
55
  agentReported.set(turnId, { body, at: Date.now() });
55
56
  try {
56
- const res = await fetch(AGENT_TURN_DONE_URL, {
57
- method: 'POST',
58
- headers: {
59
- Authorization: `Bearer ${FLEET_TOKEN}`,
60
- 'User-Agent': USER_AGENT,
61
- 'Content-Type': 'application/json',
62
- },
63
- signal: AbortSignal.timeout(30_000),
64
- body: JSON.stringify(body),
65
- });
57
+ const res = await postFleetJson(AGENT_TURN_DONE_URL, body, { timeoutMs: 30_000 });
66
58
  const j = await res.json().catch(() => null);
67
59
  /**
68
60
  * ONLY A 2xx IS DELIVERED. The server answers an already-settled,
@@ -79,7 +71,7 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
79
71
  if (res.ok) {
80
72
  agentReported.delete(turnId);
81
73
  agentRejectedUntil.delete(turnId);
82
- } else if (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {
74
+ } else if (isConsideredRefusal(res.status)) {
83
75
  agentRejectedUntil.set(turnId, Date.now() + REJECT_RETRY_MS);
84
76
  }
85
77
  return j?.data ?? null;
@@ -102,20 +94,8 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
102
94
  */
103
95
  const postAgentTrace = async (body) => {
104
96
  try {
105
- const res = await fetch(AGENT_TRACE_URL, {
106
- method: 'POST',
107
- headers: {
108
- Authorization: `Bearer ${FLEET_TOKEN}`,
109
- 'User-Agent': USER_AGENT,
110
- 'Content-Type': 'application/json',
111
- },
112
- signal: AbortSignal.timeout(15_000),
113
- body: JSON.stringify(body),
114
- });
115
- return (
116
- res.ok ||
117
- (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429)
118
- );
97
+ const res = await postFleetJson(AGENT_TRACE_URL, body, { timeoutMs: 15_000 });
98
+ return res.ok || isConsideredRefusal(res.status);
119
99
  } catch {
120
100
  return false; // a blip — the same entries go again at the same seq
121
101
  }
@@ -125,11 +105,7 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
125
105
  try {
126
106
  await fetch(AGENT_ACTIVITY_URL, {
127
107
  method: 'POST',
128
- headers: {
129
- Authorization: `Bearer ${FLEET_TOKEN}`,
130
- 'User-Agent': USER_AGENT,
131
- 'Content-Type': 'application/json',
132
- },
108
+ headers: fleetHeaders(),
133
109
  signal: AbortSignal.timeout(15_000),
134
110
  body: JSON.stringify({ agentId, text }),
135
111
  });
@@ -142,11 +118,7 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
142
118
  try {
143
119
  await fetch(AGENT_PARKED_URL, {
144
120
  method: 'POST',
145
- headers: {
146
- Authorization: `Bearer ${FLEET_TOKEN}`,
147
- 'User-Agent': USER_AGENT,
148
- 'Content-Type': 'application/json',
149
- },
121
+ headers: fleetHeaders(),
150
122
  signal: AbortSignal.timeout(15_000),
151
123
  // `runtime` scopes the park to that CLI's agents; a server that does
152
124
  // not know the key parks every agent, which is what it always did.
@@ -25,6 +25,8 @@ import { c, note } from './ui.mjs';
25
25
  import { createAgentTurnReports } from './workAgentTurnReports.mjs';
26
26
  import { createAgentTurnExecution } from './workAgentTurnExecution.mjs';
27
27
  import { failedTurnSettlement } from './workAgentTurnOutcome.mjs';
28
+ import { killGroup } from './processGroup.mjs';
29
+ import { createAgentBriefRunner } from './workAgentBriefRunner.mjs';
28
30
 
29
31
  export function createWorkAgentTurns({
30
32
  REJECT_RETRY_MS,
@@ -48,6 +50,7 @@ export function createWorkAgentTurns({
48
50
  agentRemoteAt,
49
51
  /** The turn's files, fetched into its worktree (workAttachments.mjs, 0.112.0). */
50
52
  fetchAgentFiles,
53
+ makeBrief = createAgentBriefRunner({ admit, workChildren }),
51
54
  }) {
52
55
  const {
53
56
  postAgentTurn,
@@ -91,8 +94,23 @@ export function createWorkAgentTurns({
91
94
  * directory names, and those are places.
92
95
  */
93
96
  const agentChildren = new Map(); // placeId -> child process
97
+ /**
98
+ * INTERRUPT NOW (2026-10-13) — each turn this lane has taken: its place and
99
+ * whether a person asked to stop it. A stop kills the CLI's PROCESS GROUP
100
+ * (it is spawned detached, so the commands it started go with it); a turn
101
+ * asked to stop before it spawned never spawns.
102
+ */
103
+ const agentRuns = new Map(); // turnId -> { place, interrupted }
104
+ /** Turns answered `ran: false` here, so a roster read made before that
105
+ * settle landed is not answered again at once. */
106
+ const notRunningSaid = new Map(); // turnId -> when
107
+ /** Agents whose run this lane just stopped: the next turn is the one sent to
108
+ * interrupt it, and is told so even when the roster that handed it over was
109
+ * read before the stop's settle landed. Taken once, for a little while. */
110
+ const stoppedFor = new Map(); // agentId -> when
94
111
 
95
112
  const { runAgentTurnInPlace, commitsBetween } = createAgentTurnExecution({
113
+ makeBrief,
96
114
  baseRef,
97
115
  baseDir,
98
116
  repoRoot,
@@ -172,12 +190,18 @@ export function createWorkAgentTurns({
172
190
  // A WRITER on the agent's own place — see the lane header. Only `a-<id>`
173
191
  // places: those are agents' by construction, and anything else here would
174
192
  // be a tab's directory, where a turn is a reader by the product's own law.
193
+ const run = agentRuns.get(turnId) ?? { place, interrupted: false };
175
194
  await inPlace(place, place.startsWith('a-'), async () => {
176
195
  try {
177
- await runAgentTurnInPlace(job, { turnId, agentId, place, releaseSlot, postAgentTurn: settleOnce, measured });
196
+ await runAgentTurnInPlace(job, { turnId, agentId, place, releaseSlot, postAgentTurn: settleOnce, measured, run });
197
+ if (run.interrupted && measured.ranCli) stoppedFor.set(agentId, Date.now());
178
198
  } catch (error) {
179
199
  if (settled) return;
180
200
  const commits = measured.wt ? commitsBetween(measured.wt, measured.before) : [];
201
+ if (run.interrupted) {
202
+ await postAgentTurn({ turnId, outcome: 'nothing', interrupted: true, ran: true, commits, ...(measured.usage ? { usage: measured.usage } : {}) });
203
+ return;
204
+ }
181
205
  await postAgentTurn(
182
206
  failedTurnSettlement({
183
207
  turnId,
@@ -235,7 +259,7 @@ export function createWorkAgentTurns({
235
259
  }
236
260
  };
237
261
 
238
- const processAgentTurnJobs = (jobs) => {
262
+ const processAgentTurnJobs = (jobs, { reportsOnly = false } = {}) => {
239
263
  const list = Array.isArray(jobs) ? jobs : [];
240
264
  pruneHeldReports(list);
241
265
  /** One deferral line per tick, however many turns were offered. */
@@ -244,6 +268,7 @@ export function createWorkAgentTurns({
244
268
  const id = String(job?.id || '');
245
269
  if (!id || agentTurns.has(id)) continue;
246
270
  const held = agentReported.get(id);
271
+ if (reportsOnly && !held) continue;
247
272
  // A body the server REFUSED waits out its backoff — still held, so the
248
273
  // CLI is never re-run in the meantime.
249
274
  if (held && (agentRejectedUntil.get(id) ?? 0) > Date.now()) continue;
@@ -306,11 +331,16 @@ export function createWorkAgentTurns({
306
331
  }
307
332
  const releaseSlot = admit.reserve();
308
333
  agentTurns.add(id);
309
- void runAgentTurn(job, releaseSlot).finally(() => {
334
+ agentRuns.set(id, { place: String(job.placeId), interrupted: false });
335
+ const stopped = stoppedFor.get(String(job.agentId));
336
+ stoppedFor.delete(String(job.agentId));
337
+ const run = stopped != null && Date.now() - stopped < 120_000 ? { ...job, interrupted: true } : job;
338
+ void runAgentTurn(run, releaseSlot).finally(() => {
310
339
  // Belt for every path that never reached a spawn — the release is
311
340
  // idempotent, so the normal case releases twice.
312
341
  releaseSlot();
313
342
  agentTurns.delete(id);
343
+ agentRuns.delete(id);
314
344
  });
315
345
  }
316
346
  };
@@ -318,9 +348,10 @@ export function createWorkAgentTurns({
318
348
  /**
319
349
  * SETTLE EVERYTHING IN FLIGHT, because this process is about to go away.
320
350
  *
321
- * The one caller is the displacement stand-down: the project's machine moved
322
- * to another box, so nothing here will be re-offered to us and nothing else
323
- * knows these turns were running. An abandoned turn sits pending until the
351
+ * The one caller is the roster's stand-down (holder.mjs): the project's
352
+ * machine moved to another box, this box was removed from the project, or
353
+ * Flowviant stopped it (2026-10-10) — so nothing here will be re-offered to
354
+ * us and nothing else knows these turns were running. An abandoned turn sits pending until the
324
355
  * server's six-hour expiry while the board shows an agent working on a machine
325
356
  * that has gone — the wedge every settle path in this lane exists to avoid.
326
357
  *
@@ -346,5 +377,31 @@ export function createWorkAgentTurns({
346
377
  await Promise.allSettled(posts);
347
378
  };
348
379
 
349
- return { processAgentTurnJobs, settleAgentTurns, agentTurns, agentChildren, agentReported };
380
+ /**
381
+ * THE AGENT RUNS A PERSON ASKED TO STOP (`roster.interruptAgentTurns`, read
382
+ * before the jobs). One this lane is running is stopped — the CLI's group
383
+ * killed — and reports `interrupted` once its CLI is gone. One that finished
384
+ * here keeps its own settle. One this lane is not running is answered `ran:
385
+ * false`, and the server puts it back in the queue untouched.
386
+ */
387
+ const interruptAgentTurns = (ids) => {
388
+ const at = Date.now();
389
+ for (const [id, when] of notRunningSaid) if (at - when > 30_000) notRunningSaid.delete(id);
390
+ for (const id of Array.isArray(ids) ? ids : []) {
391
+ if (typeof id !== 'string' || !id) continue;
392
+ const run = agentRuns.get(id);
393
+ if (run) {
394
+ if (!run.interrupted) note(`${c.cyan('agent')} ${c.dim('— interrupting the running turn')}`);
395
+ run.interrupted = true;
396
+ const child = agentChildren.get(run.place);
397
+ if (child) killGroup(child);
398
+ continue;
399
+ }
400
+ if (agentReported.has(id) || notRunningSaid.has(id)) continue;
401
+ notRunningSaid.set(id, at);
402
+ void postAgentTurn({ turnId: id, outcome: 'nothing', interrupted: true, ran: false }).catch(() => {});
403
+ }
404
+ };
405
+
406
+ return { processAgentTurnJobs, interruptAgentTurns, settleAgentTurns, agentTurns, agentChildren, agentReported };
350
407
  }