flowviant 0.117.0 → 0.124.1

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 (103) hide show
  1. package/README.md +41 -0
  2. package/bin/cli.mjs +44 -0
  3. package/bin/lib/agentAsk.mjs +123 -0
  4. package/bin/lib/agentBrief.mjs +36 -0
  5. package/bin/lib/agentFootprint.mjs +76 -0
  6. package/bin/lib/agentInternalWait.mjs +4 -3
  7. package/bin/lib/agentPlan.mjs +86 -7
  8. package/bin/lib/agentPrerequisiteContract.mjs +43 -0
  9. package/bin/lib/agentRepositories.mjs +48 -0
  10. package/bin/lib/agentTaskKinds.mjs +1 -1
  11. package/bin/lib/artifactContracts.mjs +91 -20
  12. package/bin/lib/artifactIteration.mjs +182 -0
  13. package/bin/lib/artifactRecovery.mjs +36 -0
  14. package/bin/lib/artifactReporter.mjs +183 -19
  15. package/bin/lib/artifacts.mjs +6 -3
  16. package/bin/lib/checkoutIdentity.mjs +25 -0
  17. package/bin/lib/claudeAuth.mjs +15 -5
  18. package/bin/lib/claudeInput.mjs +32 -0
  19. package/bin/lib/claudePosture.mjs +110 -14
  20. package/bin/lib/claudeSessions.mjs +20 -0
  21. package/bin/lib/claudeStream.mjs +20 -2
  22. package/bin/lib/codexProjectRead.mjs +63 -0
  23. package/bin/lib/codexResearch.mjs +16 -3
  24. package/bin/lib/commandSpecs.mjs +6 -0
  25. package/bin/lib/credentials.mjs +52 -17
  26. package/bin/lib/daemonLocks.mjs +26 -6
  27. package/bin/lib/desktopContract.mjs +3 -0
  28. package/bin/lib/discoveryContract.mjs +19 -0
  29. package/bin/lib/fleet.mjs +65 -10
  30. package/bin/lib/fleetReports.mjs +44 -3
  31. package/bin/lib/fleetRoster.mjs +1 -89
  32. package/bin/lib/fleetStartup.mjs +11 -14
  33. package/bin/lib/holder.mjs +4 -37
  34. package/bin/lib/hooks/mcpAllow.mjs +60 -0
  35. package/bin/lib/hooks/readGuard.mjs +13 -4
  36. package/bin/lib/imageArtifact.mjs +74 -0
  37. package/bin/lib/instance.mjs +20 -7
  38. package/bin/lib/landed.mjs +2 -2
  39. package/bin/lib/login.mjs +39 -4
  40. package/bin/lib/maintenance.mjs +47 -0
  41. package/bin/lib/maintenanceInventory.mjs +39 -0
  42. package/bin/lib/mcpInstall.mjs +163 -0
  43. package/bin/lib/mcpInventory.mjs +119 -0
  44. package/bin/lib/placeLock.mjs +9 -7
  45. package/bin/lib/planDependencies.mjs +70 -0
  46. package/bin/lib/postureTools.mjs +85 -0
  47. package/bin/lib/prWorkflow.mjs +15 -0
  48. package/bin/lib/projectLanded.mjs +16 -0
  49. package/bin/lib/projectRepositories.mjs +60 -0
  50. package/bin/lib/projectToolRuntimes.mjs +227 -3
  51. package/bin/lib/projectTools.mjs +63 -5
  52. package/bin/lib/projectWorktreeHome.mjs +25 -0
  53. package/bin/lib/prompts.mjs +170 -42
  54. package/bin/lib/repositoryBaseline.mjs +68 -0
  55. package/bin/lib/repositoryDelivery.mjs +64 -0
  56. package/bin/lib/repositoryMerge.mjs +116 -0
  57. package/bin/lib/repositoryPlaces.mjs +179 -0
  58. package/bin/lib/repositorySetup.mjs +58 -0
  59. package/bin/lib/repositoryTool.mjs +71 -0
  60. package/bin/lib/runTurn.mjs +58 -18
  61. package/bin/lib/runtimeAntigravity.mjs +2 -1
  62. package/bin/lib/runtimeCapabilities.mjs +77 -3
  63. package/bin/lib/runtimeClaude.mjs +31 -10
  64. package/bin/lib/runtimeCodex.mjs +28 -59
  65. package/bin/lib/runtimeDetection.mjs +6 -0
  66. package/bin/lib/runtimeEvents.mjs +12 -1
  67. package/bin/lib/runtimeOfferings.mjs +75 -3
  68. package/bin/lib/shipMerge.mjs +5 -2
  69. package/bin/lib/startProject.mjs +4 -4
  70. package/bin/lib/trace.mjs +8 -3
  71. package/bin/lib/turnDeclaration.mjs +88 -0
  72. package/bin/lib/turnDiagnostics.mjs +93 -0
  73. package/bin/lib/turnLock.mjs +48 -8
  74. package/bin/lib/turnProfile.mjs +15 -3
  75. package/bin/lib/work.mjs +48 -29
  76. package/bin/lib/workAgentBrief.mjs +92 -0
  77. package/bin/lib/workAgentBriefRunner.mjs +59 -0
  78. package/bin/lib/workAgentCheck.mjs +6 -5
  79. package/bin/lib/workAgentMerges.mjs +19 -2
  80. package/bin/lib/workAgentPlans.mjs +21 -70
  81. package/bin/lib/workAgentPrecheck.mjs +10 -5
  82. package/bin/lib/workAgentPublish.mjs +18 -5
  83. package/bin/lib/workAgentRecovery.mjs +85 -0
  84. package/bin/lib/workAgentReview.mjs +5 -3
  85. package/bin/lib/workAgentTurnBegun.mjs +6 -4
  86. package/bin/lib/workAgentTurnExecution.mjs +167 -29
  87. package/bin/lib/workAgentTurnOutcome.mjs +20 -5
  88. package/bin/lib/workAgentTurns.mjs +92 -12
  89. package/bin/lib/workArtifactReconcile.mjs +79 -0
  90. package/bin/lib/workCappedTurn.mjs +10 -0
  91. package/bin/lib/workCommandAudit.mjs +8 -1
  92. package/bin/lib/workDelivery.mjs +21 -0
  93. package/bin/lib/workDiffs.mjs +18 -11
  94. package/bin/lib/workLiveMessages.mjs +129 -0
  95. package/bin/lib/workPlaceReports.mjs +5 -0
  96. package/bin/lib/workPlaces.mjs +24 -3
  97. package/bin/lib/workRetire.mjs +5 -0
  98. package/bin/lib/workSessionTurns.mjs +219 -52
  99. package/bin/lib/workShip.mjs +17 -16
  100. package/bin/lib/workToolLog.mjs +70 -3
  101. package/bin/lib/workTurnShape.mjs +6 -4
  102. package/package.json +7 -3
  103. package/npm-shrinkwrap.json +0 -59
