flowviant 0.125.4 → 0.125.6

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 (86) hide show
  1. package/bin/lib/agentInternalWait.mjs +1 -5
  2. package/bin/lib/agentPlan.mjs +13 -163
  3. package/bin/lib/agentRepositories.mjs +3 -1
  4. package/bin/lib/claudeInput.mjs +28 -9
  5. package/bin/lib/claudeMachineMcp.mjs +85 -0
  6. package/bin/lib/claudePersonal.mjs +6 -6
  7. package/bin/lib/claudePosture.mjs +6 -15
  8. package/bin/lib/claudeSettingsArg.mjs +67 -0
  9. package/bin/lib/claudeSubagentProgress.mjs +12 -28
  10. package/bin/lib/claudeSubagents.mjs +4 -2
  11. package/bin/lib/codexFencedMount.mjs +275 -0
  12. package/bin/lib/codexPersonal.mjs +1 -265
  13. package/bin/lib/codexProjectRead.mjs +5 -4
  14. package/bin/lib/codexResearch.mjs +9 -3
  15. package/bin/lib/codexSubagentEvents.mjs +11 -7
  16. package/bin/lib/codexSubagents.mjs +55 -52
  17. package/bin/lib/codexToml.mjs +328 -0
  18. package/bin/lib/conversationIds.mjs +10 -0
  19. package/bin/lib/fleet.mjs +2 -2
  20. package/bin/lib/git.mjs +32 -0
  21. package/bin/lib/heldTool.mjs +49 -0
  22. package/bin/lib/hooks/mcpAllow.mjs +11 -5
  23. package/bin/lib/hooks/orchestrator.mjs +20 -10
  24. package/bin/lib/intakeDraft.mjs +1 -1
  25. package/bin/lib/jsonlTail.mjs +52 -0
  26. package/bin/lib/mcpInventory.mjs +9 -5
  27. package/bin/lib/mcpReasons.mjs +20 -0
  28. package/bin/lib/mcpReportBounds.mjs +10 -0
  29. package/bin/lib/mcpStdio.mjs +35 -0
  30. package/bin/lib/mergeBatch.mjs +6 -24
  31. package/bin/lib/mergeOrder.mjs +18 -0
  32. package/bin/lib/modelJson.mjs +173 -0
  33. package/bin/lib/nativeAgentTools.mjs +72 -0
  34. package/bin/lib/orchestrator.mjs +212 -133
  35. package/bin/lib/orchestratorClient.mjs +17 -21
  36. package/bin/lib/orchestratorHooks.mjs +21 -4
  37. package/bin/lib/orchestratorNative.mjs +58 -0
  38. package/bin/lib/orchestratorPrompt.mjs +7 -4
  39. package/bin/lib/orchestratorProtocol.mjs +25 -3
  40. package/bin/lib/postureTools.mjs +58 -13
  41. package/bin/lib/prWorkflow.mjs +13 -2
  42. package/bin/lib/projectRepositories.mjs +2 -2
  43. package/bin/lib/projectToolRuntimes.mjs +92 -245
  44. package/bin/lib/projectTools.mjs +59 -19
  45. package/bin/lib/repositoryDelivery.mjs +6 -4
  46. package/bin/lib/repositoryMerge.mjs +8 -12
  47. package/bin/lib/repositoryPlaces.mjs +2 -2
  48. package/bin/lib/repositoryPrompts.mjs +42 -0
  49. package/bin/lib/repositoryTool.mjs +9 -32
  50. package/bin/lib/researchUse.mjs +39 -0
  51. package/bin/lib/resumeMarker.mjs +43 -0
  52. package/bin/lib/runTurn.mjs +38 -52
  53. package/bin/lib/runtimeAntigravity.mjs +1 -1
  54. package/bin/lib/runtimeCapabilities.mjs +6 -16
  55. package/bin/lib/runtimeClaude.mjs +62 -19
  56. package/bin/lib/runtimeCodex.mjs +9 -6
  57. package/bin/lib/runtimeEvents.mjs +13 -2
  58. package/bin/lib/runtimeOfferings.mjs +9 -4
  59. package/bin/lib/runtimes.mjs +3 -1
  60. package/bin/lib/subagentBounds.mjs +20 -0
  61. package/bin/lib/trace.mjs +30 -5
  62. package/bin/lib/turnDeclaration.mjs +1 -1
  63. package/bin/lib/turnDiagnostics.mjs +9 -12
  64. package/bin/lib/turnReceipts.mjs +64 -0
  65. package/bin/lib/turnSubagents.mjs +91 -0
  66. package/bin/lib/turnUsage.mjs +9 -6
  67. package/bin/lib/versionOrder.mjs +11 -1
  68. package/bin/lib/work.mjs +6 -6
  69. package/bin/lib/workAgentMerges.mjs +5 -7
  70. package/bin/lib/workAgentPrecheck.mjs +5 -5
  71. package/bin/lib/workAgentRecovery.mjs +2 -1
  72. package/bin/lib/workAgentResume.mjs +121 -0
  73. package/bin/lib/workAgentTools.mjs +111 -0
  74. package/bin/lib/workAgentTurnExecution.mjs +62 -246
  75. package/bin/lib/workAgentTurnOutcome.mjs +30 -3
  76. package/bin/lib/workAgentTurnShape.mjs +90 -0
  77. package/bin/lib/workAgentTurns.mjs +24 -10
  78. package/bin/lib/workArtifactReconcile.mjs +3 -2
  79. package/bin/lib/workMergeBatch.mjs +7 -5
  80. package/bin/lib/workPlaceReports.mjs +3 -3
  81. package/bin/lib/workPlaces.mjs +14 -0
  82. package/bin/lib/workSessionRuntime.mjs +2 -5
  83. package/bin/lib/workSessionTurns.mjs +12 -63
  84. package/bin/lib/workShip.mjs +8 -18
  85. package/bin/lib/workTurnShape.mjs +6 -4
  86. 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,
