flowviant 0.125.5 → 0.125.7

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 (101) hide show
  1. package/README.md +4 -5
  2. package/bin/lib/agentInternalWait.mjs +1 -5
  3. package/bin/lib/agentPlan.mjs +13 -163
  4. package/bin/lib/agentRepositories.mjs +3 -1
  5. package/bin/lib/claudeInput.mjs +89 -10
  6. package/bin/lib/claudeMachineMcp.mjs +85 -0
  7. package/bin/lib/claudePersonal.mjs +6 -57
  8. package/bin/lib/claudePosture.mjs +6 -15
  9. package/bin/lib/claudeSettingsArg.mjs +67 -0
  10. package/bin/lib/claudeStream.mjs +30 -1
  11. package/bin/lib/claudeSubagentProgress.mjs +12 -28
  12. package/bin/lib/claudeSubagents.mjs +4 -2
  13. package/bin/lib/codexContract.mjs +52 -0
  14. package/bin/lib/codexFencedMount.mjs +275 -0
  15. package/bin/lib/codexPersonal.mjs +1 -265
  16. package/bin/lib/codexProjectRead.mjs +5 -4
  17. package/bin/lib/codexResearch.mjs +9 -3
  18. package/bin/lib/codexSubagentEvents.mjs +11 -7
  19. package/bin/lib/codexSubagents.mjs +55 -52
  20. package/bin/lib/codexToml.mjs +328 -0
  21. package/bin/lib/config.mjs +22 -1
  22. package/bin/lib/conversationIds.mjs +10 -0
  23. package/bin/lib/fleet.mjs +2 -2
  24. package/bin/lib/git.mjs +32 -0
  25. package/bin/lib/heldTool.mjs +49 -0
  26. package/bin/lib/hooks/mcpAllow.mjs +11 -5
  27. package/bin/lib/hooks/orchestrator.mjs +20 -10
  28. package/bin/lib/intakeDraft.mjs +1 -1
  29. package/bin/lib/jsonlTail.mjs +52 -0
  30. package/bin/lib/mcpInventory.mjs +11 -6
  31. package/bin/lib/mcpReasons.mjs +26 -0
  32. package/bin/lib/mcpReportBounds.mjs +10 -0
  33. package/bin/lib/mcpStdio.mjs +35 -0
  34. package/bin/lib/mergeBatch.mjs +6 -24
  35. package/bin/lib/mergeOrder.mjs +18 -0
  36. package/bin/lib/modelJson.mjs +173 -0
  37. package/bin/lib/nativeAgentTools.mjs +72 -0
  38. package/bin/lib/orchestrator.mjs +221 -133
  39. package/bin/lib/orchestratorClient.mjs +17 -21
  40. package/bin/lib/orchestratorHooks.mjs +21 -4
  41. package/bin/lib/orchestratorNative.mjs +58 -0
  42. package/bin/lib/orchestratorPrompt.mjs +7 -4
  43. package/bin/lib/orchestratorProtocol.mjs +25 -3
  44. package/bin/lib/postureTools.mjs +58 -13
  45. package/bin/lib/prWorkflow.mjs +13 -2
  46. package/bin/lib/processGroup.mjs +41 -0
  47. package/bin/lib/projectRepositories.mjs +2 -2
  48. package/bin/lib/projectToolRuntimes.mjs +92 -245
  49. package/bin/lib/projectTools.mjs +60 -19
  50. package/bin/lib/prompts.mjs +47 -18
  51. package/bin/lib/reportJournal.mjs +61 -0
  52. package/bin/lib/repositoryDelivery.mjs +6 -4
  53. package/bin/lib/repositoryMerge.mjs +8 -12
  54. package/bin/lib/repositoryPlaces.mjs +2 -2
  55. package/bin/lib/repositoryPrompts.mjs +42 -0
  56. package/bin/lib/repositoryTool.mjs +9 -32
  57. package/bin/lib/researchUse.mjs +39 -0
  58. package/bin/lib/resumeLost.mjs +21 -1
  59. package/bin/lib/resumeMarker.mjs +43 -0
  60. package/bin/lib/runTurn.mjs +55 -52
  61. package/bin/lib/runtimeAntigravity.mjs +1 -1
  62. package/bin/lib/runtimeCapabilities.mjs +13 -18
  63. package/bin/lib/runtimeClaude.mjs +68 -24
  64. package/bin/lib/runtimeCodex.mjs +12 -6
  65. package/bin/lib/runtimeEvents.mjs +13 -2
  66. package/bin/lib/runtimeOfferings.mjs +12 -5
  67. package/bin/lib/runtimes.mjs +3 -1
  68. package/bin/lib/startProject.mjs +10 -0
  69. package/bin/lib/subagentBounds.mjs +20 -0
  70. package/bin/lib/trace.mjs +30 -5
  71. package/bin/lib/turnDeclaration.mjs +1 -1
  72. package/bin/lib/turnDiagnostics.mjs +9 -12
  73. package/bin/lib/turnReceipts.mjs +64 -0
  74. package/bin/lib/turnSubagents.mjs +91 -0
  75. package/bin/lib/turnUsage.mjs +9 -6
  76. package/bin/lib/update.mjs +18 -4
  77. package/bin/lib/versionOrder.mjs +11 -1
  78. package/bin/lib/work.mjs +9 -9
  79. package/bin/lib/workAgentMerges.mjs +5 -7
  80. package/bin/lib/workAgentPrecheck.mjs +5 -5
  81. package/bin/lib/workAgentRecovery.mjs +2 -1
  82. package/bin/lib/workAgentResume.mjs +121 -0
  83. package/bin/lib/workAgentTools.mjs +111 -0
  84. package/bin/lib/workAgentTurnExecution.mjs +82 -246
  85. package/bin/lib/workAgentTurnOutcome.mjs +30 -3
  86. package/bin/lib/workAgentTurnReports.mjs +16 -0
  87. package/bin/lib/workAgentTurnShape.mjs +90 -0
  88. package/bin/lib/workAgentTurns.mjs +28 -12
  89. package/bin/lib/workArtifactReconcile.mjs +3 -2
  90. package/bin/lib/workAttachments.mjs +27 -5
  91. package/bin/lib/workMergeBatch.mjs +7 -5
  92. package/bin/lib/workNarration.mjs +32 -0
  93. package/bin/lib/workPlaceReports.mjs +3 -3
  94. package/bin/lib/workPlaces.mjs +14 -0
  95. package/bin/lib/workReportQueue.mjs +18 -1
  96. package/bin/lib/workSessionRuntime.mjs +2 -5
  97. package/bin/lib/workSessionTurns.mjs +68 -80
  98. package/bin/lib/workShip.mjs +8 -18
  99. package/bin/lib/workTurnShape.mjs +6 -4
  100. package/bin/lib/worktreeDiff.mjs +29 -5
  101. package/package.json +1 -1