@@ -1,3 +1,4 @@
1
+ import { finishRepositoryDelivery, allResolutionsCommitted } from './repositoryDelivery.mjs';
1
2
  /**
2
3
  * ONE AGENT TURN, RUN: the worktree, the CLI, and what it left behind.
3
4
  *
@@ -23,6 +24,7 @@ import { gitOrNull } from './git.mjs';
23
24
  import { stashCard } from './agentCards.mjs';
24
25
  import { c } from './ui.mjs';
25
26
  import { runTurn } from './runTurn.mjs';
27
+ import { killGroup } from './processGroup.mjs';
26
28
  import { SYSTEM_AGENT_FOR, AGENT_TASK_KICKOFF, AGENT_TASK_SPEC, AGENT_HUMAN_KICKOFF, withProjectContext } from './prompts.mjs';
27
29
  import { AGENT_TASK_KINDS, agentTaskKindOf, unknownAgentTaskKind } from './agentTaskKinds.mjs';
28
30
  import { knowledgeDirFor } from './knowledgeFiles.mjs';
@@ -30,17 +32,25 @@ import { scrub as envScrub } from './uplinkScrub.mjs';
30
32
  import { canRun, runtimeById } from './runtimes.mjs';
31
33
  import { toolEventOf, CLAUDE_TOOL_PROSE_KINDS } from './runtimeEvents.mjs';
32
34
  import { makeTraceRelay } from './trace.mjs';
33
- import { readBaseTools, prepareAgentTools } from './projectTools.mjs';
35
+ import { mcpJsonServerNamesAbove, readBaseTools, prepareAgentTools } from './projectTools.mjs';
36
+ import { TURN_PROFILES } from './turnProfile.mjs';
37
+ import { postureToolsNote } from './postureTools.mjs';
34
38
  import { runTurnResumingOnce } from './resumeLost.mjs';
35
39
  import { INTERNAL_WAIT_CONTRACT, runAgentWithInternalWaits } from './agentInternalWait.mjs';
36
40
  import { CODEX_THREAD_RE } from './workSessionRuntime.mjs';
37
41
  import { brainFor } from './workBrain.mjs';
42
+ import { refreshRestartBase } from './workAgentRecovery.mjs';
38
43
  import { begunTurnRefusal } from './workAgentTurnBegun.mjs';
39
44
  import { agentTurnSettlement } from './workAgentTurnOutcome.mjs';
40
45
  import { ensureArtifactDir, ensureFenceScratch } from './artifacts.mjs';
41
46
  import { agentBrowserHome } from './noWindowEnv.mjs';
47
+ import { CLAUDE_SESSION_RE, RESUME_MARKERS } from './conversationIds.mjs';
48
+ import { createDeliveryReporter } from './workDelivery.mjs';
49
+ import { createLiveMessages } from './workLiveMessages.mjs';
42
50
 
43
51
  export function createAgentTurnExecution({
52
+ agentRepositories,
53
+ makeBrief,
44
54
  baseRef,
45
55
  baseDir,
46
56
  repoRoot,
@@ -59,7 +69,9 @@ export function createAgentTurnExecution({
59
69
  postAgentActivity,
60
70
  postAgentParked,
61
71
  fetchAgentFiles,
72
+ makeLiveMessages = createLiveMessages,
62
73
  }) {
74
+ const reportDelivery = createDeliveryReporter();
63
75
  /** How long the final flush may hold the settle. Bounded because the settle
64
76
  * is the turn's contract and the trace is a readout: a wedged uplink costs
65
77
  * the tail of a trace, never the answer behind it. */
