flowviant 0.125.6 → 0.125.9

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 (73) hide show
  1. package/README.md +84 -6
  2. package/bin/cli.mjs +7 -1
  3. package/bin/lib/activityFeed.mjs +69 -0
  4. package/bin/lib/agentMergeReports.mjs +46 -0
  5. package/bin/lib/agentMergeResolution.mjs +44 -0
  6. package/bin/lib/agentPlan.mjs +6 -2
  7. package/bin/lib/claudeInput.mjs +61 -1
  8. package/bin/lib/claudePosture.mjs +21 -0
  9. package/bin/lib/claudeStream.mjs +54 -4
  10. package/bin/lib/codexContract.mjs +52 -0
  11. package/bin/lib/codexResearch.mjs +33 -5
  12. package/bin/lib/commandSpecs.mjs +8 -5
  13. package/bin/lib/config.mjs +22 -1
  14. package/bin/lib/daemonJournal.mjs +102 -0
  15. package/bin/lib/daemonLogging.mjs +22 -18
  16. package/bin/lib/fleet.mjs +16 -6
  17. package/bin/lib/fleetReports.mjs +12 -5
  18. package/bin/lib/hooks/orchestrator.mjs +3 -1
  19. package/bin/lib/logReplay.mjs +133 -0
  20. package/bin/lib/mcpInventory.mjs +2 -1
  21. package/bin/lib/mcpReasons.mjs +6 -0
  22. package/bin/lib/nativeAgentTools.mjs +20 -3
  23. package/bin/lib/observedLines.mjs +63 -0
  24. package/bin/lib/orchestrator.mjs +49 -7
  25. package/bin/lib/orchestratorNative.mjs +133 -3
  26. package/bin/lib/orchestratorPrompt.mjs +50 -4
  27. package/bin/lib/postureTools.mjs +14 -6
  28. package/bin/lib/printable.mjs +1 -1
  29. package/bin/lib/processGroup.mjs +41 -0
  30. package/bin/lib/projectTools.mjs +3 -2
  31. package/bin/lib/prompts.mjs +47 -18
  32. package/bin/lib/reportJournal.mjs +61 -0
  33. package/bin/lib/repositoryMerge.mjs +1 -1
  34. package/bin/lib/researchHandoff.mjs +144 -0
  35. package/bin/lib/researchHandoffText.mjs +3 -0
  36. package/bin/lib/researchKnowledge.mjs +60 -0
  37. package/bin/lib/researchRestoration.mjs +38 -0
  38. package/bin/lib/resumeLost.mjs +21 -1
  39. package/bin/lib/runTurn.mjs +98 -44
  40. package/bin/lib/runtimeCapabilities.mjs +7 -2
  41. package/bin/lib/runtimeClaude.mjs +30 -8
  42. package/bin/lib/runtimeCodex.mjs +54 -22
  43. package/bin/lib/runtimeEvents.mjs +17 -7
  44. package/bin/lib/runtimeOfferings.mjs +3 -1
  45. package/bin/lib/startProject.mjs +10 -0
  46. package/bin/lib/turnDiagnostics.mjs +2 -1
  47. package/bin/lib/turnOutput.mjs +63 -0
  48. package/bin/lib/turnRecord.mjs +11 -0
  49. package/bin/lib/turnSubagents.mjs +13 -3
  50. package/bin/lib/ui.mjs +2 -2
  51. package/bin/lib/update.mjs +18 -4
  52. package/bin/lib/uplinkScrub.mjs +49 -0
  53. package/bin/lib/views.mjs +49 -58
  54. package/bin/lib/wikiRunner.mjs +2 -2
  55. package/bin/lib/work.mjs +5 -5
  56. package/bin/lib/workActivity.mjs +65 -0
  57. package/bin/lib/workAgentBriefRunner.mjs +2 -2
  58. package/bin/lib/workAgentMerges.mjs +57 -52
  59. package/bin/lib/workAgentPlans.mjs +76 -27
  60. package/bin/lib/workAgentPrecheck.mjs +1 -1
  61. package/bin/lib/workAgentTurnExecution.mjs +62 -8
  62. package/bin/lib/workAgentTurnReports.mjs +33 -3
  63. package/bin/lib/workAgentTurnShape.mjs +6 -4
  64. package/bin/lib/workAgentTurns.mjs +12 -8
  65. package/bin/lib/workAttachments.mjs +27 -5
  66. package/bin/lib/workIntake.mjs +8 -2
  67. package/bin/lib/workMergeBatch.mjs +7 -1
  68. package/bin/lib/workNarration.mjs +32 -0
  69. package/bin/lib/workPlanRecovery.mjs +50 -0
  70. package/bin/lib/workReportQueue.mjs +18 -1
  71. package/bin/lib/workSessionTurns.mjs +64 -22
  72. package/bin/lib/worktreeDiff.mjs +29 -5
  73. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { sessionWork, reportWorkOutcome } from './workActivity.mjs';