@@ -82,38 +84,6 @@ export function createAgentTurnExecution({
82
84
 
83
85
  const lastAgentBeat = new Map(); // agentId -> last activity POST, ms
84
86
 
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
87
  /**
118
88
  * The turn, run inside its place's writer lock (the lane takes it). `turnId`,
119
89
  * `agentId` and `place` are the lane's own readings of the job, handed down
@@ -179,6 +149,7 @@ export function createAgentTurnExecution({
179
149
  measured.branch = branch;
180
150
 
181
151
  const rt = job.runtime || 'claude';
152
+ const liveInput = runtimeById(rt)?.liveInput === true;
182
153
  /**
183
154
  * WHAT THIS CARD HANDS BACK, AND THE POSTURE THAT FOLLOWS (0.97.0).
184
155
  *
@@ -279,64 +250,9 @@ export function createAgentTurnExecution({
279
250
  return;
280
251
  }
281
252
 
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;
253
+ // Which conversation this turn resumes, and the kept Codex home it is
254
+ // filed under: the agent's resume across turns (workAgentResume.mjs).
255
+ const agentResume = agentTurnResume({ rt, wt, agentId, sessionMetaPath });
340
256
 
341
257
  /**
342
258
  * THE TURN'S FILES, ON DISK BEFORE THE PROMPT NAMES THEM (0.112.0) — the
@@ -370,11 +286,11 @@ export function createAgentTurnExecution({
370
286
  * commits it made are on the branch, and a reviewer is entitled to read
371
287
  * what was asked for either way.
372
288
  *
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.
289
+ * ONE SPEC BUILDER (`AGENT_TASK_SPEC`) shared with the kickoff
290
+ * (workAgentTurnShape.mjs), so what the reviewer reads is byte-identical
291
+ * to what the agent read — the card's discussion included (0.106.0): both
292
+ * are handed the same `job.cardThread`, and the builder prints it inside
293
+ * the spec — and the card's files after it (0.112.0), fetched just above.
378
294
  * Failure is swallowed inside `stashCard`: a note about a turn may never
379
295
  * cost the turn.
380
296
  */
@@ -386,6 +302,12 @@ export function createAgentTurnExecution({
386
302
  );
387
303
  }
388
304
 
305
+ // ORCHESTRATED ONLY WHERE THE CLI CAN BE GATED (2026-10-17). v2 reaches every
306
+ // job at the floor; a fenced card or a CLI below its measured floor (unknown
307
+ // included) has no controller, so no marker: its notes fold on receipt.
308
+ const codexVersion = rt === 'codex' ? measuredRuntimeVersion('codex')?.version : undefined;
309
+ const orchestrating = job.orchestrationVersion === 2 && posture === 'build' &&
310
+ (rt === 'claude' ? claudeOrchestrationSafe(measuredRuntimeVersion('claude')?.version) : rt === 'codex' && codexSubagentsSafe(codexVersion));
389
311
  /**
390
312
  * THE WHOLE STREAM, not just its latest line — see trace.mjs.
391
313
  *
@@ -398,12 +320,12 @@ export function createAgentTurnExecution({
398
320
  agentId,
399
321
  turnId,
400
322
  post: postAgentTrace,
401
- orchestrationVersion: job.orchestrationVersion,
323
+ orchestrationVersion: orchestrating ? 2 : undefined,
402
324
  scrub: envScrub,
403
325
  });
404
326
  // A live Send can choose its path as soon as the CLI starts, including a
405
327
  // silent solo turn. Do not leave a two-second first-flush race at spawn.
406
- if (job.orchestrationVersion === 2) await trace.flush(1_000);
328
+ if (orchestrating) await trace.flush(1_000);
407
329
  /**
408
330
  * Prose the structured event will carry anyway, dropped so a read does
409
331
  * not render twice — but ONLY on a runtime whose stream reaches
@@ -479,122 +401,42 @@ export function createAgentTurnExecution({
479
401
  let projectTools = null;
480
402
  let orchestrator = null;
481
403
  try {
482
- const orchestrated = job.orchestrationVersion === 2;
483
- if (orchestrated && posture === 'build' && ['claude', 'codex'].includes(rt)) {
404
+ if (orchestrating) {
484
405
  orchestrator = await createOrchestrator({ turnId, runtime: rt, trace, admit,
485
- notify: message => { if (rt === 'claude') run.live?.inject(message); },
406
+ // Whether the words reached the CLI; Codex reads its inbox from hooks.
407
+ notify: message => liveInput ? run.live?.inject(message) === true : false,
486
408
  taken: id => run.live?.onTaken(id) });
487
409
  }
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
- });
410
+ 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 });
411
+ projectTools = tools.projectTools;
412
+ const { toolsNote } = tools;
520
413
  const agentTurnArgs = {
521
- ...((rt === 'claude' || orchestrator) ? { liveMessages: (run.live = makeLiveMessages({
414
+ ...((liveInput || orchestrator) ? { liveMessages: (run.live = makeLiveMessages({
522
415
  scope: 'agent', turnId, cwd: wt, sessionMetaPath, interrupted: () => run.interrupted,
523
416
  observe: message => orchestrator?.note(message),
524
417
  ...(rt === 'codex' ? { nativeIds: () => null } : {}),
525
418
  format: async (followup) => {
526
419
  orchestrator?.note(followup);
527
420
  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 });
421
+ return agentFollowupKickoff(job, followup, { messageFiles: attached.message, cardFiles: files.card, orchestrated: Boolean(orchestrator) });
532
422
  },
533
423
  })) } : {}),
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}` : ''),
424
+ // The kickoff and the system prompt, composed by the agent lane's
425
+ // shape (workAgentTurnShape.mjs) from what this run measured.
426
+ prompt: agentKickoff(job, { cardFiles: files.card, messageFiles: files.message }),
427
+ 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
428
  agentTools: projectTools,
581
429
  writableDirs: repositoryRun?.writableDirs,
582
- ...((projectTools.codexHome || fencedMount || repositoryRun?.tool || orchestrator) ? { mcpEnv: { ...(projectTools.codexHome ? { CODEX_HOME: projectTools.codexHome } : {}), ...(fencedMount?.env ?? {}), ...(repositoryRun?.tool?.env ?? {}), ...(orchestrator?.env ?? {}) } } : {}),
430
+ ...((tools.envNeeded || repositoryRun?.tool || orchestrator) ? { mcpEnv: { ...tools.env, ...(repositoryRun?.tool?.env ?? {}), ...(orchestrator?.env ?? {}) } } : {}),
583
431
  orchestration: Boolean(orchestrator),
432
+ ...(rt === 'codex' ? { codexVersion } : {}),
584
433
  // NOBODY IS AT THIS SCREEN (2026-09-29): no display, BROWSER=none, and
585
434
  // a browser profile of the agent's own, kept beside its Codex home —
586
435
  // so a Chrome it starts to look at its page never reaches the
587
436
  // person's (noWindowEnv.mjs).
588
437
  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 } : {}),
596
- }
597
- : fencedMount ? { mcpArgs: fencedMount.args } : {}),
438
+ // What the card mounts, as its runner takes it (workAgentTools.mjs).
439
+ ...tools.mountArgs,
598
440
  knowledgeDir: knowledgeDirFor(repoRoot),
599
441
  // The kind's posture, as the one turn profile (turnProfile.mjs):
600
442
  // build for code, the fenced design/research postures otherwise.
@@ -604,16 +446,17 @@ export function createAgentTurnExecution({
604
446
  onSpawnResult: result => orchestrator?.spawnResult(result),
605
447
  cwd: wt,
606
448
  runtime: rt,
607
- resume,
608
- resumeThreadId: codexResumeId || claudeResumeId || undefined,
449
+ resume: agentResume.resume,
450
+ resumeThreadId: agentResume.resumeThreadId,
609
451
  onInit: (init) => {
610
452
  orchestrator?.root(init.sessionId);
611
- if (claudeMarker && CLAUDE_SESSION_RE.test(init.sessionId ?? ''))
612
- try { writeFileSync(claudeMarker, init.sessionId); } catch { /* best effort */ }
453
+ // A server that did not connect, as the CLI said (unknown: nothing).
454
+ trace.mcpStatus(init.mcpServers);
455
+ agentResume.sawSession(init.sessionId);
613
456
  },
