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
@@ -16,10 +16,10 @@
16
16
  * what the turn IS, plain, plan, capture or build, with its prompt pair
17
17
  * (workTurnShape.mjs, audit 2026-10-10 F159).
18
18
  */
19
- import { existsSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
19
+ import { existsSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
20
20
  import { git } from './git.mjs';
21
21
  import { measureRepositoryBaseline } from './repositoryBaseline.mjs';
22
- import { REPOSITORY_BASELINE_PARAGRAPH } from './prompts.mjs';
22
+ import { REPOSITORY_BASELINE_PARAGRAPH, tabFilesNote } from './prompts.mjs';
23
23
  import { processStartTime } from './procRegistry.mjs';
24
24
  import { c, note, ok, warn } from './ui.mjs';
25
25
  import { mcpFor } from './claude.mjs';
@@ -31,13 +31,15 @@ import { isTerminalSessionLive } from './claudeSessions.mjs';
31
31
  import { isAgyConversationLive } from './agySessions.mjs';
32
32
  import { adoptFence } from './localSessions.mjs';
33
33
  import { resumeRetriesFresh } from './resumeLost.mjs';
34
+ import { CODEX_CONTRACT_MARKER, codexContractKnown, recordCodexContract } from './codexContract.mjs';
34
35
  import { turnLockedByLivePid } from './turnLock.mjs';
35
36
  import { createDeliveryReporter } from './workDelivery.mjs';
36
37
  import { createLiveMessages } from './workLiveMessages.mjs';
37
- import { killGroup } from './processGroup.mjs';
38
+ import { interruptThenKill, killGroup } from './processGroup.mjs';
38
39
  import { printable } from './printable.mjs';
39
40
  import { brainFor } from './workBrain.mjs';
40
41
  import { AGY_CONV_RE, CLAUDE_SESSION_RE, CODEX_THREAD_RE, RESUME_MARKERS } from './conversationIds.mjs';
42
+ import { pinId, readPinnedId } from './resumeMarker.mjs';
41
43
  import { sessionTurnKickoff, sessionTurnShape, sessionTurnSystem } from './workTurnShape.mjs';
42
44
  import { createToolLog } from './workToolLog.mjs';
43
45
  import { createCommandAudit } from './workCommandAudit.mjs';
@@ -75,6 +77,9 @@ export function createWorkSessionTurns({
75
77
  agyRegistryLookup,
76
78
  carryDirtyState,
77
79
  fetchAttachments,
80
+ // Each file landed or named as missed (workAttachments.mjs); a caller that
81
+ // hands only `fetchAttachments` (an older test) has its paths as landed.
82
+ fetchTabFiles = async (wt, attachments) => (await fetchAttachments(wt, attachments)).map((path) => ({ path })),
78
83
  makeNarrator,
79
84
  sayTurnDeferred,
80
85
  lastDeferSaid,
@@ -584,48 +589,16 @@ export function createWorkSessionTurns({
584
589
  * directory belongs to this tab alone, which is the one case it was
585
590
  * ever right for.
586
591
  */
587
- let claudeResumeId = null;
588
- if (rt.id === 'claude') {
589
- const convMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.claude, job.sessionId);
590
- if (convMarker && existsSync(convMarker)) {
591
- try {
592
- const v = readFileSync(convMarker, 'utf8').trim();
593
- if (CLAUDE_SESSION_RE.test(v)) claudeResumeId = v;
594
- } catch {
595
- /* unreadable marker — run fresh */
596
- }
597
- }
598
- }
599
- let codexResumeId = null;
600
- if (rt.id === 'codex') {
601
- const threadMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.codex, job.sessionId);
602
- if (threadMarker && existsSync(threadMarker)) {
603
- try {
604
- const v = readFileSync(threadMarker, 'utf8').trim();
605
- if (CODEX_THREAD_RE.test(v)) codexResumeId = v;
606
- } catch {
607
- /* unreadable marker — run fresh */
608
- }
609
- }
610
- }
592
+ // An unreadable or misshapen marker is unknown: run fresh (resumeMarker.mjs).
593
+ const claudeResumeId = rt.id === 'claude' ? readPinnedId(sessionMetaPath(dir.wt, RESUME_MARKERS.claude, job.sessionId), CLAUDE_SESSION_RE) : null;
594
+ const codexResumeId = rt.id === 'codex' ? readPinnedId(sessionMetaPath(dir.wt, RESUME_MARKERS.codex, job.sessionId), CODEX_THREAD_RE) : null;
611
595
  // AGY RESUMES BY CONVERSATION ID, learned once and pinned beside the
612
596
  // runtime marker: an adopted tab knows it from the adopt hint; a new
613
597
  // tab learns it from agy's own cwd registry after its first turn.
614
598
  // The marker beats `--continue` because it is the tab's OWN identity
615
599
  // — the registry maps a cwd to whatever ran there LAST, and a
616
600
  // dispatch sharing the machine could overwrite that between turns.
617
- let agyConvId = null;
618
- if (rt.id === 'antigravity' && !adopting) {
619
- const convMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.antigravity, job.sessionId);
620
- if (convMarker && existsSync(convMarker)) {
621
- try {
622
- const v = readFileSync(convMarker, 'utf8').trim();
623
- if (AGY_CONV_RE.test(v)) agyConvId = v;
624
- } catch {
625
- /* unreadable marker — run fresh */
626
- }
627
- }
628
- }
601
+ const agyConvId = rt.id === 'antigravity' && !adopting ? readPinnedId(sessionMetaPath(dir.wt, RESUME_MARKERS.antigravity, job.sessionId), AGY_CONV_RE) : null;
629
602
  // Resume iff a conversation is known to live in THIS directory. For
630
603
  // Claude that proof is the server's sessionRef — only ever a path
631
604
  // some turn actually SPOKE from (see the settle below), and it must
@@ -685,6 +658,17 @@ export function createWorkSessionTurns({
685
658
  // refused for want of an approval, by `server.tool`.
686
659
  let codexSpoke = false;
687
660
  const approvalRefused = [];
661
+ // WHAT THE CLI SAID OF ITS OWN RUN (2026-10-19): a failed result or
662
+ // error event, or an exit that was not a clean one. A usage limit, a
663
+ // signed-out CLI or a crash printed its words and settled as the
664
+ // tab's REPLY; they settle as the failure they are now, in the same
665
+ // words. Reset for the fresh retry, which is its own run.
666
+ let cliFailed = false;
667
+ let badExit = null;
668
+ // A Codex tab's contract and whether this turn sent it (codexContract.mjs).
669
+ const contractPath = rt.id === 'codex' ? sessionMetaPath(dir.wt, CODEX_CONTRACT_MARKER, job.sessionId) : null;
670
+ let turnSystem = '';
671
+ let contractSent = true;
688
672
  const spawned = []; // this turn's children, for the teardown registry
689
673
  // THE TURN'S TOOL LOG — see workToolLog.mjs for its shape rules.
690
674
  const turnLog = createToolLog(dir.wt);
@@ -706,22 +690,16 @@ export function createWorkSessionTurns({
706
690
  // Files first, then the message that references them: the agent
707
691
  // must be able to open what it is being told about. Only the ones
708
692
  // that actually landed are named.
709
- const files = await fetchAttachments(dir.wt, job.attachments);
710
- const filesNote = files.length
711
- ? `[FILES THE HUMAN ATTACHED TO THIS MESSAGE — already on disk in this worktree]\n${files
712
- .map((f) => `- ${f}`)
713
- .join('\n')}`
714
- : '';
693
+ const filesNote = tabFilesNote(await fetchTabFiles(dir.wt, job.attachments));
715
694
  const message = [job.body, filesNote, adoptNote, carryNote]
716
695
  .filter(Boolean)
717
696
  .join('\n\n');
718
697
  const turnArgs = {
719
- ...(rt.id === 'claude' ? { liveMessages: (run.live = makeLiveMessages({
698
+ ...(shape.liveInput ? { liveMessages: (run.live = makeLiveMessages({
720
699
  scope: 'session', turnId: job.id, attempt: job.attempt, cwd: dir.wt, sessionMetaPath,
721
700
  interrupted: () => run.interrupted,
722
701
  format: async (followup) => {
723
- const paths = await fetchAttachments(dir.wt, followup.attachments);
724
- const note = paths.length ? `[FILES THE HUMAN ATTACHED TO THIS MESSAGE — already on disk in this worktree]\n${paths.map((f) => `- ${f}`).join('\n')}` : '';
702
+ const note = tabFilesNote(await fetchTabFiles(dir.wt, followup.attachments));
725
703
  const words = shape.capture ? (followup.libraryPrompt ?? '') + followup.body : followup.body + (followup.libraryPrompt ?? '');
726
704
  return sessionTurnKickoff(shape, { sessionId: job.sessionId, sessionName: job.sessionName,
727
705
  askedByName: followup.askedByName, message: [words, note].filter(Boolean).join('\n\n') });
@@ -759,11 +737,18 @@ export function createWorkSessionTurns({
759
737
  // artifacts paragraph, the planning sentence appended). The
760
738
  // artifacts paragraph also waits on the server — ARTIFACTS
761
739
  // (0.94.0): only while it can show one, read here at spawn.
762
- system: sessionTurnSystem(shape, {
763
- knowledgeDir,
764
- artifactsAccepted: getArtifactsAccepted(),
765
- runtime: rt.id,
766
- }) + (toolLog.baseline ? '\n\n' + REPOSITORY_BASELINE_PARAGRAPH(toolLog.baseline) : '') + (artifactScan?.instructions ? `\n\n${artifactScan.instructions}` : ''),
740
+ // …and a Codex thread that already holds it is sent none
741
+ // (codexContract.mjs); `turnSystem` keeps the text for a
742
+ // fresh retry and the marker.
743
+ system: (() => {
744
+ turnSystem = sessionTurnSystem(shape, {
745
+ knowledgeDir,
746
+ artifactsAccepted: getArtifactsAccepted(),
747
+ runtime: rt.id,
748
+ }) + (toolLog.baseline ? '\n\n' + REPOSITORY_BASELINE_PARAGRAPH(toolLog.baseline) : '') + (artifactScan?.instructions ? `\n\n${artifactScan.instructions}` : '');
749
+ contractSent = !codexContractKnown(contractPath, codexResumeId, turnSystem);
750
+ return contractSent ? turnSystem : '';
751
+ })(),
767
752
  // The directory is OUTSIDE a worktree's cwd; Claude Code is
768
753
  // told it may read there (`--add-dir`) rather than left to
769
754
  // refuse a read-only capture turn a path it was just handed.
@@ -805,6 +790,15 @@ export function createWorkSessionTurns({
805
790
  onModel: (m) => {
806
791
  model = m;
807
792
  },
793
+ // The CLI's own context measure, live while the turn runs
794
+ // (claudeInput.mjs); the server clears it at settle.
795
+ onContext: (ctx) => narrator.context(ctx),
796
+ onCliFailure: () => {
797
+ cliFailed = true;
798
+ },
799
+ onExit: (code, signal) => {
800
+ badExit = signal || (code ? `exit ${code}` : null);
801
+ },
808
802
  // What this CLI says it can be asked for by name, and which of
809
803
  // its connectors need a sign-in, are harvested off this init by
810
804
  // runTurn itself (`initHarvestFor`, runtimeCapabilities.mjs) —
@@ -815,10 +809,7 @@ export function createWorkSessionTurns({
815
809
  // same conversation and recover its native message UUIDs.
816
810
  if (typeof i.sessionId === 'string' && i.sessionId.trim()) {
817
811
  seenClaudeSession = i.sessionId.trim();
818
- if (CLAUDE_SESSION_RE.test(seenClaudeSession)) {
819
- const marker = sessionMetaPath(dir.wt, RESUME_MARKERS.claude, job.sessionId);
820
- if (marker) try { writeFileSync(marker, seenClaudeSession); } catch { /* best effort */ }
821
- }
812
+ if (CLAUDE_SESSION_RE.test(seenClaudeSession)) pinId(sessionMetaPath(dir.wt, RESUME_MARKERS.claude, job.sessionId), CLAUDE_SESSION_RE, seenClaudeSession);
822
813
  }
823
814
  },
824
815
  cwd: dir.wt,
@@ -838,7 +829,7 @@ export function createWorkSessionTurns({
838
829
  },
839
830
  onSpawn: (ch) => {
840
831
  if (!ch) return;
841
- if (rt.id !== 'claude') reportDelivery('session', job, 'taken');
832
+ if (!shape.liveInput) reportDelivery('session', job, 'taken');
842
833
  spawned.push(ch);
843
834
  run.spawned.push(ch);
844
835
  // Asked to stop between the check and the spawn.
@@ -905,9 +896,19 @@ export function createWorkSessionTurns({
905
896
  */
906
897
  if (
907
898
  !adopting &&
908
- resumeRetriesFresh({ resume, out, runtime: rt.id, sawInit: Boolean(seenClaudeSession) })
899
+ resumeRetriesFresh({
900
+ resume,
901
+ out,
902
+ runtime: rt.id,
903
+ // The CLI's own word that it found the conversation: Claude's
904
+ // init, Codex's `thread.started` naming the pinned thread.
905
+ sawInit: rt.id === 'codex' ? Boolean(codexResumeId && seenThreadId === codexResumeId) : Boolean(seenClaudeSession),
906
+ })
909
907
  ) {
910
- out = await runCli({ ...turnArgs, resume: false });
908
+ cliFailed = false;
909
+ badExit = null;
910
+ contractSent = true;
911
+ out = await runCli({ ...turnArgs, resume: false, system: turnSystem });
911
912
  }
912
913
  } finally {
913
914
  // The CLI has stopped printing, so stop relaying. The LINE itself
@@ -931,14 +932,8 @@ export function createWorkSessionTurns({
931
932
  // `resume <id>` — and best-effort, like the runtime pin: an
932
933
  // unwritable marker just means the tab runs fresh next turn.
933
934
  if (rt.id === 'codex' && seenThreadId && CODEX_THREAD_RE.test(seenThreadId)) {
934
- const threadMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.codex, job.sessionId);
935
- if (threadMarker) {
936
- try {
937
- writeFileSync(threadMarker, seenThreadId);
938
- } catch {
939
- /* best-effort */
940
- }
941
- }
935
+ pinId(sessionMetaPath(dir.wt, RESUME_MARKERS.codex, job.sessionId), CODEX_THREAD_RE, seenThreadId);
936
+ recordCodexContract(contractPath, seenThreadId, turnSystem, contractSent);
942
937
  }
943
938
  /**
944
939
  * A PERSON INTERRUPTED IT. Whatever the CLI printed was cut off, so
@@ -984,16 +979,8 @@ export function createWorkSessionTurns({
984
979
  // the registry is never trusted again.
985
980
  if (rt.id === 'antigravity' && answer.length > 0) {
986
981
  const convMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.antigravity, job.sessionId);
987
- if (convMarker && !existsSync(convMarker)) {
988
- const learned = adopting ? job.adopt.id : agyRegistryLookup(dir.wt);
989
- if (learned && AGY_CONV_RE.test(learned)) {
990
- try {
991
- writeFileSync(convMarker, learned);
992
- } catch {
993
- /* best-effort — an unpinned tab resumes via --continue's cwd key */
994
- }
995
- }
996
- }
982
+ // Best-effort — an unpinned tab resumes via --continue's cwd key.
983
+ if (convMarker && !existsSync(convMarker)) pinId(convMarker, AGY_CONV_RE, adopting ? job.adopt.id : agyRegistryLookup(dir.wt));
997
984
  }
998
985
  /**
999
986
  * NO WORDS, SAID AS MEASURED (2026-09-29). Claude's init event means
@@ -1025,7 +1012,7 @@ export function createWorkSessionTurns({
1025
1012
  artifactFailure = `Artifact delivery is incomplete: ${error.message}`;
1026
1013
  }
1027
1014
  }
1028
- const spoke = answer.length > 0 && !loginFailure;
1015
+ const spoke = answer.length > 0 && !loginFailure && !cliFailed && !badExit;
1029
1016
  await settleTurn(job.id, {
1030
1017
  ...attempt,
1031
1018
  ok: spoke && !artifactFailure,
@@ -1048,6 +1035,7 @@ export function createWorkSessionTurns({
1048
1035
  });
1049
1036
  if (spoke) ok(`${c.cyan('tab')} ${c.dim('— replied in the session')}`);
1050
1037
  else if (loginFailure) warn('codex answered 401 for this session turn — settled as failed, in words');
1038
+ else if (answer && (cliFailed || badExit)) warn(`the CLI reported its turn failed${badExit ? ` (${badExit})` : ''} — settled as failed, in its words`);
1051
1039
  else warn('session turn produced no output — settled as failed');
1052
1040
  } catch (e) {
1053
1041
  if (toolLog) toolLog.state = 'partial';
@@ -1123,7 +1111,7 @@ export function createWorkSessionTurns({
1123
1111
  if (run) {
1124
1112
  if (!run.interrupted) note(`${c.cyan('tab')} ${c.dim('— interrupting the running turn')}`);
1125
1113
  run.interrupted = true;
1126
- for (const ch of run.spawned) killGroup(ch);
1114
+ for (const ch of run.spawned) interruptThenKill(ch);
1127
1115
  continue;
1128
1116
  }
1129
1117
  if (pendingWorkReports.has(id) || settledRecently(id) || notRunningSaid.has(id)) continue;
@@ -1,11 +1,12 @@
1
1
  /** Session branch shipping and settlement. */
2
2
  import { existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { git, baseBranchName, isSafePathSegment, sessionBranch, stripUrlUserinfo } from './git.mjs';
4
+ import { git, baseBranchName, isSafePathSegment, sessionBranch } from './git.mjs';
5
5
  import { mergeOutward as shipMergeOutward } from './shipMerge.mjs';
6
6
  import { gitMergeIn } from './gitMergeIdentity.mjs';
7
7
  import { c, note, ok, warn } from './ui.mjs';
8
8
  import { scrub as envScrub } from './uplinkScrub.mjs';
9
+ import { relayedText } from './prWorkflow.mjs';
9
10
  import { REPO_PLACE } from './workPlaces.mjs';
10
11
  import { placeTurnLockHolder } from './turnLock.mjs';
11
12
  import { printable } from './printable.mjs';
@@ -272,15 +273,10 @@ export function createWorkShipper({
272
273
  'conflicts with main — the tab is closed, so open a new session from this branch to resolve them, then ship again',
273
274
  });
274
275
  } else {
275
- // Userinfo stripped as well as scrubbed (2026-10-10, F153): the
276
- // outward merge pushes to `origin`, and a push names the remote.
277
- const line = envScrub(
278
- stripUrlUserinfo(
279
- String(detail)
280
- .split('\n')
281
- .find((l) => l.trim()) ?? 'git merge failed'
282
- )
283
- );
276
+ // Userinfo stripped as well as scrubbed (2026-10-10, F153), by
277
+ // relayedText: the outward merge pushes to `origin`, and a push
278
+ // names the remote.
279
+ const line = relayedText(String(detail).split('\n').find((l) => l.trim()) ?? 'git merge failed');
284
280
  await done({ ok: false, error: `the merge failed: ${line.slice(0, 300)}` });
285
281
  }
286
282
  return;
@@ -331,13 +327,7 @@ export function createWorkShipper({
331
327
  } else {
332
328
  // An honest error beats a fabricated conflict — the human can
333
329
  // only fix what they are told about.
334
- const line = envScrub(
335
- stripUrlUserinfo(
336
- String(detail)
337
- .split('\n')
338
- .find((l) => l.trim()) ?? 'git merge failed'
339
- )
340
- );
330
+ const line = relayedText(String(detail).split('\n').find((l) => l.trim()) ?? 'git merge failed');
341
331
  await done({ ok: false, error: `the merge failed: ${line.slice(0, 300)}` });
342
332
  }
343
333
  return;
@@ -368,7 +358,7 @@ export function createWorkShipper({
368
358
  warn(`ship failed: ${e?.message ?? e}`);
369
359
  await done({
370
360
  ok: false,
371
- error: envScrub(stripUrlUserinfo(e?.message ?? 'the merge failed')).slice(0, 500),
361
+ error: relayedText(e?.message ?? 'the merge failed').slice(0, 500),
372
362
  });
373
363
  } finally {
374
364
  if (!settled && !deferred) {
@@ -15,8 +15,8 @@ import { discoveryGuide } from './discoveryContract.mjs';
15
15
  * other answer off it; the prompt pair is composed here too.
16
16
  *
17
17
  * The runtime facts come off the row (runtime{Claude,Codex,Antigravity}.mjs):
18
- * `tab` ('plain' runs no MCP), `planTurns` (the CLI can spell plan mode) and
19
- * `adopt` ('fork' | 'move' | null). The LIFECYCLE stays the lane's — its
18
+ * `tab` ('plain' runs no MCP), `planTurns` (the CLI can spell plan mode),
19
+ * `adopt` ('fork' | 'move' | null) and `liveInput` (follow-ups on stdin). The LIFECYCLE stays the lane's — its
20
20
  * refusal order is the contract — and only this composition moved.
21
21
  */
22
22
  import { runtimeById } from './runtimes.mjs';
@@ -55,7 +55,7 @@ export const PLAN_TURN_SENTENCE =
55
55
  * verdict that this first turn adopts a terminal session.
56
56
  *
57
57
  * Returns `{ refusal?, plain, plan, capture, kickoff, systemBase,
58
- * planSentence, artifacts, profile, mountMcp, display }`:
58
+ * planSentence, artifacts, profile, mountMcp, display, liveInput }`:
59
59
  * refusal — the sentence the turn settles in, unrun, when it has one;
60
60
  * kickoff /
61
61
  * systemBase — which contract the prompt pair runs: 'plain' | 'capture' | 'work';
@@ -65,7 +65,8 @@ export const PLAN_TURN_SENTENCE =
65
65
  * one, as the lane reads it at spawn);
66
66
  * profile — the permission profile: 'build' | 'plan' | 'plan-mode';
67
67
  * mountMcp — the session's MCP config is mounted;
68
- * display — the CLI keeps its environment's display.
68
+ * display — the CLI keeps its environment's display;
69
+ * liveInput — follow-ups reach the running CLI on stdin.
69
70
  */
70
71
  export function sessionTurnShape({ runtime, adopting = false, job = null }) {
71
72
  const row = runtimeById(runtime);
@@ -137,6 +138,7 @@ export function sessionTurnShape({ runtime, adopting = false, job = null }) {
137
138
  // only reads and stages cards, and the person talking to it may be on a
138
139
  // phone (noWindowEnv.mjs).
139
140
  display: !isCapture,
141
+ liveInput: row?.liveInput === true,
140
142
  };
141
143
  }
142
144
 
@@ -191,14 +191,38 @@ function branchCommits(wt, base) {
191
191
  .split('\n')
192
192
  .filter(Boolean)
193
193
  );
194
+ // EVERY COMMIT ON THE BRANCH, NOT ONLY THE CARDED ONES (2026-10-19). A
195
+ // Terminal tab's prompt says a commit for no named card needs no trailer,
196
+ // so its own commits were never reported and the rail said "N commits
197
+ // ahead" with none of them listed or openable. One more exec names the
198
+ // rest — sha, author, date and the folded subject, NUL-separated (git
199
+ // allows a NUL in none of them) and one per line (`%s` folds its
200
+ // newlines), keyed by sha from the rev-list set with the first line
201
+ // winning, the same membership rule the trailered read keeps. No body and
202
+ // no numstat: the rail lists a commit by its subject, and its patch is the
203
+ // commit-diff job's.
204
+ const listed = new Map();
205
+ for (const line of git(['log', '--no-merges', '-n', String(MAX_COMMITS), '--format=%H%x00%an%x00%aI%x00%s', `${base}..HEAD`], wt).split('\n')) {
206
+ const [sha, author, at, subject] = line.split('\0');
207
+ if (real.includes(sha) && !listed.has(sha)) listed.set(sha, { author, at, subject });
208
+ }
194
209
  for (const sha of real) {
195
- if (!named.has(sha)) continue;
196
210
  // A commit we cannot stat (a numstat past the pipe's buffer) still names
197
211
  // its cards — send it with zero counts rather than drop it, as before.
198
- const rec = commitRecord(sha, wt, { numstat: true }) ?? commitRecord(sha, wt);
199
- if (!rec) continue;
200
- const taskIds = taskIdsFromMessage(stripDelims(rec.body));
201
- if (taskIds.length === 0) continue;
212
+ const rec = named.has(sha) ? commitRecord(sha, wt, { numstat: true }) ?? commitRecord(sha, wt) : null;
213
+ const taskIds = rec ? taskIdsFromMessage(stripDelims(rec.body)) : [];
214
+ if (taskIds.length === 0) {
215
+ const row = listed.get(sha);
216
+ if (!row) continue;
217
+ out.push({
218
+ sha: sha.slice(0, 64),
219
+ subject: stripDelims(row.subject).slice(0, 200),
220
+ author: stripDelims(row.author).slice(0, 80),
221
+ at: stripDelims(row.at).slice(0, 40),
222
+ taskIds: [],
223
+ });
224
+ continue;
225
+ }
202
226
  out.push({
203
227
  // Clamped to the server's zod caps, same rule as everything else in
204
228
  // this file: one over-cap string 400s the whole batch.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.125.5",
3
+ "version": "0.125.7",
4
4
  "description": "Run your own coding CLIs as build agents for Flowviant \u2014 Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
5
5
  "type": "module",
6
6
  "bin": {