flowviant 0.102.2 → 0.104.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.
Files changed (141) hide show
  1. package/bin/cli.mjs +87 -474
  2. package/bin/lib/admission.mjs +19 -3
  3. package/bin/lib/agentPlan.mjs +2 -2
  4. package/bin/lib/agentTaskKinds.mjs +93 -0
  5. package/bin/lib/agySessions.mjs +158 -0
  6. package/bin/lib/artifactPolicy.mjs +44 -0
  7. package/bin/lib/artifactPreview.mjs +52 -0
  8. package/bin/lib/artifactReporter.mjs +163 -0
  9. package/bin/lib/artifacts.mjs +47 -227
  10. package/bin/lib/authReject.mjs +16 -4
  11. package/bin/lib/authproxy.mjs +47 -304
  12. package/bin/lib/boxIdentity.mjs +245 -0
  13. package/bin/lib/childEnv.mjs +11 -9
  14. package/bin/lib/claude.mjs +14 -851
  15. package/bin/lib/claudeAuth.mjs +3 -3
  16. package/bin/lib/claudePosture.mjs +367 -0
  17. package/bin/lib/claudeSessions.mjs +411 -0
  18. package/bin/lib/claudeStream.mjs +207 -0
  19. package/bin/lib/cliLimit.mjs +34 -0
  20. package/bin/lib/commandSpecs.mjs +173 -0
  21. package/bin/lib/config.mjs +11 -42
  22. package/bin/lib/credentialDate.mjs +22 -0
  23. package/bin/lib/credentials.mjs +21 -26
  24. package/bin/lib/daemonLogging.mjs +55 -0
  25. package/bin/lib/daemonState.mjs +64 -0
  26. package/bin/lib/deploy.mjs +148 -647
  27. package/bin/lib/deployCheckout.mjs +265 -0
  28. package/bin/lib/deployConfig.mjs +142 -0
  29. package/bin/lib/deployRunner.mjs +311 -0
  30. package/bin/lib/deployWire.mjs +41 -0
  31. package/bin/lib/desktopContract.mjs +56 -105
  32. package/bin/lib/env.mjs +49 -484
  33. package/bin/lib/fleet.mjs +184 -2396
  34. package/bin/lib/fleetJobs.mjs +120 -0
  35. package/bin/lib/fleetPost.mjs +40 -0
  36. package/bin/lib/fleetReports.mjs +392 -0
  37. package/bin/lib/fleetRoster.mjs +470 -0
  38. package/bin/lib/fleetStartup.mjs +196 -0
  39. package/bin/lib/fleetWake.mjs +45 -0
  40. package/bin/lib/fleetWire.mjs +46 -0
  41. package/bin/lib/git.mjs +34 -3
  42. package/bin/lib/gitMergeIdentity.mjs +51 -0
  43. package/bin/lib/grant.mjs +5 -3
  44. package/bin/lib/help.mjs +3 -125
  45. package/bin/lib/holder.mjs +482 -0
  46. package/bin/lib/instance.mjs +124 -46
  47. package/bin/lib/knowledge.mjs +16 -799
  48. package/bin/lib/knowledgeFetch.mjs +38 -0
  49. package/bin/lib/knowledgeFileSync.mjs +112 -0
  50. package/bin/lib/knowledgeFiles.mjs +487 -0
  51. package/bin/lib/knowledgeLibrary.mjs +249 -0
  52. package/bin/lib/landed.mjs +2 -1
  53. package/bin/lib/listeners.mjs +99 -141
  54. package/bin/lib/localSessions.mjs +30 -555
  55. package/bin/lib/login.mjs +3 -2
  56. package/bin/lib/loginPath.mjs +3 -5
  57. package/bin/lib/machineDisconnect.mjs +203 -0
  58. package/bin/lib/machineEnv.mjs +22 -0
  59. package/bin/lib/machineLimits.mjs +61 -0
  60. package/bin/lib/machineListing.mjs +338 -0
  61. package/bin/lib/machines.mjs +78 -455
  62. package/bin/lib/machinesCommand.mjs +214 -0
  63. package/bin/lib/mcp-cli.mjs +2 -1
  64. package/bin/lib/patchRevert.mjs +60 -0
  65. package/bin/lib/prWorkflow.mjs +157 -0
  66. package/bin/lib/preflight.mjs +2 -1
  67. package/bin/lib/pressurePolicy.mjs +104 -0
  68. package/bin/lib/preview.mjs +2 -272
  69. package/bin/lib/previewFramePolicy.mjs +112 -0
  70. package/bin/lib/previewGatePolicy.mjs +236 -0
  71. package/bin/lib/previewRegistry.mjs +235 -0
  72. package/bin/lib/printable.mjs +2 -2
  73. package/bin/lib/procRegistry.mjs +28 -18
  74. package/bin/lib/procScan.mjs +146 -0
  75. package/bin/lib/processes.mjs +29 -37
  76. package/bin/lib/projectToolRuntimes.mjs +138 -0
  77. package/bin/lib/projectTools.mjs +13 -88
  78. package/bin/lib/prompts.mjs +28 -42
  79. package/bin/lib/repoPath.mjs +26 -0
  80. package/bin/lib/resources.mjs +7 -69
  81. package/bin/lib/resumeLost.mjs +107 -0
  82. package/bin/lib/runTurn.mjs +324 -0
  83. package/bin/lib/runtimeAntigravity.mjs +230 -0
  84. package/bin/lib/runtimeCapabilities.mjs +334 -0
  85. package/bin/lib/runtimeClaude.mjs +126 -0
  86. package/bin/lib/runtimeCodex.mjs +217 -0
  87. package/bin/lib/runtimeDetection.mjs +121 -0
  88. package/bin/lib/runtimeEvents.mjs +482 -0
  89. package/bin/lib/runtimeOfferings.mjs +4 -1
  90. package/bin/lib/runtimes.mjs +42 -1402
  91. package/bin/lib/safeFileName.mjs +74 -0
  92. package/bin/lib/shipMerge.mjs +33 -2
  93. package/bin/lib/standDownExit.mjs +181 -0
  94. package/bin/lib/standDownGate.mjs +48 -0
  95. package/bin/lib/startProject.mjs +279 -0
  96. package/bin/lib/trace.mjs +3 -3
  97. package/bin/lib/turnLock.mjs +68 -0
  98. package/bin/lib/turnProfile.mjs +77 -0
  99. package/bin/lib/uninstall.mjs +55 -45
  100. package/bin/lib/uninstallCopies.mjs +112 -0
  101. package/bin/lib/update.mjs +30 -17
  102. package/bin/lib/uplinkScrub.mjs +265 -0
  103. package/bin/lib/vault.mjs +22 -151
  104. package/bin/lib/vaultDiff.mjs +183 -0
  105. package/bin/lib/versionOrder.mjs +42 -0
  106. package/bin/lib/views.mjs +35 -12
  107. package/bin/lib/wikiRunner.mjs +630 -0
  108. package/bin/lib/work.mjs +188 -3037
  109. package/bin/lib/workAdoptCarry.mjs +94 -0
  110. package/bin/lib/workAgentCheck.mjs +278 -0
  111. package/bin/lib/workAgentMerges.mjs +54 -142
  112. package/bin/lib/workAgentPlans.mjs +14 -12
  113. package/bin/lib/workAgentPrecheck.mjs +393 -0
  114. package/bin/lib/workAgentPublish.mjs +177 -0
  115. package/bin/lib/workAgentReview.mjs +8 -666
  116. package/bin/lib/workAgentTurnBegun.mjs +109 -0
  117. package/bin/lib/workAgentTurnExecution.mjs +604 -0
  118. package/bin/lib/workAgentTurnOutcome.mjs +207 -0
  119. package/bin/lib/workAgentTurnReports.mjs +189 -0
  120. package/bin/lib/workAgentTurns.mjs +92 -897
  121. package/bin/lib/workAttachments.mjs +93 -0
  122. package/bin/lib/workBrain.mjs +53 -0
  123. package/bin/lib/workChildren.mjs +110 -0
  124. package/bin/lib/workCommandAudit.mjs +62 -0
  125. package/bin/lib/workDiffs.mjs +2 -1
  126. package/bin/lib/workNarration.mjs +152 -0
  127. package/bin/lib/workPlaceReports.mjs +426 -0
  128. package/bin/lib/workPlaces.mjs +228 -0
  129. package/bin/lib/workPreviews.mjs +31 -9
  130. package/bin/lib/workProcesses.mjs +4 -5
  131. package/bin/lib/workPullRequests.mjs +52 -126
  132. package/bin/lib/workReportQueue.mjs +254 -0
  133. package/bin/lib/workRetire.mjs +185 -0
  134. package/bin/lib/workSessionGroups.mjs +166 -0
  135. package/bin/lib/workSessionRuntime.mjs +164 -0
  136. package/bin/lib/workSessionTokens.mjs +68 -0
  137. package/bin/lib/workSessionTurns.mjs +892 -0
  138. package/bin/lib/workShip.mjs +9 -29
  139. package/bin/lib/workToolLog.mjs +76 -0
  140. package/package.json +3 -2
  141. package/bin/lib/patch.mjs +0 -363
