flowviant 0.39.0 → 0.40.0

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.
@@ -594,7 +594,7 @@ function handleStreamLine(line, { cwd, emit, onActivity, appendText }) {
594
594
  // returned string for sentinel detection, and each activity is handed to
595
595
  // `onActivity` so the caller can forward progress. Build-agent turns leave it
596
596
  // off and keep the raw text passthrough + line sentinels.
597
- export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEnv, runtime = 'claude', label, onSpawn, streamJson, onActivity, wikiPerm, readOnly, model, effort }) {
597
+ export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEnv, runtime = 'claude', label, onSpawn, streamJson, onActivity, wikiPerm, readOnly, vaultDir, resultSchemaArgs, model, effort }) {
598
598
  return new Promise((resolve) => {
599
599
  const rt = runtimeById(runtime);
600
600
  if (!rt.args) {
@@ -614,6 +614,17 @@ export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEn
614
614
  // all: unset means the CLI's own default, the honest resting state.
615
615
  //
616
616
  // readOnly wins over wikiPerm: a consult must never inherit write tools.
617
+ //
618
+ // TWO FORMS OF THE SAME DECISION, and the redundancy is deliberate rather
619
+ // than sloppy. `profile` is the NAME of the posture — a promise about what
620
+ // must be impossible during the turn — and every runtime expresses it in its
621
+ // own vocabulary: Claude as an `--allowedTools` verb list, Codex as a kernel
622
+ // sandbox mode plus feature toggles. `perm` is Claude's expression, still
623
+ // computed here only because those three arrays live in this file; it
624
+ // collapses into the registry the day every runtime expresses every profile.
625
+ // Both derive from the same branch, so they cannot disagree about which
626
+ // posture a turn is running under.
627
+ const profile = readOnly ? 'consult' : wikiPerm ? 'wiki' : 'build';
617
628
  const args = rt.args({
618
629
  prompt,
619
630
  system,
@@ -621,6 +632,16 @@ export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEn
621
632
  effort,
622
633
  resume,
623
634
  streamJson,
635
+ profile,
636
+ // Only the wiki profile uses it, but it is passed unconditionally: a
637
+ // runtime that can path-scope its writes needs to know WHERE the vault is,
638
+ // and Claude — which cannot — simply ignores it.
639
+ vaultDir,
640
+ // Structured-output flags for the MEDIATED path. Handed to the adapter
641
+ // rather than appended here for the same reason `mcp` is: Codex takes its
642
+ // prompt as a trailing positional, so a flag after it is in the wrong
643
+ // place.
644
+ resultSchemaArgs,
624
645
  perm: readOnly ? CONSULT_PERM : wikiPerm ? WIKI_PERM : PERM,
625
646
  // Handed to the adapter rather than appended here, because WHERE these go
626
647
  // is a property of the CLI: Codex reads its prompt as a trailing
@@ -692,8 +713,18 @@ export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEn
692
713
  });
693
714
  child.on('error', (e) => {
694
715
  if (e.code === 'ENOENT') {
716
+ // A MISSING CLI FAILS THE TURN, NOT THE DAEMON. This called
717
+ // process.exit(1), which was defensible while `claude` was the only
718
+ // runtime and preflight refused to start without it — the process
719
+ // could not reach here. Both halves of that are gone: preflight is now
720
+ // fatal only when NOTHING is drivable, so a Codex-only machine starts
721
+ // legitimately, and the wiki/plan-check/consult turns still ask for
722
+ // Claude by default. On such a machine the first wiki sweep would have
723
+ // killed the whole daemon, taking every in-flight build with it,
724
+ // because one background job could not find one binary.
695
725
  console.error(`\nerror: '${rt.bin}' CLI not found on PATH. Install ${rt.label} first: ${rt.install}`);
696
- process.exit(1);
726
+ resolve('');
727
+ return;
697
728
  }
698
729
  console.error(e);
699
730
  resolve(out);
package/bin/lib/fleet.mjs CHANGED
@@ -76,7 +76,7 @@ import {
76
76
  } from './env.mjs';
77
77
  import { processDeployJobs, reportDeployConfig } from './deploy.mjs';
78
78
  import { machineSnapshot } from './resources.mjs';
79
- import { detectRuntimes } from './runtimes.mjs';
79
+ import { detectRuntimes, pickRuntimeFor, RUNTIMES } from './runtimes.mjs';
80
80
 
81
81
  async function fetchRoster(haveIds) {
82
82
  const url = new URL(FLEET_URL);
@@ -699,6 +699,14 @@ export async function runFleetDaemon() {
699
699
  checkingPlans.add(job.id);
700
700
  (async () => {
701
701
  try {
702
+ // WHICH CLI answers a turn nobody @mentioned. Resolved per job rather
703
+ // than once at startup: a CLI can be installed while the daemon runs.
704
+ const planRt = pickRuntimeFor('consult');
705
+ if (!planRt) {
706
+ warn(`plan check for "${job.title}" skipped — no installed CLI can run a read-only turn`);
707
+ checkingPlans.delete(job.id);
708
+ return;
709
+ }
702
710
  note(`${c.cyan('plan')} ${c.dim(`— checking "${job.title}" against your code…`)}`);
703
711
  const out = await withWikiLock(async () => {
704
712
  ensureWikiWorktree();
@@ -709,6 +717,7 @@ export async function runFleetDaemon() {
709
717
  cwd: wikiWt,
710
718
  // Reads the repo and reports JSON — it authors nothing either.
711
719
  readOnly: true,
720
+ runtime: planRt,
712
721
  label: c.cyan('[plan]'),
713
722
  });
714
723
  });
@@ -804,6 +813,8 @@ export async function runFleetDaemon() {
804
813
  // Compare-and-set BEFORE spending a Claude turn: two lanes can wake on
805
814
  // the same push, and running one instruction twice into one worktree
806
815
  // is exactly the double-edit this is meant to avoid.
816
+ const quickRt = pickRuntimeFor('build');
817
+ if (!quickRt) return; // nothing here can edit code; leave the join unclaimed
807
818
  const claim = await postForData(JOIN_TAKE_URL, { joinId: job.id });
808
819
  if (!claim?.taken) return;
809
820
  note(
@@ -821,6 +832,7 @@ export async function runFleetDaemon() {
821
832
  resume: false,
822
833
  system: SYSTEM_QUICK_EDIT,
823
834
  cwd: target.wt,
835
+ runtime: quickRt,
824
836
  // No MCP: a join records no run, claims nothing, completes nothing.
825
837
  // Its only report is the one this daemon posts below.
826
838
  label: c.cyan('[quick]'),
@@ -864,6 +876,15 @@ export async function runFleetDaemon() {
864
876
  // Serialised against the wiki queue as well: that queue hard-resets
865
877
  // this worktree mid-turn, which would pull the files out from under a
866
878
  // consult that is reading them.
879
+ // A consult's prompt is steered by a question ANY project editor can
880
+ // type, so the profile is the enforcement and a runtime that cannot
881
+ // express it does not get the job. Today that means Claude; see the
882
+ // `profiles` notes in runtimes.mjs for exactly what Codex is missing.
883
+ const consultRt = pickRuntimeFor('consult');
884
+ if (!consultRt) {
885
+ warn('a consult is waiting, but no installed CLI can run a read-only turn');
886
+ return;
887
+ }
867
888
  await withWikiLock(async () => {
868
889
  ensureWikiWorktree();
869
890
  const out = await runTurn({
@@ -879,6 +900,7 @@ export async function runFleetDaemon() {
879
900
  // not to change anything, but the prompt is what an injected
880
901
  // question competes with; the toolset is what it cannot.
881
902
  readOnly: true,
903
+ runtime: consultRt,
882
904
  label: c.cyan('[ask]'),
883
905
  });
884
906
  const answer = (out || '').trim();
@@ -1296,14 +1318,24 @@ export async function runFleetDaemon() {
1296
1318
  warn(`wiki vault sync failed: ${e.message} — pages stay local; next turn retries`);
1297
1319
  }
1298
1320
  };
1321
+ // The cartographer needs to read the repo and write ONLY the vault —
1322
+ // a narrower promise than "build", so it is its own profile.
1323
+ const wikiRt = pickRuntimeFor('wiki');
1324
+ if (!wikiRt) {
1325
+ warn('wiki generation skipped — no installed CLI can run a vault-scoped turn');
1326
+ return;
1327
+ }
1328
+ const wikiLabel = RUNTIMES[wikiRt].label;
1299
1329
  if (task.type === 'sweep') {
1300
- note(`${c.cyan('wiki')} ${c.dim('— regenerating: your Claude is reading the repo…')}`);
1330
+ note(`${c.cyan('wiki')} ${c.dim(`— regenerating: your ${wikiLabel} is reading the repo…`)}`);
1301
1331
  const out = await runTurn({
1302
1332
  prompt: WIKI_KICKOFF(sha, vaultDir),
1303
1333
  resume: false,
1304
1334
  system: SYSTEM_WIKI(vaultDir),
1305
1335
  cwd: wikiWt,
1306
1336
  wikiPerm: true,
1337
+ vaultDir,
1338
+ runtime: wikiRt,
1307
1339
  label: c.cyan('[wiki]'),
1308
1340
  streamJson: true,
1309
1341
  onActivity,
@@ -1346,6 +1378,8 @@ export async function runFleetDaemon() {
1346
1378
  system: SYSTEM_REGROUND(vaultDir),
1347
1379
  cwd: wikiWt,
1348
1380
  wikiPerm: true,
1381
+ vaultDir,
1382
+ runtime: wikiRt,
1349
1383
  label: c.cyan('[wiki]'),
1350
1384
  streamJson: true,
1351
1385
  onActivity,