flowviant 0.116.0 → 0.117.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 +31 -12
- package/bin/cli.mjs +13 -17
- package/bin/lib/admission.mjs +7 -14
- package/bin/lib/agentInternalWait.mjs +83 -0
- package/bin/lib/agentPlan.mjs +56 -24
- package/bin/lib/agentPublish.mjs +12 -13
- package/bin/lib/agentTaskKinds.mjs +4 -7
- package/bin/lib/agySessions.mjs +3 -3
- package/bin/lib/artifactPreview.mjs +92 -18
- package/bin/lib/artifactReporter.mjs +16 -8
- package/bin/lib/artifacts.mjs +64 -60
- package/bin/lib/authproxy.mjs +2 -1
- package/bin/lib/childEnv.mjs +9 -4
- package/bin/lib/claude.mjs +10 -20
- package/bin/lib/claudePersonal.mjs +71 -6
- package/bin/lib/claudePosture.mjs +119 -42
- package/bin/lib/claudeSessions.mjs +7 -7
- package/bin/lib/claudeStream.mjs +7 -11
- package/bin/lib/cliLimit.mjs +2 -2
- package/bin/lib/codexHome.mjs +17 -0
- package/bin/lib/codexPersonal.mjs +44 -3
- package/bin/lib/codexRelay.mjs +21 -5
- package/bin/lib/codexResearch.mjs +180 -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 +88 -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 +3 -5
- package/bin/lib/env.mjs +104 -35
- package/bin/lib/fleet.mjs +7 -9
- package/bin/lib/fleetPost.mjs +172 -25
- package/bin/lib/fleetReports.mjs +130 -170
- 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/readGuard.mjs +41 -7
- 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/mcp-cli.mjs +9 -10
- package/bin/lib/noWindowEnv.mjs +23 -3
- package/bin/lib/ownedDirs.mjs +99 -0
- package/bin/lib/pinnedCapture.mjs +383 -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 +101 -16
- package/bin/lib/projectTools.mjs +50 -32
- package/bin/lib/prompts.mjs +41 -19
- package/bin/lib/repoPath.mjs +19 -0
- package/bin/lib/repoState.mjs +17 -19
- package/bin/lib/runTurn.mjs +268 -210
- package/bin/lib/runtimeAntigravity.mjs +16 -5
- package/bin/lib/runtimeCapabilities.mjs +140 -20
- package/bin/lib/runtimeClaude.mjs +55 -5
- package/bin/lib/runtimeCodex.mjs +245 -205
- package/bin/lib/runtimeDetection.mjs +2 -2
- package/bin/lib/runtimeEvents.mjs +16 -10
- package/bin/lib/runtimeLimits.mjs +5 -6
- package/bin/lib/runtimeOfferings.mjs +24 -11
- 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/turnLock.mjs +7 -14
- package/bin/lib/turnProfile.mjs +1 -1
- 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 +17 -7
- package/bin/lib/workAdoptCarry.mjs +2 -4
- package/bin/lib/workAgentCheck.mjs +104 -57
- package/bin/lib/workAgentMerges.mjs +62 -57
- package/bin/lib/workAgentPlans.mjs +71 -139
- package/bin/lib/workAgentPrecheck.mjs +71 -138
- package/bin/lib/workAgentPublish.mjs +6 -8
- package/bin/lib/workAgentTurnBegun.mjs +15 -11
- package/bin/lib/workAgentTurnExecution.mjs +46 -53
- package/bin/lib/workAgentTurnReports.mjs +9 -37
- package/bin/lib/workAgentTurns.mjs +4 -3
- package/bin/lib/workAttachments.mjs +66 -11
- package/bin/lib/workBrain.mjs +9 -4
- package/bin/lib/workCappedTurn.mjs +109 -0
- package/bin/lib/workChildren.mjs +5 -5
- package/bin/lib/workCommandAudit.mjs +11 -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 +96 -30
- 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 +111 -189
- package/bin/lib/workShip.mjs +43 -23
- package/bin/lib/workTurnShape.mjs +163 -0
- package/npm-shrinkwrap.json +59 -0
- package/package.json +3 -2
- 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';
|
|
@@ -28,6 +30,78 @@ import { claudeAuthContext } from './claudeAuth.mjs';
|
|
|
28
30
|
import { readBaseTools, toolReadout } from './projectTools.mjs';
|
|
29
31
|
import { runnerToolCapabilities } from './projectToolRuntimes.mjs';
|
|
30
32
|
|
|
33
|
+
/**
|
|
34
|
+
* ONE READOUT LANE (audit 2026-10-10, F087/F752). The four readouts below —
|
|
35
|
+
* local sessions, repo state, env and tools — were four hand copies of one
|
|
36
|
+
* state machine over module-level `let`s, and their gate orders and refusal
|
|
37
|
+
* rules had drifted without a word. The machine, once:
|
|
38
|
+
*
|
|
39
|
+
* 1. latched quiet? say nothing (`scanBeforeGate` moves this AFTER the
|
|
40
|
+
* build — the env lane, whose scan feeds the scrubber and must run
|
|
41
|
+
* whether or not anything can be posted);
|
|
42
|
+
* 2. scanned less than `scanMs` ago? say nothing, else stamp the scan;
|
|
43
|
+
* 3. build the payload STRING — a throw says nothing (a readout must never
|
|
44
|
+
* throw into the poll), and null means there is nothing to say;
|
|
45
|
+
* 4. equal to the last payload the server ACCEPTED (and, with `resendMs`,
|
|
46
|
+
* accepted inside that window)? say nothing;
|
|
47
|
+
* 5. POST it with the machine credential;
|
|
48
|
+
* 6. a status `isPermanent` names latches the lane quiet until restart;
|
|
49
|
+
* 7. remember the payload only on a 2xx — anything else forgets it, so the
|
|
50
|
+
* next scan retries rather than dedup-suppressing a report nobody got.
|
|
51
|
+
*
|
|
52
|
+
* Each lane's real differences are PARAMETERS, kept exactly as they were:
|
|
53
|
+
* only local sessions has a resend window; local sessions and repo state
|
|
54
|
+
* latch on a 404 alone, env and tools on `envReportIsPermanent`. Unifying the
|
|
55
|
+
* latch rule (wire.md's one "a permanent 4xx is delivered") stays an open
|
|
56
|
+
* call: a 400 latch would silence repo-state for the life of the process
|
|
57
|
+
* after one bad payload.
|
|
58
|
+
*
|
|
59
|
+
* Returns the lane's verdict — 'quiet' | 'throttled' | 'scan-failed' |
|
|
60
|
+
* 'nothing' | 'deduped' | 'accepted' | 'retry' | 'stopped' — which the
|
|
61
|
+
* daemon ignores (every caller is a bare `void`) and the tests drive.
|
|
62
|
+
*/
|
|
63
|
+
function createReportLane({
|
|
64
|
+
endpoint,
|
|
65
|
+
scanMs = 60_000,
|
|
66
|
+
resendMs = null,
|
|
67
|
+
timeoutMs = 15_000,
|
|
68
|
+
isPermanent = (status) => status === 404,
|
|
69
|
+
scanBeforeGate = false,
|
|
70
|
+
}) {
|
|
71
|
+
const url = fleetEndpoint(endpoint, FLEET_URL);
|
|
72
|
+
let unsupported = false; // the server refused for good — quiet until restart
|
|
73
|
+
let scanAt = 0;
|
|
74
|
+
let sent = null; // the last payload the server ACCEPTED, stringified
|
|
75
|
+
let sentAt = 0;
|
|
76
|
+
const report = async (build) => {
|
|
77
|
+
if (!scanBeforeGate && unsupported) return 'quiet';
|
|
78
|
+
if (Date.now() - scanAt < scanMs) return 'throttled';
|
|
79
|
+
scanAt = Date.now();
|
|
80
|
+
let payload;
|
|
81
|
+
try {
|
|
82
|
+
payload = build();
|
|
83
|
+
} catch {
|
|
84
|
+
return 'scan-failed';
|
|
85
|
+
}
|
|
86
|
+
if (scanBeforeGate && unsupported) return 'quiet';
|
|
87
|
+
if (payload == null) return 'nothing';
|
|
88
|
+
if (payload === sent && (resendMs == null || Date.now() - sentAt < resendMs)) return 'deduped';
|
|
89
|
+
const r = await fleetRequest(url, { body: payload, timeoutMs });
|
|
90
|
+
if (isPermanent(r.status)) {
|
|
91
|
+
unsupported = true;
|
|
92
|
+
return 'stopped';
|
|
93
|
+
}
|
|
94
|
+
sent = r.ok ? payload : null;
|
|
95
|
+
sentAt = r.ok ? Date.now() : 0;
|
|
96
|
+
return r.ok ? 'accepted' : 'retry';
|
|
97
|
+
};
|
|
98
|
+
/** "Look again on the next beat" — clears the throttle, nothing else. */
|
|
99
|
+
report.rescan = () => {
|
|
100
|
+
scanAt = 0;
|
|
101
|
+
};
|
|
102
|
+
return report;
|
|
103
|
+
}
|
|
104
|
+
|
|
31
105
|
/**
|
|
32
106
|
* Terminal-session presence: tell the server which Claude sessions exist in
|
|
33
107
|
* this repo (localSessions.mjs reads them off Claude's own on-disk state), so
|
|
@@ -40,57 +114,17 @@ import { runnerToolCapabilities } from './projectToolRuntimes.mjs';
|
|
|
40
114
|
* process stops asking (a deploy that adds it also restarts nothing on this
|
|
41
115
|
* machine, so silence-until-restart costs one daemon restart, not a feature).
|
|
42
116
|
*/
|
|
43
|
-
const LOCAL_SESSIONS_URL = fleetEndpoint('local-sessions', FLEET_URL);
|
|
44
|
-
const LOCAL_SESSIONS_SCAN_MS = 60_000;
|
|
45
117
|
// The web hides a report older than 10 minutes (presence must not linger as
|
|
46
118
|
// fact after the machine dies), so an UNCHANGED report is re-sent inside that
|
|
47
119
|
// window anyway — the re-send is the machine's heartbeat on this fact, and
|
|
48
120
|
// 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
|
-
}
|
|
121
|
+
const localSessionsLane = createReportLane({ endpoint: 'local-sessions', resendMs: 5 * 60_000 });
|
|
122
|
+
export function maybeReportLocalSessions({ repoRoot, excludeDirs, excludeIds }) {
|
|
123
|
+
// scanLocalSessions orders deterministically, so this string only changes
|
|
124
|
+
// when the facts on disk do — the dedup compares whole payloads.
|
|
125
|
+
return localSessionsLane(() =>
|
|
126
|
+
JSON.stringify({ sessions: scanLocalSessions({ repoRoot, excludeDirs, excludeIds }) })
|
|
127
|
+
);
|
|
94
128
|
}
|
|
95
129
|
|
|
96
130
|
/**
|
|
@@ -108,32 +142,24 @@ export async function maybeReportLocalSessions({ repoRoot, excludeDirs, excludeI
|
|
|
108
142
|
* exists — so re-posting an unchanged list would be a write per machine per
|
|
109
143
|
* five minutes to say nothing at all.
|
|
110
144
|
*/
|
|
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 {
|
|
145
|
+
const repoStateLane = createReportLane({ endpoint: 'repo-state' });
|
|
146
|
+
export function maybeReportRepoState({ repoRoot, baseRef }) {
|
|
147
|
+
return repoStateLane(() => {
|
|
122
148
|
// The PARAM, and nothing else: this function sits at MODULE scope, where
|
|
123
149
|
// 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.
|
|
150
|
+
// reached for it anyway, the ReferenceError landed in the lane's build
|
|
151
|
+
// catch — written for an unreadable repo, silent by design — and this
|
|
152
|
+
// endpoint was never once posted to. The caller reads the loop's live
|
|
153
|
+
// `baseRef` at call time, so the value here is always current.
|
|
128
154
|
const state = repoState(repoRoot, baseRef);
|
|
129
|
-
if (!state) return; // not readable — say nothing rather than say "none"
|
|
155
|
+
if (!state) return null; // not readable — say nothing rather than say "none"
|
|
130
156
|
/**
|
|
131
157
|
* WHICH CREDENTIAL THIS MACHINE'S CLI RESOLVES, on the same beat.
|
|
132
158
|
*
|
|
133
159
|
* It rides this report rather than getting an endpoint of its own because
|
|
134
160
|
* it is the same KIND of fact — something only the machine can see, pushed
|
|
135
161
|
* because a pull client can never be asked — and it inherits this
|
|
136
|
-
*
|
|
162
|
+
* lane's three economies for free: scanned once a minute, deduped
|
|
137
163
|
* against the last ACCEPTED payload, and silent forever once an older
|
|
138
164
|
* server 404s.
|
|
139
165
|
*
|
|
@@ -149,32 +175,8 @@ export async function maybeReportRepoState({ repoRoot, baseRef }) {
|
|
|
149
175
|
* report, and an older SERVER strips the unknown key in its zod parse and
|
|
150
176
|
* stores the rest exactly as before.
|
|
151
177
|
*/
|
|
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
|
-
}
|
|
178
|
+
return JSON.stringify({ ...state, auth: claudeAuthContext() });
|
|
179
|
+
});
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
/**
|
|
@@ -247,16 +249,24 @@ export async function maybeReportRepoState({ repoRoot, baseRef }) {
|
|
|
247
249
|
* Pure and exported so the decision can be proved without a credential, a
|
|
248
250
|
* server or a poll — the same shape `shouldStop` and `createHolderWatch` keep
|
|
249
251
|
* in holder.mjs.
|
|
252
|
+
*
|
|
253
|
+
* DELIBERATELY NARROWER than fleetWire.mjs's `isConsideredRefusal` (every 4xx
|
|
254
|
+
* but 408/429), which the settle lanes read: a settle that treats a 401 as
|
|
255
|
+
* decided loses one body, a readout that latches on it goes dark for the life
|
|
256
|
+
* of the process. Two vocabularies, each named, never folded together.
|
|
250
257
|
*/
|
|
251
258
|
export function envReportIsPermanent(status) {
|
|
252
259
|
return status === 400 || status === 404 || status === 422;
|
|
253
260
|
}
|
|
254
261
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
262
|
+
// Scans BEFORE the latch: the scrubber has no dedup and must reflect the
|
|
263
|
+
// newest read every time, so a server that cannot take the report never costs
|
|
264
|
+
// this box its redaction (see the docblock above `envReportIsPermanent`).
|
|
265
|
+
const envLane = createReportLane({
|
|
266
|
+
endpoint: 'env-report',
|
|
267
|
+
isPermanent: envReportIsPermanent,
|
|
268
|
+
scanBeforeGate: true,
|
|
269
|
+
});
|
|
260
270
|
/**
|
|
261
271
|
* Returns a one-word VERDICT — 'throttled' | 'scan-failed' | 'quiet' |
|
|
262
272
|
* 'deduped' | 'accepted' | 'retry' | 'stopped'. The daemon ignores it (the
|
|
@@ -265,83 +275,33 @@ let envReportSent = null; // the last report the server ACCEPTED, stringified
|
|
|
265
275
|
* readout whose only proof is that its source LOOKS right is the inert-pin
|
|
266
276
|
* class this repo has caught five times.
|
|
267
277
|
*/
|
|
268
|
-
export
|
|
269
|
-
|
|
270
|
-
envReportScanAt = Date.now();
|
|
271
|
-
let report;
|
|
272
|
-
try {
|
|
278
|
+
export function maybeReportEnv(repoRoot) {
|
|
279
|
+
return envLane(() => {
|
|
273
280
|
// The SCAN happens even when the POST cannot — see the docblock: the
|
|
274
281
|
// 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,
|
|
282
|
+
const report = scanEnvForScrub(repoRoot);
|
|
283
|
+
return JSON.stringify({
|
|
284
|
+
pubkey: myPubB64(),
|
|
285
|
+
files: report.files,
|
|
286
|
+
filesTotal: report.filesTotal,
|
|
287
|
+
varsTotal: report.varsTotal,
|
|
297
288
|
});
|
|
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
|
-
}
|
|
289
|
+
});
|
|
315
290
|
}
|
|
316
291
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
let toolsReportAt = 0;
|
|
292
|
+
// Latches on the env lane's short list for the same reason: a 401 or a 429 is
|
|
293
|
+
// a "later", and a lane that dies invisibly must not die for one.
|
|
294
|
+
const toolsLane = createReportLane({ endpoint: 'tools-report', isPermanent: envReportIsPermanent });
|
|
321
295
|
/** Repo tool readiness is a per-box daemon report. Never include env values. */
|
|
322
|
-
export
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const pubkey = myPubB64();
|
|
327
|
-
if (!pubkey) return;
|
|
328
|
-
let payload;
|
|
329
|
-
try {
|
|
296
|
+
export function maybeReportTools(repoRoot, baseRef) {
|
|
297
|
+
return toolsLane(() => {
|
|
298
|
+
const pubkey = myPubB64();
|
|
299
|
+
if (!pubkey) return null;
|
|
330
300
|
const snapshot = readBaseTools(repoRoot, baseRef);
|
|
331
301
|
const installed = detectRuntimes().filter((r) => r.installed);
|
|
332
302
|
const runner = installed.find((r) => runnerToolCapabilities(r.id).mcp)?.id ?? installed[0]?.id ?? 'none';
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
if (payload === toolsReportSent) return;
|
|
336
|
-
try {
|
|
337
|
-
const res = await fetch(TOOLS_REPORT_URL, {
|
|
338
|
-
method: 'POST',
|
|
339
|
-
headers: { Authorization: `Bearer ${FLEET_TOKEN}`, 'User-Agent': USER_AGENT, 'Content-Type': 'application/json' },
|
|
340
|
-
signal: AbortSignal.timeout(15_000), body: payload,
|
|
341
|
-
});
|
|
342
|
-
if (envReportIsPermanent(res.status)) toolsReportUnsupported = true;
|
|
343
|
-
toolsReportSent = res.ok ? payload : null;
|
|
344
|
-
} catch { toolsReportSent = null; }
|
|
303
|
+
return JSON.stringify({ pubkey, tools: toolReadout(snapshot, runner) });
|
|
304
|
+
});
|
|
345
305
|
}
|
|
346
306
|
|
|
347
307
|
/**
|
|
@@ -350,7 +310,7 @@ export async function maybeReportTools(repoRoot, baseRef) {
|
|
|
350
310
|
* it (through fleet.mjs's `onRepoChanged`) after a ship, a retirement or a cut.
|
|
351
311
|
*/
|
|
352
312
|
export function repoStateChanged() {
|
|
353
|
-
|
|
313
|
+
repoStateLane.rescan();
|
|
354
314
|
}
|
|
355
315
|
|
|
356
316
|
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
|
+
}
|