@@ -6,8 +6,9 @@ import { finishRepositoryDelivery, allResolutionsCommitted } from './repositoryD
6
6
  * decides WHICH turns run and makes sure each one is settled exactly once; this
7
7
  * runs one, inside the place lock the lane already holds — the begun-guard
8
8
  * (workAgentTurnBegun.mjs), the worktree and its branch facts, the kind's
9
- * refusals, the prompt, the CLI and its resume, the trace and pulse relays,
10
- * the markers and the re-measurement after it exits — then hands the
9
+ * refusals, the prompt, the CLI and its resume (whose markers are
10
+ * workAgentResume.mjs's since 2026-10-18), the trace and pulse relays, and the
11
+ * re-measurement after it exits — then hands the
11
12
  * measurements to the one settlement decision (workAgentTurnOutcome.mjs) and
12
13
  * posts the body that decision chose.
13
14
  *
@@ -17,38 +18,39 @@ import { finishRepositoryDelivery, allResolutionsCommitted } from './repositoryD
17
18
  * answered. `measured` is the same bargain the other way: the branch facts and
18
19
  * the spend, written here as they are learned, read by that catch.
19
20
  */
20
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
21
21
  import { PROJECT_ID } from './config.mjs';
22
22
  import { setRuntimeLimit } from './daemonState.mjs';
23
23
  import { gitOrNull } from './git.mjs';
24
+ import { turnCommits } from './turnReceipts.mjs';
25
+ import { addUsage } from './turnUsage.mjs';
24
26
  import { stashCard } from './agentCards.mjs';
25
27
  import { c } from './ui.mjs';
26
28
  import { runTurn } from './runTurn.mjs';
27
29
  import { killGroup } from './processGroup.mjs';
28
- import { SYSTEM_AGENT_FOR, AGENT_TASK_KICKOFF, AGENT_TASK_SPEC, AGENT_HUMAN_KICKOFF, withProjectContext } from './prompts.mjs';
30
+ import { AGENT_TASK_SPEC } from './prompts.mjs';
31
+ import { agentKickoff, agentFollowupKickoff, agentTurnSystem } from './workAgentTurnShape.mjs';
29
32
  import { AGENT_TASK_KINDS, agentTaskKindOf, unknownAgentTaskKind } from './agentTaskKinds.mjs';
30
33
  import { knowledgeDirFor } from './knowledgeFiles.mjs';
31
34
  import { scrub as envScrub } from './uplinkScrub.mjs';
32
35
  import { canRun, runtimeById } from './runtimes.mjs';
33
36
  import { toolEventOf, CLAUDE_TOOL_PROSE_KINDS, scrubNativeText } from './runtimeEvents.mjs';
34
37
  import { makeTraceRelay } from './trace.mjs';
35
- import { mcpJsonServerNamesAbove, readBaseTools, prepareAgentTools } from './projectTools.mjs';
36
- import { TURN_PROFILES } from './turnProfile.mjs';
37
- import { postureToolsNote } from './postureTools.mjs';
38
+ import { prepareCardTools } from './workAgentTools.mjs';
38
39
  import { runTurnResumingOnce } from './resumeLost.mjs';
39
- import { INTERNAL_WAIT_CONTRACT, runAgentWithInternalWaits } from './agentInternalWait.mjs';
40
- import { CODEX_THREAD_RE } from './workSessionRuntime.mjs';
40
+ import { runAgentWithInternalWaits } from './agentInternalWait.mjs';
41
41
  import { brainFor } from './workBrain.mjs';
42
42
  import { refreshRestartBase } from './workAgentRecovery.mjs';
43
43
  import { begunTurnRefusal } from './workAgentTurnBegun.mjs';
44
- import { agentTurnSettlement } from './workAgentTurnOutcome.mjs';
44
+ import { agentTurnSettlement, interruptedTurnSettlement } from './workAgentTurnOutcome.mjs';
45
45
  import { ensureArtifactDir, ensureFenceScratch } from './artifacts.mjs';
46
46
  import { agentBrowserHome } from './noWindowEnv.mjs';
47
- import { CLAUDE_SESSION_RE, RESUME_MARKERS } from './conversationIds.mjs';
47
+ import { agentTurnResume } from './workAgentResume.mjs';
48
48
  import { createDeliveryReporter } from './workDelivery.mjs';
49
49
  import { createLiveMessages } from './workLiveMessages.mjs';
50
50
  import { createOrchestrator } from './orchestrator.mjs';
51
- import { ORCHESTRATOR_CONTRACT } from './orchestratorPrompt.mjs';
51
+ import { claudeOrchestrationSafe } from './orchestratorHooks.mjs';
52
+ import { codexSubagentsSafe } from './runtimeCodex.mjs';
53
+ import { measuredRuntimeVersion } from './runtimeDetection.mjs';
52
54
 
53
55
  export function createAgentTurnExecution({
54
56
  admit,
@@ -70,6 +72,7 @@ export function createAgentTurnExecution({
70
72
  agentChildren,
71
73
  postAgentTrace,
72
74
  postAgentActivity,
75
+ postAgentContext = () => {},
73
76
  postAgentParked,
74
77
  fetchAgentFiles,
75
78
  makeLiveMessages = createLiveMessages,
@@ -82,38 +85,6 @@ export function createAgentTurnExecution({
82
85
 
83
86
  const lastAgentBeat = new Map(); // agentId -> last activity POST, ms
84
87
 
85
- /** Every sha that landed on this branch between two points. Measured, never
86
- * asserted — this is the whole reason an agent is not asked for its own. */
87
- const commitsBetween = (wt, from) => {
88
- if (!from) return [];
89
- /**
90
- * `--not <base>` and `--no-merges`, because `from..HEAD` alone reports
91
- * BASE'S commits as this turn's receipts the moment anything folds base in
92
- * — which the stale path does on purpose before a merge. A receipt naming
93
- * somebody else's commit is worse than a missing one.
94
- */
95
- /**
96
- * IT CANNOT THROW, and that guard is the whole point of it being here.
97
- *
98
- * `git()` throws on a non-zero exit, and `baseRef()` is built from the
99
- * project's Base branch setting — free text an owner types, never verified
100
- * against the remote. Point it at a branch with no tracking ref (`develop`
101
- * on a repo whose remote branch is `dev`) and this exits "fatal: ambiguous
102
- * argument". The throw escaped `runAgentTurn` AFTER the CLI had already run
103
- * the card, so `postAgentTurn` was never reached, the server never settled
104
- * the turn, and the next poll handed back the identical turn — the same
105
- * card re-run every poll for six hours, on the operator's shared account,
106
- * piling commits onto the review branch, silently.
107
- *
108
- * An unreadable range means we cannot MEASURE the receipts, which is a
109
- * smaller failure than not settling: the turn still reports, and ship-time
110
- * reconciliation books whatever no card claimed. Missing beats fabricated
111
- * and both beat a stall.
112
- */
113
- const out = gitOrNull(['log', '--format=%H', '--no-merges', `${from}..HEAD`, '--not', baseRef()], wt);
114
- return out?.split('\n').map((x) => x.trim()).filter(Boolean).slice(0, 50) ?? [];
115
- };
116
-
117
88
  /**
118
89
  * The turn, run inside its place's writer lock (the lane takes it). `turnId`,
119
90
  * `agentId` and `place` are the lane's own readings of the job, handed down
@@ -179,6 +150,7 @@ export function createAgentTurnExecution({
179
150
  measured.branch = branch;
180
151
 
181
152
  const rt = job.runtime || 'claude';
153
+ const liveInput = runtimeById(rt)?.liveInput === true;
182
154
  /**
183
155
  * WHAT THIS CARD HANDS BACK, AND THE POSTURE THAT FOLLOWS (0.97.0).
184
156
  *
@@ -279,64 +251,9 @@ export function createAgentTurnExecution({
279
251
  return;
280
252
  }
281
253
 
282
- // ONE AGENT IS ONE DIRECTORY, so the CLI's own cwd-keyed resume is exactly
283
- // right here — the ambiguity that forced per-tab session pinning in the
284
- // Workbench (many tabs, one place) cannot arise. The marker is what
285
- // distinguishes the first turn from every later one across restarts.
286
- //
287
- // IT MEANS "CLAUDE HAS SPOKEN HERE", and since 2026-09-24 only a Claude
288
- // turn writes it: an agent can be switched between CLIs, and a marker a
289
- // codex turn left behind would send the first Claude turn to
290
- // `--continue` a directory holding no Claude conversation at all.
291
- const ranMarker = sessionMetaPath(wt, 'flowviant-agent-ran');
292
- /**
293
- * CODEX RESUMES BY ITS OWN THREAD ID, PINNED PER AGENT (2026-09-24).
294
- *
295
- * Resume was Claude-only, because codex's `resume --last` is
296
- * MACHINE-GLOBAL and would cross-resume whichever conversation spoke
297
- * last anywhere on the box. The cost was that every codex card started
298
- * a stranger: no memory of the card before it, and — worse — an answer
299
- * to the agent's own question arrived at a codex that never asked it.
300
- * The Workbench solved the same problem in 0.69.0 by pinning the id
301
- * `thread.started` announces; this is that, keyed by the AGENT rather
302
- * than a tab. The id is shape-checked before it rides argv.
303
- */
304
- const codexThreadMarker =
305
- rt === 'codex' ? sessionMetaPath(wt, 'flowviant-agent-codex-thread', agentId) : null;
306
- let codexResumeId = null;
307
- if (codexThreadMarker && existsSync(codexThreadMarker)) {
308
- try {
309
- const v = readFileSync(codexThreadMarker, 'utf8').trim();
310
- if (CODEX_THREAD_RE.test(v)) codexResumeId = v;
311
- } catch {
312
- /* unreadable marker — run fresh */
313
- }
314
- }
315
- let seenThreadId = null;
316
- const claudeMarker = rt === 'claude' ? sessionMetaPath(wt, RESUME_MARKERS.claude, agentId) : null;
317
- let claudeResumeId = null;
318
- try { const id = readFileSync(claudeMarker, 'utf8').trim(); if (CLAUDE_SESSION_RE.test(id)) claudeResumeId = id; } catch { /* unknown */ }
319
- const resume =
320
- rt === 'codex'
321
- ? Boolean(codexResumeId)
322
- : rt === 'claude' && (Boolean(claudeResumeId) || Boolean(ranMarker && existsSync(ranMarker)));
323
- /**
324
- * …AND THE HOME THAT THREAD IS FILED UNDER, KEPT PER AGENT (2026-09-29).
325
- *
326
- * Codex's thread index records each rollout under the CODEX_HOME the turn
327
- * ran with, as that path — not the personal store the link leads to — so
328
- * the per-turn home in /tmp every turn used to get died with its turn and
329
- * took the pinned thread with it: the next card's `resume` failed "no
330
- * rollout found" and the agent went to Stuck. Beside the thread marker,
331
- * in the worktree's private git dir: outside the working tree (never
332
- * committed, never seen by `git add -A`), refreshed by every turn and
333
- * removed only with the worktree, when the agent retires and its threads
334
- * stop mattering. One agent runs one turn at a time (its place's writer
335
- * lock), so no two turns refresh it at once. Null (no git dir to hold it)
336
- * is the per-turn home again, and a lost resume then runs once fresh.
337
- */
338
- const codexAgentHome =
339
- rt === 'codex' ? sessionMetaPath(wt, 'flowviant-agent-codex-home', agentId) : null;
254
+ // Which conversation this turn resumes, and the kept Codex home it is
255
+ // filed under: the agent's resume across turns (workAgentResume.mjs).
256
+ const agentResume = agentTurnResume({ rt, wt, agentId, sessionMetaPath });
340
257
 
341
258
  /**
342
259
  * THE TURN'S FILES, ON DISK BEFORE THE PROMPT NAMES THEM (0.112.0) — the
@@ -370,11 +287,11 @@ export function createAgentTurnExecution({
370
287
  * commits it made are on the branch, and a reviewer is entitled to read
371
288
  * what was asked for either way.
372
289
  *
373
- * ONE SPEC BUILDER (`AGENT_TASK_SPEC`) shared with the kickoff below, so
374
- * what the reviewer reads is byte-identical to what the agent read — the
375
- * card's discussion included (0.106.0): both are handed the same
376
- * `job.cardThread`, and the builder prints it inside the spec — and the
377
- * card's files after it (0.112.0), fetched just above.
290
+ * ONE SPEC BUILDER (`AGENT_TASK_SPEC`) shared with the kickoff
291
+ * (workAgentTurnShape.mjs), so what the reviewer reads is byte-identical
292
+ * to what the agent read — the card's discussion included (0.106.0): both
293
+ * are handed the same `job.cardThread`, and the builder prints it inside
294
+ * the spec — and the card's files after it (0.112.0), fetched just above.
378
295
  * Failure is swallowed inside `stashCard`: a note about a turn may never
379
296
  * cost the turn.
380
297
  */
@@ -386,6 +303,12 @@ export function createAgentTurnExecution({
386
303
  );
387
304
  }
388
305
 
306
+ // ORCHESTRATED ONLY WHERE THE CLI CAN BE GATED (2026-10-17). v2 reaches every
307
+ // job at the floor; a fenced card or a CLI below its measured floor (unknown
308
+ // included) has no controller, so no marker: its notes fold on receipt.
309
+ const codexVersion = rt === 'codex' ? measuredRuntimeVersion('codex')?.version : undefined;
310
+ const orchestrating = job.orchestrationVersion === 2 && posture === 'build' &&
311
+ (rt === 'claude' ? claudeOrchestrationSafe(measuredRuntimeVersion('claude')?.version) : rt === 'codex' && codexSubagentsSafe(codexVersion));
389
312
  /**
390
313
  * THE WHOLE STREAM, not just its latest line — see trace.mjs.
391
314
  *
@@ -398,12 +321,12 @@ export function createAgentTurnExecution({
398
321
  agentId,
399
322
  turnId,
400
323
  post: postAgentTrace,
401
- orchestrationVersion: job.orchestrationVersion,
324
+ orchestrationVersion: orchestrating ? 2 : undefined,
402
325
  scrub: envScrub,
403
326
  });
404
327
  // A live Send can choose its path as soon as the CLI starts, including a
405
328
  // silent solo turn. Do not leave a two-second first-flush race at spawn.
406
- if (job.orchestrationVersion === 2) await trace.flush(1_000);
329
+ if (orchestrating) await trace.flush(1_000);
407
330
  /**
408
331
  * Prose the structured event will carry anyway, dropped so a read does
409
332
  * not render twice — but ONLY on a runtime whose stream reaches
@@ -479,122 +402,42 @@ export function createAgentTurnExecution({
479
402
  let projectTools = null;
480
403
  let orchestrator = null;
481
404
  try {
482
- const orchestrated = job.orchestrationVersion === 2;
483
- if (orchestrated && posture === 'build' && ['claude', 'codex'].includes(rt)) {
405
+ if (orchestrating) {
484
406
  orchestrator = await createOrchestrator({ turnId, runtime: rt, trace, admit,
485
- notify: message => { if (rt === 'claude') run.live?.inject(message); },
407
+ // Whether the words reached the CLI; Codex reads its inbox from hooks.
408
+ notify: message => liveInput ? run.live?.inject(message) === true : false,
486
409
  taken: id => run.live?.onTaken(id) });
487
410
  }
488
- // A turn reads the base ref afresh. Changes the agent has made to its
489
- // own worktree cannot silently change the next turn's tools.
490
- const sources = repositoryRun?.sources ?? [];
491
- const source = sources.find((r) => r.wt === wt);
492
- const snapshot = readBaseTools(source?.repoRoot ?? repoRoot, source?.baseRef ?? baseRef());
493
- for (const other of sources.filter((r) => r !== source)) {
494
- const base = readBaseTools(other.repoRoot, other.baseRef);
495
- snapshot.instructions.push(...[...base.instructions, ...base.skillFiles].map((file) => ({ ...file, path: `repositories/${other.folder}/${file.path}` })));
496
- }
497
- projectTools = prepareAgentTools(snapshot, rt, process.env, wt, { codexHome: codexAgentHome, turnServers: {
498
- ...(repositoryRun?.tool ? { card_repositories: repositoryRun.tool.config } : {}),
499
- ...(orchestrator ? { card_orchestrator: orchestrator.config } : {}),
500
- } });
501
- const mountsProjectMcp = TURN_PROFILES[posture]?.projectMcp === true && (rt === 'claude' || rt === 'codex');
502
- // A fenced posture on a runner that ignores its own file there (Codex's
503
- // `--ignore-user-config`) takes the servers as `fencedMount` instead.
504
- const fencedMount = mountsProjectMcp && posture !== 'build' ? projectTools.fencedMount : null;
505
- // THE MACHINE'S OWN SERVERS ON CLAUDE (2026-10-11, the owner): the turn
506
- // is not strict, and every `.mcp.json` name from the worktree up is
507
- // disabled so only the base copy speaks for the project. Null (an
508
- // unboundable walk) mounts strictly: the base copy alone.
509
- const claudeDisabled = mountsProjectMcp && rt === 'claude' ? mcpJsonServerNamesAbove(wt) : null;
510
- // What this card's posture withholds, said to the agent — with the
511
- // servers it did and did not mount, and why — so it relays the fence
512
- // instead of inventing a setting (postureTools.mjs). Empty for a code card.
513
- const toolsNote = postureToolsNote({
514
- posture,
515
- runtime: rt,
516
- mounted: !mountsProjectMcp ? [] : fencedMount ? fencedMount.mounted : projectTools.mcpServers,
517
- machine: mountsProjectMcp && (rt === 'codex' || claudeDisabled != null),
518
- unmounted: [...projectTools.mcpUnmounted.map((u) => ({ ...u, source: 'project' })), ...(fencedMount ? fencedMount.unmounted : [])],
519
- });
411
+ const tools = prepareCardTools({ rt, posture, wt, repoRoot, baseRef, sources: repositoryRun?.sources ?? [], repositoryTool: repositoryRun?.tool ?? null, orchestratorConfig: orchestrator?.config ?? null, orchestrated: Boolean(orchestrator), codexHome: agentResume.codexHome });
412
+ projectTools = tools.projectTools;
413
+ const { toolsNote } = tools;
520
414
  const agentTurnArgs = {
521
- ...((rt === 'claude' || orchestrator) ? { liveMessages: (run.live = makeLiveMessages({
415
+ ...((liveInput || orchestrator) ? { liveMessages: (run.live = makeLiveMessages({
522
416
  scope: 'agent', turnId, cwd: wt, sessionMetaPath, interrupted: () => run.interrupted,
523
417
  observe: message => orchestrator?.note(message),
524
418
  ...(rt === 'codex' ? { nativeIds: () => null } : {}),
525
419
  format: async (followup) => {
526
420
  orchestrator?.note(followup);
527
421
  const attached = followup.attachments?.length ? await fetchAgentFiles(wt, followup.attachments.map((file) => ({ ...file, from: 'message' }))) : { message: [], card: [] };
528
- return AGENT_HUMAN_KICKOFF({ agentName: job.agentName, message: followup.body + (followup.libraryPrompt ?? '') + (orchestrator ? `\nMessage id: ${followup.id}. Choose and report its route with card_orchestrator.route_message.` : ''),
529
- askedByName: followup.askedByName, task: job.task, cardThread: job.cardThread,
530
- investigationContext: job.investigationContext, researchContext: job.researchContext,
531
- messageFiles: attached.message, cardFiles: files.card, position: job.position ?? 1, total: job.total ?? 1 });
422
+ return agentFollowupKickoff(job, followup, { messageFiles: attached.message, cardFiles: files.card, orchestrated: Boolean(orchestrator) });
532
423
  },
533
424
  })) } : {}),
534
- prompt:
535
- job.kind === 'task' && job.task
536
- ? AGENT_TASK_KICKOFF({
537
- agentName: job.agentName,
538
- task: job.task,
539
- // The card's discussion (0.106.0), printed inside THE CARD by
540
- // the same builder the stash above wrote down — and the files
541
- // on it (0.112.0), after it, the same way.
542
- resumeBody: job.body,
543
- investigationContext: job.investigationContext,
544
- researchContext: job.researchContext,
545
- cardThread: job.cardThread,
546
- cardFiles: files.card,
547
- position: job.position ?? 1,
548
- total: job.total ?? 1,
549
- })
550
- : AGENT_HUMAN_KICKOFF({
551
- agentName: job.agentName,
552
- // INTERRUPT NOW: the previous run was stopped to send this.
553
- interrupted: job.interrupted === true,
554
- message: job.body ?? '',
555
- investigationContext: job.investigationContext,
556
- researchContext: job.researchContext,
557
- askedByName: job.askedByName,
558
- task: job.task,
559
- // The discussion on the one card this message is about, in a
560
- // fence of its own after the person's words (0.106.0).
561
- cardThread: job.cardThread,
562
- // The files (0.112.0): the person's inside their words, the
563
- // card's at the foot of its discussion.
564
- messageFiles: files.message,
565
- cardFiles: files.card,
566
- position: job.position ?? 1,
567
- total: job.total ?? 1,
568
- }),
569
- // The knowledge paragraph, when this box holds a library — the same
570
- // composer the tabs use, so an agent and a tab can never be told two
571
- // different things about the same directory.
572
- // Spoken to the CLI that runs it (0.115.0): a mockup on Codex is
573
- // told it is Codex, never Claude.
574
- system: withProjectContext(SYSTEM_AGENT_FOR(taskKind, runtimeById(rt)?.label, { prerequisitesAccepted: job.prerequisitesAccepted === true }), {
575
- knowledgeDir: knowledgeDirFor(repoRoot),
576
- // ARTIFACTS (0.94.0), while the server can show one — an agent's
577
- // land on its page, under the facts row.
578
- artifacts: getArtifactsAccepted(),
579
- }) + (toolsNote ? `\n\n${toolsNote}` : '') + (artifactInstructions ? `\n\n${artifactInstructions}` : '') + `\n\n${INTERNAL_WAIT_CONTRACT}` + (projectTools.instructions ? `\n\n${projectTools.instructions}` : '') + (repositoryRun ? `\n\n${repositoryRun.note}\nRepository command argv: ${JSON.stringify([repositoryRun.tool?.config.command, ...(repositoryRun.tool?.config.args.slice(0, -1) ?? []), "__repository", "<name>"])}` : '') + (orchestrator ? `\n\n${ORCHESTRATOR_CONTRACT}` : ''),
425
+ // The kickoff and the system prompt, composed by the agent lane's
426
+ // shape (workAgentTurnShape.mjs) from what this run measured.
427
+ prompt: agentKickoff(job, { cardFiles: files.card, messageFiles: files.message }),
428
+ system: agentTurnSystem({ taskKind, runtimeLabel: runtimeById(rt)?.label, prerequisitesAccepted: job.prerequisitesAccepted === true, knowledgeDir: knowledgeDirFor(repoRoot), artifactsAccepted: getArtifactsAccepted(), toolsNote, artifactInstructions, toolInstructions: projectTools.instructions, repositoryRun, orchestrated: Boolean(orchestrator) }),
580
429
  agentTools: projectTools,
581
430
  writableDirs: repositoryRun?.writableDirs,
582
- ...((projectTools.codexHome || fencedMount || repositoryRun?.tool || orchestrator) ? { mcpEnv: { ...(projectTools.codexHome ? { CODEX_HOME: projectTools.codexHome } : {}), ...(fencedMount?.env ?? {}), ...(repositoryRun?.tool?.env ?? {}), ...(orchestrator?.env ?? {}) } } : {}),
431
+ ...((tools.envNeeded || repositoryRun?.tool || orchestrator) ? { mcpEnv: { ...tools.env, ...(repositoryRun?.tool?.env ?? {}), ...(orchestrator?.env ?? {}) } } : {}),
583
432
  orchestration: Boolean(orchestrator),
433
+ ...(rt === 'codex' ? { codexVersion } : {}),
584
434
  // NOBODY IS AT THIS SCREEN (2026-09-29): no display, BROWSER=none, and
585
435
  // a browser profile of the agent's own, kept beside its Codex home —
586
436
  // so a Chrome it starts to look at its page never reaches the
587
437
  // person's (noWindowEnv.mjs).
588
438
  browserHome: agentBrowserHome(sessionMetaPath, wt, agentId),
589
- // Reviewed project servers and the machine's configured servers, on
590
- // initial cards, answers and review iterations alike.
591
- ...(mountsProjectMcp && rt === 'claude'
592
- ? {
593
- mcpArgs: [...(claudeDisabled ? [] : ['--strict-mcp-config']), '--mcp-config', projectTools.mcpPath],
594
- projectMcpServers: projectTools.mcpServers,
595
- ...(claudeDisabled ? { machineMcp: claudeDisabled, mcpCheckout: source?.repoRoot ?? repoRoot } : {}),
596
- }
597
- : fencedMount ? { mcpArgs: fencedMount.args } : {}),
439
+ // What the card mounts, as its runner takes it (workAgentTools.mjs).
440
+ ...tools.mountArgs,
598
441
  knowledgeDir: knowledgeDirFor(repoRoot),
599
442
  // The kind's posture, as the one turn profile (turnProfile.mjs):
600
443
  // build for code, the fenced design/research postures otherwise.
@@ -604,16 +447,17 @@ export function createAgentTurnExecution({
604
447
  onSpawnResult: result => orchestrator?.spawnResult(result),
605
448
  cwd: wt,
606
449
  runtime: rt,
607
- resume,
608
- resumeThreadId: codexResumeId || claudeResumeId || undefined,
450
+ resume: agentResume.resume,
451
+ resumeThreadId: agentResume.resumeThreadId,
609
452
  onInit: (init) => {
610
453
  orchestrator?.root(init.sessionId);
611
- if (claudeMarker && CLAUDE_SESSION_RE.test(init.sessionId ?? ''))
612
- try { writeFileSync(claudeMarker, init.sessionId); } catch { /* best effort */ }
454
+ // A server that did not connect, as the CLI said (unknown: nothing).
455
+ trace.mcpStatus(init.mcpServers);
456
+ agentResume.sawSession(init.sessionId);
613
457
  },
614
458
  onThreadId: (id) => {
615
459
  orchestrator?.root(id);
616
- seenThreadId = String(id ?? '').trim() || seenThreadId;
460
+ agentResume.sawThread(id);
617
461
  },
618
462
  onAnswer: (t) => {
619
463
  lastAnswer = t;
@@ -622,6 +466,25 @@ export function createAgentTurnExecution({
622
466
  ...brain,
623
467
  answerFromResult: true,
624
468
  label: c.cyan('[agent]'),
469
+ // The CLI's own context measure while the turn runs (claudeInput.mjs),
470
+ // the latest one at a time; the server clears it at turn end.
471
+ onContext: (() => {
472
+ let next = null;
473
+ let busy = false;
474
+ return (ctx) => {
475
+ next = ctx;
476
+ if (busy) return;
477
+ busy = true;
478
+ void (async () => {
479
+ while (next) {
480
+ const measured = next;
481
+ next = null;
482
+ await postAgentContext(agentId, turnId, measured);
483
+ }
484
+ busy = false;
485
+ })();
486
+ };
487
+ })(),
625
488
  // The CLI's own tail, relayed. Throttled by the same rule the tab's
626
489
  // narrator keeps: overwritten, never appended, and ignored by the
627
490
  // board past ~90 seconds.
@@ -757,7 +620,7 @@ export function createAgentTurnExecution({
757
620
  resume: rt === 'codex' && posture === 'research' ? false : args.resume,
758
621
  runtime: rt,
759
622
  beforeFresh: () => {
760
- seenThreadId = null;
623
+ agentResume.forgetThread();
761
624
  lastAnswer = null;
762
625
  model = null;
763
626
  },
@@ -775,13 +638,10 @@ export function createAgentTurnExecution({
775
638
  const counted = usage;
776
639
  return runCli({
777
640
  ...agentTurnArgs, prompt, liveContinuation: true,
778
- onUsage: (next) => agentTurnArgs.onUsage(Object.fromEntries(
779
- ['input', 'output', 'cacheCreate', 'cacheRead'].map((k) =>
780
- [k, (counted?.[k] ?? 0) + (next?.[k] ?? 0)])
781
- )),
641
+ onUsage: (next) => agentTurnArgs.onUsage(addUsage(counted, next)),
782
642
  writableDirs: repositoryRun.sourcesNow().filter((r) => r.wt !== wt).map((r) => r.wt),
783
643
  resume: true,
784
- resumeThreadId: seenThreadId ?? agentTurnArgs.resumeThreadId,
644
+ resumeThreadId: agentResume.threadNow() ?? agentTurnArgs.resumeThreadId,
785
645
  });
786
646
  },
787
647
  });
@@ -829,23 +689,9 @@ export function createAgentTurnExecution({
829
689
  }
830
690
  for (const ch of spawnedChildren) workChildren.delete(ch);
831
691
  if (agentChildren.get(place) === child) agentChildren.delete(place);
832
- if (ranMarker && rt === 'claude') {
833
- try {
834
- writeFileSync(ranMarker, '1');
835
- } catch {
836
- /* a missing marker only costs one un-resumed turn */
837
- }
838
- }
839
- // The thread THIS turn spoke under, pinned so the next card, answer
840
- // or merge-resolve resumes it. After the turn, like the tab lane: an
841
- // id learned mid-turn is only true once the turn that learned it ends.
842
- if (codexThreadMarker && seenThreadId && CODEX_THREAD_RE.test(seenThreadId)) {
843
- try {
844
- writeFileSync(codexThreadMarker, seenThreadId);
845
- } catch {
846
- /* best-effort — the next turn runs fresh */
847
- }
848
- }
692
+ // The ran marker and the thread this turn spoke under, written now
693
+ // that the turn is over (workAgentResume.mjs).
694
+ agentResume.persist();
849
695
  /**
850
696
  * AN ACTION THAT CHANGES WHAT THE MACHINE WOULD MEASURE MUST CAUSE A
851
697
  * NEW MEASUREMENT — the rule every session settle keeps, and the one
@@ -866,7 +712,7 @@ export function createAgentTurnExecution({
866
712
  }
867
713
 
868
714
 
869
- const commits = repositoryRun ? [...new Set(repositoryRun.receipts().map((r) => r.sha))].slice(0, 50) : commitsBetween(wt, before);
715
+ const commits = turnCommits({ repositoryReceipts: repositoryRun?.receipts, wt, before, baseRef });
870
716
  /**
871
717
  * A PERSON INTERRUPTED IT (Interrupt now). Whatever the CLI printed was
872
718
  * cut off, so there is no outcome to read: the turn reports `interrupted`
@@ -877,17 +723,7 @@ export function createAgentTurnExecution({
877
723
  */
878
724
  if (run.interrupted) {
879
725
  measured.ranCli = spawnedChildren.size > 0;
880
- await postAgentTurn({
881
- turnId,
882
- outcome: 'nothing',
883
- interrupted: true,
884
- ran: spawnedChildren.size > 0,
885
- commits,
886
- ...(usage ? { usage } : {}),
887
- ...(model ? { model } : {}),
888
- ...(branch ? { branch } : {}),
889
- worktree: wt,
890
- });
726
+ await postAgentTurn(interruptedTurnSettlement({ turnId, ran: spawnedChildren.size > 0, commits, usage, model, branch, wt }));
891
727
  return;
892
728
  }
893
729
  // WHAT THE TURN REPORTS is one decision, off what was measured here
@@ -926,5 +762,5 @@ export function createAgentTurnExecution({
926
762
  } finally { await repositoryRun?.tool?.close(); }
927
763
  };
928
764
 
929
- return { runAgentTurnInPlace, commitsBetween };
765
+ return { runAgentTurnInPlace };
930
766
  }
@@ -16,6 +16,11 @@
16
16
  * and settles once, so a crash in the run can never leave a turn unsettled to
17
17
  * be re-offered and re-run on the operator's account.
18
18
  *
19
+ * `interruptedTurnSettlement` is the sixth (2026-10-18, the SOLID pass): a turn
20
+ * a person stopped, where no outcome is read. The run and the lane's catch
21
+ * both send it — the catch when the stopped run then threw — and they had
22
+ * already drifted apart, so its body is built here once.
23
+ *
19
24
  * `agentTurnSettlement` returns `{ res, limit, body, final }`; the caller posts
20
25
  * `body` through the one turn-done wire, clears or sets the machine's limit off
21
26
  * `res`/`limit`, parks on `limit`, and runs the review-entry beat only after a
@@ -27,6 +32,7 @@ import { changedArtifacts, scanArtifacts } from './artifacts.mjs';
27
32
  import { scrub as envScrub } from './uplinkScrub.mjs';
28
33
  import { usableImageArtifact } from './imageArtifact.mjs';
29
34
  import { withoutTurnDeclaration } from './turnDeclaration.mjs';
35
+ import { ANSWER_DELIVERED_USES, researchUseOf, withResearchHeader } from './researchUse.mjs';
30
36
 
31
37
  export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage, model = null, commits, artifactsBefore, branch, wt }) {
32
38
  const res = (lastAnswer && parseTurnResult(lastAnswer)) || parseTurnResult(out);
@@ -117,9 +123,9 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
117
123
  */
118
124
  // Existing document deliveries default to the Markdown proof. Only an
119
125
  // explicit temporary-use declaration can deliver through the stored answer.
120
- const briefUse = /^Research intended use: (tasks|context|document)(?:\r?\n|$)/.exec(job.task?.brief ?? '')?.[1];
126
+ const briefUse = researchUseOf(job.task?.brief);
121
127
  const researchUse = kind.posture === 'research' ? briefUse ?? res.researchUse : null;
122
- const temporaryResearch = ['tasks', 'context'].includes(researchUse);
128
+ const temporaryResearch = ANSWER_DELIVERED_USES.includes(researchUse);
123
129
  if (res.outcome === 'delivered' && temporaryResearch && !res.answer?.trim()) {
124
130
  return report({ turnId, outcome: 'nothing', answer: 'The research turn returned no findings.', branch, worktree: wt });
125
131
  }
@@ -154,7 +160,7 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
154
160
  ...(prerequisite && !cannotRequest ? { prerequisite } : {}),
155
161
  outcome: res.outcome,
156
162
  answer: cannotRequest ?? envScrub(kind.posture === 'research' && researchUse
157
- ? `Research intended use: ${researchUse}\n\n${res.answer ?? ''}` : res.answer ?? ''),
163
+ ? withResearchHeader(researchUse, res.answer ?? '') : res.answer ?? ''),
158
164
  ...(commits.length ? { commits } : {}),
159
165
  // SCRUBBED like the answer beside it: a raised card lands in the
160
166
  // project doc every member reads, and a brief quoting the `.env` the
@@ -235,3 +241,24 @@ export function failedTurnSettlement({ turnId, error, commits = [], usage = null
235
241
  ...(wt ? { branch, worktree: wt } : {}),
236
242
  };
237
243
  }
244
+
245
+ /**
246
+ * A PERSON INTERRUPTED IT (Interrupt now), so there is no outcome to read:
247
+ * whatever the CLI printed was cut off. What was measured rides along — the
248
+ * commits it made, its spend, the model it ran on, the branch and the
249
+ * worktree — and nothing measured is nothing claimed. `ran` is whether its
250
+ * CLI started at all.
251
+ */
252
+ export function interruptedTurnSettlement({ turnId, ran, commits, usage = null, model = null, branch = null, wt = null }) {
253
+ return {
254
+ turnId,
255
+ outcome: 'nothing',
256
+ interrupted: true,
257
+ ran,
258
+ commits,
259
+ ...(usage ? { usage } : {}),
260
+ ...(model ? { model } : {}),
261
+ ...(branch ? { branch } : {}),
262
+ ...(wt ? { worktree: wt } : {}),
263
+ };
264
+ }
@@ -114,6 +114,21 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
114
114
  }
115
115
  };
116
116
 
117
+ /** The CLI's context measure for a running turn (2026-10-19); the server
118
+ * drops one whose turn already settled. Best effort, like the line. */
119
+ const postAgentContext = async (agentId, turnId, context) => {
120
+ try {
121
+ await fetch(AGENT_ACTIVITY_URL, {
122
+ method: 'POST',
123
+ headers: fleetHeaders(),
124
+ signal: AbortSignal.timeout(15_000),
125
+ body: JSON.stringify({ agentId, turnId, context }),
126
+ });
127
+ } catch {
128
+ /* a readout; the next measurement replaces it */
129
+ }
130
+ };
131
+
117
132
  const postAgentParked = async (reason, runtime) => {
118
133
  try {
119
134
  await fetch(AGENT_PARKED_URL, {
@@ -153,6 +168,7 @@ export function createAgentTurnReports({ REJECT_RETRY_MS }) {
153
168
  postAgentTurn,
154
169
  postAgentTrace,
155
170
  postAgentActivity,
171
+ postAgentContext,
156
172
  postAgentParked,
157
173
  pruneHeldReports,
158
174
  agentReported,