@bli-cockpit/cli 0.2.53 → 0.2.55
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/dist/adapters/attribution-core-fallbacks.js +247 -0
- package/dist/adapters/attribution-core-paths.js +182 -0
- package/dist/adapters/attribution-core-score.js +159 -0
- package/dist/adapters/attribution-core-types.js +13 -0
- package/dist/adapters/attribution-core.js +13 -565
- package/dist/adapters/claude-attribution-discovery.js +186 -0
- package/dist/adapters/claude-attribution-score.js +204 -0
- package/dist/adapters/claude-attribution-signals.js +180 -0
- package/dist/adapters/claude-attribution-types.js +25 -0
- package/dist/adapters/claude-attribution.js +14 -569
- package/dist/commands/doctor-access.js +129 -0
- package/dist/commands/doctor-pipeline.js +326 -0
- package/dist/commands/doctor-registration.js +105 -0
- package/dist/commands/doctor-report.js +111 -0
- package/dist/commands/doctor-update.js +120 -0
- package/dist/commands/doctor.js +8 -753
- package/dist/commands/heartbeat.js +8 -0
- package/dist/commands/jarvis-contracts.js +8 -0
- package/dist/commands/jarvis-render.js +413 -0
- package/dist/commands/jarvis-turn.js +305 -0
- package/dist/commands/jarvis.js +23 -698
- package/dist/commands/local-args-collector-setup.js +250 -0
- package/dist/commands/local-args-collector-status.js +227 -0
- package/dist/commands/local-args-collector-work.js +175 -0
- package/dist/commands/local-args-collector.js +19 -624
- package/dist/commands/local-args-tower-admin.js +456 -0
- package/dist/commands/local-args-tower-chat.js +194 -0
- package/dist/commands/local-args-tower-pages.js +314 -0
- package/dist/commands/local-args-tower.js +13 -880
- package/dist/commands/local-help.js +10 -2
- package/dist/commands/onboard-completion.js +136 -0
- package/dist/commands/onboard-flows.js +165 -0
- package/dist/commands/onboard-setup.js +102 -0
- package/dist/commands/onboard.js +5 -392
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-counters.js +55 -0
- package/dist/commands/session-sync-health.js +8 -1
- package/dist/commands/session-sync-plan.js +47 -7
- package/dist/commands/session-sync-scan.js +4 -4
- package/dist/commands/session-sync.js +6 -0
- package/dist/commands/settings-render.js +27 -0
- package/dist/commands/sync-followups.js +5 -1
- package/dist/commands/sync.js +5 -1
- package/dist/commands/team-device-reasons.js +16 -0
- package/dist/commands/team.js +87 -7
- package/dist/evidence-upload-client.js +14 -763
- package/dist/evidence-upload-object.js +181 -0
- package/dist/evidence-upload-plan.js +233 -0
- package/dist/evidence-upload-terminal.js +309 -0
- package/dist/evidence-upload-transport.js +104 -0
- package/dist/spool/local-spool-io.js +122 -0
- package/dist/spool/local-spool-mutations.js +174 -0
- package/dist/spool/local-spool-parse.js +143 -0
- package/dist/spool/local-spool-types.js +22 -0
- package/dist/spool/local-spool.js +20 -426
- package/dist/upload-evidence-delivery-offer.js +144 -0
- package/dist/upload-evidence-delivery-reconcile.js +134 -0
- package/dist/upload-evidence-delivery-summary.js +205 -0
- package/dist/upload-evidence-delivery.js +12 -482
- package/package.json +3 -3
package/dist/commands/onboard.js
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { agentRuleHostLabel } from "./agent-rules-command.js";
|
|
3
|
-
import { autostartLocationLine } from "./autostart-command.js";
|
|
4
|
-
import { backfillRetryCommand, runBackfill, } from "./backfill.js";
|
|
5
|
-
import { attributedSyncRunStatus, shortSha, worktreeSyncRow, writeAgentSessionSummary, } from "./collection-report.js";
|
|
6
|
-
import { collectionRootConsentAliases, persistOnboardingRootConfig, resolveOnboardingRootsForCommand, } from "./collection-roots.js";
|
|
7
|
-
import { addInstallEvent, reportInstallEventsBestEffort } from "./install-receipts.js";
|
|
8
|
-
import { canReuseOnboardSession, pairLocalCollectorWithAuthFallback, readOnboardSessionReuseCandidate, requestPairingAccessTokenDetailed, resolveOnboardEmail, } from "./local-auth.js";
|
|
1
|
+
import { resolveOnboardEmail } from "./local-auth.js";
|
|
9
2
|
import { discoverCommandWorktrees } from "./local-discovery.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { installAutostartAgent } from "../autostart.js";
|
|
15
|
-
import { inspectLocalCollectorStatus, logoutLocalCollector, startLocalWorkContext, } from "../local-state.js";
|
|
3
|
+
import { handleOnboardFailure, } from "./onboard-completion.js";
|
|
4
|
+
import { runOnboardMultiRepoFlow, runOnboardSingleRepoFlow, } from "./onboard-flows.js";
|
|
5
|
+
import { writeOnboardBanner } from "./onboard-report.js";
|
|
6
|
+
import { prepareOnboardRoots, installOnboardConfig, pairForOnboarding } from "./onboard-setup.js";
|
|
16
7
|
/**
|
|
17
8
|
* The onboarding runbook: resolve roots, install, pair, discover worktrees,
|
|
18
9
|
* then hand off to whichever arm matches what was found — one repo runs the
|
|
@@ -68,382 +59,4 @@ export async function runOnboard(command, io) {
|
|
|
68
59
|
catch (error) {
|
|
69
60
|
return await handleOnboardFailure(error, command, rootsResult, install, pair, syncHolder.current, installEvents, io);
|
|
70
61
|
}
|
|
71
|
-
}
|
|
72
|
-
/** The parent-folder arm: sync every discovered worktree, then backfill and finish setup once. */
|
|
73
|
-
async function runOnboardMultiRepoFlow(ctx) {
|
|
74
|
-
const { command, resolvedCommand, collectionRoots, rootsResult, install, pair, installEvents, io, worktrees, } = ctx;
|
|
75
|
-
const multi = await runMultiRepoOnboard(resolvedCommand, io, worktrees);
|
|
76
|
-
addInstallEvent(installEvents, "work_context", "ok");
|
|
77
|
-
addInstallEvent(installEvents, "sync", multi.ok ? "ok" : "fail", multi.ok ? undefined : "sync_blocked");
|
|
78
|
-
if (!multi.ok) {
|
|
79
|
-
if (command.json)
|
|
80
|
-
writeOnboardMultiRepoSyncBlocked(ctx, multi);
|
|
81
|
-
return finishOnboardRun(command, installEvents, io, 1);
|
|
82
|
-
}
|
|
83
|
-
const backfill = await runRequiredOnboardBackfill(resolvedCommand, io);
|
|
84
|
-
const completedBackfill = backfill.status === "complete" ? backfill.result : null;
|
|
85
|
-
addInstallEvent(installEvents, "backfill", completedBackfill ? "ok" : "fail", completedBackfill
|
|
86
|
-
? undefined
|
|
87
|
-
: backfill.failureReason ?? "backfill_incomplete");
|
|
88
|
-
if (!completedBackfill) {
|
|
89
|
-
writeOnboardBackfillBlockedResult(io, {
|
|
90
|
-
json: command.json,
|
|
91
|
-
base: onboardJsonPayload("blocked", resolvedCommand, install, pair, null, null, rootsResult),
|
|
92
|
-
extra: {
|
|
93
|
-
mode: "multi_repo",
|
|
94
|
-
repos: multi.results,
|
|
95
|
-
codex_sessions: multi.codex_sessions,
|
|
96
|
-
},
|
|
97
|
-
backfill,
|
|
98
|
-
});
|
|
99
|
-
return finishOnboardRun(command, installEvents, io, 1);
|
|
100
|
-
}
|
|
101
|
-
const multiSetup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
|
|
102
|
-
reportOnboardMultiRepoResult(ctx, {
|
|
103
|
-
multi,
|
|
104
|
-
backfill: completedBackfill,
|
|
105
|
-
setup: multiSetup,
|
|
106
|
-
});
|
|
107
|
-
return finishOnboardRun(command, installEvents, io, multiSetup.ok ? 0 : 1);
|
|
108
|
-
}
|
|
109
|
-
/** The single-repo arm: start the work context, sync it, backfill, then finish setup once. */
|
|
110
|
-
async function runOnboardSingleRepoFlow(ctx, single) {
|
|
111
|
-
const { command, resolvedCommand, collectionRoots, rootsResult, install, pair, installEvents, io, worktrees, } = ctx;
|
|
112
|
-
const worktreeRoot = worktrees[0]?.repo_root ?? single.primaryRoot;
|
|
113
|
-
const context = await startLocalWorkContext({
|
|
114
|
-
homeDir: command.homeDir,
|
|
115
|
-
repoRoot: worktreeRoot,
|
|
116
|
-
branch: command.branch,
|
|
117
|
-
activeTicketId: command.activeTicketId,
|
|
118
|
-
});
|
|
119
|
-
if (!command.json)
|
|
120
|
-
writeOnboardWorkContextStarted(io, context);
|
|
121
|
-
addInstallEvent(installEvents, "work_context", "ok");
|
|
122
|
-
const run = await runAttributedWorktreeSync({
|
|
123
|
-
homeDir: command.homeDir,
|
|
124
|
-
dashboardUrl: command.dashboardUrl,
|
|
125
|
-
collectionRoots: single.attributionCollectionRoots,
|
|
126
|
-
startContexts: false,
|
|
127
|
-
worktrees,
|
|
128
|
-
fetchImpl: io.fetch,
|
|
129
|
-
});
|
|
130
|
-
const sync = run.outcomes[0]?.sync ?? null;
|
|
131
|
-
single.syncHolder.current = sync;
|
|
132
|
-
if (!sync) {
|
|
133
|
-
throw new Error("Onboard sync produced no result for the repo worktree.");
|
|
134
|
-
}
|
|
135
|
-
const status = await inspectLocalCollectorStatus({
|
|
136
|
-
homeDir: command.homeDir,
|
|
137
|
-
repoRoot: worktreeRoot,
|
|
138
|
-
branch: command.branch,
|
|
139
|
-
});
|
|
140
|
-
if (!run.ok || sync.status !== "uploaded") {
|
|
141
|
-
addInstallEvent(installEvents, "sync", "fail", "sync_blocked");
|
|
142
|
-
reportOnboardSyncBlocked(ctx, { run, sync, status, worktreeRoot });
|
|
143
|
-
return finishOnboardRun(command, installEvents, io, 1);
|
|
144
|
-
}
|
|
145
|
-
addInstallEvent(installEvents, "sync", "ok");
|
|
146
|
-
const backfill = await runRequiredOnboardBackfill(resolvedCommand, io);
|
|
147
|
-
const completedBackfill = backfill.status === "complete" ? backfill.result : null;
|
|
148
|
-
addInstallEvent(installEvents, "backfill", completedBackfill ? "ok" : "fail", completedBackfill
|
|
149
|
-
? undefined
|
|
150
|
-
: backfill.failureReason ?? "backfill_incomplete");
|
|
151
|
-
if (!completedBackfill) {
|
|
152
|
-
writeOnboardBackfillBlockedResult(io, {
|
|
153
|
-
json: command.json,
|
|
154
|
-
base: onboardJsonPayload("blocked", resolvedCommand, install, pair, sync, status, rootsResult),
|
|
155
|
-
extra: { codex_sessions: run.summary },
|
|
156
|
-
backfill,
|
|
157
|
-
});
|
|
158
|
-
return finishOnboardRun(command, installEvents, io, 1);
|
|
159
|
-
}
|
|
160
|
-
if (command.json) {
|
|
161
|
-
const jsonSetup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
|
|
162
|
-
writeOnboardSingleRepoResultJson(ctx, {
|
|
163
|
-
run,
|
|
164
|
-
sync,
|
|
165
|
-
status,
|
|
166
|
-
backfill: completedBackfill,
|
|
167
|
-
setup: jsonSetup,
|
|
168
|
-
});
|
|
169
|
-
return finishOnboardRun(command, installEvents, io, jsonSetup.ok ? 0 : 1);
|
|
170
|
-
}
|
|
171
|
-
writeOnboardUploadNarrative(io, { command, run, sync, status });
|
|
172
|
-
const setup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
|
|
173
|
-
const { agentRules, autostart } = setup;
|
|
174
|
-
if (!setup.ok) {
|
|
175
|
-
writeOnboardAutostartBlocker(io, autostart);
|
|
176
|
-
return finishOnboardRun(command, installEvents, io, 1);
|
|
177
|
-
}
|
|
178
|
-
writeLine(io.stdout, "PASS: Tower is set up and collecting.");
|
|
179
|
-
writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
|
|
180
|
-
pair,
|
|
181
|
-
status,
|
|
182
|
-
sync,
|
|
183
|
-
agentRules,
|
|
184
|
-
autostart,
|
|
185
|
-
initialSyncOk: true,
|
|
186
|
-
backfill: completedBackfill,
|
|
187
|
-
});
|
|
188
|
-
return finishOnboardRun(command, installEvents, io, 0);
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Every uncaught error from either onboarding arm lands here. `status` is
|
|
192
|
-
* always recomputed fresh against whichever root got furthest, never trusted
|
|
193
|
-
* from before the throw — the arms already recompute it themselves on their
|
|
194
|
-
* own success paths, so nothing upstream is lost by not threading it through.
|
|
195
|
-
*/
|
|
196
|
-
async function handleOnboardFailure(error, command, rootsResult, install, pair, sync, installEvents, io) {
|
|
197
|
-
const message = errorMessage(error);
|
|
198
|
-
const statusRoot = rootsResult?.roots[0] ?? command.repoRoot;
|
|
199
|
-
const status = statusRoot
|
|
200
|
-
? await inspectLocalCollectorStatus({
|
|
201
|
-
homeDir: command.homeDir,
|
|
202
|
-
repoRoot: statusRoot,
|
|
203
|
-
branch: command.branch,
|
|
204
|
-
}).catch(() => null)
|
|
205
|
-
: null;
|
|
206
|
-
const blocker = classifyOnboardBlocker(message);
|
|
207
|
-
addOnboardFailureEvent(installEvents, blocker);
|
|
208
|
-
const nextStep = nextStepForOnboardBlocker(blocker, command);
|
|
209
|
-
if (command.json) {
|
|
210
|
-
writeOnboardJson(io, onboardJsonPayload("blocked", command, install, pair, sync, status, rootsResult, {
|
|
211
|
-
blocker,
|
|
212
|
-
message,
|
|
213
|
-
next_step: nextStep,
|
|
214
|
-
}));
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
writeLine(io.stderr, `BLOCKED: ${message}`);
|
|
218
|
-
writeLine(io.stderr, `Next: ${nextStep}`);
|
|
219
|
-
}
|
|
220
|
-
return finishOnboardRun(command, installEvents, io, 1);
|
|
221
|
-
}
|
|
222
|
-
/** Step 0: which folders are approved, and who claims to own this machine's uploads. */
|
|
223
|
-
async function prepareOnboardRoots(command, installEvents, io) {
|
|
224
|
-
const resolvedRoots = await resolveOnboardingRootsForCommand(command, io);
|
|
225
|
-
const rootsResult = resolvedRoots.rootsResult;
|
|
226
|
-
const collectionRoots = rootsResult.roots;
|
|
227
|
-
const attributionCollectionRoots = await collectionRootConsentAliases(collectionRoots);
|
|
228
|
-
const primaryRoot = resolvedRoots.primaryRoot;
|
|
229
|
-
const resolvedCommand = {
|
|
230
|
-
...command,
|
|
231
|
-
repoRoot: primaryRoot,
|
|
232
|
-
collectionRoots,
|
|
233
|
-
};
|
|
234
|
-
if (!command.json) {
|
|
235
|
-
writeLine(io.stdout, `Collecting from: ${collectionRoots.join(", ")}`);
|
|
236
|
-
}
|
|
237
|
-
if (rootsResult.homeRootOptIn) {
|
|
238
|
-
addInstallEvent(installEvents, "home_root_optin", "ok");
|
|
239
|
-
}
|
|
240
|
-
return {
|
|
241
|
-
resolvedRoots,
|
|
242
|
-
rootsResult,
|
|
243
|
-
collectionRoots,
|
|
244
|
-
attributionCollectionRoots,
|
|
245
|
-
primaryRoot,
|
|
246
|
-
existingConfig: resolvedRoots.existingConfig,
|
|
247
|
-
resolvedCommand,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
/** Step 1: write the local config that makes this machine a known collector. */
|
|
251
|
-
async function installOnboardConfig(command, resolvedRoots, installEvents, io) {
|
|
252
|
-
const install = await persistOnboardingRootConfig(command, resolvedRoots);
|
|
253
|
-
addInstallEvent(installEvents, "install", "ok");
|
|
254
|
-
if (!command.json) {
|
|
255
|
-
writeLine(io.stdout, "1/5 Installed local collector.");
|
|
256
|
-
writeLine(io.stdout, `Config: ${install.paths.config_file}`);
|
|
257
|
-
}
|
|
258
|
-
return install;
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Step 2 of onboarding: reuse the device session when it already belongs to
|
|
262
|
-
* this owner on this dashboard, otherwise sign in and pair — logging the old
|
|
263
|
-
* session out first when it belongs to someone else.
|
|
264
|
-
*
|
|
265
|
-
* Returns null when an existing session was reused, which is why the caller's
|
|
266
|
-
* `pair` stays null and the readiness proof falls back to the status file.
|
|
267
|
-
*/
|
|
268
|
-
async function pairForOnboarding(command, input, installEvents, io) {
|
|
269
|
-
const installedStatus = await inspectLocalCollectorStatus({
|
|
270
|
-
homeDir: command.homeDir,
|
|
271
|
-
repoRoot: input.primaryRoot,
|
|
272
|
-
branch: command.branch,
|
|
273
|
-
});
|
|
274
|
-
const installedSession = await readOnboardSessionReuseCandidate(command.homeDir);
|
|
275
|
-
const canReuseInstalledSession = canReuseOnboardSession(installedSession, input.claimedOwnerEmail, command.dashboardUrl);
|
|
276
|
-
if (installedStatus.session_state === "valid" && canReuseInstalledSession) {
|
|
277
|
-
addInstallEvent(installEvents, "auth", "skipped", "existing_session");
|
|
278
|
-
addInstallEvent(installEvents, "pair", "skipped", "existing_session");
|
|
279
|
-
if (!command.json) {
|
|
280
|
-
writeLine(io.stdout, "2/5 Existing valid device session found; pairing skipped.");
|
|
281
|
-
}
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
if (installedStatus.session_state === "valid") {
|
|
285
|
-
await logoutLocalCollector({ homeDir: command.homeDir });
|
|
286
|
-
if (!command.json) {
|
|
287
|
-
writeLine(io.stdout, "2/5 Existing valid device session does not match requested owner or dashboard; pairing again.");
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
const authResult = await requestPairingAccessTokenDetailed({
|
|
291
|
-
dashboardUrl: command.dashboardUrl,
|
|
292
|
-
email: input.claimedOwnerEmail,
|
|
293
|
-
noAuth: command.noAuth,
|
|
294
|
-
json: command.json,
|
|
295
|
-
}, io);
|
|
296
|
-
addInstallEvent(installEvents, "auth", authResult.status, authResult.errorCode);
|
|
297
|
-
const pair = await pairLocalCollectorWithAuthFallback({
|
|
298
|
-
homeDir: command.homeDir,
|
|
299
|
-
dashboardUrl: command.dashboardUrl,
|
|
300
|
-
claimedOwnerEmail: input.claimedOwnerEmail,
|
|
301
|
-
deviceName: command.deviceName,
|
|
302
|
-
pollIntervalMs: command.pollIntervalMs,
|
|
303
|
-
timeoutMs: command.timeoutMs,
|
|
304
|
-
pairingAccessToken: authResult.accessToken,
|
|
305
|
-
fetch: io.fetch,
|
|
306
|
-
onPairStarted: command.json
|
|
307
|
-
? undefined
|
|
308
|
-
: (request) => writePairingInstructions(io, request),
|
|
309
|
-
}, io);
|
|
310
|
-
addInstallEvent(installEvents, "pair", "ok");
|
|
311
|
-
if (!command.json) {
|
|
312
|
-
writeLine(io.stdout, "2/5 Device paired.");
|
|
313
|
-
writeLine(io.stdout, `User: ${pair.session.email ?? pair.session.auth_subject_id}`);
|
|
314
|
-
writeLine(io.stdout, `Device: ${pair.session.device_name ?? pair.session.device_id ?? "unknown"}`);
|
|
315
|
-
}
|
|
316
|
-
return pair;
|
|
317
|
-
}
|
|
318
|
-
async function runMultiRepoOnboard(command, io, worktrees) {
|
|
319
|
-
if (!command.json) {
|
|
320
|
-
writeLine(io.stdout, `3/5 Parent folder mode: discovered ${worktrees.length} git worktree(s).`);
|
|
321
|
-
}
|
|
322
|
-
const collectionRoots = await collectionRootConsentAliases(command.collectionRoots ??
|
|
323
|
-
(command.repoRoot ? [command.repoRoot] : []));
|
|
324
|
-
const run = await runAttributedWorktreeSync({
|
|
325
|
-
homeDir: command.homeDir,
|
|
326
|
-
dashboardUrl: command.dashboardUrl,
|
|
327
|
-
branch: command.branch,
|
|
328
|
-
activeTicketId: command.activeTicketId,
|
|
329
|
-
collectionRoots,
|
|
330
|
-
startContexts: true,
|
|
331
|
-
worktrees,
|
|
332
|
-
fetchImpl: io.fetch,
|
|
333
|
-
});
|
|
334
|
-
const results = run.outcomes.map((outcome) => worktreeSyncRow(outcome, run));
|
|
335
|
-
const collectionRunStatus = attributedSyncRunStatus(run);
|
|
336
|
-
if (!command.json) {
|
|
337
|
-
for (const [index, outcome] of run.outcomes.entries()) {
|
|
338
|
-
const row = results[index];
|
|
339
|
-
if (!row)
|
|
340
|
-
continue;
|
|
341
|
-
const uploaded = outcome.sync.status === "uploaded";
|
|
342
|
-
writeLine(uploaded ? io.stdout : io.stderr, `${uploaded ? "SYNCED" : "BLOCKED"} ${row["repo_label"]}/${row["worktree_label"]} (${row["branch"]}) head:${shortSha(outcome.sync.head_sha ?? outcome.worktree.head_sha)} ${outcome.sync.status} objects:${outcome.sync.raw_evidence_uploaded_object_count} chunks:${outcome.sync.raw_evidence_uploaded_chunk_count} reused:${outcome.sync.raw_evidence_reused_count} failed:${outcome.sync.raw_evidence_failed_count} sessions:${row["attributed_session_count"]}${row["failure_reason"] ? ` reason:${row["failure_reason"]}` : ""}`);
|
|
343
|
-
}
|
|
344
|
-
writeLine(io.stdout, "4/5 Parent worktree sync complete.");
|
|
345
|
-
writeLine(io.stdout, `Uploaded: ${results.filter((row) => row["upload_status"] === "uploaded").length}/${results.length}`);
|
|
346
|
-
writeAgentSessionSummary(io, run.summary);
|
|
347
|
-
if (run.ok) {
|
|
348
|
-
writeLine(io.stdout, "Live sync receipts complete; verifying all-history Codex and Claude backfill.");
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
writeLine(io.stderr, `BLOCKED: Tower collection is ${collectionRunStatus}; retry until every eligible session has a durable receipt or explicit terminal reason.`);
|
|
352
|
-
}
|
|
353
|
-
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
354
|
-
}
|
|
355
|
-
return { ok: run.ok, results, codex_sessions: run.summary };
|
|
356
|
-
}
|
|
357
|
-
function requiredOnboardBackfillCommand(command) {
|
|
358
|
-
return {
|
|
359
|
-
homeDir: command.homeDir,
|
|
360
|
-
all: true,
|
|
361
|
-
dryRun: false,
|
|
362
|
-
maxDepth: command.maxDepth,
|
|
363
|
-
maxRepos: command.maxRepos,
|
|
364
|
-
yes: true,
|
|
365
|
-
json: true,
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
async function runRequiredOnboardBackfill(command, io) {
|
|
369
|
-
const backfillCommand = requiredOnboardBackfillCommand(command);
|
|
370
|
-
const retryCommand = backfillRetryCommand(backfillCommand);
|
|
371
|
-
try {
|
|
372
|
-
const result = await runBackfill(backfillCommand, io);
|
|
373
|
-
return {
|
|
374
|
-
status: result.status,
|
|
375
|
-
failureReason: result.status === "complete"
|
|
376
|
-
? null
|
|
377
|
-
: result.failure_reason ?? `backfill_${result.status}`,
|
|
378
|
-
retryCommand: result.retry_command,
|
|
379
|
-
result,
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
catch (error) {
|
|
383
|
-
return {
|
|
384
|
-
status: "failed",
|
|
385
|
-
failureReason: errorMessage(error),
|
|
386
|
-
retryCommand,
|
|
387
|
-
result: null,
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
async function completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io) {
|
|
392
|
-
const agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
393
|
-
addInstallEvent(installEvents, "agent_rules", "ok");
|
|
394
|
-
const autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
395
|
-
addAutostartInstallEvent(installEvents, autostart);
|
|
396
|
-
return { agentRules, autostart, ok: !onboardAutostartFailed(autostart) };
|
|
397
|
-
}
|
|
398
|
-
async function refreshOnboardAgentRules(command, roots, io) {
|
|
399
|
-
const result = await installAgentRules({
|
|
400
|
-
homeDir: command.homeDir,
|
|
401
|
-
scopePaths: roots,
|
|
402
|
-
});
|
|
403
|
-
if (command.json)
|
|
404
|
-
return result;
|
|
405
|
-
writeLine(io.stdout, `Agent rules: ${onboardAgentRulesInstallLine(result)}`);
|
|
406
|
-
for (const target of result.targets) {
|
|
407
|
-
writeLine(io.stdout, `${agentRuleHostLabel(target.host)}: ${target.rules_file}`);
|
|
408
|
-
}
|
|
409
|
-
return result;
|
|
410
|
-
}
|
|
411
|
-
/** Refreshes the host autostart backend after successful onboarding. */
|
|
412
|
-
async function refreshOnboardAutostart(command, roots, io) {
|
|
413
|
-
if (!io.exec)
|
|
414
|
-
return null;
|
|
415
|
-
const result = await installAutostartAgent({
|
|
416
|
-
homeDir: command.homeDir,
|
|
417
|
-
repoRoot: roots[0],
|
|
418
|
-
repoRoots: roots,
|
|
419
|
-
dashboardUrl: command.dashboardUrl,
|
|
420
|
-
exec: io.exec,
|
|
421
|
-
});
|
|
422
|
-
if (command.json)
|
|
423
|
-
return result;
|
|
424
|
-
if (result.status === "unsupported") {
|
|
425
|
-
writeLine(io.stdout, `Background autostart unsupported: ${result.message}`);
|
|
426
|
-
return result;
|
|
427
|
-
}
|
|
428
|
-
writeLine(io.stdout, result.loaded
|
|
429
|
-
? "Background autostart refreshed; Tower syncs at login and every 15 min."
|
|
430
|
-
: "Background autostart refreshed, but the operating-system scheduler reported a problem; check `cockpit autostart status`.");
|
|
431
|
-
writeLine(io.stdout, autostartLocationLine(result));
|
|
432
|
-
return result;
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Every onboard exit funnels through here so the best-effort install-event
|
|
436
|
-
* report always fires, pass or blocked — replaces a `finish` closure that used
|
|
437
|
-
* to live inside `runOnboard` so the flow functions below can call it too.
|
|
438
|
-
*/
|
|
439
|
-
async function finishOnboardRun(command, installEvents, io, code) {
|
|
440
|
-
await reportInstallEventsBestEffort({
|
|
441
|
-
homeDir: command.homeDir,
|
|
442
|
-
dashboardUrl: command.dashboardUrl,
|
|
443
|
-
command: "onboard",
|
|
444
|
-
events: installEvents,
|
|
445
|
-
json: command.json,
|
|
446
|
-
io,
|
|
447
|
-
});
|
|
448
|
-
return code;
|
|
449
62
|
}
|
|
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
18
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.55");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two honest counters the heartbeat carries beside the raw scan count
|
|
3
|
+
* (BLI-3645).
|
|
4
|
+
*
|
|
5
|
+
* `sessions_observed` is a re-scanned STOCK — every session file inside the
|
|
6
|
+
* current scan window, whether this tick found it for the first time or the
|
|
7
|
+
* two-hundredth (`docs/reports/collection-coverage-probe-2026-09-05.md`). It
|
|
8
|
+
* keeps its real name and keeps shipping, because the fleet board still wants
|
|
9
|
+
* "how big is this machine's session store". Neither counter below replaces
|
|
10
|
+
* it; both are FLOWS, computed against the cursor as it stood before this
|
|
11
|
+
* tick touched a single file:
|
|
12
|
+
*
|
|
13
|
+
* - `sessions_new_this_tick` — sessions this tick's scan found that were NOT
|
|
14
|
+
* already in that source's cursor. A session already tracked (uploaded,
|
|
15
|
+
* retrying, or merely re-scanned inside the normal window) is not "new".
|
|
16
|
+
* - `sessions_pending_upload` — sessions INSIDE an approved root, from this
|
|
17
|
+
* tick's report, that still have no durable pointer. This is the "only
|
|
18
|
+
* copy" set a manager actually needs: files that exist nowhere but this
|
|
19
|
+
* machine, right now. Outside-root sessions are a consented absence
|
|
20
|
+
* (session-first commandment) and are never counted here, the same rule
|
|
21
|
+
* `session-sync-health.ts` applies to `sessions_outside_root`.
|
|
22
|
+
*/
|
|
23
|
+
import { OUTSIDE_APPROVED_ROOT_REASONS } from "./session-sync-health.js";
|
|
24
|
+
/**
|
|
25
|
+
* How many of this tick's attributed sessions are new to their source's
|
|
26
|
+
* cursor — never seen by a previous tick at all.
|
|
27
|
+
*
|
|
28
|
+
* Compares against the cursor as it stood BEFORE this tick (the `plan` step's
|
|
29
|
+
* read), not the cursor after recording, because the point is "first seen
|
|
30
|
+
* since the last tick's watermark" and the cursor only gains this tick's
|
|
31
|
+
* entries once recording runs.
|
|
32
|
+
*/
|
|
33
|
+
export function countSessionsNewThisTick(options) {
|
|
34
|
+
let count = 0;
|
|
35
|
+
for (const session of options.sessions) {
|
|
36
|
+
const cursorBefore = session.source === "claude_code"
|
|
37
|
+
? options.claudeCursorBefore
|
|
38
|
+
: options.codexCursorBefore;
|
|
39
|
+
if (!cursorBefore.sessions[session.codex_session_id])
|
|
40
|
+
count += 1;
|
|
41
|
+
}
|
|
42
|
+
return count;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* How many of this tick's attributed sessions are inside an approved root and
|
|
46
|
+
* still have no durable pointer — the "only copy" set. A session that never
|
|
47
|
+
* gets one because it is genuinely withheld (a stated `upload_reason`, not the
|
|
48
|
+
* outside-root one) still counts: it is still, right now, the only copy this
|
|
49
|
+
* session exists as, and that is exactly the fact a manager needs surfaced,
|
|
50
|
+
* not silently excluded because the withholding was deliberate.
|
|
51
|
+
*/
|
|
52
|
+
export function countSessionsPendingUpload(sessions) {
|
|
53
|
+
return sessions.filter((session) => !session.raw_evidence_pointer_id &&
|
|
54
|
+
!OUTSIDE_APPROVED_ROOT_REASONS.has(session.attribution_reason)).length;
|
|
55
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Nothing here parses a rendered sentence back apart.
|
|
11
11
|
*/
|
|
12
12
|
import { createSyncFailureLedger, recordSourceScanFailures, recordUnexplainedFailure, recordUnpostedSessionReportFailure, recordWorktreeDeliveryFailures, } from "./session-sync-failures.js";
|
|
13
|
+
import { countSessionsNewThisTick, countSessionsPendingUpload, } from "./session-sync-counters.js";
|
|
13
14
|
/**
|
|
14
15
|
* Decide whether this tick failed, and record every condition that decided it.
|
|
15
16
|
*
|
|
@@ -50,6 +51,12 @@ export function decideSyncHealth(options) {
|
|
|
50
51
|
failure_records: records,
|
|
51
52
|
notice,
|
|
52
53
|
sessions_outside_root: sessionsOutsideRoot,
|
|
54
|
+
sessions_new_this_tick: countSessionsNewThisTick({
|
|
55
|
+
sessions: options.sessions,
|
|
56
|
+
codexCursorBefore: options.plan.codexCursorBefore,
|
|
57
|
+
claudeCursorBefore: options.plan.claudeCursorBefore,
|
|
58
|
+
}),
|
|
59
|
+
sessions_pending_upload: countSessionsPendingUpload(options.sessions),
|
|
53
60
|
};
|
|
54
61
|
}
|
|
55
62
|
/**
|
|
@@ -60,7 +67,7 @@ export function decideSyncHealth(options) {
|
|
|
60
67
|
* `attribution-core.ts` writes both of these and nothing else means
|
|
61
68
|
* "outside the boundary".
|
|
62
69
|
*/
|
|
63
|
-
const OUTSIDE_APPROVED_ROOT_REASONS = new Set([
|
|
70
|
+
export const OUTSIDE_APPROVED_ROOT_REASONS = new Set([
|
|
64
71
|
"cwd_outside_scanned_worktrees",
|
|
65
72
|
"no_matching_worktree_signals",
|
|
66
73
|
]);
|
|
@@ -4,14 +4,36 @@
|
|
|
4
4
|
* Every read here degrades on purpose: a missing or corrupt local file becomes
|
|
5
5
|
* an empty cursor or a null spool, because a collector that cannot read its own
|
|
6
6
|
* bookkeeping must still collect. The one decision this step makes that changes
|
|
7
|
-
* what gets collected is the retry width — whether a source reopens
|
|
8
|
-
*
|
|
7
|
+
* what gets collected is the retry width — whether a source reopens its scan
|
|
8
|
+
* window, and how far.
|
|
9
|
+
*
|
|
10
|
+
* BLI-3645: a `retry_pending` source used to widen to "all local history since
|
|
11
|
+
* the epoch", every tick, forever, which is what made `sessions_observed` a
|
|
12
|
+
* 14-day-to-lifetime rolling STOCK instead of an incremental FLOW (see
|
|
13
|
+
* `docs/reports/collection-coverage-probe-2026-09-05.md`). The window is now
|
|
14
|
+
* bounded to the AGE of the oldest undurable file that source's own cursor is
|
|
15
|
+
* still carrying, plus one day of buffer — a real, finite number that shrinks
|
|
16
|
+
* back down once that file finally uploads, rather than a cutoff that never
|
|
17
|
+
* moves. When no cursor entry explains the retry (a legacy spooled upload with
|
|
18
|
+
* no per-file record), {@link RETRY_WINDOW_FALLBACK_MINUTES} is the bound
|
|
19
|
+
* instead — generous, but still a bound, never "since epoch".
|
|
9
20
|
*/
|
|
10
21
|
import { readLocalCollectorConfig, } from "../local-state.js";
|
|
11
22
|
import { CLAUDE_CURSOR_FILENAME, emptyRawEvidenceCursorState, readRawEvidenceCursor, } from "../cursors/raw-evidence-cursor.js";
|
|
12
23
|
import { normalizeCollectionRoots } from "../root-normalization.js";
|
|
13
24
|
import { readLocalUploadSpoolState, } from "../spool/local-spool.js";
|
|
14
25
|
import { liveSyncCursorEntryRequiresRetry } from "./session-sync-attribution.js";
|
|
26
|
+
/**
|
|
27
|
+
* The bound applied when a retry is pending but no cursor entry names the
|
|
28
|
+
* stuck file (e.g. a legacy spooled upload recorded before per-source retry
|
|
29
|
+
* tracking existed). 30 days is well past the normal 14-day Codex / 24h
|
|
30
|
+
* Claude window and comfortably covers a real backlog, without re-reading a
|
|
31
|
+
* machine's entire lifetime of session files every 15 minutes.
|
|
32
|
+
*/
|
|
33
|
+
export const RETRY_WINDOW_FALLBACK_MINUTES = 30 * 24 * 60;
|
|
34
|
+
/** Buffer added past the oldest undurable file's exact age, for clock skew
|
|
35
|
+
* and so a file sitting right at the edge of the window is not dropped. */
|
|
36
|
+
export const RETRY_WINDOW_BUFFER_MINUTES = 24 * 60;
|
|
15
37
|
/**
|
|
16
38
|
* Read the local state this sync is bound by — config, both cursors, the upload
|
|
17
39
|
* spool — and turn it into the decisions the rest of the pass reads.
|
|
@@ -49,13 +71,31 @@ export async function planSyncFromLocalState(options) {
|
|
|
49
71
|
codexRetryPending: sourceHasPendingRetries("codex", uploadSpool, legacyRetryPending, codexSourceRetryPending, codexCursorBefore),
|
|
50
72
|
claudeRetryPending: claudeEnabled &&
|
|
51
73
|
sourceHasPendingRetries("claude_code", uploadSpool, legacyRetryPending, claudeSourceRetryPending, claudeCursorBefore),
|
|
52
|
-
|
|
74
|
+
codexRetrySinceMinutes: boundedRetrySinceMinutes(codexCursorBefore, options.now),
|
|
75
|
+
claudeRetrySinceMinutes: boundedRetrySinceMinutes(claudeCursorBefore, options.now),
|
|
53
76
|
};
|
|
54
77
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
78
|
+
/**
|
|
79
|
+
* How far back a retry-widened scan should reopen: the age of the oldest
|
|
80
|
+
* undurable file this cursor is still carrying, plus a day of buffer — or
|
|
81
|
+
* {@link RETRY_WINDOW_FALLBACK_MINUTES} when no cursor entry explains the
|
|
82
|
+
* retry at all. Exported for the tests, which is the point: this is the exact
|
|
83
|
+
* arithmetic that used to be "since the epoch" and is now a real number.
|
|
84
|
+
*/
|
|
85
|
+
export function boundedRetrySinceMinutes(cursor, now) {
|
|
86
|
+
let oldestUndurableMtimeMs = null;
|
|
87
|
+
for (const entry of Object.values(cursor.sessions)) {
|
|
88
|
+
if (!liveSyncCursorEntryRequiresRetry(entry))
|
|
89
|
+
continue;
|
|
90
|
+
if (oldestUndurableMtimeMs === null ||
|
|
91
|
+
entry.file_mtime_ms < oldestUndurableMtimeMs) {
|
|
92
|
+
oldestUndurableMtimeMs = entry.file_mtime_ms;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (oldestUndurableMtimeMs === null)
|
|
96
|
+
return RETRY_WINDOW_FALLBACK_MINUTES;
|
|
97
|
+
const ageMinutes = Math.ceil((now.getTime() - oldestUndurableMtimeMs) / 60_000);
|
|
98
|
+
return Math.max(0, ageMinutes) + RETRY_WINDOW_BUFFER_MINUTES;
|
|
59
99
|
}
|
|
60
100
|
/** Whether the spool already recorded a source-scan retry for this source. */
|
|
61
101
|
function sourceScanRetryIsPending(uploadSpool, source) {
|
|
@@ -29,7 +29,7 @@ export async function scanAndAttributeBothSources(options) {
|
|
|
29
29
|
now: options.now,
|
|
30
30
|
collectionRoots: plan.collectionRoots,
|
|
31
31
|
retryPending: plan.codexRetryPending,
|
|
32
|
-
|
|
32
|
+
retrySinceMinutes: plan.codexRetrySinceMinutes,
|
|
33
33
|
});
|
|
34
34
|
// First run (D B.4 §8): without a Claude cursor yet, widen the window to 14
|
|
35
35
|
// days so the first sync captures retroactive history instead of only 24h.
|
|
@@ -42,7 +42,7 @@ export async function scanAndAttributeBothSources(options) {
|
|
|
42
42
|
now: options.now,
|
|
43
43
|
collectionRoots: plan.collectionRoots,
|
|
44
44
|
retryPending: plan.claudeRetryPending,
|
|
45
|
-
|
|
45
|
+
retrySinceMinutes: plan.claudeRetrySinceMinutes,
|
|
46
46
|
firstRunBackfill,
|
|
47
47
|
});
|
|
48
48
|
await reconcileSourceScanRetry({
|
|
@@ -71,7 +71,7 @@ export async function scanAndAttributeBothSources(options) {
|
|
|
71
71
|
*/
|
|
72
72
|
async function scanCodexSessionsForSync(options) {
|
|
73
73
|
const sinceMinutes = options.retryPending
|
|
74
|
-
? options.
|
|
74
|
+
? options.retrySinceMinutes
|
|
75
75
|
: CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES;
|
|
76
76
|
let scan = await scanAndAttributeCodexSessions({
|
|
77
77
|
sessionsDirs: defaultCodexSessionDirs(options.homeDir),
|
|
@@ -106,7 +106,7 @@ async function scanClaudeSessionsForSync(options) {
|
|
|
106
106
|
if (!options.claudeEnabled)
|
|
107
107
|
return emptyClaudeScan();
|
|
108
108
|
const sinceMinutes = options.retryPending
|
|
109
|
-
? options.
|
|
109
|
+
? options.retrySinceMinutes
|
|
110
110
|
: options.firstRunBackfill
|
|
111
111
|
? CLAUDE_FIRST_RUN_BACKFILL_MINUTES
|
|
112
112
|
: undefined;
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
* class-lock test reads that file (BLI-3551).
|
|
26
26
|
* - `session-sync-health.ts` — the verdict, and the notice a quiet-but-working
|
|
27
27
|
* machine still emits.
|
|
28
|
+
* - `session-sync-counters.ts` — the two honest heartbeat counters beside the
|
|
29
|
+
* raw scan count: sessions new since the last tick's cursor, and sessions
|
|
30
|
+
* still pending a durable pointer (BLI-3645).
|
|
28
31
|
*
|
|
29
32
|
* What a run REPORTS — the per-session rows and the CLI funnel counts — lives
|
|
30
33
|
* next door in `agent-session-report.ts` and is re-exported from here
|
|
@@ -116,6 +119,7 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
116
119
|
sessions,
|
|
117
120
|
delivery,
|
|
118
121
|
collectionRootCount: plan.collectionRoots.length,
|
|
122
|
+
plan,
|
|
119
123
|
});
|
|
120
124
|
return {
|
|
121
125
|
ok: health.ok,
|
|
@@ -124,6 +128,8 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
124
128
|
notice: health.notice,
|
|
125
129
|
sessions_observed: sessions.length,
|
|
126
130
|
sessions_outside_root: health.sessions_outside_root,
|
|
131
|
+
sessions_new_this_tick: health.sessions_new_this_tick,
|
|
132
|
+
sessions_pending_upload: health.sessions_pending_upload,
|
|
127
133
|
outcomes: worktreePass.outcomes,
|
|
128
134
|
codexAttribution: scan.codexAttribution,
|
|
129
135
|
claudeAttribution: scan.claudeAttribution,
|