@@ -107,7 +119,14 @@ export function createAgentTurnExecution({
107
119
  * `releaseSlot` hands back the admission reservation the lane took on this
108
120
  * turn's behalf, at the moment the CLI actually exists. Idempotent.
109
121
  */
110
- const runAgentTurnInPlace = async (job, { turnId, agentId, place, releaseSlot, postAgentTurn, measured }) => {
122
+ /** `run` is the lane's record of this turn (`interrupted`, set by
123
+ * `interruptAgentTurns`); absent, nothing can interrupt it. */
124
+ const runAgentTurnInPlace = async (job, { turnId, agentId, place, releaseSlot, postAgentTurn, measured, run = { interrupted: false } }) => {
125
+ let repositoryRun = null;
126
+ const setupController = new AbortController();
127
+ run.abortSetup = () => setupController.abort();
128
+ if (run.interrupted) setupController.abort();
129
+ try {
111
130
  // WORK THAT HAS BEGUN LIVES ON EXACTLY ONE BOX — read BEFORE anything is
112
131
  // cut, because `placeWtFor` would cut a rival branch of the same name. The
113
132
  // rule and its three-state branch measurement: workAgentTurnBegun.mjs.
@@ -118,7 +137,10 @@ export function createAgentTurnExecution({
118
137
  return;
119
138
  }
120
139
  }
121
- const dir = placeWtFor(place);
140
+ if (!(await refreshRestartBase(job, repoRoot))) return;
141
+ repositoryRun = await agentRepositories?.prepare(job, place, { signal: setupController.signal });
142
+ measured.repositoryReceipts = repositoryRun?.receipts;
143
+ const dir = repositoryRun ?? placeWtFor(place);
122
144
  if (!dir) {
123
145
  // No worktree and none could be cut. `nothing`, WITH git's own words:
124
146
  // without them the agent sat in Stuck with no question and no reason
@@ -288,10 +310,13 @@ export function createAgentTurnExecution({
288
310
  }
289
311
  }
290
312
  let seenThreadId = null;
313
+ const claudeMarker = rt === 'claude' ? sessionMetaPath(wt, RESUME_MARKERS.claude, agentId) : null;
314
+ let claudeResumeId = null;
315
+ try { const id = readFileSync(claudeMarker, 'utf8').trim(); if (CLAUDE_SESSION_RE.test(id)) claudeResumeId = id; } catch { /* unknown */ }
291
316
  const resume =
292
317
  rt === 'codex'
293
318
  ? Boolean(codexResumeId)
294
- : rt === 'claude' && Boolean(ranMarker && existsSync(ranMarker));
319
+ : rt === 'claude' && (Boolean(claudeResumeId) || Boolean(ranMarker && existsSync(ranMarker)));
295
320
  /**
296
321
  * …AND THE HOME THAT THREAD IS FILED UNDER, KEPT PER AGENT (2026-09-29).
297
322
  *
@@ -375,8 +400,8 @@ export function createAgentTurnExecution({
375
400
  /**
376
401
  * Prose the structured event will carry anyway, dropped so a read does
377
402
  * not render twice — but ONLY on a runtime whose stream reaches
378
- * `onToolEvent` at all. Codex and agy have their own parsers and never
379
- * call it, so dropping their tool prose would blank their agents' traces.
403
+ * `onToolEvent` here. Codex uses completed activity; agy has its own
404
+ * parser. Dropping their tool prose would blank their agents' traces.
380
405
  * `parse: null` is exactly the claudeStream.mjs stream path. See
381
406
  * CLAUDE_TOOL_PROSE_KINDS.
382
407
  */
@@ -408,6 +433,9 @@ export function createAgentTurnExecution({
408
433
  * ever, which holds the self-update and the tray's install for ever.
409
434
  */
410
435
  const spawnedChildren = new Set();
436
+ /** The CLI, unless a person already asked to stop this turn (Interrupt
437
+ * now): then nothing starts — no fresh retry, no continuation. */
438
+ const runCli = (args) => (run.interrupted ? Promise.resolve('') : runTurn(args));
411
439
  /**
412
440
  * WHAT THIS TURN SPENT, AS THE CLI COUNTED IT (2026-09-19).
413
441
  *
@@ -426,6 +454,8 @@ export function createAgentTurnExecution({
426
454
  /** THE MODEL THE TURN RAN ON, as its CLI named it (turnModel.mjs) — rides
427
455
  * every settle `usage` rides, for the same reason; null sends no key. */
428
456
  let model = null;
457
+ const brief = makeBrief?.({ agentId, turnId, runtime: rt, model: () => model ?? job.model });
458
+ 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);
429
459
  /**
430
460
  * THE AGENT'S LAST MESSAGE, ALONE — where the final JSON object lives.
431
461
  * Codex's `out` is every message plus stderr plus error text, and
@@ -437,12 +467,49 @@ export function createAgentTurnExecution({
437
467
  /** The agent's artifact directory as it stood before this turn — the
438
468
  * tab lane's rule, in the agent's own worktree (2026-09-22). */
439
469
  const artifactsBefore = beforeArtifacts(wt);
470
+ const artifactInstructions = getArtifactsAccepted()
471
+ ? await artifacts.prepare?.({ placeDir: wt, before: artifactsBefore, agentId }) ?? '' : '';
440
472
  let projectTools = null;
441
473
  try {
442
474
  // A turn reads the base ref afresh. Changes the agent has made to its
443
475
  // own worktree cannot silently change the next turn's tools.
444
- projectTools = prepareAgentTools(readBaseTools(repoRoot, baseRef()), rt, process.env, wt, { codexHome: codexAgentHome });
476
+ const sources = repositoryRun?.sources ?? [];
477
+ const source = sources.find((r) => r.wt === wt);
478
+ const snapshot = readBaseTools(source?.repoRoot ?? repoRoot, source?.baseRef ?? baseRef());
479
+ for (const other of sources.filter((r) => r !== source)) {
480
+ const base = readBaseTools(other.repoRoot, other.baseRef);
481
+ snapshot.instructions.push(...[...base.instructions, ...base.skillFiles].map((file) => ({ ...file, path: `repositories/${other.folder}/${file.path}` })));
482
+ }
483
+ projectTools = prepareAgentTools(snapshot, rt, process.env, wt, { codexHome: codexAgentHome, turnServers: repositoryRun?.tool ? { card_repositories: repositoryRun.tool.config } : {} });
484
+ const mountsProjectMcp = TURN_PROFILES[posture]?.projectMcp === true && (rt === 'claude' || rt === 'codex');
485
+ // A fenced posture on a runner that ignores its own file there (Codex's
486
+ // `--ignore-user-config`) takes the servers as `fencedMount` instead.
487
+ const fencedMount = mountsProjectMcp && posture !== 'build' ? projectTools.fencedMount : null;
488
+ // THE MACHINE'S OWN SERVERS ON CLAUDE (2026-10-11, the owner): the turn
489
+ // is not strict, and every `.mcp.json` name from the worktree up is
490
+ // disabled so only the base copy speaks for the project. Null (an
491
+ // unboundable walk) mounts strictly: the base copy alone.
492
+ const claudeDisabled = mountsProjectMcp && rt === 'claude' ? mcpJsonServerNamesAbove(wt) : null;
493
+ // What this card's posture withholds, said to the agent — with the
494
+ // servers it did and did not mount, and why — so it relays the fence
495
+ // instead of inventing a setting (postureTools.mjs). Empty for a code card.
496
+ const toolsNote = postureToolsNote({
497
+ posture,
498
+ runtime: rt,
499
+ mounted: !mountsProjectMcp ? [] : fencedMount ? fencedMount.mounted : projectTools.mcpServers,
500
+ machine: mountsProjectMcp && (rt === 'codex' || claudeDisabled != null),
501
+ unmounted: [...projectTools.mcpUnmounted.map((u) => ({ ...u, source: 'project' })), ...(fencedMount ? fencedMount.unmounted : [])],
502
+ });
445
503
  const agentTurnArgs = {
504
+ ...(rt === 'claude' ? { liveMessages: (run.live = makeLiveMessages({
505
+ scope: 'agent', turnId, cwd: wt, sessionMetaPath, interrupted: () => run.interrupted,
506
+ format: async (followup) => {
507
+ const attached = followup.attachments?.length ? await fetchAgentFiles(wt, followup.attachments.map((file) => ({ ...file, from: 'message' }))) : { message: [], card: [] };
508
+ return AGENT_HUMAN_KICKOFF({ agentName: job.agentName, message: followup.body + (followup.libraryPrompt ?? ''),
509
+ askedByName: followup.askedByName, task: job.task, cardThread: job.cardThread,
510
+ messageFiles: attached.message, cardFiles: files.card, position: job.position ?? 1, total: job.total ?? 1 });
511
+ },
512
+ })) } : {}),
446
513
  prompt:
447
514
  job.kind === 'task' && job.task
448
515
  ? AGENT_TASK_KICKOFF({
@@ -451,6 +518,8 @@ export function createAgentTurnExecution({
451
518
  // The card's discussion (0.106.0), printed inside THE CARD by
452
519
  // the same builder the stash above wrote down — and the files
453
520
  // on it (0.112.0), after it, the same way.
521
+ resumeBody: job.body,
522
+ investigationContext: job.investigationContext,
454
523
  cardThread: job.cardThread,
455
524
  cardFiles: files.card,
456
525
  position: job.position ?? 1,
@@ -458,7 +527,10 @@ export function createAgentTurnExecution({
458
527
  })
459
528
  : AGENT_HUMAN_KICKOFF({
460
529
  agentName: job.agentName,
530
+ // INTERRUPT NOW: the previous run was stopped to send this.
531
+ interrupted: job.interrupted === true,
461
532
  message: job.body ?? '',
533
+ investigationContext: job.investigationContext,
462
534
  askedByName: job.askedByName,
463
535
  task: job.task,
464
536
  // The discussion on the one card this message is about, in a
@@ -476,26 +548,29 @@ export function createAgentTurnExecution({
476
548
  // different things about the same directory.
477
549
  // Spoken to the CLI that runs it (0.115.0): a mockup on Codex is
478
550
  // told it is Codex, never Claude.
479
- system: withProjectContext(SYSTEM_AGENT_FOR(taskKind, runtimeById(rt)?.label), {
551
+ system: withProjectContext(SYSTEM_AGENT_FOR(taskKind, runtimeById(rt)?.label, { prerequisitesAccepted: job.prerequisitesAccepted === true }), {
480
552
  knowledgeDir: knowledgeDirFor(repoRoot),
481
553
  // ARTIFACTS (0.94.0), while the server can show one — an agent's
482
554
  // land on its page, under the facts row.
483
555
  artifacts: getArtifactsAccepted(),
484
- }) + `\n\n${INTERNAL_WAIT_CONTRACT}` + (projectTools.instructions ? `\n\n${projectTools.instructions}` : ''),
556
+ }) + (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>"])}` : ''),
485
557
  agentTools: projectTools,
486
- ...(projectTools.codexHome ? { mcpEnv: { CODEX_HOME: projectTools.codexHome } } : {}),
558
+ writableDirs: repositoryRun?.writableDirs,
559
+ ...((projectTools.codexHome || fencedMount || repositoryRun?.tool) ? { mcpEnv: { ...(projectTools.codexHome ? { CODEX_HOME: projectTools.codexHome } : {}), ...(fencedMount?.env ?? {}), ...(repositoryRun?.tool?.env ?? {}) } } : {}),
487
560
  // NOBODY IS AT THIS SCREEN (2026-09-29): no display, BROWSER=none, and
488
561
  // a browser profile of the agent's own, kept beside its Codex home —
489
562
  // so a Chrome it starts to look at its page never reaches the
490
563
  // person's (noWindowEnv.mjs).
491
564
  browserHome: agentBrowserHome(sessionMetaPath, wt, agentId),
492
- // The base branch's MCP servers, for a code card: Claude's in this
493
- // turn's own 0600 file, strictly; Codex's already in its private
494
- // home's config.toml (projectToolRuntimes.mjs, 2026-10-10), which
495
- // its fenced postures ignore — never `-c` pairs on argv.
496
- ...(posture === 'build' && rt === 'claude'
497
- ? { mcpArgs: ['--strict-mcp-config', '--mcp-config', projectTools.mcpPath] }
498
- : {}),
565
+ // Reviewed project servers and the machine's configured servers, on
566
+ // initial cards, answers and review iterations alike.
567
+ ...(mountsProjectMcp && rt === 'claude'
568
+ ? {
569
+ mcpArgs: [...(claudeDisabled ? [] : ['--strict-mcp-config']), '--mcp-config', projectTools.mcpPath],
570
+ projectMcpServers: projectTools.mcpServers,
571
+ ...(claudeDisabled ? { machineMcp: claudeDisabled } : {}),
572
+ }
573
+ : fencedMount ? { mcpArgs: fencedMount.args } : {}),
499
574
  knowledgeDir: knowledgeDirFor(repoRoot),
500
575
  // The kind's posture, as the one turn profile (turnProfile.mjs):
501
576
  // build for code, the fenced design/research postures otherwise.
@@ -503,7 +578,11 @@ export function createAgentTurnExecution({
503
578
  cwd: wt,
504
579
  runtime: rt,
505
580
  resume,
506
- resumeThreadId: codexResumeId || undefined,
581
+ resumeThreadId: codexResumeId || claudeResumeId || undefined,
582
+ onInit: (init) => {
583
+ if (claudeMarker && CLAUDE_SESSION_RE.test(init.sessionId ?? ''))
584
+ try { writeFileSync(claudeMarker, init.sessionId); } catch { /* best effort */ }
585
+ },
507
586
  onThreadId: (id) => {
508
587
  seenThreadId = String(id ?? '').trim() || seenThreadId;
509
588
  },
@@ -518,7 +597,8 @@ export function createAgentTurnExecution({
518
597
  // narrator keeps: overwritten, never appended, and ignored by the
519
598
  // board past ~90 seconds.
520
599
  onActivity: (a) => {
521
- const line = a?.label;
600
+ brief?.append(a?.kind ?? 'activity', a?.full ?? a?.label);
601
+ let line = a?.label;
522
602
  if (!line) return;
523
603
  // THE TRACE TAKES EVERY LINE; the pulse takes one every two
524
604
  // seconds. Two channels, one stream, and the drop-sampler stays a
@@ -533,7 +613,12 @@ export function createAgentTurnExecution({
533
613
  // daemon carried for all of them. The PULSE below is deliberately
534
614
  // still `line`: it is one overwritten line on a board, and a
535
615
  // paragraph there would be a paragraph nobody can read.
536
- if (!doubledKinds || !doubledKinds.has(a.kind)) trace.prose(a.kind, a.full ?? line);
616
+ if (!doubledKinds || !doubledKinds.has(a.kind)) {
617
+ const full = a.full ?? line;
618
+ const readable = trace.prose(a.kind, full);
619
+ if (readable === null) return;
620
+ if (readable !== full) line = envScrub(readable).replace(/\s+/g, ' ').trim().slice(0, 160);
621
+ }
537
622
  const now = Date.now();
538
623
  if (now - (lastAgentBeat.get(agentId) ?? 0) < 2_000) return;
539
624
  lastAgentBeat.set(agentId, now);
@@ -543,7 +628,13 @@ export function createAgentTurnExecution({
543
628
  // Workbench's tool cards are built from, scrubbed at collection by
544
629
  // the builder itself (bounded window BEFORE its caps; see
545
630
  // toolEventOf). A tool it does not know pushes nothing.
546
- onToolEvent: (name, input) => {
631
+ onToolEvent: (name, input, meta) => {
632
+ brief?.append(meta?.result ? 'tool result' : 'tool call', meta?.result
633
+ ? { id: meta.id, exitCode: meta.exitCode, isError: meta.isError, output: meta.output }
634
+ : { name, input });
635
+ // Codex request/result pairs feed capture/planner evidence. This
636
+ // append-only agent trace already relays its completed activity.
637
+ if (meta?.runtime === 'codex') return;
547
638
  trace.tool(toolEventOf(name, input, wt, envScrub));
548
639
  },
549
640
  // The CLI's own per-turn token counts, off the `result` event. One
@@ -566,8 +657,12 @@ export function createAgentTurnExecution({
566
657
  // (runTurn.mjs, `initHarvestFor`). An agent-only box learns from the
567
658
  // startup probe and its Terminal tabs.
568
659
  onSpawn: (ch) => {
660
+ if (rt !== 'claude') reportDelivery('agent', job, 'taken');
661
+ brief?.running(true);
662
+ ch.once('close', () => { if (child === ch) brief?.running(false); });
569
663
  child = ch;
570
664
  spawnedChildren.add(ch);
665
+ measured.ranCli = true;
571
666
  // The AGENT it serves — what the machine snapshot charges this
572
667
  // child's memory to.
573
668
  workChildren.set(ch, agentId);
@@ -583,6 +678,8 @@ export function createAgentTurnExecution({
583
678
  // and a place id IS one. It is what lets a hard stop actually reach
584
679
  // the CLI — see `retireWorkSessions`.
585
680
  agentChildren.set(place, ch);
681
+ // Asked to stop between the check and the spawn.
682
+ if (run.interrupted) killGroup(ch);
586
683
  },
587
684
  };
588
685
  /**
@@ -597,7 +694,7 @@ export function createAgentTurnExecution({
597
694
  * MENTIONS a missing session is never run twice.
598
695
  */
599
696
  out = await runAgentWithInternalWaits(
600
- (args, continuing) => continuing ? runTurn(args) : runTurnResumingOnce(runTurn, args, {
697
+ (args, continuing) => continuing ? runCli(args) : runTurnResumingOnce(runCli, args, {
601
698
  // A Codex write-up always runs fresh over its copy (codexResearch
602
699
  // .mjs), so there is no lost conversation to retry it for.
603
700
  resume: rt === 'codex' && posture === 'research' ? false : args.resume,
@@ -614,11 +711,30 @@ export function createAgentTurnExecution({
614
711
  void postAgentActivity(agentId, 'resuming after an internal wait');
615
712
  } }
616
713
  );
714
+ out = await finishRepositoryDelivery({
715
+ job, run: repositoryRun, out, answer: () => lastAnswer,
716
+ resume: async (prompt) => {
717
+ lastAnswer = null;
718
+ const counted = usage;
719
+ return runCli({
720
+ ...agentTurnArgs, prompt, liveContinuation: true,
721
+ onUsage: (next) => agentTurnArgs.onUsage(Object.fromEntries(
722
+ ['input', 'output', 'cacheCreate', 'cacheRead'].map((k) =>
723
+ [k, (counted?.[k] ?? 0) + (next?.[k] ?? 0)])
724
+ )),
725
+ writableDirs: repositoryRun.sourcesNow().filter((r) => r.wt !== wt).map((r) => r.wt),
726
+ resume: true,
727
+ resumeThreadId: seenThreadId ?? agentTurnArgs.resumeThreadId,
728
+ });
729
+ },
730
+ });
617
731
  } finally {
732
+ await brief?.stop();
618
733
  // What the CLI counted, for the lane's catch if anything below throws.
619
734
  measured.usage = usage;
620
735
  measured.model = model;
621
736
  projectTools?.cleanup();
737
+
622
738
  /**
623
739
  * THE TAIL, BEFORE THE SETTLE — so the last thing the agent did is on
624
740
  * the record by the time the board is told the turn is over.
@@ -664,16 +780,36 @@ export function createAgentTurnExecution({
664
780
  * floor: it must never delay the settle behind it.
665
781
  */
666
782
  void reportSessionWorktree(place).catch(() => {});
667
- // …and what it drew to SHOW the person (2026-09-22): uploaded against
668
- // the AGENT, so it lands on the agent's page whoever is looking.
669
- // Fire-and-forget for the same reason — never delay the settle.
670
- void artifacts
671
- .report({ placeDir: wt, before: artifactsBefore, agentId, turnId })
672
- .catch(() => {});
783
+ // A file card delivers uploaded bytes, not an in-memory retry. Otherwise
784
+ // its dependent card can start while Review has nothing to show.
785
+ await artifacts.report({ placeDir: wt, before: artifactsBefore, agentId, turnId, requireStored: kind.artifact != null });
673
786
  }
674
787
 
675
788
 
676
- const commits = commitsBetween(wt, before);
789
+ const commits = repositoryRun ? [...new Set(repositoryRun.receipts().map((r) => r.sha))].slice(0, 50) : commitsBetween(wt, before);
790
+ /**
791
+ * A PERSON INTERRUPTED IT (Interrupt now). Whatever the CLI printed was
792
+ * cut off, so there is no outcome to read: the turn reports `interrupted`
793
+ * with what it measured — the commits it made, its spend, the branch —
794
+ * and leaves the worktree exactly as the stopped run left it. The message
795
+ * sent to interrupt it runs next, here, resuming this conversation. No
796
+ * limit, no park and no review entry: none of that happened.
797
+ */
798
+ if (run.interrupted) {
799
+ measured.ranCli = spawnedChildren.size > 0;
800
+ await postAgentTurn({
801
+ turnId,
802
+ outcome: 'nothing',
803
+ interrupted: true,
804
+ ran: spawnedChildren.size > 0,
805
+ commits,
806
+ ...(usage ? { usage } : {}),
807
+ ...(model ? { model } : {}),
808
+ ...(branch ? { branch } : {}),
809
+ worktree: wt,
810
+ });
811
+ return;
812
+ }
677
813
  // WHAT THE TURN REPORTS is one decision, off what was measured here
678
814
  // (workAgentTurnOutcome.mjs). The side effects it implies stay on this
679
815
  // side: the machine's limit state, the park, and the review-entry beat.
@@ -700,12 +836,14 @@ export function createAgentTurnExecution({
700
836
  // reported as `nothing` — it did not deliver and it did not ask.
701
837
  await postAgentParked(limit, rt);
702
838
  }
839
+ if (job.kind === 'merge_resolve' && repositoryRun) body.mergeResolved = allResolutionsCommitted(repositoryRun.sourcesNow());
703
840
  const reply = await postAgentTurn(body);
704
841
  // The queue just emptied. Run the project's own check and the AI
705
842
  // pre-review HERE, in the worktree we are already standing in and still
706
843
  // hold the lock on — see `runReviewEntry`. Only after the settle that may
707
844
  // say delivered: the three `nothing` settles never opened it.
708
845
  if (final && reply?.review === true) await runReviewEntry(agentId, wt, job.agentName);
846
+ } finally { await repositoryRun?.tool?.close(); }
709
847
  };
710
848
 
711
849
  return { runAgentTurnInPlace, commitsBetween };
@@ -25,6 +25,8 @@ 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';
29
+ import { withoutTurnDeclaration } from './turnDeclaration.mjs';
28
30
 
29
31
  export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage, model = null, commits, artifactsBefore, branch, wt }) {
30
32
  const res = (lastAnswer && parseTurnResult(lastAnswer)) || parseTurnResult(out);
@@ -38,7 +40,9 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
38
40
  * makes the match mean what it says: the CLI failed and this is the
39
41
  * sentence it failed with.
40
42
  */
41
- const limit = res ? null : limitLine(out);
43
+ // An unreadable protocol object is not a native CLI diagnostic either.
44
+ const readableOut = res ? '' : withoutTurnDeclaration(out);
45
+ const limit = res ? null : limitLine(readableOut);
42
46
  /** What the caller posts, and whether it is the one settle that may say
43
47
  * delivered (the only one whose reply can open the review-entry beat).
44
48
  * Every one carries the model the CLI said it ran on, when it said
@@ -59,13 +63,16 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
59
63
  }
60
64
 
61
65
  if (!res) {
66
+ const raw = lastAnswer || out;
67
+ const readable = lastAnswer ? withoutTurnDeclaration(raw) : readableOut;
62
68
  return report({
63
69
  turnId,
64
70
  outcome: 'nothing',
65
71
  // The CLI's own words when it produced any. A turn that explained why
66
72
  // it stopped is far more use than "the agent stopped".
67
- answer: out.trim()
68
- ? envScrub(out).slice(-1500)
73
+ answer: raw.trim()
74
+ ? readable.trim() ? envScrub(readable).slice(-1500)
75
+ : 'The turn returned an unreadable outcome declaration. This work has not been delivered.'
69
76
  : 'the turn produced no output on the machine — its CLI may be signed out',
70
77
  ...(commits.length ? { commits } : {}),
71
78
  ...(usage ? { usage } : {}),
@@ -110,7 +117,8 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
110
117
  */
111
118
  if (res.outcome === 'delivered' && kind.artifact) {
112
119
  const want = kind.artifact.match;
113
- const has = (l) => l.some((e) => want.test(e.name));
120
+ const has = (l) => l.some((e) => want.test(e.name)
121
+ && (kind.posture !== 'image' || usableImageArtifact(e)));
114
122
  const standing = scanArtifacts(wt);
115
123
  const wrote = has(changedArtifacts(artifactsBefore, standing));
116
124
  const present = wrote || ((job.kind !== 'task' || job.redo === true) && has(standing));
@@ -126,11 +134,18 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
126
134
  });
127
135
  }
128
136
  }
137
+ const prerequisite = res.prerequisite;
138
+ const cannotRequest = prerequisite && job.prerequisitesAccepted !== true
139
+ ? 'This server does not support prerequisite-card requests yet. This work has not been delivered.'
140
+ : prerequisite?.taskKind === 'image' && (job.runtime ?? 'claude') !== 'codex'
141
+ ? '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.'
142
+ : null;
129
143
  return report(
130
144
  {
131
145
  turnId,
146
+ ...(prerequisite && !cannotRequest ? { prerequisite } : {}),
132
147
  outcome: res.outcome,
133
- answer: envScrub(res.answer ?? '').slice(0, 8000),
148
+ answer: cannotRequest ?? envScrub(res.answer ?? '').slice(0, 8000),
134
149
  ...(commits.length ? { commits } : {}),
135
150
  // SCRUBBED like the answer beside it: a raised card lands in the
136
151
  // project doc every member reads, and a brief quoting the `.env` the