flowviant 0.93.0 → 0.95.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.
- package/README.md +22 -0
- package/bin/cli.mjs +122 -30
- package/bin/lib/artifacts.mjs +298 -0
- package/bin/lib/claude.mjs +9 -1
- package/bin/lib/credentials.mjs +55 -0
- package/bin/lib/fleet.mjs +34 -0
- package/bin/lib/instance.mjs +98 -65
- package/bin/lib/knowledge.mjs +531 -0
- package/bin/lib/login.mjs +77 -2
- package/bin/lib/machines.mjs +216 -5
- package/bin/lib/prompts.mjs +89 -0
- package/bin/lib/runtimes.mjs +6 -1
- package/bin/lib/work.mjs +110 -4
- package/package.json +1 -1
package/bin/lib/fleet.mjs
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
isValidPrUrl,
|
|
48
48
|
isValidBranch,
|
|
49
49
|
isSafePathSegment,
|
|
50
|
+
excludeInWorktree,
|
|
50
51
|
} from './git.mjs';
|
|
51
52
|
import { c, info, note, ok, warn, fail } from './ui.mjs';
|
|
52
53
|
import { revertPatch, withPatchLock } from './patch.mjs';
|
|
@@ -80,6 +81,7 @@ import {
|
|
|
80
81
|
THINK_MARKER,
|
|
81
82
|
} from './runtimes.mjs';
|
|
82
83
|
import { createWorkManager } from './work.mjs';
|
|
84
|
+
import { createKnowledgeSync, knowledgeFetcher, FLOWVIANT_OWN_PATHS } from './knowledge.mjs';
|
|
83
85
|
import { effectiveMaxTurns, setServerMaxTurns } from './admission.mjs';
|
|
84
86
|
import { scanLocalSessions, ourConversationIds } from './localSessions.mjs';
|
|
85
87
|
import { repoState } from './repoState.mjs';
|
|
@@ -1102,6 +1104,23 @@ export async function runFleetDaemon() {
|
|
|
1102
1104
|
*/
|
|
1103
1105
|
let baseRef = detectBaseRef(repoRoot);
|
|
1104
1106
|
const getBaseRef = () => baseRef;
|
|
1107
|
+
/**
|
|
1108
|
+
* THE PROJECT'S KNOWLEDGE LIBRARY (0.94.0) — synced into THIS checkout's
|
|
1109
|
+
* `.flowviant/knowledge/` whenever the roster's manifest moves. One copy per
|
|
1110
|
+
* box, in the checkout, because every worktree on the box can read an
|
|
1111
|
+
* absolute path (knowledge.mjs says why a copy per worktree is wrong). The
|
|
1112
|
+
* exclude is written first so the library never shows as a change in the
|
|
1113
|
+
* operator's own `git status` or in a checkout tab's diffstat.
|
|
1114
|
+
*/
|
|
1115
|
+
/** The roster's `artifactsAccepted`, latest poll — see `getArtifactsAccepted`
|
|
1116
|
+
* on the work manager. False until a server says otherwise. */
|
|
1117
|
+
let artifactsAccepted = false;
|
|
1118
|
+
const knowledgeSync = createKnowledgeSync({
|
|
1119
|
+
checkoutDir: repoRoot,
|
|
1120
|
+
fetchFile: knowledgeFetcher({ fleetUrl: FLEET_URL, token: FLEET_TOKEN, userAgent: USER_AGENT }),
|
|
1121
|
+
onExclude: (dir) => excludeInWorktree(dir, FLOWVIANT_OWN_PATHS),
|
|
1122
|
+
log: (line) => note(c.dim(line)),
|
|
1123
|
+
});
|
|
1105
1124
|
info(SAFE ? 'mode · safe (restricted toolset)' : 'mode · unattended (skips permission prompts)');
|
|
1106
1125
|
// WHICH PROJECT, before anything connects — the roster names it again a few
|
|
1107
1126
|
// seconds later with the server's word, but "which project is this daemon
|
|
@@ -1527,6 +1546,12 @@ export async function runFleetDaemon() {
|
|
|
1527
1546
|
* same one turn and this can never double-count.
|
|
1528
1547
|
*/
|
|
1529
1548
|
extraLiveTurns: () => (wikiBusy || wikiChild ? 1 : 0),
|
|
1549
|
+
/**
|
|
1550
|
+
* Whether the server takes artifacts — the roster's own word, latest poll
|
|
1551
|
+
* (2026-09-22). Read lazily for the reason above: the roster loop that
|
|
1552
|
+
* writes it runs long after this manager is built.
|
|
1553
|
+
*/
|
|
1554
|
+
getArtifactsAccepted: () => artifactsAccepted,
|
|
1530
1555
|
/**
|
|
1531
1556
|
* "THE REPO JUST CHANGED — look again."
|
|
1532
1557
|
*
|
|
@@ -2554,6 +2579,15 @@ export async function runFleetDaemon() {
|
|
|
2554
2579
|
// per-session: `git show` runs from the repo ROOT, which can see a closed
|
|
2555
2580
|
// tab's branch and a shipped commit on main alike.
|
|
2556
2581
|
processDiffJobs(roster.diffJobs);
|
|
2582
|
+
// The knowledge library: synced when its rev moves, left ALONE when the key
|
|
2583
|
+
// is absent (an older server, or a project that never had one). Never
|
|
2584
|
+
// awaited — a fifty-megabyte library must not hold a roster tick, and the
|
|
2585
|
+
// sync serialises itself. The next turn to spawn after it lands reads it.
|
|
2586
|
+
void knowledgeSync.onRoster(roster.knowledge);
|
|
2587
|
+
// ARTIFACTS (0.94.0): the server's own word on whether it can show one,
|
|
2588
|
+
// re-read every poll. Absent is an older server and means false — the turn
|
|
2589
|
+
// prompts then say nothing about a panel nobody can draw.
|
|
2590
|
+
artifactsAccepted = roster.artifactsAccepted === true;
|
|
2557
2591
|
// Shares to open or tear down. CLAIMED before acted on — two daemons on one
|
|
2558
2592
|
// credential are both handed this array, and both opening a tunnel strands
|
|
2559
2593
|
// a public hostname nobody can settle.
|
package/bin/lib/instance.mjs
CHANGED
|
@@ -780,6 +780,101 @@ function makeRelease(path) {
|
|
|
780
780
|
return release;
|
|
781
781
|
}
|
|
782
782
|
|
|
783
|
+
/**
|
|
784
|
+
* ONE LOCK FILE, EXAMINED AND — IF IT NAMES A LIVE DAEMON WE CAN IDENTIFY —
|
|
785
|
+
* STOPPED. The per-file half of `stopAllDaemons`, split out (0.95.0) so that
|
|
786
|
+
* `stopDaemonFor` can run the identical ritual on ONE credential's lock: the
|
|
787
|
+
* `machines` command's disconnect stops the daemon serving the project being
|
|
788
|
+
* disconnected and must not touch the daemons serving anything else on the
|
|
789
|
+
* box, which a sweep of every lock cannot promise. Same tri-state, same
|
|
790
|
+
* sentences, same refusal to guess — there is one copy of this on purpose,
|
|
791
|
+
* for the reason `standDown` gives about SIGKILLs.
|
|
792
|
+
*
|
|
793
|
+
* `tally` is mutated in place: `running` counts locks naming a process we
|
|
794
|
+
* believe is, or might be, a daemon; the rest are what the caller reports.
|
|
795
|
+
*/
|
|
796
|
+
function stopLock(path, log, tally) {
|
|
797
|
+
const holder = readHolder(path);
|
|
798
|
+
if (!holder) return; // absent, truncated, half-written — no claim to answer
|
|
799
|
+
const where = holder.repoRoot ? ` in ${holder.repoRoot}` : '';
|
|
800
|
+
const what = holder.version ? ` ${holder.version}` : '';
|
|
801
|
+
|
|
802
|
+
if (!alive(holder.pid)) {
|
|
803
|
+
log(`pid ${holder.pid}${where} is already gone — nothing to stop.`);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
// Us. standDown refuses this too, but reaching it would print a stand-down
|
|
807
|
+
// line and then a failure for the one pid we are certain is not a daemon.
|
|
808
|
+
if (holder.pid === process.pid) return;
|
|
809
|
+
|
|
810
|
+
const identified = stillTheHolder(holder);
|
|
811
|
+
if (identified === false) {
|
|
812
|
+
// A live pid, but measured NOT to be the process that wrote this lock. The
|
|
813
|
+
// daemon is gone; the number was handed to something else. Not counted as
|
|
814
|
+
// running, and deliberately not counted as a failure either.
|
|
815
|
+
log(`pid ${holder.pid} is no longer the daemon that took this lock — nothing signalled.`);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
tally.running++;
|
|
819
|
+
if (identified === null) {
|
|
820
|
+
tally.unconfirmed++;
|
|
821
|
+
tally.failed++;
|
|
822
|
+
// NAMES BOTH CAUSES, because null has two and we did not measure which:
|
|
823
|
+
// the lock may carry no witness to match against (neither `entry` nor
|
|
824
|
+
// `startedAt` — a hand-edited or half-written file), or this host may hide
|
|
825
|
+
// the process from us (hidepid=2, a pid namespace, an image with no `ps`).
|
|
826
|
+
// "no readable /proc" alone is what takeover says, and said HERE it would
|
|
827
|
+
// assert a diagnosis nobody established — over a live process we are about
|
|
828
|
+
// to tell someone to kill.
|
|
829
|
+
// `kill` is not a command on Windows, where platform() is 'win32' and
|
|
830
|
+
// processStartedAt has no implementation at all — so EVERY lock lands in
|
|
831
|
+
// this branch and the whole command is a no-op that exits 1. Say that
|
|
832
|
+
// once, in the platform's own vocabulary, rather than handing someone a
|
|
833
|
+
// remedy their shell does not have.
|
|
834
|
+
const byHand =
|
|
835
|
+
platform() === 'win32'
|
|
836
|
+
? `taskkill /PID ${holder.pid} /F`
|
|
837
|
+
: `kill ${holder.pid}`;
|
|
838
|
+
log(
|
|
839
|
+
`could not confirm that pid ${holder.pid} is still a flowviant daemon — its lock carries ` +
|
|
840
|
+
`nothing to match it against, or this host hides the process from us` +
|
|
841
|
+
`${platform() === 'win32' ? ' (identifying a process is not implemented on Windows)' : ''}` +
|
|
842
|
+
` — so it was NOT signalled. Stop it by hand: ${byHand}`
|
|
843
|
+
);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const bad = standDown(holder, path, log);
|
|
848
|
+
if (bad) {
|
|
849
|
+
tally.failed++;
|
|
850
|
+
log(`could not stop daemon pid ${holder.pid}${where}: ${bad.failed}`);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
tally.stopped++;
|
|
854
|
+
log(`stopped daemon${what} pid ${holder.pid}${where}.`);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* STOP THE DAEMON SERVING ONE CREDENTIAL — `flowviant machines` disconnecting
|
|
859
|
+
* this box from one project (0.95.0).
|
|
860
|
+
*
|
|
861
|
+
* `stopAllDaemons` is the right shape for a person who does not know what is
|
|
862
|
+
* running; this is for a person who has just NAMED a project in a menu and
|
|
863
|
+
* expects exactly that project's daemon to stop. The lock is keyed on the
|
|
864
|
+
* credential (`instanceLockPath`), so the file to examine is known without a
|
|
865
|
+
* sweep, and a daemon serving a sibling project in another directory is never
|
|
866
|
+
* examined, let alone signalled.
|
|
867
|
+
*
|
|
868
|
+
* NO LOCK IS NOT AN ERROR: a project connected here but not currently running
|
|
869
|
+
* is the ordinary case for a disconnect, and the caller says "nothing was
|
|
870
|
+
* running" rather than failing. Returns the same tally `stopAllDaemons` does.
|
|
871
|
+
*/
|
|
872
|
+
export function stopDaemonFor(fleetToken, { log = (m) => console.log(m) } = {}) {
|
|
873
|
+
const tally = { stopped: 0, unconfirmed: 0, failed: 0, running: 0 };
|
|
874
|
+
stopLock(instanceLockPath(fleetToken), log, tally);
|
|
875
|
+
return tally;
|
|
876
|
+
}
|
|
877
|
+
|
|
783
878
|
/**
|
|
784
879
|
* STOP EVERY FLOWVIANT DAEMON ON THIS MACHINE — `flowviant stop`.
|
|
785
880
|
*
|
|
@@ -828,71 +923,9 @@ function makeRelease(path) {
|
|
|
828
923
|
* exit code.
|
|
829
924
|
*/
|
|
830
925
|
export function stopAllDaemons({ log = (m) => console.log(m) } = {}) {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
let running = 0; // locks naming a process we believe is, or might be, a daemon
|
|
835
|
-
|
|
836
|
-
for (const path of lockFiles()) {
|
|
837
|
-
const holder = readHolder(path);
|
|
838
|
-
if (!holder) continue; // absent, truncated, half-written — no claim to answer
|
|
839
|
-
const where = holder.repoRoot ? ` in ${holder.repoRoot}` : '';
|
|
840
|
-
const what = holder.version ? ` ${holder.version}` : '';
|
|
841
|
-
|
|
842
|
-
if (!alive(holder.pid)) {
|
|
843
|
-
log(`pid ${holder.pid}${where} is already gone — nothing to stop.`);
|
|
844
|
-
continue;
|
|
845
|
-
}
|
|
846
|
-
// Us. standDown refuses this too, but reaching it would print a stand-down
|
|
847
|
-
// line and then a failure for the one pid we are certain is not a daemon.
|
|
848
|
-
if (holder.pid === process.pid) continue;
|
|
849
|
-
|
|
850
|
-
const identified = stillTheHolder(holder);
|
|
851
|
-
if (identified === false) {
|
|
852
|
-
// A live pid, but measured NOT to be the process that wrote this lock. The
|
|
853
|
-
// daemon is gone; the number was handed to something else. Not counted as
|
|
854
|
-
// running, and deliberately not counted as a failure either.
|
|
855
|
-
log(`pid ${holder.pid} is no longer the daemon that took this lock — nothing signalled.`);
|
|
856
|
-
continue;
|
|
857
|
-
}
|
|
858
|
-
running++;
|
|
859
|
-
if (identified === null) {
|
|
860
|
-
unconfirmed++;
|
|
861
|
-
failed++;
|
|
862
|
-
// NAMES BOTH CAUSES, because null has two and we did not measure which:
|
|
863
|
-
// the lock may carry no witness to match against (neither `entry` nor
|
|
864
|
-
// `startedAt` — a hand-edited or half-written file), or this host may hide
|
|
865
|
-
// the process from us (hidepid=2, a pid namespace, an image with no `ps`).
|
|
866
|
-
// "no readable /proc" alone is what takeover says, and said HERE it would
|
|
867
|
-
// assert a diagnosis nobody established — over a live process we are about
|
|
868
|
-
// to tell someone to kill.
|
|
869
|
-
// `kill` is not a command on Windows, where platform() is 'win32' and
|
|
870
|
-
// processStartedAt has no implementation at all — so EVERY lock lands in
|
|
871
|
-
// this branch and the whole command is a no-op that exits 1. Say that
|
|
872
|
-
// once, in the platform's own vocabulary, rather than handing someone a
|
|
873
|
-
// remedy their shell does not have.
|
|
874
|
-
const byHand =
|
|
875
|
-
platform() === 'win32'
|
|
876
|
-
? `taskkill /PID ${holder.pid} /F`
|
|
877
|
-
: `kill ${holder.pid}`;
|
|
878
|
-
log(
|
|
879
|
-
`could not confirm that pid ${holder.pid} is still a flowviant daemon — its lock carries ` +
|
|
880
|
-
`nothing to match it against, or this host hides the process from us` +
|
|
881
|
-
`${platform() === 'win32' ? ' (identifying a process is not implemented on Windows)' : ''}` +
|
|
882
|
-
` — so it was NOT signalled. Stop it by hand: ${byHand}`
|
|
883
|
-
);
|
|
884
|
-
continue;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
const bad = standDown(holder, path, log);
|
|
888
|
-
if (bad) {
|
|
889
|
-
failed++;
|
|
890
|
-
log(`could not stop daemon pid ${holder.pid}${where}: ${bad.failed}`);
|
|
891
|
-
continue;
|
|
892
|
-
}
|
|
893
|
-
stopped++;
|
|
894
|
-
log(`stopped daemon${what} pid ${holder.pid}${where}.`);
|
|
895
|
-
}
|
|
926
|
+
const tally = { stopped: 0, unconfirmed: 0, failed: 0, running: 0 };
|
|
927
|
+
for (const path of lockFiles()) stopLock(path, log, tally);
|
|
928
|
+
const { stopped, unconfirmed, failed, running } = tally;
|
|
896
929
|
|
|
897
930
|
// WHAT WE ACTUALLY MEASURED IS LOCKS, so that is what this says. The old
|
|
898
931
|
// sentence — "no flowviant daemon is running on this machine" — was asserted
|