1
2
  /**
2
3
  * A WORKBENCH TAB'S TURN — offered, admitted, placed, adopted, pinned to its
3
4
  * CLI, credentialed, resumed, run, persisted and settled.
@@ -19,7 +20,7 @@
19
20
  import { existsSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
20
21
  import { git } from './git.mjs';
21
22
  import { measureRepositoryBaseline } from './repositoryBaseline.mjs';
22
- import { REPOSITORY_BASELINE_PARAGRAPH } from './prompts.mjs';
23
+ import { REPOSITORY_BASELINE_PARAGRAPH, tabFilesNote } from './prompts.mjs';
23
24
  import { processStartTime } from './procRegistry.mjs';
24
25
  import { c, note, ok, warn } from './ui.mjs';
25
26
  import { mcpFor } from './claude.mjs';
@@ -31,10 +32,11 @@ import { isTerminalSessionLive } from './claudeSessions.mjs';
31
32
  import { isAgyConversationLive } from './agySessions.mjs';
32
33
  import { adoptFence } from './localSessions.mjs';
33
34
  import { resumeRetriesFresh } from './resumeLost.mjs';
35
+ import { CODEX_CONTRACT_MARKER, codexContractKnown, recordCodexContract } from './codexContract.mjs';
34
36
  import { turnLockedByLivePid } from './turnLock.mjs';
35
37
  import { createDeliveryReporter } from './workDelivery.mjs';
36
38
  import { createLiveMessages } from './workLiveMessages.mjs';
37
- import { killGroup } from './processGroup.mjs';
39
+ import { interruptThenKill, killGroup } from './processGroup.mjs';
38
40
  import { printable } from './printable.mjs';
39
41
  import { brainFor } from './workBrain.mjs';
40
42
  import { AGY_CONV_RE, CLAUDE_SESSION_RE, CODEX_THREAD_RE, RESUME_MARKERS } from './conversationIds.mjs';
@@ -76,6 +78,9 @@ export function createWorkSessionTurns({
76
78
  agyRegistryLookup,
77
79
  carryDirtyState,
78
80
  fetchAttachments,
81
+ // Each file landed or named as missed (workAttachments.mjs); a caller that
82
+ // hands only `fetchAttachments` (an older test) has its paths as landed.
83
+ fetchTabFiles = async (wt, attachments) => (await fetchAttachments(wt, attachments)).map((path) => ({ path })),
79
84
  makeNarrator,
80
85
  sayTurnDeferred,
81
86
  lastDeferSaid,
@@ -145,6 +150,8 @@ export function createWorkSessionTurns({
145
150
  return settledHere.has(turnId);
146
151
  };
147
152
  const settleTurn = (turnId, payload) => {
153
+ const run = runs.get(turnId);
154
+ if (run?.job) reportWorkOutcome(sessionWork(run.job), { ...payload, outcome: payload.ok ? 'replied' : 'nothing' });
148
155
  const at = now();
149
156
  settledHere.delete(turnId); // re-inserted at the end: the map stays in time order
150
157
  settledHere.set(turnId, at);
@@ -219,7 +226,7 @@ export function createWorkSessionTurns({
219
226
  const releaseSlot = admit.reserve();
220
227
  lastDeferSaid.delete(job.sessionId);
221
228
  workAnswering.add(job.id);
222
- const run = { interrupted: false, spawned: [], sessionId: job.sessionId, live: null };
229
+ const run = { interrupted: false, spawned: [], sessionId: job.sessionId, live: null, job };
223
230
  runs.set(job.id, run);
224
231
  /** The execution generation the server handed, echoed on every settle
225
232
  * so the server can tell this execution's answer from an older one's. */
