flowviant 0.116.0 → 0.124.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 +66 -12
- package/bin/cli.mjs +50 -17
- package/bin/lib/admission.mjs +7 -14
- package/bin/lib/agentAsk.mjs +123 -0
- package/bin/lib/agentBrief.mjs +36 -0
- package/bin/lib/agentFootprint.mjs +76 -0
- package/bin/lib/agentInternalWait.mjs +83 -0
- package/bin/lib/agentPlan.mjs +134 -31
- package/bin/lib/agentPrerequisiteContract.mjs +43 -0
- package/bin/lib/agentPublish.mjs +12 -13
- package/bin/lib/agentTaskKinds.mjs +5 -8
- package/bin/lib/agySessions.mjs +3 -3
- package/bin/lib/artifactContracts.mjs +91 -20
- package/bin/lib/artifactIteration.mjs +182 -0
- package/bin/lib/artifactPreview.mjs +92 -18
- package/bin/lib/artifactRecovery.mjs +36 -0
- package/bin/lib/artifactReporter.mjs +194 -22
- package/bin/lib/artifacts.mjs +70 -63
- package/bin/lib/authproxy.mjs +2 -1
- package/bin/lib/childEnv.mjs +9 -4
- package/bin/lib/claude.mjs +10 -20
- package/bin/lib/claudeAuth.mjs +15 -5
- package/bin/lib/claudePersonal.mjs +71 -6
- package/bin/lib/claudePosture.mjs +227 -54
- package/bin/lib/claudeSessions.mjs +7 -7
- package/bin/lib/claudeStream.mjs +27 -13
- package/bin/lib/cliLimit.mjs +2 -2
- package/bin/lib/codexHome.mjs +17 -0
- package/bin/lib/codexPersonal.mjs +44 -3
- package/bin/lib/codexProjectRead.mjs +63 -0
- package/bin/lib/codexRelay.mjs +21 -5
- package/bin/lib/codexResearch.mjs +193 -0
- package/bin/lib/commandSpecs.mjs +6 -0
- package/bin/lib/config.mjs +37 -24
- package/bin/lib/conversationIds.mjs +51 -0
- package/bin/lib/credentials.mjs +2 -7
- package/bin/lib/daemonLocks.mjs +90 -0
- package/bin/lib/daemonState.mjs +5 -5
- package/bin/lib/deploy.mjs +6 -9
- package/bin/lib/deployCheckout.mjs +26 -22
- package/bin/lib/deployRunner.mjs +54 -18
- package/bin/lib/deployWire.mjs +32 -23
- package/bin/lib/desktopContract.mjs +6 -5
- package/bin/lib/discoveryContract.mjs +19 -0
- package/bin/lib/env.mjs +104 -35
- package/bin/lib/fleet.mjs +53 -13
- package/bin/lib/fleetPost.mjs +172 -25
- package/bin/lib/fleetReports.mjs +172 -171
- package/bin/lib/fleetStartup.mjs +6 -6
- package/bin/lib/fleetWire.mjs +42 -0
- package/bin/lib/git.mjs +111 -61
- package/bin/lib/gitMergeIdentity.mjs +2 -8
- package/bin/lib/grant.mjs +23 -7
- package/bin/lib/holder.mjs +108 -81
- package/bin/lib/hooks/mcpAllow.mjs +60 -0
- package/bin/lib/hooks/readGuard.mjs +54 -11
- package/bin/lib/imageArtifact.mjs +74 -0
- package/bin/lib/install.mjs +80 -33
- package/bin/lib/instance.mjs +36 -83
- package/bin/lib/intakeDraft.mjs +4 -1
- package/bin/lib/knowledgeFileSync.mjs +31 -0
- package/bin/lib/knowledgeFiles.mjs +28 -83
- package/bin/lib/landed.mjs +22 -29
- package/bin/lib/launchCommand.mjs +91 -0
- package/bin/lib/listeners.mjs +0 -5
- package/bin/lib/localSessions.mjs +52 -39
- package/bin/lib/login.mjs +22 -12
- package/bin/lib/loginPath.mjs +22 -17
- package/bin/lib/machineListing.mjs +6 -3
- package/bin/lib/maintenance.mjs +47 -0
- package/bin/lib/maintenanceInventory.mjs +39 -0
- package/bin/lib/mcp-cli.mjs +9 -10
- package/bin/lib/mcpInstall.mjs +163 -0
- package/bin/lib/mcpInventory.mjs +119 -0
- package/bin/lib/noWindowEnv.mjs +23 -3
- package/bin/lib/ownedDirs.mjs +99 -0
- package/bin/lib/pinnedCapture.mjs +383 -0
- package/bin/lib/planDependencies.mjs +70 -0
- package/bin/lib/postureTools.mjs +85 -0
- package/bin/lib/prWorkflow.mjs +94 -17
- package/bin/lib/preflight.mjs +8 -25
- package/bin/lib/preview.mjs +19 -25
- package/bin/lib/previewGatePolicy.mjs +34 -9
- package/bin/lib/previewRegistry.mjs +4 -11
- package/bin/lib/printable.mjs +70 -7
- package/bin/lib/procRegistry.mjs +33 -13
- package/bin/lib/procScan.mjs +2 -3
- package/bin/lib/procStat.mjs +76 -0
- package/bin/lib/processGroup.mjs +48 -0
- package/bin/lib/processes.mjs +10 -24
- package/bin/lib/projectToolRuntimes.mjs +328 -19
- package/bin/lib/projectTools.mjs +109 -34
- package/bin/lib/prompts.mjs +205 -60
- package/bin/lib/repoPath.mjs +19 -0
- package/bin/lib/repoState.mjs +17 -19
- package/bin/lib/repositoryBaseline.mjs +68 -0
- package/bin/lib/runTurn.mjs +286 -209
- package/bin/lib/runtimeAntigravity.mjs +16 -5
- package/bin/lib/runtimeCapabilities.mjs +215 -21
- package/bin/lib/runtimeClaude.mjs +78 -10
- package/bin/lib/runtimeCodex.mjs +217 -209
- package/bin/lib/runtimeDetection.mjs +8 -2
- package/bin/lib/runtimeEvents.mjs +28 -11
- package/bin/lib/runtimeLimits.mjs +5 -6
- package/bin/lib/runtimeOfferings.mjs +99 -14
- package/bin/lib/runtimes.mjs +10 -0
- package/bin/lib/shipSweep.mjs +5 -3
- package/bin/lib/shot.mjs +91 -17
- package/bin/lib/sleep.mjs +11 -0
- package/bin/lib/startProject.mjs +5 -5
- package/bin/lib/statePaths.mjs +40 -0
- package/bin/lib/tty.mjs +25 -15
- package/bin/lib/turnDiagnostics.mjs +93 -0
- package/bin/lib/turnLock.mjs +7 -14
- package/bin/lib/turnProfile.mjs +16 -4
- package/bin/lib/ui.mjs +0 -3
- package/bin/lib/uninstall.mjs +30 -26
- package/bin/lib/update.mjs +79 -27
- package/bin/lib/uplinkScrub.mjs +296 -31
- package/bin/lib/vault.mjs +78 -10
- package/bin/lib/vaultDiff.mjs +5 -2
- package/bin/lib/views.mjs +37 -12
- package/bin/lib/wikiRunner.mjs +40 -61
- package/bin/lib/work.mjs +44 -13
- package/bin/lib/workAdoptCarry.mjs +2 -4
- package/bin/lib/workAgentBrief.mjs +92 -0
- package/bin/lib/workAgentBriefRunner.mjs +59 -0
- package/bin/lib/workAgentCheck.mjs +104 -57
- package/bin/lib/workAgentMerges.mjs +62 -57
- package/bin/lib/workAgentPlans.mjs +82 -199
- package/bin/lib/workAgentPrecheck.mjs +76 -138
- package/bin/lib/workAgentPublish.mjs +6 -8
- package/bin/lib/workAgentTurnBegun.mjs +15 -11
- package/bin/lib/workAgentTurnExecution.mjs +131 -67
- package/bin/lib/workAgentTurnOutcome.mjs +11 -2
- package/bin/lib/workAgentTurnReports.mjs +9 -37
- package/bin/lib/workAgentTurns.mjs +64 -7
- package/bin/lib/workArtifactReconcile.mjs +79 -0
- package/bin/lib/workAttachments.mjs +66 -11
- package/bin/lib/workBrain.mjs +9 -4
- package/bin/lib/workCappedTurn.mjs +119 -0
- package/bin/lib/workChildren.mjs +5 -5
- package/bin/lib/workCommandAudit.mjs +18 -2
- package/bin/lib/workDiffs.mjs +52 -6
- package/bin/lib/workIntake.mjs +35 -60
- package/bin/lib/workPlaceReports.mjs +7 -4
- package/bin/lib/workPlaces.mjs +113 -31
- package/bin/lib/workPreviews.mjs +47 -112
- package/bin/lib/workProcesses.mjs +8 -35
- package/bin/lib/workPullRequests.mjs +37 -56
- package/bin/lib/workReportQueue.mjs +100 -148
- package/bin/lib/workRetire.mjs +2 -1
- package/bin/lib/workSessionRuntime.mjs +38 -37
- package/bin/lib/workSessionTurns.mjs +256 -199
- package/bin/lib/workShip.mjs +43 -23
- package/bin/lib/workToolLog.mjs +70 -3
- package/bin/lib/workTurnShape.mjs +165 -0
- package/npm-shrinkwrap.json +69 -0
- package/package.json +9 -4
- package/bin/lib/fleetJobs.mjs +0 -120
- package/bin/lib/patchRevert.mjs +0 -60
package/bin/lib/fleetReports.mjs
CHANGED
|
@@ -6,18 +6,20 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Split out of fleet.mjs (SOLID audit 2026-09-26, F038). Each lane is its own
|
|
8
8
|
* throttle, its own dedup against the last ACCEPTED payload, and its own
|
|
9
|
-
* "the server refused for good — quiet until restart" flag;
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* "the server refused for good — quiet until restart" flag; since 2026-10-10
|
|
10
|
+
* (F087) that state lives in one `createReportLane` closure per lane rather
|
|
11
|
+
* than four hand-kept sets of module `let`s, and none of it is the reconcile
|
|
12
|
+
* loop's business. The loop fires each one and forgets it (`void`), which is
|
|
13
|
+
* the one contract they all keep: a readout must never throw into, or delay,
|
|
14
|
+
* the poll.
|
|
13
15
|
*
|
|
14
16
|
* Every lane is a DAEMON→SERVER report, so none needs a floor: an older server
|
|
15
17
|
* 404s once and the lane goes quiet; an older daemon simply never posts.
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
|
-
import { FLEET_URL
|
|
20
|
+
import { FLEET_URL } from './config.mjs';
|
|
19
21
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
20
|
-
import { postToFleet } from './fleetPost.mjs';
|
|
22
|
+
import { fleetRequest, postToFleet } from './fleetPost.mjs';
|
|
21
23
|
import { myPubB64 } from './boxIdentity.mjs';
|
|
22
24
|
import { scanEnvForScrub } from './env.mjs';
|
|
23
25
|
import { machineSnapshot } from './resources.mjs';
|
|
@@ -25,8 +27,83 @@ import { detectRuntimes } from './runtimeDetection.mjs';
|
|
|
25
27
|
import { scanLocalSessions } from './localSessions.mjs';
|
|
26
28
|
import { repoState } from './repoState.mjs';
|
|
27
29
|
import { claudeAuthContext } from './claudeAuth.mjs';
|
|
28
|
-
import { readBaseTools, toolReadout } from './projectTools.mjs';
|
|
30
|
+
import { prepareAgentTools, readBaseTools, toolReadout } from './projectTools.mjs';
|
|
29
31
|
import { runnerToolCapabilities } from './projectToolRuntimes.mjs';
|
|
32
|
+
import { buildMcpInventory } from './mcpInventory.mjs';
|
|
33
|
+
import { knownClaudeMcpInventory, probeMcpNow } from './runtimeCapabilities.mjs';
|
|
34
|
+
import { codexMcpServers } from './runtimeOfferings.mjs';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* ONE READOUT LANE (audit 2026-10-10, F087/F752). The four readouts below —
|
|
38
|
+
* local sessions, repo state, env and tools — were four hand copies of one
|
|
39
|
+
* state machine over module-level `let`s, and their gate orders and refusal
|
|
40
|
+
* rules had drifted without a word. The machine, once:
|
|
41
|
+
*
|
|
42
|
+
* 1. latched quiet? say nothing (`scanBeforeGate` moves this AFTER the
|
|
43
|
+
* build — the env lane, whose scan feeds the scrubber and must run
|
|
44
|
+
* whether or not anything can be posted);
|
|
45
|
+
* 2. scanned less than `scanMs` ago? say nothing, else stamp the scan;
|
|
46
|
+
* 3. build the payload STRING — a throw says nothing (a readout must never
|
|
47
|
+
* throw into the poll), and null means there is nothing to say;
|
|
48
|
+
* 4. equal to the last payload the server ACCEPTED (and, with `resendMs`,
|
|
49
|
+
* accepted inside that window)? say nothing;
|
|
50
|
+
* 5. POST it with the machine credential;
|
|
51
|
+
* 6. a status `isPermanent` names latches the lane quiet until restart;
|
|
52
|
+
* 7. remember the payload only on a 2xx — anything else forgets it, so the
|
|
53
|
+
* next scan retries rather than dedup-suppressing a report nobody got.
|
|
54
|
+
*
|
|
55
|
+
* Each lane's real differences are PARAMETERS, kept exactly as they were:
|
|
56
|
+
* only local sessions has a resend window; local sessions and repo state
|
|
57
|
+
* latch on a 404 alone, env and tools on `envReportIsPermanent`. Unifying the
|
|
58
|
+
* latch rule (wire.md's one "a permanent 4xx is delivered") stays an open
|
|
59
|
+
* call: a 400 latch would silence repo-state for the life of the process
|
|
60
|
+
* after one bad payload.
|
|
61
|
+
*
|
|
62
|
+
* Returns the lane's verdict — 'quiet' | 'throttled' | 'scan-failed' |
|
|
63
|
+
* 'nothing' | 'deduped' | 'accepted' | 'retry' | 'stopped' — which the
|
|
64
|
+
* daemon ignores (every caller is a bare `void`) and the tests drive.
|
|
65
|
+
*/
|
|
66
|
+
function createReportLane({
|
|
67
|
+
endpoint,
|
|
68
|
+
scanMs = 60_000,
|
|
69
|
+
resendMs = null,
|
|
70
|
+
timeoutMs = 15_000,
|
|
71
|
+
isPermanent = (status) => status === 404,
|
|
72
|
+
scanBeforeGate = false,
|
|
73
|
+
}) {
|
|
74
|
+
const url = fleetEndpoint(endpoint, FLEET_URL);
|
|
75
|
+
let unsupported = false; // the server refused for good — quiet until restart
|
|
76
|
+
let scanAt = 0;
|
|
77
|
+
let sent = null; // the last payload the server ACCEPTED, stringified
|
|
78
|
+
let sentAt = 0;
|
|
79
|
+
const report = async (build) => {
|
|
80
|
+
if (!scanBeforeGate && unsupported) return 'quiet';
|
|
81
|
+
if (Date.now() - scanAt < scanMs) return 'throttled';
|
|
82
|
+
scanAt = Date.now();
|
|
83
|
+
let payload;
|
|
84
|
+
try {
|
|
85
|
+
payload = build();
|
|
86
|
+
} catch {
|
|
87
|
+
return 'scan-failed';
|
|
88
|
+
}
|
|
89
|
+
if (scanBeforeGate && unsupported) return 'quiet';
|
|
90
|
+
if (payload == null) return 'nothing';
|
|
91
|
+
if (payload === sent && (resendMs == null || Date.now() - sentAt < resendMs)) return 'deduped';
|
|
92
|
+
const r = await fleetRequest(url, { body: payload, timeoutMs });
|
|
93
|
+
if (isPermanent(r.status)) {
|
|
94
|
+
unsupported = true;
|
|
95
|
+
return 'stopped';
|
|
96
|
+
}
|
|
97
|
+
sent = r.ok ? payload : null;
|
|
98
|
+
sentAt = r.ok ? Date.now() : 0;
|
|
99
|
+
return r.ok ? 'accepted' : 'retry';
|
|
100
|
+
};
|
|
101
|
+
/** "Look again on the next beat" — clears the throttle, nothing else. */
|
|
102
|
+
report.rescan = () => {
|
|
103
|
+
scanAt = 0;
|
|
104
|
+
};
|
|
105
|
+
return report;
|
|
106
|
+
}
|
|
30
107
|
|
|
31
108
|
/**
|
|
32
109
|
* Terminal-session presence: tell the server which Claude sessions exist in
|
|
@@ -40,57 +117,17 @@ import { runnerToolCapabilities } from './projectToolRuntimes.mjs';
|
|
|
40
117
|
* process stops asking (a deploy that adds it also restarts nothing on this
|
|
41
118
|
* machine, so silence-until-restart costs one daemon restart, not a feature).
|
|
42
119
|
*/
|
|
43
|
-
const LOCAL_SESSIONS_URL = fleetEndpoint('local-sessions', FLEET_URL);
|
|
44
|
-
const LOCAL_SESSIONS_SCAN_MS = 60_000;
|
|
45
120
|
// The web hides a report older than 10 minutes (presence must not linger as
|
|
46
121
|
// fact after the machine dies), so an UNCHANGED report is re-sent inside that
|
|
47
122
|
// window anyway — the re-send is the machine's heartbeat on this fact, and
|
|
48
123
|
// suppressing it entirely would blank the strip while everything still holds.
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (Date.now() - localSessionsScanAt < LOCAL_SESSIONS_SCAN_MS) return;
|
|
57
|
-
localSessionsScanAt = Date.now();
|
|
58
|
-
let payload;
|
|
59
|
-
try {
|
|
60
|
-
// scanLocalSessions orders deterministically, so this string only changes
|
|
61
|
-
// when the facts on disk do — the dedup below compares whole payloads.
|
|
62
|
-
payload = JSON.stringify({
|
|
63
|
-
sessions: scanLocalSessions({ repoRoot, excludeDirs, excludeIds }),
|
|
64
|
-
});
|
|
65
|
-
} catch {
|
|
66
|
-
return; // presence must never throw into the poll loop
|
|
67
|
-
}
|
|
68
|
-
if (payload === localSessionsSent && Date.now() - localSessionsSentAt < LOCAL_SESSIONS_RESEND_MS)
|
|
69
|
-
return;
|
|
70
|
-
try {
|
|
71
|
-
const res = await fetch(LOCAL_SESSIONS_URL, {
|
|
72
|
-
method: 'POST',
|
|
73
|
-
headers: {
|
|
74
|
-
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
75
|
-
'User-Agent': USER_AGENT,
|
|
76
|
-
'Content-Type': 'application/json',
|
|
77
|
-
},
|
|
78
|
-
signal: AbortSignal.timeout(15_000),
|
|
79
|
-
body: payload,
|
|
80
|
-
});
|
|
81
|
-
if (res.status === 404) {
|
|
82
|
-
localSessionsUnsupported = true; // older server — it REPLACED nothing here
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
// Only an accepted report counts as sent; anything else forgets the
|
|
86
|
-
// last-sent payload so the next pass retries instead of dedup-suppressing
|
|
87
|
-
// a report the server never received.
|
|
88
|
-
localSessionsSent = res.ok ? payload : null;
|
|
89
|
-
localSessionsSentAt = res.ok ? Date.now() : 0;
|
|
90
|
-
} catch {
|
|
91
|
-
localSessionsSent = null;
|
|
92
|
-
localSessionsSentAt = 0;
|
|
93
|
-
}
|
|
124
|
+
const localSessionsLane = createReportLane({ endpoint: 'local-sessions', resendMs: 5 * 60_000 });
|
|
125
|
+
export function maybeReportLocalSessions({ repoRoot, excludeDirs, excludeIds }) {
|
|
126
|
+
// scanLocalSessions orders deterministically, so this string only changes
|
|
127
|
+
// when the facts on disk do — the dedup compares whole payloads.
|
|
128
|
+
return localSessionsLane(() =>
|
|
129
|
+
JSON.stringify({ sessions: scanLocalSessions({ repoRoot, excludeDirs, excludeIds }) })
|
|
130
|
+
);
|
|
94
131
|
}
|
|
95
132
|
|
|
96
133
|
/**
|
|
@@ -108,32 +145,24 @@ export async function maybeReportLocalSessions({ repoRoot, excludeDirs, excludeI
|
|
|
108
145
|
* exists — so re-posting an unchanged list would be a write per machine per
|
|
109
146
|
* five minutes to say nothing at all.
|
|
110
147
|
*/
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
let repoStateScanAt = 0;
|
|
115
|
-
let repoStateSent = null; // last payload the server ACCEPTED, stringified
|
|
116
|
-
export async function maybeReportRepoState({ repoRoot, baseRef }) {
|
|
117
|
-
if (repoStateUnsupported) return;
|
|
118
|
-
if (Date.now() - repoStateScanAt < REPO_STATE_SCAN_MS) return;
|
|
119
|
-
repoStateScanAt = Date.now();
|
|
120
|
-
let payload;
|
|
121
|
-
try {
|
|
148
|
+
const repoStateLane = createReportLane({ endpoint: 'repo-state' });
|
|
149
|
+
export function maybeReportRepoState({ repoRoot, baseRef }) {
|
|
150
|
+
return repoStateLane(() => {
|
|
122
151
|
// The PARAM, and nothing else: this function sits at MODULE scope, where
|
|
123
152
|
// runFleetDaemon's `getBaseRef` closure does not exist. An earlier version
|
|
124
|
-
// reached for it anyway, the ReferenceError landed in the
|
|
125
|
-
// written for an unreadable repo, silent by design — and this
|
|
126
|
-
// never once posted to. The caller reads the loop's live
|
|
127
|
-
// time, so the value here is always current.
|
|
153
|
+
// reached for it anyway, the ReferenceError landed in the lane's build
|
|
154
|
+
// catch — written for an unreadable repo, silent by design — and this
|
|
155
|
+
// endpoint was never once posted to. The caller reads the loop's live
|
|
156
|
+
// `baseRef` at call time, so the value here is always current.
|
|
128
157
|
const state = repoState(repoRoot, baseRef);
|
|
129
|
-
if (!state) return; // not readable — say nothing rather than say "none"
|
|
158
|
+
if (!state) return null; // not readable — say nothing rather than say "none"
|
|
130
159
|
/**
|
|
131
160
|
* WHICH CREDENTIAL THIS MACHINE'S CLI RESOLVES, on the same beat.
|
|
132
161
|
*
|
|
133
162
|
* It rides this report rather than getting an endpoint of its own because
|
|
134
163
|
* it is the same KIND of fact — something only the machine can see, pushed
|
|
135
164
|
* because a pull client can never be asked — and it inherits this
|
|
136
|
-
*
|
|
165
|
+
* lane's three economies for free: scanned once a minute, deduped
|
|
137
166
|
* against the last ACCEPTED payload, and silent forever once an older
|
|
138
167
|
* server 404s.
|
|
139
168
|
*
|
|
@@ -149,32 +178,8 @@ export async function maybeReportRepoState({ repoRoot, baseRef }) {
|
|
|
149
178
|
* report, and an older SERVER strips the unknown key in its zod parse and
|
|
150
179
|
* stores the rest exactly as before.
|
|
151
180
|
*/
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
return; // a readout must never throw into the poll loop
|
|
155
|
-
}
|
|
156
|
-
if (payload === repoStateSent) return;
|
|
157
|
-
try {
|
|
158
|
-
const res = await fetch(REPO_STATE_URL, {
|
|
159
|
-
method: 'POST',
|
|
160
|
-
headers: {
|
|
161
|
-
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
162
|
-
'User-Agent': USER_AGENT,
|
|
163
|
-
'Content-Type': 'application/json',
|
|
164
|
-
},
|
|
165
|
-
signal: AbortSignal.timeout(15_000),
|
|
166
|
-
body: payload,
|
|
167
|
-
});
|
|
168
|
-
if (res.status === 404) {
|
|
169
|
-
repoStateUnsupported = true; // older server — nothing was replaced here
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
// Only an ACCEPTED report counts: anything else forgets it so the next
|
|
173
|
-
// pass retries rather than dedup-suppressing a report nobody received.
|
|
174
|
-
repoStateSent = res.ok ? payload : null;
|
|
175
|
-
} catch {
|
|
176
|
-
repoStateSent = null;
|
|
177
|
-
}
|
|
181
|
+
return JSON.stringify({ ...state, auth: claudeAuthContext() });
|
|
182
|
+
});
|
|
178
183
|
}
|
|
179
184
|
|
|
180
185
|
/**
|
|
@@ -247,16 +252,24 @@ export async function maybeReportRepoState({ repoRoot, baseRef }) {
|
|
|
247
252
|
* Pure and exported so the decision can be proved without a credential, a
|
|
248
253
|
* server or a poll — the same shape `shouldStop` and `createHolderWatch` keep
|
|
249
254
|
* in holder.mjs.
|
|
255
|
+
*
|
|
256
|
+
* DELIBERATELY NARROWER than fleetWire.mjs's `isConsideredRefusal` (every 4xx
|
|
257
|
+
* but 408/429), which the settle lanes read: a settle that treats a 401 as
|
|
258
|
+
* decided loses one body, a readout that latches on it goes dark for the life
|
|
259
|
+
* of the process. Two vocabularies, each named, never folded together.
|
|
250
260
|
*/
|
|
251
261
|
export function envReportIsPermanent(status) {
|
|
252
262
|
return status === 400 || status === 404 || status === 422;
|
|
253
263
|
}
|
|
254
264
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
265
|
+
// Scans BEFORE the latch: the scrubber has no dedup and must reflect the
|
|
266
|
+
// newest read every time, so a server that cannot take the report never costs
|
|
267
|
+
// this box its redaction (see the docblock above `envReportIsPermanent`).
|
|
268
|
+
const envLane = createReportLane({
|
|
269
|
+
endpoint: 'env-report',
|
|
270
|
+
isPermanent: envReportIsPermanent,
|
|
271
|
+
scanBeforeGate: true,
|
|
272
|
+
});
|
|
260
273
|
/**
|
|
261
274
|
* Returns a one-word VERDICT — 'throttled' | 'scan-failed' | 'quiet' |
|
|
262
275
|
* 'deduped' | 'accepted' | 'retry' | 'stopped'. The daemon ignores it (the
|
|
@@ -265,83 +278,71 @@ let envReportSent = null; // the last report the server ACCEPTED, stringified
|
|
|
265
278
|
* readout whose only proof is that its source LOOKS right is the inert-pin
|
|
266
279
|
* class this repo has caught five times.
|
|
267
280
|
*/
|
|
268
|
-
export
|
|
269
|
-
|
|
270
|
-
envReportScanAt = Date.now();
|
|
271
|
-
let report;
|
|
272
|
-
try {
|
|
281
|
+
export function maybeReportEnv(repoRoot) {
|
|
282
|
+
return envLane(() => {
|
|
273
283
|
// The SCAN happens even when the POST cannot — see the docblock: the
|
|
274
284
|
// scrubber has no dedup and must reflect the newest read every time.
|
|
275
|
-
report = scanEnvForScrub(repoRoot);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
pubkey: myPubB64(),
|
|
282
|
-
files: report.files,
|
|
283
|
-
filesTotal: report.filesTotal,
|
|
284
|
-
varsTotal: report.varsTotal,
|
|
285
|
-
});
|
|
286
|
-
if (payload === envReportSent) return 'deduped';
|
|
287
|
-
try {
|
|
288
|
-
const res = await fetch(ENV_REPORT_URL, {
|
|
289
|
-
method: 'POST',
|
|
290
|
-
headers: {
|
|
291
|
-
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
292
|
-
'User-Agent': USER_AGENT,
|
|
293
|
-
'Content-Type': 'application/json',
|
|
294
|
-
},
|
|
295
|
-
signal: AbortSignal.timeout(15_000),
|
|
296
|
-
body: payload,
|
|
285
|
+
const report = scanEnvForScrub(repoRoot);
|
|
286
|
+
return JSON.stringify({
|
|
287
|
+
pubkey: myPubB64(),
|
|
288
|
+
files: report.files,
|
|
289
|
+
filesTotal: report.filesTotal,
|
|
290
|
+
varsTotal: report.varsTotal,
|
|
297
291
|
});
|
|
298
|
-
|
|
299
|
-
// `envReportIsPermanent`. A 429 or a 401 during a credential blip is a
|
|
300
|
-
// "later", and treating it as a "never" killed env reporting for the life
|
|
301
|
-
// of the process with nothing anywhere saying so.
|
|
302
|
-
if (envReportIsPermanent(res.status)) {
|
|
303
|
-
envReportUnsupported = true;
|
|
304
|
-
return 'stopped';
|
|
305
|
-
}
|
|
306
|
-
// Only an ACCEPTED report counts, the rule `maybeReportRepoState` keeps:
|
|
307
|
-
// anything else forgets it so the next pass retries rather than
|
|
308
|
-
// dedup-suppressing a report nobody received.
|
|
309
|
-
envReportSent = res.ok ? payload : null;
|
|
310
|
-
return res.ok ? 'accepted' : 'retry';
|
|
311
|
-
} catch {
|
|
312
|
-
envReportSent = null;
|
|
313
|
-
return 'retry';
|
|
314
|
-
}
|
|
292
|
+
});
|
|
315
293
|
}
|
|
316
294
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
295
|
+
// Latches on the env lane's short list for the same reason: a 401 or a 429 is
|
|
296
|
+
// a "later", and a lane that dies invisibly must not die for one.
|
|
297
|
+
const toolsLane = createReportLane({ endpoint: 'tools-report', isPermanent: envReportIsPermanent });
|
|
298
|
+
/**
|
|
299
|
+
* Repo tool readiness is a per-box daemon report. Never include env values.
|
|
300
|
+
*
|
|
301
|
+
* `mcp` (2026-10-13) is this box's MCP inventory — every server each installed
|
|
302
|
+
* CLI has, by name, source and status, with a connected server's tool names
|
|
303
|
+
* (mcpInventory.mjs). It rides here rather than on the poll because tool names
|
|
304
|
+
* do not fit a URL, and this lane already posts only on change. An older
|
|
305
|
+
* server's schema strips the key.
|
|
306
|
+
*/
|
|
307
|
+
export function maybeReportTools(repoRoot, baseRef) {
|
|
308
|
+
return toolsLane(() => {
|
|
309
|
+
const pubkey = myPubB64();
|
|
310
|
+
if (!pubkey) return null;
|
|
330
311
|
const snapshot = readBaseTools(repoRoot, baseRef);
|
|
331
312
|
const installed = detectRuntimes().filter((r) => r.installed);
|
|
332
313
|
const runner = installed.find((r) => runnerToolCapabilities(r.id).mcp)?.id ?? installed[0]?.id ?? 'none';
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
314
|
+
const ids = installed.map((r) => r.id);
|
|
315
|
+
let codex = null;
|
|
316
|
+
if (ids.includes('codex')) {
|
|
317
|
+
try { codex = codexMcpServers(); } catch { /* a failed local read is unknown */ }
|
|
318
|
+
}
|
|
319
|
+
const mcp = buildMcpInventory({ installed: ids, snapshot, claude: knownClaudeMcpInventory(), codex });
|
|
320
|
+
return JSON.stringify({ pubkey, tools: toolReadout(snapshot, runner), mcp });
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* LOOK AGAIN AT THIS BOX'S MCP SERVERS, NOW (2026-10-13) — after a confirmed
|
|
326
|
+
* install (mcpInstall.mjs). Codex's list is re-read, Claude Code is probed
|
|
327
|
+
* with the base branch's servers mounted (`probeMcpNow`), and the tool report
|
|
328
|
+
* — whose "command not on PATH" was measured before the install — is rescanned
|
|
329
|
+
* and posted without waiting for its beat. Every step is best-effort.
|
|
330
|
+
*/
|
|
331
|
+
export async function recheckProjectMcp(repoRoot, baseRef) {
|
|
336
332
|
try {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
333
|
+
if (detectRuntimes().some((r) => r.id === 'codex' && r.installed)) codexMcpServers({ refresh: true });
|
|
334
|
+
} catch { /* unknown stays unknown */ }
|
|
335
|
+
if (detectRuntimes().some((r) => r.id === 'claude' && r.installed)) {
|
|
336
|
+
let tools = null;
|
|
337
|
+
try {
|
|
338
|
+
tools = prepareAgentTools(readBaseTools(repoRoot, baseRef), 'claude');
|
|
339
|
+
await probeMcpNow(repoRoot, tools.mcpPath);
|
|
340
|
+
} catch { /* no base read, or no claude: the next turn teaches it */ } finally {
|
|
341
|
+
tools?.cleanup();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
toolsLane.rescan();
|
|
345
|
+
await maybeReportTools(repoRoot, baseRef);
|
|
345
346
|
}
|
|
346
347
|
|
|
347
348
|
/**
|
|
@@ -350,7 +351,7 @@ export async function maybeReportTools(repoRoot, baseRef) {
|
|
|
350
351
|
* it (through fleet.mjs's `onRepoChanged`) after a ship, a retirement or a cut.
|
|
351
352
|
*/
|
|
352
353
|
export function repoStateChanged() {
|
|
353
|
-
|
|
354
|
+
repoStateLane.rescan();
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
const MACHINE_URL = fleetEndpoint('machine', FLEET_URL);
|
package/bin/lib/fleetStartup.mjs
CHANGED
|
@@ -11,14 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
import { mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
13
13
|
import { execFileSync } from 'node:child_process';
|
|
14
|
-
import {
|
|
15
|
-
import { homedir } from 'node:os';
|
|
16
|
-
import { join, basename } from 'node:path';
|
|
14
|
+
import { join } from 'node:path';
|
|
17
15
|
import { FLEET_TOKEN } from './config.mjs';
|
|
18
16
|
import { git } from './git.mjs';
|
|
19
17
|
import { info, note, warn, fail } from './ui.mjs';
|
|
20
18
|
import { acquireInstanceLock } from './instance.mjs';
|
|
21
19
|
import { emitMachineEvent } from './daemonLogging.mjs';
|
|
20
|
+
import { worktreeHomeDir, worktreeHomeKey } from './statePaths.mjs';
|
|
22
21
|
|
|
23
22
|
/** Takes the lock, or says why not and exits 1. Returns only on success. */
|
|
24
23
|
export function takeInstanceLockOrExit(repoRoot) {
|
|
@@ -90,9 +89,10 @@ export function takeInstanceLockOrExit(repoRoot) {
|
|
|
90
89
|
*/
|
|
91
90
|
export function worktreeHome(repoRoot) {
|
|
92
91
|
// Persistent worktree home (0.9.0) — survives daemon restarts AND reboots,
|
|
93
|
-
// so Ctrl+C mid-task never loses local work. Keyed per repo path
|
|
94
|
-
|
|
95
|
-
const
|
|
92
|
+
// so Ctrl+C mid-task never loses local work. Keyed per repo path, by the
|
|
93
|
+
// one derivation deploy checkouts and `flowviant clean` share (statePaths.mjs).
|
|
94
|
+
const repoKey = worktreeHomeKey(repoRoot);
|
|
95
|
+
const baseDir = worktreeHomeDir(repoRoot);
|
|
96
96
|
mkdirSync(baseDir, { recursive: true });
|
|
97
97
|
return { repoKey, baseDir };
|
|
98
98
|
}
|
package/bin/lib/fleetWire.mjs
CHANGED
|
@@ -44,3 +44,45 @@ export function fleetEndpoint(name, rosterUrl) {
|
|
|
44
44
|
}
|
|
45
45
|
return String(rosterUrl).replace(ROSTER_TAIL, `/${name}`);
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* ── WHAT A STATUS SAYS (audit 2026-10-10, F139/F155) ───────────────────────
|
|
50
|
+
*
|
|
51
|
+
* Whether a failed settle is retried or treated as decided is a correctness
|
|
52
|
+
* rule: wrong one way and a side-effecting turn re-runs (workReportQueue's
|
|
53
|
+
* 'reject' history, workAgentTurnReports' six-hour loop); wrong the other way
|
|
54
|
+
* and a refused body is hammered forever. It was spelled as the literal
|
|
55
|
+
* expression below five times and re-derived twice more as if-chains, so a
|
|
56
|
+
* new "later" status (425, say) was seven edits. These are the vocabulary;
|
|
57
|
+
* each lane still chooses its own POLICY over them (held and backed off,
|
|
58
|
+
* counted as delivered, latched quiet), and `envReportIsPermanent`
|
|
59
|
+
* (fleetReports.mjs) is deliberately NARROWER and stays its own rule.
|
|
60
|
+
*
|
|
61
|
+
* Pure and config-free like `fleetEndpoint`, so a module handed URLs rather
|
|
62
|
+
* than config (the artifact reporter) can read them too.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/** "Not now": a timeout or a rate limit wearing a 4xx. Nothing was decided. */
|
|
66
|
+
export function isNotNow(status) {
|
|
67
|
+
return status === 408 || status === 429;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The server's CONSIDERED answer: a 4xx other than 408/429. Retrying it
|
|
71
|
+
* only spends the same refusal again. A missing status (a network error)
|
|
72
|
+
* is never a refusal. */
|
|
73
|
+
export function isConsideredRefusal(status) {
|
|
74
|
+
return status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Retry-After, in ms — seconds or an HTTP-date — capped so one bad or
|
|
78
|
+
* hostile header cannot stall a caller for an hour. Falls back to the
|
|
79
|
+
* caller's own backoff when the header is absent or unparseable. (Moved
|
|
80
|
+
* verbatim from workReportQueue.mjs, 2026-10-10.) */
|
|
81
|
+
export function retryAfterMs(res, fallbackMs) {
|
|
82
|
+
const h = res?.headers?.get?.('retry-after');
|
|
83
|
+
if (h == null) return fallbackMs;
|
|
84
|
+
const secs = Number(h);
|
|
85
|
+
if (Number.isFinite(secs)) return Math.max(0, Math.min(secs * 1000, 60_000));
|
|
86
|
+
const at = Date.parse(h);
|
|
87
|
+
return Number.isFinite(at) ? Math.max(0, Math.min(at - Date.now(), 60_000)) : fallbackMs;
|
|
88
|
+
}
|