614
457
  onThreadId: (id) => {
615
458
  orchestrator?.root(id);
616
- seenThreadId = String(id ?? '').trim() || seenThreadId;
459
+ agentResume.sawThread(id);
617
460
  },
618
461
  onAnswer: (t) => {
619
462
  lastAnswer = t;
@@ -757,7 +600,7 @@ export function createAgentTurnExecution({
757
600
  resume: rt === 'codex' && posture === 'research' ? false : args.resume,
758
601
  runtime: rt,
759
602
  beforeFresh: () => {
760
- seenThreadId = null;
603
+ agentResume.forgetThread();
761
604
  lastAnswer = null;
762
605
  model = null;
763
606
  },
@@ -775,13 +618,10 @@ export function createAgentTurnExecution({
775
618
  const counted = usage;
776
619
  return runCli({
777
620
  ...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
- )),
621
+ onUsage: (next) => agentTurnArgs.onUsage(addUsage(counted, next)),
782
622
  writableDirs: repositoryRun.sourcesNow().filter((r) => r.wt !== wt).map((r) => r.wt),
783
623
  resume: true,
784
- resumeThreadId: seenThreadId ?? agentTurnArgs.resumeThreadId,
624
+ resumeThreadId: agentResume.threadNow() ?? agentTurnArgs.resumeThreadId,
785
625
  });
786
626
  },
787
627
  });