@@ -654,6 +661,17 @@ export function createWorkSessionTurns({
654
661
  // refused for want of an approval, by `server.tool`.
655
662
  let codexSpoke = false;
656
663
  const approvalRefused = [];
664
+ // WHAT THE CLI SAID OF ITS OWN RUN (2026-10-19): a failed result or
665
+ // error event, or an exit that was not a clean one. A usage limit, a
666
+ // signed-out CLI or a crash printed its words and settled as the
667
+ // tab's REPLY; they settle as the failure they are now, in the same
668
+ // words. Reset for the fresh retry, which is its own run.
669
+ let cliFailed = false;
670
+ let badExit = null;
671
+ // A Codex tab's contract and whether this turn sent it (codexContract.mjs).
672
+ const contractPath = rt.id === 'codex' ? sessionMetaPath(dir.wt, CODEX_CONTRACT_MARKER, job.sessionId) : null;
673
+ let turnSystem = '';
674
+ let contractSent = true;
657
675
  const spawned = []; // this turn's children, for the teardown registry
658
676
  // THE TURN'S TOOL LOG — see workToolLog.mjs for its shape rules.
659
677
  const turnLog = createToolLog(dir.wt);
@@ -675,12 +693,7 @@ export function createWorkSessionTurns({
675
693
  // Files first, then the message that references them: the agent
676
694
  // must be able to open what it is being told about. Only the ones
677
695
  // that actually landed are named.
678
- const files = await fetchAttachments(dir.wt, job.attachments);
679
- const filesNote = files.length
680
- ? `[FILES THE HUMAN ATTACHED TO THIS MESSAGE — already on disk in this worktree]\n${files
681
- .map((f) => `- ${f}`)
682
- .join('\n')}`
683
- : '';
696
+ const filesNote = tabFilesNote(await fetchTabFiles(dir.wt, job.attachments));
684
697
  const message = [job.body, filesNote, adoptNote, carryNote]
685
698
  .filter(Boolean)
686
699
  .join('\n\n');
@@ -689,8 +702,7 @@ export function createWorkSessionTurns({
689
702
  scope: 'session', turnId: job.id, attempt: job.attempt, cwd: dir.wt, sessionMetaPath,
690
703
  interrupted: () => run.interrupted,
691
704
  format: async (followup) => {
692
- const paths = await fetchAttachments(dir.wt, followup.attachments);
693
- const note = paths.length ? `[FILES THE HUMAN ATTACHED TO THIS MESSAGE — already on disk in this worktree]\n${paths.map((f) => `- ${f}`).join('\n')}` : '';
705
+ const note = tabFilesNote(await fetchTabFiles(dir.wt, followup.attachments));
694
706
  const words = shape.capture ? (followup.libraryPrompt ?? '') + followup.body : followup.body + (followup.libraryPrompt ?? '');
695
707
  return sessionTurnKickoff(shape, { sessionId: job.sessionId, sessionName: job.sessionName,
696
708
  askedByName: followup.askedByName, message: [words, note].filter(Boolean).join('\n\n') });
@@ -728,11 +740,18 @@ export function createWorkSessionTurns({
728
740
  // artifacts paragraph, the planning sentence appended). The
729
741
  // artifacts paragraph also waits on the server — ARTIFACTS
730
742
  // (0.94.0): only while it can show one, read here at spawn.
731
- system: sessionTurnSystem(shape, {
732
- knowledgeDir,
733
- artifactsAccepted: getArtifactsAccepted(),
734
- runtime: rt.id,
735
- }) + (toolLog.baseline ? '\n\n' + REPOSITORY_BASELINE_PARAGRAPH(toolLog.baseline) : '') + (artifactScan?.instructions ? `\n\n${artifactScan.instructions}` : ''),
743
+ // …and a Codex thread that already holds it is sent none
744
+ // (codexContract.mjs); `turnSystem` keeps the text for a
745
+ // fresh retry and the marker.
746
+ system: (() => {
747
+ turnSystem = sessionTurnSystem(shape, {
748
+ knowledgeDir,
749
+ artifactsAccepted: getArtifactsAccepted(),
750
+ runtime: rt.id,
751
+ }) + (toolLog.baseline ? '\n\n' + REPOSITORY_BASELINE_PARAGRAPH(toolLog.baseline) : '') + (artifactScan?.instructions ? `\n\n${artifactScan.instructions}` : '');
752
+ contractSent = !codexContractKnown(contractPath, codexResumeId, turnSystem);
753
+ return contractSent ? turnSystem : '';
754
+ })(),
736
755
  // The directory is OUTSIDE a worktree's cwd; Claude Code is
737
756
  // told it may read there (`--add-dir`) rather than left to
738
757
  // refuse a read-only capture turn a path it was just handed.
@@ -774,6 +793,15 @@ export function createWorkSessionTurns({
774
793
  onModel: (m) => {
775
794
  model = m;
776
795
  },
796
+ // The CLI's own context measure, live while the turn runs
797
+ // (claudeInput.mjs); the server clears it at settle.
798
+ onContext: (ctx) => narrator.context(ctx),
799
+ onCliFailure: () => {
800
+ cliFailed = true;
801
+ },
802
+ onExit: (code, signal) => {
803
+ badExit = signal || (code ? `exit ${code}` : null);
804
+ },
777
805
  // What this CLI says it can be asked for by name, and which of
778
806
  // its connectors need a sign-in, are harvested off this init by
779
807
  // runTurn itself (`initHarvestFor`, runtimeCapabilities.mjs) —
@@ -794,7 +822,7 @@ export function createWorkSessionTurns({
794
822
  // chat does not (workTurnShape.mjs).
795
823
  display: shape.display,
796
824
  runtime: rt.id,
797
- label: c.cyan('[tab]'),
825
+ work: sessionWork(job),
798
826
  // Only codex announces one (thread.started); held here so the id
799
827
  // this turn actually SPOKE under is what gets persisted after it
800
828
  // ends. Last write wins on purpose: a failed resume that fell
@@ -871,9 +899,19 @@ export function createWorkSessionTurns({
871
899
  */
872
900
  if (
873
901
  !adopting &&
874
- resumeRetriesFresh({ resume, out, runtime: rt.id, sawInit: Boolean(seenClaudeSession) })
902
+ resumeRetriesFresh({
903
+ resume,
904
+ out,
905
+ runtime: rt.id,
906
+ // The CLI's own word that it found the conversation: Claude's
907
+ // init, Codex's `thread.started` naming the pinned thread.
908
+ sawInit: rt.id === 'codex' ? Boolean(codexResumeId && seenThreadId === codexResumeId) : Boolean(seenClaudeSession),
909
+ })
875
910
  ) {
876
- out = await runCli({ ...turnArgs, resume: false });
911
+ cliFailed = false;
912
+ badExit = null;
913
+ contractSent = true;
914
+ out = await runCli({ ...turnArgs, resume: false, system: turnSystem });
877
915
  }
878
916
  } finally {
879
917
  // The CLI has stopped printing, so stop relaying. The LINE itself
@@ -896,7 +934,10 @@ export function createWorkSessionTurns({
896
934
  // before it ever touches disk — it later rides in argv as
897
935
  // `resume <id>` — and best-effort, like the runtime pin: an
898
936
  // unwritable marker just means the tab runs fresh next turn.
899
- if (rt.id === 'codex' && seenThreadId && CODEX_THREAD_RE.test(seenThreadId)) pinId(sessionMetaPath(dir.wt, RESUME_MARKERS.codex, job.sessionId), CODEX_THREAD_RE, seenThreadId);
937
+ if (rt.id === 'codex' && seenThreadId && CODEX_THREAD_RE.test(seenThreadId)) {
938
+ pinId(sessionMetaPath(dir.wt, RESUME_MARKERS.codex, job.sessionId), CODEX_THREAD_RE, seenThreadId);
939
+ recordCodexContract(contractPath, seenThreadId, turnSystem, contractSent);
940
+ }
900
941
  /**
901
942
  * A PERSON INTERRUPTED IT. Whatever the CLI printed was cut off, so
902
943
  * it is not an answer: the turn settles `interrupted`, with what it
@@ -974,7 +1015,7 @@ export function createWorkSessionTurns({
974
1015
  artifactFailure = `Artifact delivery is incomplete: ${error.message}`;
975
1016
  }
976
1017
  }
977
- const spoke = answer.length > 0 && !loginFailure;
1018
+ const spoke = answer.length > 0 && !loginFailure && !cliFailed && !badExit;
978
1019
  await settleTurn(job.id, {
979
1020
  ...attempt,
980
1021
  ok: spoke && !artifactFailure,
@@ -997,6 +1038,7 @@ export function createWorkSessionTurns({
997
1038
  });
998
1039
  if (spoke) ok(`${c.cyan('tab')} ${c.dim('— replied in the session')}`);
999
1040
  else if (loginFailure) warn('codex answered 401 for this session turn — settled as failed, in words');
1041
+ else if (answer && (cliFailed || badExit)) warn(`the CLI reported its turn failed${badExit ? ` (${badExit})` : ''} — settled as failed, in its words`);
1000
1042
  else warn('session turn produced no output — settled as failed');
1001
1043
  } catch (e) {
1002
1044
  if (toolLog) toolLog.state = 'partial';
@@ -1072,7 +1114,7 @@ export function createWorkSessionTurns({
1072
1114
  if (run) {
1073
1115
  if (!run.interrupted) note(`${c.cyan('tab')} ${c.dim('— interrupting the running turn')}`);
1074
1116
  run.interrupted = true;
1075
- for (const ch of run.spawned) killGroup(ch);
1117
+ for (const ch of run.spawned) interruptThenKill(ch);
1076
1118
  continue;
1077
1119
  }
1078
1120
  if (pendingWorkReports.has(id) || settledRecently(id) || notRunningSaid.has(id)) continue;
@@ -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.6",
3
+ "version": "0.125.9",
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": {