@@ -24,7 +24,7 @@
24
24
  * AFTER the work token is minted and the attachments are fetched — a window
25
25
  * containing a network round trip — so both daemons clear the check and both
26
26
  * spawn a CLI into one held conversation. It was built for a RESTARTED daemon
27
- * (its own comment says so, work.mjs), where the holder is already live when
27
+ * (its own comment says so, turnLock.mjs), where the holder is already live when
28
28
  * the successor looks; it was never a concurrency primitive.
29
29
  *
30
30
  * What a duplicate run cost before the session lease, all of it invisible in
@@ -89,7 +89,7 @@ import {
89
89
  openSync,
90
90
  readdirSync,
91
91
  readFileSync,
92
- realpathSync,
92
+ renameSync,
93
93
  rmSync,
94
94
  writeFileSync,
95
95
  writeSync,
@@ -98,6 +98,8 @@ import { homedir, platform } from 'node:os';
98
98
  import { join } from 'node:path';
99
99
  import { createHash } from 'node:crypto';
100
100
  import { VERSION, PROCESS_STARTED_AT } from './config.mjs';
101
+ import { compareVersions } from './versionOrder.mjs';
102
+ import { samePath } from './repoPath.mjs';
101
103
 
102
104
  /** Deliberately a HASH: a credential must never become a filename. */
103
105
  export function instanceLockPath(fleetToken) {
@@ -105,20 +107,6 @@ export function instanceLockPath(fleetToken) {
105
107
  return join(homedir(), '.flowviant', `daemon-${key}.lock`);
106
108
  }
107
109
 
108
- /** Numeric dotted compare, -1/0/1. Unparsable compares EQUAL, so a version we
109
- * cannot read never silently authorises a downgrade. */
110
- function cmpVersion(a, b) {
111
- const x = String(a).split('.').map((n) => Number.parseInt(n, 10));
112
- const y = String(b).split('.').map((n) => Number.parseInt(n, 10));
113
- for (let i = 0; i < Math.max(x.length, y.length); i++) {
114
- const p = x[i] ?? 0;
115
- const q = y[i] ?? 0;
116
- if (Number.isNaN(p) || Number.isNaN(q)) return 0;
117
- if (p !== q) return p > q ? 1 : -1;
118
- }
119
- return 0;
120
- }
121
-
122
110
  /** Signal 0 — a liveness probe, not a kill. EPERM means alive and not ours. */
123
111
  function alive(pid) {
124
112
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -139,6 +127,19 @@ function readHolder(path) {
139
127
  }
140
128
  }
141
129
 
130
+ /**
131
+ * WHAT THE DAEMON HOLDING THIS CREDENTIAL'S LOCK IS FINISHING — the `target →
132
+ * env` words its draining mark carries (`markLockDraining`), or null. Read-only,
133
+ * for `status --json` (desktopContract.mjs), so the tray can say the box is
134
+ * finishing a deploy rather than idle. Null covers no lock, no mark, and an
135
+ * older daemon that never writes one; the caller asks only of a daemon it has
136
+ * already measured running.
137
+ */
138
+ export function drainingFor(fleetToken) {
139
+ const what = readHolder(instanceLockPath(fleetToken))?.draining?.what;
140
+ return typeof what === 'string' && what ? what : null;
141
+ }
142
+
142
143
  /** Read-only desktop measurement. No lock is unknown because an unguarded or
143
144
  * older daemon may still run; a stale lock with a dead pid measures empty. */
144
145
  export function daemonRunningFor(fleetToken) {
@@ -258,20 +259,6 @@ function isOurReexecParent(pid) {
258
259
  return false;
259
260
  }
260
261
 
261
- /** Same directory, whatever it is spelled as — symlinks and trailing slashes
262
- * included. A repo compared by string would let `/repo` and `/repo/` past. */
263
- function samePath(a, b) {
264
- if (!a || !b) return false;
265
- const norm = (v) => {
266
- try {
267
- return realpathSync(v);
268
- } catch {
269
- return String(v).replace(/\/+$/, '');
270
- }
271
- };
272
- return norm(a) === norm(b);
273
- }
274
-
275
262
  /**
276
263
  * A LIVE daemon in this same checkout, under a DIFFERENT credential.
277
264
  *
@@ -636,10 +623,21 @@ const TAKEOVER_GRACE_MS = 20_000;
636
623
  * this same lock through the ppid branch. Treating that as free steals a live
637
624
  * daemon's lock and leaves it running unguarded — measured doing exactly that.
638
625
  *
626
+ * A DRAINING HOLDER IS NEVER SIGNALLED (ruling 2026-09-26, "a stand-down lets
627
+ * a deploy finish and report"). It is already standing down and leaves by
628
+ * itself once its deploy's outcome is posted, and a signal can only cost that
629
+ * outcome. So the mark is read BEFORE the SIGTERM, and again on every look
630
+ * after it (the holder writes it the moment our SIGTERM finds a deploy in
631
+ * flight). What happens then is the caller's: `flowviant stop` is told so and
632
+ * returns (`draining`), a takeover waits for the lock to clear
633
+ * (`waitOutDrain`) — one line said once, no deadline, and Ctrl+C on the
634
+ * waiting process abandons only the wait (no handler is installed yet, so the
635
+ * default action ends the process; the draining daemon is untouched).
636
+ *
639
637
  * Returns null when the holder is stopped and its lock file is cleared, or
640
638
  * `{ failed }` with a sentence the caller can print as-is.
641
639
  */
642
- function standDown(holder, path, log) {
640
+ function standDown(holder, path, log, { waitOutDrain = false } = {}) {
643
641
  // NEVER SIGNAL OURSELVES, and this is not a theoretical guard — it was
644
642
  // reproduced end to end. A 0.54.0+ lock records `entry` = the daemon's
645
643
  // argv[1], i.e. `…/bin/cli.mjs`. Run `flowviant stop` and OUR cmdline is
@@ -653,19 +651,23 @@ function standDown(holder, path, log) {
653
651
  // identity check cannot catch it and the guard belongs here, at the one place
654
652
  // that signals, rather than in each caller.
655
653
  if (holder.pid === process.pid) return { failed: 'refusing to signal this very process' };
656
- log?.(`asking daemon pid ${holder.pid} to stand down…`);
657
- try {
658
- process.kill(holder.pid, 'SIGTERM');
659
- } catch {
660
- return { failed: `could not signal pid ${holder.pid}` };
661
- }
662
-
663
654
  const standing = () => {
664
655
  const now = readHolder(path);
665
656
  if (!now || !alive(now.pid)) return null;
666
657
  return now;
667
658
  };
659
+ const before = standing();
660
+ if (!(before?.pid === holder.pid && before.draining)) {
661
+ log?.(`asking daemon pid ${holder.pid} to stand down…`);
662
+ try {
663
+ process.kill(holder.pid, 'SIGTERM');
664
+ } catch {
665
+ return { failed: `could not signal pid ${holder.pid}` };
666
+ }
667
+ }
668
+
668
669
  const deadline = Date.now() + TAKEOVER_GRACE_MS;
670
+ let saidWaiting = false;
669
671
  for (;;) {
670
672
  const now = standing();
671
673
  if (!now) break;
@@ -674,6 +676,28 @@ function standDown(holder, path, log) {
674
676
  failed: `the daemon handed over to pid ${now.pid}${now.version ? ` (${now.version})` : ''} while we waited — it is mid-update, so try again in a moment`,
675
677
  };
676
678
  }
679
+ /**
680
+ * IT IS FINISHING A DEPLOY — never forced (see above). The lock's mark
681
+ * (`markLockDraining`) says it has stopped taking work and leaves by
682
+ * itself once the outcome is posted; a SIGKILL here would lose exactly
683
+ * that outcome, so the server would hand the same irreversible job out
684
+ * again.
685
+ */
686
+ if (now.draining) {
687
+ const what = typeof now.draining.what === 'string' && now.draining.what ? ` (${now.draining.what})` : '';
688
+ if (!waitOutDrain) {
689
+ return {
690
+ draining: true,
691
+ failed: `pid ${holder.pid} is finishing a deploy${what} and stops by itself once its outcome is reported — it was not forced`,
692
+ };
693
+ }
694
+ if (!saidWaiting) {
695
+ saidWaiting = true;
696
+ log?.(`pid ${holder.pid} is finishing a deploy${what} — waiting…`);
697
+ }
698
+ sleep(1000);
699
+ continue;
700
+ }
677
701
  if (Date.now() >= deadline) {
678
702
  log?.(`pid ${holder.pid} did not stand down within ${TAKEOVER_GRACE_MS / 1000}s — forcing it.`);
679
703
  try {
@@ -734,13 +758,23 @@ function takeOverFrom(holder, path, log, { allowDowngrade = false } = {}) {
734
758
  // clears the corpse the same way it clears a dead pid's.
735
759
  return { stale: true };
736
760
  }
737
- if (!allowDowngrade && holder.version && cmpVersion(VERSION, holder.version) < 0) {
761
+ /**
762
+ * THE DOWNGRADE GUARD refuses only a MEASURED downgrade. A holder version
763
+ * this daemon cannot read (versionOrder.mjs answers unknown) gates nothing,
764
+ * the same as before the one comparator existed: ignorance never refuses
765
+ * (CLAUDE.md, "What Flowviant is"). The old comment here claimed the
766
+ * opposite while its code let the takeover through; the code was the rule.
767
+ * A lock with NO version (an older daemon wrote it) is not compared either.
768
+ */
769
+ if (!allowDowngrade && holder.version && compareVersions(VERSION, holder.version) === -1) {
738
770
  return {
739
771
  failed: `the running daemon is ${holder.version} and this one is ${VERSION} — refusing to replace a newer daemon with an older one (--takeover-downgrade if you mean it)`,
740
772
  };
741
773
  }
742
774
 
743
- const bad = standDown(holder, path, log);
775
+ // A takeover WAITS OUT a drain (ruling 2026-09-26): the new start is what
776
+ // was asked for, and it arrives once the deploy has reported.
777
+ const bad = standDown(holder, path, log, { waitOutDrain: true });
744
778
  if (bad) return bad;
745
779
  log?.(`daemon pid ${holder.pid} stopped — taking over.`);
746
780
  return null;
@@ -875,6 +909,37 @@ export function acquireInstanceLock(fleetToken, repoRoot, opts = {}) {
875
909
  return { ok: false, holder: readHolder(path) };
876
910
  }
877
911
 
912
+ /**
913
+ * MARK THIS PROCESS'S LOCK AS DRAINING — it is standing down and waits only
914
+ * for its in-flight deploys to report (ruling 2026-09-26; standDownExit.mjs).
915
+ * The lock stays held, so `status --json` still shows the daemon running and
916
+ * busy until the outcome lands; the mark is what tells a takeover (which then
917
+ * waits) or a `flowviant stop` (which then says so) never to signal it
918
+ * (see standDown).
919
+ *
920
+ * Written only while the lock still names this pid, and by rename, so a
921
+ * reader never sees half a record. Returns whether it was written; an
922
+ * unguarded daemon (no lock) has nothing to mark.
923
+ */
924
+ export function markLockDraining(fleetToken, what) {
925
+ const path = instanceLockPath(fleetToken);
926
+ const holder = readHolder(path);
927
+ if (!holder || holder.pid !== process.pid) return false;
928
+ const tmp = `${path}.${process.pid}.tmp`;
929
+ try {
930
+ writeFileSync(tmp, JSON.stringify({ ...holder, draining: { what: String(what ?? ''), since: new Date().toISOString() } }));
931
+ renameSync(tmp, path);
932
+ return true;
933
+ } catch {
934
+ try {
935
+ rmSync(tmp, { force: true });
936
+ } catch {
937
+ /* best-effort */
938
+ }
939
+ return false;
940
+ }
941
+ }
942
+
878
943
  /** Release ONLY what we still own: a successor that adopted the lock (see the
879
944
  * ppid branch) must not have it deleted out from under it when we exit. */
880
945
  function makeRelease(path) {
@@ -890,7 +955,8 @@ function makeRelease(path) {
890
955
  /* best-effort; a stale file is cleared by the next acquire */
891
956
  }
892
957
  };
893
- // 'exit' covers the SIGINT/SIGTERM handlers too — both call process.exit().
958
+ // 'exit' covers the SIGINT/SIGTERM handlers too — both leave through
959
+ // standDownExit.mjs's `leave`, which ends in process.exit().
894
960
  process.on('exit', release);
895
961
  return release;
896
962
  }
@@ -906,7 +972,9 @@ function makeRelease(path) {
906
972
  * for the reason `standDown` gives about SIGKILLs.
907
973
  *
908
974
  * `tally` is mutated in place: `running` counts locks naming a process we
909
- * believe is, or might be, a daemon; the rest are what the caller reports.
975
+ * believe is, or might be, a daemon; `draining` counts the ones that were
976
+ * obeyed but are still alive, finishing a deploy (also counted `stopped`); the
977
+ * rest are what the caller reports.
910
978
  */
911
979
  function stopLock(path, log, tally) {
912
980
  const holder = readHolder(path);
@@ -960,6 +1028,16 @@ function stopLock(path, log, tally) {
960
1028
  }
961
1029
 
962
1030
  const bad = standDown(holder, path, log);
1031
+ if (bad?.draining) {
1032
+ // Asked, heard, and leaving once its deploy reports: the stop was obeyed,
1033
+ // so this is not a failure — and nothing is forced (see standDown). It is
1034
+ // counted apart too, because it is still ALIVE: a disconnect must not go
1035
+ // on to leave and forget under it (machineDisconnect.mjs).
1036
+ tally.stopped++;
1037
+ tally.draining++;
1038
+ log(`daemon pid ${holder.pid}${where}: ${bad.failed}.`);
1039
+ return;
1040
+ }
963
1041
  if (bad) {
964
1042
  tally.failed++;
965
1043
  log(`could not stop daemon pid ${holder.pid}${where}: ${bad.failed}`);
@@ -985,7 +1063,7 @@ function stopLock(path, log, tally) {
985
1063
  * running" rather than failing. Returns the same tally `stopAllDaemons` does.
986
1064
  */
987
1065
  export function stopDaemonFor(fleetToken, { log = (m) => console.log(m) } = {}) {
988
- const tally = { stopped: 0, unconfirmed: 0, failed: 0, running: 0 };
1066
+ const tally = { stopped: 0, unconfirmed: 0, failed: 0, running: 0, draining: 0 };
989
1067
  stopLock(instanceLockPath(fleetToken), log, tally);
990
1068
  return tally;
991
1069
  }
@@ -1038,7 +1116,7 @@ export function stopDaemonFor(fleetToken, { log = (m) => console.log(m) } = {})
1038
1116
  * exit code.
1039
1117
  */
1040
1118
  export function stopAllDaemons({ log = (m) => console.log(m) } = {}) {
1041
- const tally = { stopped: 0, unconfirmed: 0, failed: 0, running: 0 };
1119
+ const tally = { stopped: 0, unconfirmed: 0, failed: 0, running: 0, draining: 0 };
1042
1120
  for (const path of lockFiles()) stopLock(path, log, tally);
1043
1121
  const { stopped, unconfirmed, failed, running } = tally;
1044
1122
 
@@ -1046,8 +1124,8 @@ export function stopAllDaemons({ log = (m) => console.log(m) } = {}) {
1046
1124
  // sentence — "no flowviant daemon is running on this machine" — was asserted
1047
1125
  // from lock files alone, and there are several ways to run a daemon that
1048
1126
  // holds no readable lock: FLOWVIANT_ALLOW_MULTI=1 returns before the
1049
- // filesystem is touched (and fleet.mjs PRINTS that flag as the way out of an
1050
- // "already running" refusal, so a stuck user is steered straight onto it), an
1127
+ // filesystem is touched (and fleetStartup.mjs PRINTS that flag as the way out
1128
+ // of an "already running" refusal, so a stuck user is steered onto it), an
1051
1129
  // unwritable ~/.flowviant runs `unguarded`, and every daemon before 0.51.2
1052
1130
  // predates the lock entirely. Telling somebody "nothing is running" while
1053
1131
  // something is, is this product's cardinal sin: it turns ignorance into a