@@ -829,23 +669,9 @@ export function createAgentTurnExecution({
829
669
  }
830
670
  for (const ch of spawnedChildren) workChildren.delete(ch);
831
671
  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
- }
672
+ // The ran marker and the thread this turn spoke under, written now
673
+ // that the turn is over (workAgentResume.mjs).
674
+ agentResume.persist();
849
675
  /**
850
676
  * AN ACTION THAT CHANGES WHAT THE MACHINE WOULD MEASURE MUST CAUSE A
851
677
  * NEW MEASUREMENT — the rule every session settle keeps, and the one
@@ -866,7 +692,7 @@ export function createAgentTurnExecution({
866
692
  }
867
693
 
868
694
 
869
- const commits = repositoryRun ? [...new Set(repositoryRun.receipts().map((r) => r.sha))].slice(0, 50) : commitsBetween(wt, before);
695
+ const commits = turnCommits({ repositoryReceipts: repositoryRun?.receipts, wt, before, baseRef });
870
696
  /**
871
697
  * A PERSON INTERRUPTED IT (Interrupt now). Whatever the CLI printed was
872
698
  * cut off, so there is no outcome to read: the turn reports `interrupted`
@@ -877,17 +703,7 @@ export function createAgentTurnExecution({
877
703
  */
878
704
  if (run.interrupted) {
879
705
  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
- });
706
+ await postAgentTurn(interruptedTurnSettlement({ turnId, ran: spawnedChildren.size > 0, commits, usage, model, branch, wt }));
891
707
  return;
