flowviant 0.125.28 → 0.125.30

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.
@@ -71,7 +71,13 @@ import { createIntermediateArtifacts, IntermediateArtifactCapacityError } from '
71
71
  import { createUiReview } from './uiReview.mjs';
72
72
  import { createVideoRender } from './videoRender.mjs';
73
73
  import { createManagedBrowser } from './managedBrowser.mjs';
74
+ import { completeBrowserReview, browserHandoffPrompt } from './browserHandoff.mjs';
74
75
  import { withUiReview, appendUiReviewEvidence } from './uiReviewTurn.mjs';
76
+ import { setupRepository } from './repositorySetup.mjs';
77
+ import { readEnvironmentRecipe } from './reviewEnvironmentConfig.mjs';
78
+ import { environmentInstructions, environmentOverlay, holdWorktreeEnvironment } from './reviewEnvironment.mjs';
79
+ import { environmentCredentialNames } from './reviewDatabase.mjs';
80
+ import { createRuntimeReview } from './runtimeReview.mjs';
75
81
 
76
82
  export function createAgentTurnExecution({
77
83
  admit,
@@ -104,6 +110,7 @@ export function createAgentTurnExecution({
104
110
  makeUiReview = createUiReview,
105
111
  makeVideoRender = createVideoRender,
106
112
  makeManagedBrowser = createManagedBrowser,
113
+ makeRuntimeReview = createRuntimeReview,
107
114
  }) {
108
115
  const reportDelivery = createDeliveryReporter();
109
116
  /** How long the final flush may hold the settle. Bounded because the settle
@@ -125,6 +132,7 @@ export function createAgentTurnExecution({
125
132
  * `interruptAgentTurns`); absent, nothing can interrupt it. */
126
133
  const runAgentTurnInPlace = async (job, { turnId, agentId, place, releaseSlot, postAgentTurn, measured, run = { interrupted: false } }) => {
127
134
  let repositoryRun = null;
135
+ const environmentReleases = [];
128
136
  const setupController = new AbortController();
129
137
  run.abortSetup = () => setupController.abort();
130
138
  if (run.interrupted) setupController.abort();
@@ -416,6 +424,8 @@ export function createAgentTurnExecution({
416
424
  let browserNote = '';
417
425
  let uiReviewNote = '';
418
426
  let uiReviewEvidence = '';
427
+ let environmentNote = '', reviewEnv = null;
428
+ let runtimeReview = null, runtimeNote = '', runtimeEvidence = '';
419
429
  // A video card's render tool (videoRender.mjs), and the words a turn
420
430
  // gets when it could not be held, so it stops rather than guesses.
421
431
  let videoRender = null;
@@ -429,6 +439,8 @@ export function createAgentTurnExecution({
429
439
  args = withUiReview(args, uiReviewNote, uiReview?.env);
430
440
  args = withUiReview(args, videoNote);
431
441
  args = withUiReview(args, browserNote, managedBrowser?.env);
442
+ args = withUiReview(args, environmentNote, reviewEnv);
443
+ args = withUiReview(args, runtimeNote, runtimeReview?.env);
432
444
  args.managedBrowser = Boolean(managedBrowser);
433
445
  return runTurnImpl(args);
434
446
  };
@@ -479,6 +491,22 @@ export function createAgentTurnExecution({
479
491
  let orchestrator = null;
480
492
  let intermediateDelivery = null;
481
493
  try {
494
+ if (posture === 'build') {
495
+ const sources = repositoryRun?.sources ?? [{ wt, repoRoot: worktreeRepo, baseRef: baseRef() }];
496
+ const administrativeNames = new Set();
497
+ for (const source of sources) {
498
+ const snapshot = readEnvironmentRecipe(source.repoRoot, source.baseRef);
499
+ if (!snapshot) continue;
500
+ const environment = await setupRepository(source.repoRoot, source.wt, { snapshot, signal: setupController.signal,
501
+ onSpawn: ch => { workChildren.set(ch, agentId); groupKillChildren?.add(ch); },
502
+ onClose: ch => { workChildren.delete(ch); groupKillChildren?.delete(ch); } });
503
+ environmentReleases.push(holdWorktreeEnvironment(environment));
504
+ environmentNote += '\n' + environmentInstructions(environment);
505
+ for (const name of environmentCredentialNames(snapshot.recipe)) administrativeNames.add(name);
506
+ if (source.wt === wt) reviewEnv = environmentOverlay(environment);
507
+ }
508
+ if (administrativeNames.size) reviewEnv = { ...reviewEnv, ...Object.fromEntries([...administrativeNames].map(name => [name, undefined])) };
509
+ }
482
510
  if (posture === 'build' && job.kind !== 'merge_resolve' && ['claude', 'codex'].includes(rt)) {
483
511
  try {
484
512
  managedBrowser = await makeManagedBrowser({ wt, turnId, signal: setupController.signal, onChild: ch => {
@@ -489,6 +517,14 @@ export function createAgentTurnExecution({
489
517
  } catch (error) { browserNote = `The managed browser could not start: ${envScrub(error.message)}. Report interactive browser checks unavailable.`; }
490
518
  }
491
519
  if (posture === 'build' && job.kind !== 'merge_resolve') {
520
+ try {
521
+ runtimeReview = await makeRuntimeReview({ wt, repoRoot: worktreeRepo, baseRef: baseRef(), sources: repositoryRun?.sources ?? [],
522
+ browser: managedBrowser, signal: setupController.signal, onChild: ch => {
523
+ workChildren.set(ch, agentId); groupKillChildren?.add(ch);
524
+ return () => { workChildren.delete(ch); groupKillChildren?.delete(ch); };
525
+ } });
526
+ runtimeNote = runtimeReview?.instructions ?? '';
527
+ } catch (error) { runtimeNote = `Configured runtime review is unavailable: ${envScrub(error.message)}. Use the repository’s existing checks and report unavailable runtime verification.`; }
492
528
  try {
493
529
  uiReview = await makeUiReview({ wt, signal: setupController.signal, onChild: ch => {
494
530
  workChildren.set(ch, agentId); groupKillChildren?.add(ch);
@@ -514,10 +550,13 @@ export function createAgentTurnExecution({
514
550
  notify: message => liveInput ? run.live?.inject(message) === true : false,
515
551
  taken: id => run.live?.onTaken(id) });
516
552
  }
517
- const tools = prepareCardTools({ rt, posture, wt, repoRoot, baseRef, sources: repositoryRun?.sources ?? [], repositoryTool: repositoryRun?.tool ?? null, orchestratorConfig: orchestrator?.config ?? null, uiReviewConfig: uiReview?.config, videoConfig: videoRender?.config ?? null, browserConfig: managedBrowser?.config, orchestrated: Boolean(orchestrator), codexHome: agentResume.codexHome });
553
+ const tools = prepareCardTools({ rt, posture, wt, repoRoot, baseRef, sources: repositoryRun?.sources ?? [], repositoryTool: repositoryRun?.tool ?? null, orchestratorConfig: orchestrator?.config ?? null, uiReviewConfig: uiReview?.config, videoConfig: videoRender?.config ?? null, browserConfig: managedBrowser?.config, orchestrated: Boolean(orchestrator), codexHome: agentResume.codexHome,
554
+ runtimeConfig: runtimeReview?.config,
555
+ ...(reviewEnv ? { env: { ...process.env, ...reviewEnv } } : {}) });
518
556
  projectTools = tools.projectTools;
519
557
  localCleanup?.begin(turnId);
520
558
  const { toolsNote } = tools;
559
+ let browserThread = agentResume.resumeThreadId;
521
560
  const agentTurnArgs = {
522
561
  ...((liveInput || orchestrator) ? { liveMessages: (run.live = makeLiveMessages({
523
562
  scope: 'agent', turnId, cwd: wt, sessionMetaPath, interrupted: () => run.interrupted,
@@ -565,12 +604,14 @@ export function createAgentTurnExecution({
565
604
  resume: agentResume.resume,
566
605
  resumeThreadId: agentResume.resumeThreadId,
567
606
  onInit: (init) => {
607
+ if (rt === 'claude' && typeof init.sessionId === 'string') browserThread = init.sessionId;
568
608
  orchestrator?.root(init.sessionId);
569
609
  // A server that did not connect, as the CLI said (unknown: nothing).
570
610
  trace.mcpStatus(init.mcpServers);
571
611
  agentResume.sawSession(init.sessionId);
572
612
  },
573
613
  onThreadId: (id) => {
614
+ browserThread = id;
574
615
  orchestrator?.root(id);
575
616
  agentResume.sawThread(id);
576
617
  },
@@ -741,6 +782,7 @@ export function createAgentTurnExecution({
741
782
  runtime: rt,
742
783
  beforeFresh: () => {
743
784
  agentResume.forgetThread();
785
+ browserThread = undefined;
744
786
  lastAnswer = null;
745
787
  model = null;
746
788
  },
@@ -751,19 +793,34 @@ export function createAgentTurnExecution({
751
793
  void postAgentActivity(agentId, 'resuming after an internal wait');
752
794
  } }
753
795
  );
754
- out = await finishRepositoryDelivery({
755
- job, run: repositoryRun, out, answer: () => lastAnswer,
756
- resume: async (prompt) => {
796
+ out = await completeBrowserReview(out, {
797
+ browser: managedBrowser, signal: setupController.signal,
798
+ resume: async (handoff) => {
799
+ const thread = agentResume.threadNow() ?? browserThread;
800
+ if (!thread) throw new Error('The browser handoff could not resume: the CLI reported no conversation id. Interactive verification is incomplete.');
757
801
  lastAnswer = null;
758
802
  const counted = usage;
759
- return runCli({
760
- ...agentTurnArgs, prompt, liveContinuation: true,
761
- onUsage: (next) => agentTurnArgs.onUsage(addUsage(counted, next)),
762
- writableDirs: repositoryRun.sourcesNow().filter((r) => r.wt !== wt).map((r) => r.wt),
763
- resume: true,
764
- resumeThreadId: agentResume.threadNow() ?? agentTurnArgs.resumeThreadId,
803
+ trace.prose('text', handoff.reason === 'returned' ? 'Browser control returned. Resuming verification.' : 'The browser handoff ended. Resuming to report incomplete interactive checks.');
804
+ return runAgentWithInternalWaits(args => runCli(args), {
805
+ ...agentTurnArgs, prompt: browserHandoffPrompt(handoff), resume: true, resumeThreadId: thread, liveContinuation: true,
806
+ ...(repositoryRun ? { writableDirs: repositoryRun.sourcesNow().filter(r => r.wt !== wt).map(r => r.wt) } : {}),
807
+ onUsage: next => agentTurnArgs.onUsage(addUsage(counted, next)),
765
808
  });
766
809
  },
810
+ finish: current => finishRepositoryDelivery({
811
+ job, run: repositoryRun, out: current, answer: () => lastAnswer,
812
+ resume: async (prompt) => {
813
+ lastAnswer = null;
814
+ const counted = usage;
815
+ return runCli({
816
+ ...agentTurnArgs, prompt, liveContinuation: true,
817
+ onUsage: (next) => agentTurnArgs.onUsage(addUsage(counted, next)),
818
+ writableDirs: repositoryRun.sourcesNow().filter((r) => r.wt !== wt).map((r) => r.wt),
819
+ resume: true,
820
+ resumeThreadId: agentResume.threadNow() ?? browserThread,
821
+ });
822
+ },
823
+ }),
767
824
  });
768
825
  } finally {
769
826
  // The loopback capability stays registered until every retained CLI
@@ -774,12 +831,15 @@ export function createAgentTurnExecution({
774
831
  // owned by its review tool, then bind evidence to the final source
775
832
  // before the existing artifact upload and delivery settlement.
776
833
  try { await videoRender?.close(); } catch { /* a closed listener */ }
834
+ try { await runtimeReview?.close(); } catch { /* turn teardown continues */ }
777
835
  try { await managedBrowser?.close(); } catch { /* turn teardown continues */ }
778
836
  try { await uiReview?.close(); }
779
837
  finally {
780
838
  try { uiReviewEvidence = uiReview?.finalize() ?? uiReviewEvidence; }
781
839
  catch (error) { uiReviewEvidence = `Local UI review final source/evidence measurement unavailable: ${envScrub(error.message)}. Earlier checks do not establish the final source.`; }
782
840
  }
841
+ try { runtimeEvidence = runtimeReview?.finalize() ?? ''; }
842
+ catch { runtimeEvidence = 'Runtime review final source could not be measured; earlier results do not verify final source.'; }
783
843
  await run.live?.stop();
784
844
  await orchestrator?.close(run.interrupted);
785
845
  await brief?.stop();
@@ -890,6 +950,7 @@ export function createAgentTurnExecution({
890
950
  modelExports: intermediateDelivery?.modelExports,
891
951
  });
892
952
  appendUiReviewEvidence(body, uiReviewEvidence);
953
+ appendUiReviewEvidence(body, runtimeEvidence);
893
954
  // The agent's own account of what it did not deliver, for the pre-review.
894
955
  if (job.kind === 'task' && job.task && body.outcome === 'delivered' && res?.gaps)
895
956
  stashGaps(sessionMetaPath(wt, 'flowviant-agent-cards', agentId), job.task.id, res.gaps);
@@ -945,7 +1006,10 @@ export function createAgentTurnExecution({
945
1006
  // hold the lock on — see `runReviewEntry`. Only after the settle that may
946
1007
  // say delivered: the three `nothing` settles never opened it.
947
1008
  if (final && reply?.review === true) await runReviewEntry(agentId, wt, job.agentName, undefined, job);
948
- } finally { await repositoryRun?.tool?.close(); }
1009
+ } finally {
1010
+ for (const release of environmentReleases) release();
1011
+ await repositoryRun?.tool?.close();
1012
+ }
949
1013
  };
950
1014
 
951
1015
  return { runAgentTurnInPlace };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.125.28",
3
+ "version": "0.125.30",
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": {