892
708
  }
893
709
  // WHAT THE TURN REPORTS is one decision, off what was measured here
@@ -926,5 +742,5 @@ export function createAgentTurnExecution({
926
742
  } finally { await repositoryRun?.tool?.close(); }
927
743
  };
928
744
 
929
- return { runAgentTurnInPlace, commitsBetween };
745
+ return { runAgentTurnInPlace };
930
746
  }
@@ -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
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * AN AGENT TURN'S PROMPT PAIR — its kickoff, a live message's follow-up and
3
+ * its system prompt (pure, 2026-10-18, the SOLID pass). The twin of
4
+ * workTurnShape.mjs, which is the same thing for a tab.
5
+ *
6
+ * Split out of workAgentTurnExecution.mjs, which composed all three inline in
7
+ * the turn: two AGENT_HUMAN_KICKOFF argument bags spelling the same ten
8
+ * fields, the task kickoff beside them, and a system prompt of eight sections
9
+ * on one line. The copies had already drifted: investigationContext reached
10
+ * the task and human kickoffs on 2026-10-12, the follow-up written two days
11
+ * later went without it, and live messages carried less context than the
12
+ * first prompt until 2026-10-15 added it beside researchContext at all three
13
+ * sites. So the fields every kickoff shares are spelled once (`shared`), and
14
+ * the next job context key is one edit here.
15
+ *
16
+ * ONE SPEC BUILDER (`AGENT_TASK_SPEC`) is shared by the stash the run writes
17
+ * before the CLI and the kickoff here, so what the reviewer reads is
18
+ * byte-identical to what the agent read — the card's discussion included
19
+ * (0.106.0): both are handed the same `job.cardThread`, and the builder
20
+ * prints it inside the spec — and the card's files after it (0.112.0).
21
+ *
22
+ * Pure: the run measures (the files, the roster's answers, the tools note)
23
+ * and hands the values in; nothing here reads a disk or a clock.
24
+ */
25
+ import { AGENT_TASK_KICKOFF, AGENT_HUMAN_KICKOFF, SYSTEM_AGENT_FOR, withProjectContext } from './prompts.mjs';
26
+ import { INTERNAL_WAIT_CONTRACT } from './agentInternalWait.mjs';
27
+ import { ORCHESTRATOR_CONTRACT } from './orchestratorPrompt.mjs';
28
+
29
+ /** What every kickoff of this job is handed: the agent, the card and its
30
+ * discussion, the job's context handoffs, the card's files, the place in
31
+ * the run. */
32
+ const shared = (job, cardFiles) => ({
33
+ agentName: job.agentName,
34
+ task: job.task,
35
+ cardThread: job.cardThread,
36
+ investigationContext: job.investigationContext,
37
+ researchContext: job.researchContext,
38
+ cardFiles,
39
+ position: job.position ?? 1,
40
+ total: job.total ?? 1,
41
+ });
42
+
43
+ /** The turn's first prompt: the card, on a task turn; the person's words
44
+ * otherwise. `cardFiles` and `messageFiles` are the files already on disk. */
45
+ export const agentKickoff = (job, { cardFiles, messageFiles }) =>
46
+ job.kind === 'task' && job.task
47
+ ? AGENT_TASK_KICKOFF({
48
+ // The card's discussion (0.106.0), printed inside THE CARD by
49
+ // the same builder the run's stash wrote down — and the files
50
+ // on it (0.112.0), after it, the same way.
51
+ ...shared(job, cardFiles),
52
+ resumeBody: job.body,
53
+ })
54
+ : AGENT_HUMAN_KICKOFF({
55
+ // The discussion on the one card this message is about, in a
56
+ // fence of its own after the person's words (0.106.0).
57
+ ...shared(job, cardFiles),
58
+ // INTERRUPT NOW: the previous run was stopped to send this.
59
+ interrupted: job.interrupted === true,
60
+ message: job.body ?? '',
61
+ askedByName: job.askedByName,
62
+ // The files (0.112.0): the person's inside their words, the
63
+ // card's at the foot of its discussion.
64
+ messageFiles,
65
+ });
66
+
67
+ /** A live message sent while the turn runs, as the CLI is handed it — on an
68
+ * orchestrated card with its id, so the route it takes is reported. */
69
+ export const agentFollowupKickoff = (job, followup, { cardFiles, messageFiles, orchestrated }) =>
70
+ AGENT_HUMAN_KICKOFF({
71
+ ...shared(job, cardFiles),
72
+ message: followup.body + (followup.libraryPrompt ?? '') + (orchestrated ? `\nMessage id: ${followup.id}. Choose and report its route with card_orchestrator.route_message.` : ''),
73
+ askedByName: followup.askedByName,
74
+ messageFiles,
75
+ });
76
+
77
+ /** The turn's system prompt, in its fixed section order. */
78
+ export function agentTurnSystem({ taskKind, runtimeLabel, prerequisitesAccepted, knowledgeDir, artifactsAccepted, toolsNote, artifactInstructions, toolInstructions, repositoryRun, orchestrated }) {
79
+ // The knowledge paragraph, when this box holds a library — the same
80
+ // composer the tabs use, so an agent and a tab can never be told two
81
+ // different things about the same directory.
82
+ // Spoken to the CLI that runs it (0.115.0): a mockup on Codex is
83
+ // told it is Codex, never Claude.
84
+ return withProjectContext(SYSTEM_AGENT_FOR(taskKind, runtimeLabel, { prerequisitesAccepted }), {
85
+ knowledgeDir,
86
+ // ARTIFACTS (0.94.0), while the server can show one — an agent's
87
+ // land on its page, under the facts row.
88
+ artifacts: artifactsAccepted,
89
+ }) + (toolsNote ? `\n\n${toolsNote}` : '') + (artifactInstructions ? `\n\n${artifactInstructions}` : '') + `\n\n${INTERNAL_WAIT_CONTRACT}` + (toolInstructions ? `\n\n${toolInstructions}` : '') + (repositoryRun ? `\n\n${repositoryRun.note}\nRepository command argv: ${JSON.stringify([repositoryRun.tool?.config.command, ...(repositoryRun.tool?.config.args.slice(0, -1) ?? []), "__repository", "<name>"])}` : '') + (orchestrated ? `\n\n${ORCHESTRATOR_CONTRACT}` : '');
90
+ }