@bli-cockpit/cli 0.2.4 → 0.2.7
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 +27 -7
- package/dist/adapters/attribution-core.js +152 -31
- package/dist/adapters/claude-attribution.js +14 -7
- package/dist/adapters/codex-attribution.js +8 -2
- package/dist/adapters/raw-evidence.js +33 -12
- package/dist/autostart.js +460 -20
- package/dist/commands/backfill.js +582 -87
- package/dist/commands/doctor.js +72 -60
- package/dist/commands/local-args.js +19 -2
- package/dist/commands/local.js +518 -247
- package/dist/commands/public-root.js +6 -1
- package/dist/commands/session-sync.js +259 -54
- package/dist/cursors/backfill-cursor.js +169 -1
- package/dist/cursors/raw-evidence-cursor.js +16 -2
- package/dist/evidence-upload-client.js +139 -107
- package/dist/local-state.js +82 -6
- package/dist/onboarding-roots.js +42 -17
- package/dist/process-runner.js +103 -0
- package/dist/raw-evidence-attribution-policy.js +25 -0
- package/dist/repo-identity.js +142 -26
- package/dist/root-normalization.js +29 -16
- package/dist/spool/install-event-outbox.js +191 -0
- package/dist/spool/local-spool.js +335 -58
- package/dist/upload.js +309 -66
- package/package.json +2 -2
package/dist/commands/local.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { execFile
|
|
2
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createCollectorServer } from "../server.js";
|
|
6
6
|
import { inspectAgentRules, installAgentRules, uninstallAgentRules, } from "../agent-rules.js";
|
|
7
|
-
import { runBackfillCommand } from "./backfill.js";
|
|
7
|
+
import { backfillRetryCommand, runBackfill, runBackfillCommand, } from "./backfill.js";
|
|
8
8
|
import { runDoctor } from "./doctor.js";
|
|
9
9
|
import { inspectBackfillLock } from "../backfill-lock.js";
|
|
10
10
|
import { parseLocalArgs, normalizeUrl } from "./local-args.js";
|
|
@@ -12,12 +12,15 @@ import { autostartStatus, installAutostartAgent, uninstallAutostartAgent, } from
|
|
|
12
12
|
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext, } from "../local-state.js";
|
|
13
13
|
import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES, defaultCodexSessionDirs, scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
|
|
14
14
|
import { scanAndAttributeClaudeSessions } from "../adapters/claude-attribution.js";
|
|
15
|
-
import {
|
|
15
|
+
import { backfillCompletionCovers, emptyBackfillCursorState, prepareBackfillCursorForScope, readBackfillCompletionMarker, readBackfillCursor, } from "../cursors/backfill-cursor.js";
|
|
16
16
|
import { acquireSyncLock } from "../sync-lock.js";
|
|
17
|
-
import {
|
|
18
|
-
import { runAttributedWorktreeSync, } from "./session-sync.js";
|
|
17
|
+
import { collectionRootPathAliases, discoverGitWorktreesInRootsWithStatus, } from "../repo-identity.js";
|
|
18
|
+
import { runAttributedWorktreeSync, matchesLiveSyncWorktree, } from "./session-sync.js";
|
|
19
19
|
import { COLLECTION_ROOT_REQUIRED, missingCollectionRootMessage, normalizeRootsDetailed, resolveOnboardingRoots, rootRejectionExplanation, } from "../onboarding-roots.js";
|
|
20
20
|
import { rawEvidenceGcSummary, runRawEvidenceLocalGc, } from "../raw-evidence-gc.js";
|
|
21
|
+
import { enqueueInstallEventEntry, readPendingInstallEventEntries, recordInstallEventAttemptFailure, removeInstallEventEntry, } from "../spool/install-event-outbox.js";
|
|
22
|
+
import { createCapturedExecRunner, createInteractiveExecRunner, } from "../process-runner.js";
|
|
23
|
+
import { normalizeCollectionRoots } from "../root-normalization.js";
|
|
21
24
|
export const rootCommandNames = new Set([
|
|
22
25
|
"onboard",
|
|
23
26
|
"update",
|
|
@@ -107,7 +110,7 @@ export function localCommandHelp(command) {
|
|
|
107
110
|
" cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--branch <name>] [--no-auth] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
108
111
|
" cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--no-auth] [--json]",
|
|
109
112
|
" cockpit upgrade [same flags as update]",
|
|
110
|
-
" cockpit do-everything [--workspace <path>] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
113
|
+
" cockpit do-everything [--workspace <path>] [--dashboard-url <url>] [--update-tag <tag>] [--dry-run] [--json]",
|
|
111
114
|
" cockpit fix [same flags as do-everything]",
|
|
112
115
|
" cockpit install [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--json]",
|
|
113
116
|
" cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
|
|
@@ -116,7 +119,7 @@ export function localCommandHelp(command) {
|
|
|
116
119
|
" cockpit start [--ticket <id>|--clear-ticket] [--topic <label>] [--intent <intent>] [--phase <phase>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
117
120
|
" cockpit sync [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
118
121
|
" cockpit analyze [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
119
|
-
" cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--yes] [--workspace <path>] [--json]",
|
|
122
|
+
" cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--max-depth <n>] [--max-repos <n>] [--yes] [--workspace <path>] [--json]",
|
|
120
123
|
" cockpit status [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
121
124
|
" cockpit sessions [--source codex|claude] [--since-days <n>|--all] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
122
125
|
" cockpit serve [--port <port>] [--workspace <path>]",
|
|
@@ -132,9 +135,10 @@ function localSubcommandHelp(command) {
|
|
|
132
135
|
[
|
|
133
136
|
"onboard",
|
|
134
137
|
[
|
|
135
|
-
"Usage: cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--branch <name>] [--no-auth] [--json]",
|
|
138
|
+
"Usage: cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--branch <name>] [--no-auth] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
136
139
|
"",
|
|
137
|
-
"Installs, pairs, starts work context(s), syncs once,
|
|
140
|
+
"Installs, pairs, starts work context(s), syncs once, completes all-history",
|
|
141
|
+
"Codex and Claude backfill for the saved roots, and then prints readiness proof.",
|
|
138
142
|
"If --workspace is a parent folder, scans child git repos/worktrees and rolls them up by repo.",
|
|
139
143
|
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
140
144
|
`Omit --dashboard-url for normal production setup (${DEFAULT_DASHBOARD_URL}).`,
|
|
@@ -159,8 +163,8 @@ function localSubcommandHelp(command) {
|
|
|
159
163
|
"Usage: cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--no-auth] [--json]",
|
|
160
164
|
"",
|
|
161
165
|
"Updates the global public CLI from npm, then reruns `cockpit onboard`",
|
|
162
|
-
"with the same setup flags so pairing, saved roots,
|
|
163
|
-
"autostart, and the initial sync are refreshed in one command.",
|
|
166
|
+
"with the same setup flags so pairing, saved roots, all-history backfill,",
|
|
167
|
+
"agent rules, autostart, and the initial sync are refreshed in one command.",
|
|
164
168
|
"`cockpit upgrade` is an alias.",
|
|
165
169
|
],
|
|
166
170
|
],
|
|
@@ -175,10 +179,11 @@ function localSubcommandHelp(command) {
|
|
|
175
179
|
[
|
|
176
180
|
"do-everything",
|
|
177
181
|
[
|
|
178
|
-
"Usage: cockpit do-everything [--workspace <path>] [--dashboard-url <url>] [--dry-run] [--json]",
|
|
182
|
+
"Usage: cockpit do-everything [--workspace <path>] [--dashboard-url <url>] [--update-tag <tag>] [--dry-run] [--json]",
|
|
179
183
|
"",
|
|
180
|
-
"Converges
|
|
184
|
+
"Converges a blank, existing, or reused intern machine: latest CLI, interactive auth and collection-root recovery when needed, saved roots, autostart, all-history backfill, raw-evidence GC, and sync freshness.",
|
|
181
185
|
"`cockpit fix` is an alias.",
|
|
186
|
+
"Maintainer canary: use `--update-tag next` so self-update and re-exec stay on the prerelease candidate.",
|
|
182
187
|
"--dry-run prints the checks and would-fix steps without writing config, plists, cursors, or install telemetry.",
|
|
183
188
|
],
|
|
184
189
|
],
|
|
@@ -254,12 +259,14 @@ function localSubcommandHelp(command) {
|
|
|
254
259
|
[
|
|
255
260
|
"backfill",
|
|
256
261
|
[
|
|
257
|
-
"Usage: cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--yes] [--workspace <path>] [--json]",
|
|
262
|
+
"Usage: cockpit backfill (--since-days <n>|--all) [--source codex|claude] [--dry-run] [--max-files <n>] [--max-depth <n>] [--max-repos <n>] [--yes] [--workspace <path>] [--json]",
|
|
258
263
|
"",
|
|
259
264
|
"Backfills historical Codex and Claude Code session evidence using the saved collection roots from local config when --workspace is omitted.",
|
|
260
265
|
"Omit --source to scan both sources; there is no --source all literal.",
|
|
261
266
|
"The non-all window is capped at the collector session paired_at timestamp.",
|
|
262
267
|
"--all requires a dry-run review and TTY confirmation; pass --yes for headless agent runs.",
|
|
268
|
+
"Repo discovery scans 3 folder levels and up to 50 repos by default.",
|
|
269
|
+
"Raise --max-depth or --max-repos when a partial result reports a discovery cap.",
|
|
263
270
|
"--dry-run validates the paired collector and dashboard reachability, prints the same summary tables, and writes nothing.",
|
|
264
271
|
],
|
|
265
272
|
],
|
|
@@ -299,12 +306,15 @@ function localSubcommandHelp(command) {
|
|
|
299
306
|
[
|
|
300
307
|
"Usage: cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
301
308
|
"",
|
|
302
|
-
"Installs
|
|
303
|
-
"
|
|
309
|
+
"Installs background sync through launchd on Apple Silicon macOS or a",
|
|
310
|
+
"per-user Task Scheduler task on native Windows.",
|
|
311
|
+
"Runs `cockpit sync` every 15 min by default and survives reboots;",
|
|
312
|
+
"the Windows task runs while the user is signed in.",
|
|
304
313
|
"Action defaults to `install`. `--workspace` is the parent work folder to sync.",
|
|
305
314
|
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
306
315
|
"Omit --dashboard-url for production; pass it only for staging/custom dashboards.",
|
|
307
|
-
"
|
|
316
|
+
"See docs/runbooks/cockpit-launchd-sync.md and",
|
|
317
|
+
"docs/runbooks/cockpit-windows-task-scheduler-sync.md.",
|
|
308
318
|
],
|
|
309
319
|
],
|
|
310
320
|
[
|
|
@@ -417,7 +427,7 @@ async function runUpdate(command, io) {
|
|
|
417
427
|
writeLine(io.stdout, JSON.stringify({
|
|
418
428
|
status: "blocked",
|
|
419
429
|
step: "npm_install",
|
|
420
|
-
command: `npm ${
|
|
430
|
+
command: `npm ${selfUpdateInstallArgs().join(" ")}`,
|
|
421
431
|
exit_code: error.result.code,
|
|
422
432
|
}, null, 2));
|
|
423
433
|
}
|
|
@@ -442,12 +452,6 @@ async function runUpdate(command, io) {
|
|
|
442
452
|
addInstallEvent(installEvents, "onboard_rerun", onboard.code === 0 ? "ok" : "fail", onboard.code === 0 ? undefined : updateOnboardFailureCode(onboard));
|
|
443
453
|
return finish(onboard.code);
|
|
444
454
|
}
|
|
445
|
-
const SELF_UPDATE_INSTALL_ARGS = [
|
|
446
|
-
"install",
|
|
447
|
-
"-g",
|
|
448
|
-
"@bli-cockpit/cli@latest",
|
|
449
|
-
"--prefer-online",
|
|
450
|
-
];
|
|
451
455
|
export class SelfUpdateError extends Error {
|
|
452
456
|
result;
|
|
453
457
|
eacces;
|
|
@@ -460,16 +464,25 @@ export class SelfUpdateError extends Error {
|
|
|
460
464
|
}
|
|
461
465
|
export async function runSelfUpdate(io, options = {}) {
|
|
462
466
|
const exec = io.exec ?? defaultExec();
|
|
467
|
+
const tag = options.tag ?? "latest";
|
|
463
468
|
if (!options.json) {
|
|
464
|
-
writeLine(io.stdout,
|
|
469
|
+
writeLine(io.stdout, `Updating Cockpit CLI from npm (${tag})...`);
|
|
465
470
|
}
|
|
466
|
-
const install = await exec("npm",
|
|
471
|
+
const install = await exec("npm", selfUpdateInstallArgs(tag));
|
|
467
472
|
writeExecOutput(io, install, { stdout: !options.json, stderr: true });
|
|
468
473
|
if (install.code !== 0) {
|
|
469
474
|
throw new SelfUpdateError(install);
|
|
470
475
|
}
|
|
471
476
|
return { updated: true, version: LOCAL_COLLECTOR_VERSION };
|
|
472
477
|
}
|
|
478
|
+
function selfUpdateInstallArgs(tag = "latest") {
|
|
479
|
+
return [
|
|
480
|
+
"install",
|
|
481
|
+
"-g",
|
|
482
|
+
`@bli-cockpit/cli@${tag}`,
|
|
483
|
+
"--prefer-online",
|
|
484
|
+
];
|
|
485
|
+
}
|
|
473
486
|
async function runRelease(command, io) {
|
|
474
487
|
const releaseRoot = await findPublicReleaseRoot(process.cwd());
|
|
475
488
|
if (!releaseRoot) {
|
|
@@ -643,6 +656,17 @@ function addAutostartInstallEvent(events, result) {
|
|
|
643
656
|
}
|
|
644
657
|
addInstallEvent(events, "autostart", result.loaded === false ? "fail" : "ok", result.loaded === false ? "autostart_load_failed" : undefined);
|
|
645
658
|
}
|
|
659
|
+
function onboardAutostartFailed(result) {
|
|
660
|
+
return (result !== null &&
|
|
661
|
+
result.status !== "unsupported" &&
|
|
662
|
+
result.loaded === false);
|
|
663
|
+
}
|
|
664
|
+
function writeOnboardAutostartBlocker(io, result) {
|
|
665
|
+
writeLine(io.stderr, "BLOCKED: initial collection succeeded, but recurring background collection is not running.");
|
|
666
|
+
if (result?.message)
|
|
667
|
+
writeLine(io.stderr, `Failure: ${result.message}`);
|
|
668
|
+
writeLine(io.stderr, "Next: run `cockpit autostart install`, then require `cockpit autostart status --json` to exit 0.");
|
|
669
|
+
}
|
|
646
670
|
function onboardFailureStep(blocker, events) {
|
|
647
671
|
if (blocker === COLLECTION_ROOT_REQUIRED)
|
|
648
672
|
return "install";
|
|
@@ -671,6 +695,28 @@ export async function reportInstallEventsBestEffort(options) {
|
|
|
671
695
|
if (options.events.length === 0)
|
|
672
696
|
return;
|
|
673
697
|
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
698
|
+
try {
|
|
699
|
+
await enqueueInstallEventEntry(paths, {
|
|
700
|
+
dashboardUrl: options.dashboardUrl,
|
|
701
|
+
cliVersion: LOCAL_COLLECTOR_VERSION,
|
|
702
|
+
command: options.command,
|
|
703
|
+
osPlatform: os.platform(),
|
|
704
|
+
events: options.events.map((event) => ({
|
|
705
|
+
step: event.step.trim().slice(0, 120),
|
|
706
|
+
status: event.status,
|
|
707
|
+
...(event.error_code
|
|
708
|
+
? { error_code: sanitizeInstallErrorCode(event.error_code) }
|
|
709
|
+
: {}),
|
|
710
|
+
...(event.at ? { at: event.at } : {}),
|
|
711
|
+
})),
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
if (options.json) {
|
|
716
|
+
writeLine(options.io.stderr, "Install event outbox unavailable: local_write_failed");
|
|
717
|
+
}
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
674
720
|
const session = await readLocalCollectorSessionFile(paths).catch(() => null);
|
|
675
721
|
if (!session ||
|
|
676
722
|
session.session_state !== "valid" ||
|
|
@@ -678,34 +724,47 @@ export async function reportInstallEventsBestEffort(options) {
|
|
|
678
724
|
!session.device_token) {
|
|
679
725
|
return;
|
|
680
726
|
}
|
|
681
|
-
const
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
727
|
+
const pending = (await readPendingInstallEventEntries(paths)).slice(0, 20);
|
|
728
|
+
const failures = [];
|
|
729
|
+
for (let offset = 0; offset < pending.length; offset += 5) {
|
|
730
|
+
await Promise.all(pending.slice(offset, offset + 5).map(async (entry) => {
|
|
731
|
+
const controller = new AbortController();
|
|
732
|
+
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
733
|
+
try {
|
|
734
|
+
const response = await options.io.fetch(`${entry.dashboard_url}/api/ambient/install-events`, {
|
|
735
|
+
method: "POST",
|
|
736
|
+
headers: {
|
|
737
|
+
"Content-Type": "application/json",
|
|
738
|
+
Authorization: `Bearer ${session.device_token}`,
|
|
739
|
+
},
|
|
740
|
+
body: JSON.stringify({
|
|
741
|
+
cli_version: entry.cli_version,
|
|
742
|
+
command: entry.command,
|
|
743
|
+
os_platform: entry.os_platform,
|
|
744
|
+
events: entry.events,
|
|
745
|
+
}),
|
|
746
|
+
signal: controller.signal,
|
|
747
|
+
});
|
|
748
|
+
if (!response.ok) {
|
|
749
|
+
throw new Error(`http_${response.status}`);
|
|
750
|
+
}
|
|
751
|
+
await removeInstallEventEntry(paths, entry.outbox_id);
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
const failureReason = classifyInstallTelemetryError(error);
|
|
755
|
+
failures.push(failureReason);
|
|
756
|
+
await recordInstallEventAttemptFailure(paths, entry, {
|
|
757
|
+
attemptedAt: new Date().toISOString(),
|
|
758
|
+
failureReason,
|
|
759
|
+
}).catch(() => undefined);
|
|
760
|
+
}
|
|
761
|
+
finally {
|
|
762
|
+
clearTimeout(timeout);
|
|
763
|
+
}
|
|
764
|
+
}));
|
|
706
765
|
}
|
|
707
|
-
|
|
708
|
-
|
|
766
|
+
if (options.json && failures.length > 0) {
|
|
767
|
+
writeLine(options.io.stderr, `Install event telemetry queued for retry: ${[...new Set(failures)].join(",")}`);
|
|
709
768
|
}
|
|
710
769
|
}
|
|
711
770
|
function classifyInstallTelemetryError(error) {
|
|
@@ -987,11 +1046,7 @@ async function readGitConfigEmail(root) {
|
|
|
987
1046
|
});
|
|
988
1047
|
});
|
|
989
1048
|
}
|
|
990
|
-
/**
|
|
991
|
-
* After a successful onboard, refreshes the launchd autostart agent so intern
|
|
992
|
-
* machines keep syncing without anyone re-running Cockpit. Runtimes without an
|
|
993
|
-
* exec runner skip this because launchd installation cannot be attempted there.
|
|
994
|
-
*/
|
|
1049
|
+
/** Refreshes the host autostart backend after successful onboarding. */
|
|
995
1050
|
async function refreshOnboardAutostart(command, roots, io) {
|
|
996
1051
|
if (!io.exec)
|
|
997
1052
|
return null;
|
|
@@ -1010,8 +1065,8 @@ async function refreshOnboardAutostart(command, roots, io) {
|
|
|
1010
1065
|
}
|
|
1011
1066
|
writeLine(io.stdout, result.loaded
|
|
1012
1067
|
? "Background autostart refreshed; Cockpit syncs at login and every 15 min."
|
|
1013
|
-
: "Background autostart refreshed, but
|
|
1014
|
-
writeLine(io.stdout,
|
|
1068
|
+
: "Background autostart refreshed, but the operating-system scheduler reported a problem; check `cockpit autostart status`.");
|
|
1069
|
+
writeLine(io.stdout, autostartLocationLine(result));
|
|
1015
1070
|
return result;
|
|
1016
1071
|
}
|
|
1017
1072
|
async function refreshOnboardAgentRules(command, roots, io) {
|
|
@@ -1052,21 +1107,66 @@ function writeOnboardLiveStatus(io, command, roots, options) {
|
|
|
1052
1107
|
(options.status?.session_state === "valid" ? "existing valid session" : "pending")}`);
|
|
1053
1108
|
writeLine(io.stdout, `Background sync: ${backgroundSyncLine(options.autostart)}`);
|
|
1054
1109
|
writeLine(io.stdout, `Initial sync: ${options.initialSyncOk ? options.sync?.status ?? "uploaded" : "blocked"}`);
|
|
1110
|
+
writeLine(io.stdout, `Historical backfill: complete (${options.backfill.counts.backfilled} uploaded, ${options.backfill.counts.skipped} explicit skip(s))`);
|
|
1055
1111
|
writeLine(io.stdout, `Agent rules: ${options.agentRules ? onboardAgentRulesInstallLine(options.agentRules) : "not refreshed"}`);
|
|
1056
1112
|
writeLine(io.stdout, `Dashboard: ${command.dashboardUrl}/my-work`);
|
|
1057
|
-
if (options.backfillHint)
|
|
1058
|
-
writeLine(io.stdout, options.backfillHint);
|
|
1059
1113
|
writeLine(io.stdout, "Next: cockpit status");
|
|
1060
1114
|
}
|
|
1115
|
+
function requiredOnboardBackfillCommand(command) {
|
|
1116
|
+
return {
|
|
1117
|
+
homeDir: command.homeDir,
|
|
1118
|
+
all: true,
|
|
1119
|
+
dryRun: false,
|
|
1120
|
+
maxDepth: command.maxDepth,
|
|
1121
|
+
maxRepos: command.maxRepos,
|
|
1122
|
+
yes: true,
|
|
1123
|
+
json: true,
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
async function runRequiredOnboardBackfill(command, io) {
|
|
1127
|
+
const backfillCommand = requiredOnboardBackfillCommand(command);
|
|
1128
|
+
const retryCommand = backfillRetryCommand(backfillCommand);
|
|
1129
|
+
try {
|
|
1130
|
+
const result = await runBackfill(backfillCommand, io);
|
|
1131
|
+
return {
|
|
1132
|
+
status: result.status,
|
|
1133
|
+
failureReason: result.status === "complete"
|
|
1134
|
+
? null
|
|
1135
|
+
: result.failure_reason ?? `backfill_${result.status}`,
|
|
1136
|
+
retryCommand: result.retry_command,
|
|
1137
|
+
result,
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
catch (error) {
|
|
1141
|
+
return {
|
|
1142
|
+
status: "failed",
|
|
1143
|
+
failureReason: errorMessage(error),
|
|
1144
|
+
retryCommand,
|
|
1145
|
+
result: null,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function onboardBackfillPayload(outcome) {
|
|
1150
|
+
return (outcome.result ?? {
|
|
1151
|
+
status: "blocked",
|
|
1152
|
+
failure_reason: outcome.failureReason,
|
|
1153
|
+
retry_command: outcome.retryCommand,
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
function writeOnboardBackfillBlocker(io, outcome) {
|
|
1157
|
+
writeLine(io.stderr, "BLOCKED: all-history Codex and Claude backfill is incomplete for the approved collection roots.");
|
|
1158
|
+
writeLine(io.stderr, `Failure: ${outcome.failureReason ?? `backfill_${outcome.status}`}`);
|
|
1159
|
+
writeLine(io.stderr, `Retry: ${outcome.retryCommand}`);
|
|
1160
|
+
}
|
|
1061
1161
|
function backgroundSyncLine(result) {
|
|
1062
1162
|
if (!result)
|
|
1063
|
-
return "not refreshed (
|
|
1163
|
+
return "not refreshed (autostart runner unavailable)";
|
|
1064
1164
|
if (result.status === "unsupported")
|
|
1065
1165
|
return `unsupported (${result.message})`;
|
|
1066
1166
|
if (result.status === "installed" && result.loaded)
|
|
1067
1167
|
return "installed";
|
|
1068
1168
|
if (result.status === "installed") {
|
|
1069
|
-
return `installed with warning (${result.message ?? "
|
|
1169
|
+
return `installed with warning (${result.message ?? "operating-system scheduler load failed"})`;
|
|
1070
1170
|
}
|
|
1071
1171
|
return result.status;
|
|
1072
1172
|
}
|
|
@@ -1148,18 +1248,17 @@ async function runOnboard(command, io) {
|
|
|
1148
1248
|
let rootsResult = null;
|
|
1149
1249
|
let agentRules = null;
|
|
1150
1250
|
let autostart = null;
|
|
1151
|
-
let backfillHint = null;
|
|
1152
1251
|
try {
|
|
1153
1252
|
if (!command.json) {
|
|
1154
1253
|
writeLine(io.stdout, "Cockpit harvest onboarding");
|
|
1155
1254
|
writeLine(io.stdout, `Dashboard: ${command.dashboardUrl}`);
|
|
1156
1255
|
writeLine(io.stdout, `Ticket: ${command.activeTicketId ?? "general ambient"}`);
|
|
1157
1256
|
}
|
|
1158
|
-
backfillHint = await onboardBackfillHint(command.homeDir);
|
|
1159
1257
|
const resolvedRoots = await resolveOnboardingRootsForCommand(command, io);
|
|
1160
1258
|
const existingConfig = resolvedRoots.existingConfig;
|
|
1161
1259
|
rootsResult = resolvedRoots.rootsResult;
|
|
1162
1260
|
const collectionRoots = rootsResult.roots;
|
|
1261
|
+
const attributionCollectionRoots = await collectionRootConsentAliases(collectionRoots);
|
|
1163
1262
|
const primaryRoot = resolvedRoots.primaryRoot;
|
|
1164
1263
|
const resolvedCommand = {
|
|
1165
1264
|
...command,
|
|
@@ -1227,31 +1326,81 @@ async function runOnboard(command, io) {
|
|
|
1227
1326
|
writeLine(io.stdout, `Device: ${pair.session.device_name ?? pair.session.device_id ?? "unknown"}`);
|
|
1228
1327
|
}
|
|
1229
1328
|
}
|
|
1230
|
-
const worktrees = await discoverCommandWorktrees(collectionRoots, {
|
|
1329
|
+
const worktrees = await discoverCommandWorktrees(collectionRoots, {
|
|
1330
|
+
maxDepth: command.maxDepth,
|
|
1331
|
+
maxRepos: command.maxRepos,
|
|
1332
|
+
allowEmpty: true,
|
|
1333
|
+
}, io);
|
|
1231
1334
|
if (worktrees.length > 1) {
|
|
1232
1335
|
const multi = await runMultiRepoOnboard(resolvedCommand, io, worktrees);
|
|
1233
1336
|
addInstallEvent(installEvents, "work_context", "ok");
|
|
1234
1337
|
addInstallEvent(installEvents, "sync", multi.ok ? "ok" : "fail", multi.ok ? undefined : "sync_blocked");
|
|
1235
|
-
if (multi.ok) {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1338
|
+
if (!multi.ok) {
|
|
1339
|
+
if (command.json) {
|
|
1340
|
+
writeLine(io.stdout, JSON.stringify({
|
|
1341
|
+
...onboardResult("blocked", resolvedCommand, install, pair, null, null),
|
|
1342
|
+
collection_roots: collectionRoots,
|
|
1343
|
+
root_resolution: rootsResult,
|
|
1344
|
+
mode: "multi_repo",
|
|
1345
|
+
repos: multi.results,
|
|
1346
|
+
codex_sessions: multi.codex_sessions,
|
|
1347
|
+
blocker: "sync_blocked",
|
|
1348
|
+
next_step: "Run `cockpit sync --json` until every root returns an uploaded receipt.",
|
|
1349
|
+
}, null, 2));
|
|
1350
|
+
}
|
|
1351
|
+
return finish(1);
|
|
1240
1352
|
}
|
|
1353
|
+
const backfill = await runRequiredOnboardBackfill(resolvedCommand, io);
|
|
1354
|
+
const completedBackfill = backfill.status === "complete" ? backfill.result : null;
|
|
1355
|
+
addInstallEvent(installEvents, "backfill", completedBackfill ? "ok" : "fail", completedBackfill
|
|
1356
|
+
? undefined
|
|
1357
|
+
: backfill.failureReason ?? "backfill_incomplete");
|
|
1358
|
+
if (!completedBackfill) {
|
|
1359
|
+
if (command.json) {
|
|
1360
|
+
writeLine(io.stdout, JSON.stringify({
|
|
1361
|
+
...onboardResult("blocked", resolvedCommand, install, pair, null, null),
|
|
1362
|
+
collection_roots: collectionRoots,
|
|
1363
|
+
root_resolution: rootsResult,
|
|
1364
|
+
mode: "multi_repo",
|
|
1365
|
+
repos: multi.results,
|
|
1366
|
+
codex_sessions: multi.codex_sessions,
|
|
1367
|
+
backfill: onboardBackfillPayload(backfill),
|
|
1368
|
+
blocker: "backfill_incomplete",
|
|
1369
|
+
backfill_failure_reason: backfill.failureReason,
|
|
1370
|
+
next_step: backfill.retryCommand,
|
|
1371
|
+
}, null, 2));
|
|
1372
|
+
}
|
|
1373
|
+
else {
|
|
1374
|
+
writeOnboardBackfillBlocker(io, backfill);
|
|
1375
|
+
}
|
|
1376
|
+
return finish(1);
|
|
1377
|
+
}
|
|
1378
|
+
agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
1379
|
+
addInstallEvent(installEvents, "agent_rules", "ok");
|
|
1380
|
+
autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
1381
|
+
addAutostartInstallEvent(installEvents, autostart);
|
|
1382
|
+
const onboardOk = !onboardAutostartFailed(autostart);
|
|
1241
1383
|
if (command.json) {
|
|
1242
1384
|
writeLine(io.stdout, JSON.stringify({
|
|
1243
|
-
...onboardResult(
|
|
1385
|
+
...onboardResult(onboardOk ? "pass" : "blocked", resolvedCommand, install, pair, null, null),
|
|
1244
1386
|
collection_roots: collectionRoots,
|
|
1245
1387
|
root_resolution: rootsResult,
|
|
1246
1388
|
agent_rules: agentRules,
|
|
1247
1389
|
autostart,
|
|
1248
|
-
|
|
1390
|
+
backfill: completedBackfill,
|
|
1249
1391
|
mode: "multi_repo",
|
|
1250
1392
|
repos: multi.results,
|
|
1251
1393
|
codex_sessions: multi.codex_sessions,
|
|
1394
|
+
...(!onboardOk && multi.ok
|
|
1395
|
+
? {
|
|
1396
|
+
blocker: "autostart_load_failed",
|
|
1397
|
+
next_step: "Run `cockpit autostart install`, then require `cockpit autostart status --json` to exit 0.",
|
|
1398
|
+
}
|
|
1399
|
+
: {}),
|
|
1252
1400
|
}, null, 2));
|
|
1253
1401
|
}
|
|
1254
|
-
if (
|
|
1402
|
+
if (onboardOk && !command.json) {
|
|
1403
|
+
writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
|
|
1255
1404
|
writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
|
|
1256
1405
|
pair,
|
|
1257
1406
|
status,
|
|
@@ -1259,10 +1408,13 @@ async function runOnboard(command, io) {
|
|
|
1259
1408
|
agentRules,
|
|
1260
1409
|
autostart,
|
|
1261
1410
|
initialSyncOk: true,
|
|
1262
|
-
|
|
1411
|
+
backfill: completedBackfill,
|
|
1263
1412
|
});
|
|
1264
1413
|
}
|
|
1265
|
-
|
|
1414
|
+
else if (multi.ok && !command.json) {
|
|
1415
|
+
writeOnboardAutostartBlocker(io, autostart);
|
|
1416
|
+
}
|
|
1417
|
+
return finish(onboardOk ? 0 : 1);
|
|
1266
1418
|
}
|
|
1267
1419
|
const worktreeRoot = worktrees[0]?.repo_root ?? primaryRoot;
|
|
1268
1420
|
const context = await startLocalWorkContext({
|
|
@@ -1282,6 +1434,7 @@ async function runOnboard(command, io) {
|
|
|
1282
1434
|
const run = await runAttributedWorktreeSync({
|
|
1283
1435
|
homeDir: command.homeDir,
|
|
1284
1436
|
dashboardUrl: command.dashboardUrl,
|
|
1437
|
+
collectionRoots: attributionCollectionRoots,
|
|
1285
1438
|
startContexts: false,
|
|
1286
1439
|
worktrees,
|
|
1287
1440
|
fetchImpl: io.fetch,
|
|
@@ -1295,39 +1448,76 @@ async function runOnboard(command, io) {
|
|
|
1295
1448
|
repoRoot: worktreeRoot,
|
|
1296
1449
|
branch: command.branch,
|
|
1297
1450
|
});
|
|
1298
|
-
if (sync.status !== "uploaded") {
|
|
1451
|
+
if (!run.ok || sync.status !== "uploaded") {
|
|
1299
1452
|
addInstallEvent(installEvents, "sync", "fail", "sync_blocked");
|
|
1453
|
+
const runStatus = attributedSyncRunStatus(run);
|
|
1300
1454
|
if (command.json) {
|
|
1301
1455
|
writeLine(io.stdout, JSON.stringify({
|
|
1302
1456
|
...onboardResult("blocked", resolvedCommand, install, pair, sync, status),
|
|
1303
1457
|
collection_roots: collectionRoots,
|
|
1304
1458
|
root_resolution: rootsResult,
|
|
1305
1459
|
codex_sessions: run.summary,
|
|
1460
|
+
collection_status: runStatus,
|
|
1306
1461
|
}, null, 2));
|
|
1307
1462
|
}
|
|
1308
1463
|
else {
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1464
|
+
const syncFailureReason = sync.status === "spooled" ? sync.failure_reason : null;
|
|
1465
|
+
const retryCommand = sync.status === "spooled"
|
|
1466
|
+
? sync.retry_command
|
|
1467
|
+
: `cockpit sync --workspace ${JSON.stringify(worktreeRoot)}`;
|
|
1468
|
+
writeLine(io.stderr, "BLOCKED: initial collection is incomplete; retry until every eligible session has a durable receipt or explicit terminal reason.");
|
|
1469
|
+
writeLine(io.stderr, `Failure: ${syncFailureReason ?? (run.summary.report_posted ? runStatus : run.summary.report_reason)}`);
|
|
1470
|
+
writeAgentSessionSummary(io, run.summary);
|
|
1471
|
+
writeLine(io.stderr, `Retry: ${retryCommand}`);
|
|
1312
1472
|
}
|
|
1313
1473
|
return finish(1);
|
|
1314
1474
|
}
|
|
1315
1475
|
addInstallEvent(installEvents, "sync", "ok");
|
|
1476
|
+
const backfill = await runRequiredOnboardBackfill(resolvedCommand, io);
|
|
1477
|
+
const completedBackfill = backfill.status === "complete" ? backfill.result : null;
|
|
1478
|
+
addInstallEvent(installEvents, "backfill", completedBackfill ? "ok" : "fail", completedBackfill
|
|
1479
|
+
? undefined
|
|
1480
|
+
: backfill.failureReason ?? "backfill_incomplete");
|
|
1481
|
+
if (!completedBackfill) {
|
|
1482
|
+
if (command.json) {
|
|
1483
|
+
writeLine(io.stdout, JSON.stringify({
|
|
1484
|
+
...onboardResult("blocked", resolvedCommand, install, pair, sync, status),
|
|
1485
|
+
collection_roots: collectionRoots,
|
|
1486
|
+
root_resolution: rootsResult,
|
|
1487
|
+
codex_sessions: run.summary,
|
|
1488
|
+
backfill: onboardBackfillPayload(backfill),
|
|
1489
|
+
blocker: "backfill_incomplete",
|
|
1490
|
+
backfill_failure_reason: backfill.failureReason,
|
|
1491
|
+
next_step: backfill.retryCommand,
|
|
1492
|
+
}, null, 2));
|
|
1493
|
+
}
|
|
1494
|
+
else {
|
|
1495
|
+
writeOnboardBackfillBlocker(io, backfill);
|
|
1496
|
+
}
|
|
1497
|
+
return finish(1);
|
|
1498
|
+
}
|
|
1316
1499
|
if (command.json) {
|
|
1317
1500
|
agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
1318
1501
|
addInstallEvent(installEvents, "agent_rules", "ok");
|
|
1319
1502
|
autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
1320
1503
|
addAutostartInstallEvent(installEvents, autostart);
|
|
1504
|
+
const onboardOk = !onboardAutostartFailed(autostart);
|
|
1321
1505
|
writeLine(io.stdout, JSON.stringify({
|
|
1322
|
-
...onboardResult("pass", resolvedCommand, install, pair, sync, status),
|
|
1506
|
+
...onboardResult(onboardOk ? "pass" : "blocked", resolvedCommand, install, pair, sync, status),
|
|
1323
1507
|
collection_roots: collectionRoots,
|
|
1324
1508
|
root_resolution: rootsResult,
|
|
1325
1509
|
agent_rules: agentRules,
|
|
1326
1510
|
autostart,
|
|
1327
|
-
|
|
1511
|
+
backfill: completedBackfill,
|
|
1328
1512
|
codex_sessions: run.summary,
|
|
1513
|
+
...(!onboardOk
|
|
1514
|
+
? {
|
|
1515
|
+
blocker: "autostart_load_failed",
|
|
1516
|
+
next_step: "Run `cockpit autostart install`, then require `cockpit autostart status --json` to exit 0.",
|
|
1517
|
+
}
|
|
1518
|
+
: {}),
|
|
1329
1519
|
}, null, 2));
|
|
1330
|
-
return finish(0);
|
|
1520
|
+
return finish(onboardOk ? 0 : 1);
|
|
1331
1521
|
}
|
|
1332
1522
|
writeLine(io.stdout, "4/5 Ambient metadata uploaded.");
|
|
1333
1523
|
writeLine(io.stdout, `HTTP: ${sync.http_status}`);
|
|
@@ -1339,12 +1529,16 @@ async function runOnboard(command, io) {
|
|
|
1339
1529
|
writeAgentSessionSummary(io, run.summary);
|
|
1340
1530
|
writeLine(io.stdout, "5/5 Status ready.");
|
|
1341
1531
|
writeLine(io.stdout, `Upload state: ${status.upload_state}`);
|
|
1342
|
-
writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
|
|
1343
1532
|
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
1344
1533
|
agentRules = await refreshOnboardAgentRules(resolvedCommand, collectionRoots, io);
|
|
1345
1534
|
addInstallEvent(installEvents, "agent_rules", "ok");
|
|
1346
1535
|
autostart = await refreshOnboardAutostart(resolvedCommand, collectionRoots, io);
|
|
1347
1536
|
addAutostartInstallEvent(installEvents, autostart);
|
|
1537
|
+
if (onboardAutostartFailed(autostart)) {
|
|
1538
|
+
writeOnboardAutostartBlocker(io, autostart);
|
|
1539
|
+
return finish(1);
|
|
1540
|
+
}
|
|
1541
|
+
writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
|
|
1348
1542
|
writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
|
|
1349
1543
|
pair,
|
|
1350
1544
|
status,
|
|
@@ -1352,7 +1546,7 @@ async function runOnboard(command, io) {
|
|
|
1352
1546
|
agentRules,
|
|
1353
1547
|
autostart,
|
|
1354
1548
|
initialSyncOk: true,
|
|
1355
|
-
|
|
1549
|
+
backfill: completedBackfill,
|
|
1356
1550
|
});
|
|
1357
1551
|
return finish(0);
|
|
1358
1552
|
}
|
|
@@ -1420,7 +1614,8 @@ function shortSha(value) {
|
|
|
1420
1614
|
return value ? value.slice(0, 12) : "unknown";
|
|
1421
1615
|
}
|
|
1422
1616
|
function sourceFunnelLine(label, counts) {
|
|
1423
|
-
|
|
1617
|
+
const readFailures = counts.read_failures > 0 ? `, read_failures ${counts.read_failures}` : "";
|
|
1618
|
+
return `${label} sessions: attributed ${counts.attributed}, fallback ${counts.attributed_fallback}, ambiguous ${counts.ambiguous}, unattributed ${counts.unattributed}, skipped ${counts.skipped}, stale ${counts.stale}${readFailures}`;
|
|
1424
1619
|
}
|
|
1425
1620
|
function attributionReportLine(summary) {
|
|
1426
1621
|
return summary.report_posted
|
|
@@ -1475,7 +1670,10 @@ function rawEvidenceSyncLine(sync) {
|
|
|
1475
1670
|
const failures = sync.raw_evidence_failure_reasons.length > 0
|
|
1476
1671
|
? ` failures: ${sync.raw_evidence_failure_reasons.join(",")}`
|
|
1477
1672
|
: "";
|
|
1478
|
-
|
|
1673
|
+
const retries = sync.raw_evidence_retry_reasons.length > 0
|
|
1674
|
+
? ` retry_required: ${sync.raw_evidence_retry_reasons.join(",")}`
|
|
1675
|
+
: "";
|
|
1676
|
+
return `Raw evidence: uploaded ${sync.raw_evidence_uploaded_object_count} object(s) in ${sync.raw_evidence_uploaded_chunk_count} chunk(s), reused ${sync.raw_evidence_reused_count}, failed ${sync.raw_evidence_failed_count}${failures}${retries}`;
|
|
1479
1677
|
}
|
|
1480
1678
|
function cursorStatusLine(sync) {
|
|
1481
1679
|
return `Cursor: ${sync.cursor_tracked_object_count} durable object(s) tracked`;
|
|
@@ -1485,75 +1683,69 @@ const DEFAULT_DISCOVERY_MAX_REPOS = 50;
|
|
|
1485
1683
|
const ALL_SESSION_SCAN_WINDOW_MINUTES = 20 * 365 * 24 * 60;
|
|
1486
1684
|
const SESSION_SCAN_OVERRIDE_LIMIT = 10_000;
|
|
1487
1685
|
async function discoverCommandWorktrees(repoRoot, discovery = {}, io) {
|
|
1488
|
-
if (Array.isArray(repoRoot)) {
|
|
1489
|
-
const discovered = [];
|
|
1490
|
-
for (const root of repoRoot) {
|
|
1491
|
-
discovered.push(...(await discoverCommandWorktrees(root, discovery, io)));
|
|
1492
|
-
}
|
|
1493
|
-
return dedupeWorktrees(discovered);
|
|
1494
|
-
}
|
|
1495
1686
|
const maxWorktrees = discovery.maxRepos ?? DEFAULT_DISCOVERY_MAX_REPOS;
|
|
1496
|
-
const
|
|
1687
|
+
const roots = Array.isArray(repoRoot)
|
|
1688
|
+
? repoRoot
|
|
1689
|
+
: [repoRoot ?? process.cwd()];
|
|
1690
|
+
const result = await discoverGitWorktreesInRootsWithStatus(roots, {
|
|
1497
1691
|
maxDepth: discovery.maxDepth ?? DEFAULT_DISCOVERY_MAX_DEPTH,
|
|
1498
1692
|
maxWorktrees,
|
|
1499
1693
|
});
|
|
1500
|
-
|
|
1501
|
-
|
|
1694
|
+
const worktrees = result.worktrees;
|
|
1695
|
+
if (!result.complete) {
|
|
1696
|
+
const reasons = result.incomplete_reasons.join(",");
|
|
1697
|
+
if (io) {
|
|
1698
|
+
writeLine(io.stderr, `BLOCKED: repo discovery is incomplete (${reasons}); no collection cursor was advanced.`);
|
|
1699
|
+
}
|
|
1700
|
+
throw new Error(`repo discovery incomplete: ${reasons}. Raise --max-depth or --max-repos until every approved-root repo is included.`);
|
|
1502
1701
|
}
|
|
1503
|
-
if (
|
|
1504
|
-
|
|
1702
|
+
if (worktrees.length === 0 && !discovery.allowEmpty) {
|
|
1703
|
+
throw new Error("No git repos found. Run from a git repo, or from a parent folder containing git repos.");
|
|
1505
1704
|
}
|
|
1506
1705
|
return worktrees;
|
|
1507
1706
|
}
|
|
1508
|
-
function dedupeWorktrees(worktrees) {
|
|
1509
|
-
const seen = new Set();
|
|
1510
|
-
const deduped = [];
|
|
1511
|
-
for (const worktree of worktrees) {
|
|
1512
|
-
const key = worktree.worktree_fingerprint || path.resolve(worktree.repo_root);
|
|
1513
|
-
if (seen.has(key))
|
|
1514
|
-
continue;
|
|
1515
|
-
seen.add(key);
|
|
1516
|
-
deduped.push(worktree);
|
|
1517
|
-
}
|
|
1518
|
-
return deduped;
|
|
1519
|
-
}
|
|
1520
1707
|
async function runMultiRepoOnboard(command, io, worktrees) {
|
|
1521
1708
|
if (!command.json) {
|
|
1522
1709
|
writeLine(io.stdout, `3/5 Parent folder mode: discovered ${worktrees.length} git worktree(s).`);
|
|
1523
1710
|
}
|
|
1711
|
+
const collectionRoots = await collectionRootConsentAliases(command.collectionRoots ??
|
|
1712
|
+
(command.repoRoot ? [command.repoRoot] : []));
|
|
1524
1713
|
const run = await runAttributedWorktreeSync({
|
|
1525
1714
|
homeDir: command.homeDir,
|
|
1526
1715
|
dashboardUrl: command.dashboardUrl,
|
|
1527
1716
|
branch: command.branch,
|
|
1528
1717
|
activeTicketId: command.activeTicketId,
|
|
1718
|
+
collectionRoots,
|
|
1529
1719
|
startContexts: true,
|
|
1530
1720
|
worktrees,
|
|
1531
1721
|
fetchImpl: io.fetch,
|
|
1532
1722
|
});
|
|
1533
1723
|
const results = run.outcomes.map((outcome) => worktreeSyncRow(outcome, run));
|
|
1724
|
+
const runStatus = attributedSyncRunStatus(run);
|
|
1534
1725
|
if (!command.json) {
|
|
1535
1726
|
for (const [index, outcome] of run.outcomes.entries()) {
|
|
1536
1727
|
const row = results[index];
|
|
1537
1728
|
if (!row)
|
|
1538
1729
|
continue;
|
|
1539
1730
|
const uploaded = outcome.sync.status === "uploaded";
|
|
1540
|
-
writeLine(uploaded ? io.stdout : io.stderr, `${uploaded ? "
|
|
1731
|
+
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"]}` : ""}`);
|
|
1541
1732
|
}
|
|
1542
1733
|
writeLine(io.stdout, "4/5 Parent worktree sync complete.");
|
|
1543
1734
|
writeLine(io.stdout, `Uploaded: ${results.filter((row) => row["upload_status"] === "uploaded").length}/${results.length}`);
|
|
1544
1735
|
writeAgentSessionSummary(io, run.summary);
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1736
|
+
if (run.ok) {
|
|
1737
|
+
writeLine(io.stdout, "Live sync receipts complete; verifying all-history Codex and Claude backfill.");
|
|
1738
|
+
}
|
|
1739
|
+
else {
|
|
1740
|
+
writeLine(io.stderr, `BLOCKED: Cockpit collection is ${runStatus}; retry until every eligible session has a durable receipt or explicit terminal reason.`);
|
|
1741
|
+
}
|
|
1549
1742
|
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
1550
1743
|
}
|
|
1551
1744
|
return { ok: run.ok, results, codex_sessions: run.summary };
|
|
1552
1745
|
}
|
|
1553
1746
|
function worktreeSyncRow(outcome, run) {
|
|
1554
1747
|
const { worktree, context, sync } = outcome;
|
|
1555
|
-
const matchesWorktree = (result) => result
|
|
1556
|
-
result.worktree?.worktree_fingerprint === worktree.worktree_fingerprint;
|
|
1748
|
+
const matchesWorktree = (result) => matchesLiveSyncWorktree(result, worktree);
|
|
1557
1749
|
const codexSessionCount = run.codexAttribution.results.filter(matchesWorktree).length;
|
|
1558
1750
|
const claudeSessionCount = run.claudeAttribution.results.filter(matchesWorktree).length;
|
|
1559
1751
|
const attributedSessionCount = codexSessionCount + claudeSessionCount;
|
|
@@ -1572,6 +1764,8 @@ function worktreeSyncRow(outcome, run) {
|
|
|
1572
1764
|
raw_evidence_reused_count: sync.raw_evidence_reused_count,
|
|
1573
1765
|
raw_evidence_failed_count: sync.raw_evidence_failed_count,
|
|
1574
1766
|
raw_evidence_failure_reasons: sync.raw_evidence_failure_reasons,
|
|
1767
|
+
raw_evidence_retry_required: sync.raw_evidence_retry_required,
|
|
1768
|
+
raw_evidence_retry_reasons: sync.raw_evidence_retry_reasons,
|
|
1575
1769
|
attributed_session_count: attributedSessionCount,
|
|
1576
1770
|
codex_session_count: codexSessionCount,
|
|
1577
1771
|
claude_session_count: claudeSessionCount,
|
|
@@ -1742,6 +1936,48 @@ async function runStart(command, io) {
|
|
|
1742
1936
|
return 0;
|
|
1743
1937
|
}
|
|
1744
1938
|
async function runSync(command, io) {
|
|
1939
|
+
const paths = getCollectorRuntimePaths(command.homeDir);
|
|
1940
|
+
const config = await readLocalCollectorConfig(paths).catch(() => null);
|
|
1941
|
+
const dashboardUrl = command.dashboardUrl ?? config?.dashboard_url ?? DEFAULT_DASHBOARD_URL;
|
|
1942
|
+
await reportInstallEventsBestEffort({
|
|
1943
|
+
homeDir: command.homeDir,
|
|
1944
|
+
dashboardUrl,
|
|
1945
|
+
command: "sync",
|
|
1946
|
+
events: [{ step: "sync_started", status: "ok" }],
|
|
1947
|
+
json: command.json,
|
|
1948
|
+
io,
|
|
1949
|
+
});
|
|
1950
|
+
try {
|
|
1951
|
+
const result = await runSyncWithHealthReceipt(command, io);
|
|
1952
|
+
await reportInstallEventsBestEffort({
|
|
1953
|
+
homeDir: command.homeDir,
|
|
1954
|
+
dashboardUrl,
|
|
1955
|
+
command: "sync",
|
|
1956
|
+
events: [result.completion],
|
|
1957
|
+
json: command.json,
|
|
1958
|
+
io,
|
|
1959
|
+
});
|
|
1960
|
+
return result.exitCode;
|
|
1961
|
+
}
|
|
1962
|
+
catch (error) {
|
|
1963
|
+
await reportInstallEventsBestEffort({
|
|
1964
|
+
homeDir: command.homeDir,
|
|
1965
|
+
dashboardUrl,
|
|
1966
|
+
command: "sync",
|
|
1967
|
+
events: [
|
|
1968
|
+
{
|
|
1969
|
+
step: "sync_complete",
|
|
1970
|
+
status: "fail",
|
|
1971
|
+
error_code: classifySyncHealthError(error),
|
|
1972
|
+
},
|
|
1973
|
+
],
|
|
1974
|
+
json: command.json,
|
|
1975
|
+
io,
|
|
1976
|
+
});
|
|
1977
|
+
throw error;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
async function runSyncWithHealthReceipt(command, io) {
|
|
1745
1981
|
const backfillLock = await inspectBackfillLock(getCollectorRuntimePaths(command.homeDir));
|
|
1746
1982
|
if (backfillLock.held) {
|
|
1747
1983
|
if (command.json) {
|
|
@@ -1749,48 +1985,101 @@ async function runSync(command, io) {
|
|
|
1749
1985
|
status: "live_sync_paused_during_backfill",
|
|
1750
1986
|
reason: "live sync paused during backfill",
|
|
1751
1987
|
held_since: backfillLock.held_since,
|
|
1988
|
+
collection_complete: false,
|
|
1989
|
+
upload_state: "not_uploaded",
|
|
1990
|
+
retryable: true,
|
|
1752
1991
|
}, null, 2));
|
|
1753
1992
|
}
|
|
1754
1993
|
else {
|
|
1755
1994
|
writeLine(io.stdout, "live sync paused during backfill");
|
|
1756
1995
|
}
|
|
1757
|
-
return
|
|
1996
|
+
return {
|
|
1997
|
+
exitCode: 0,
|
|
1998
|
+
completion: {
|
|
1999
|
+
step: "sync_complete",
|
|
2000
|
+
status: "skipped",
|
|
2001
|
+
error_code: "live_sync_paused_during_backfill",
|
|
2002
|
+
},
|
|
2003
|
+
};
|
|
1758
2004
|
}
|
|
1759
2005
|
// Single-flight: a launchd timer and a manual sync must not interleave the
|
|
1760
2006
|
// cursor read-modify-write. A blocked invocation exits cleanly (B.4 §7).
|
|
1761
2007
|
const lock = await acquireSyncLock(getCollectorRuntimePaths(command.homeDir));
|
|
1762
2008
|
if (!lock.acquired) {
|
|
1763
2009
|
if (command.json) {
|
|
1764
|
-
writeLine(io.stdout, JSON.stringify({
|
|
2010
|
+
writeLine(io.stdout, JSON.stringify({
|
|
2011
|
+
status: "sync_already_running",
|
|
2012
|
+
reason: "another sync owns the collection lock",
|
|
2013
|
+
held_since: lock.held_since,
|
|
2014
|
+
collection_complete: false,
|
|
2015
|
+
upload_state: "not_uploaded",
|
|
2016
|
+
retryable: true,
|
|
2017
|
+
}, null, 2));
|
|
1765
2018
|
}
|
|
1766
2019
|
else {
|
|
1767
2020
|
writeLine(io.stdout, "Cockpit sync already running; skipping this run.");
|
|
1768
2021
|
}
|
|
1769
|
-
return
|
|
2022
|
+
return {
|
|
2023
|
+
exitCode: 0,
|
|
2024
|
+
completion: {
|
|
2025
|
+
step: "sync_complete",
|
|
2026
|
+
status: "skipped",
|
|
2027
|
+
error_code: "sync_already_running",
|
|
2028
|
+
},
|
|
2029
|
+
};
|
|
1770
2030
|
}
|
|
1771
2031
|
try {
|
|
1772
|
-
|
|
2032
|
+
const exitCode = await runSyncLocked(command, io);
|
|
2033
|
+
return {
|
|
2034
|
+
exitCode,
|
|
2035
|
+
completion: {
|
|
2036
|
+
step: "sync_complete",
|
|
2037
|
+
status: exitCode === 0 ? "ok" : "fail",
|
|
2038
|
+
...(exitCode === 0 ? {} : { error_code: "sync_failed" }),
|
|
2039
|
+
},
|
|
2040
|
+
};
|
|
1773
2041
|
}
|
|
1774
2042
|
finally {
|
|
1775
2043
|
await lock.handle.release();
|
|
1776
2044
|
}
|
|
1777
2045
|
}
|
|
2046
|
+
function classifySyncHealthError(error) {
|
|
2047
|
+
const message = errorMessage(error);
|
|
2048
|
+
if (/auth|token|session|unauthorized|forbidden|401|403/iu.test(message)) {
|
|
2049
|
+
return "auth_failed";
|
|
2050
|
+
}
|
|
2051
|
+
if (/fetch|network|enotfound|econnrefused|timeout/iu.test(message)) {
|
|
2052
|
+
return "network_failed";
|
|
2053
|
+
}
|
|
2054
|
+
if (/collection.root|workspace|repo|worktree/iu.test(message)) {
|
|
2055
|
+
return "collection_root_failed";
|
|
2056
|
+
}
|
|
2057
|
+
return "sync_failed";
|
|
2058
|
+
}
|
|
1778
2059
|
async function runSyncLocked(command, io) {
|
|
1779
|
-
const
|
|
2060
|
+
const collectionRoots = await resolveSyncCollectionRoots(command);
|
|
2061
|
+
const worktrees = await discoverCommandWorktrees(collectionRoots, {
|
|
2062
|
+
maxDepth: command.maxDepth,
|
|
2063
|
+
maxRepos: command.maxRepos,
|
|
2064
|
+
allowEmpty: true,
|
|
2065
|
+
}, io);
|
|
1780
2066
|
const run = await runAttributedWorktreeSync({
|
|
1781
2067
|
homeDir: command.homeDir,
|
|
1782
2068
|
dashboardUrl: command.dashboardUrl,
|
|
2069
|
+
collectionRoots,
|
|
1783
2070
|
startContexts: false,
|
|
1784
2071
|
worktrees,
|
|
1785
2072
|
fetchImpl: io.fetch,
|
|
1786
2073
|
});
|
|
1787
|
-
if (
|
|
2074
|
+
if (run.outcomes.length > 1) {
|
|
1788
2075
|
const rows = run.outcomes.map((outcome) => worktreeSyncRow(outcome, run));
|
|
2076
|
+
const runStatus = attributedSyncRunStatus(run);
|
|
1789
2077
|
const gc = run.ok ? await runSyncRawEvidenceGc(command, io) : null;
|
|
1790
2078
|
if (command.json) {
|
|
1791
2079
|
writeLine(io.stdout, JSON.stringify({
|
|
1792
2080
|
mode: "multi_repo",
|
|
1793
|
-
status:
|
|
2081
|
+
status: runStatus,
|
|
2082
|
+
collection_complete: run.ok,
|
|
1794
2083
|
results: run.outcomes.map((outcome) => outcome.sync),
|
|
1795
2084
|
repos: rows,
|
|
1796
2085
|
codex_sessions: run.summary,
|
|
@@ -1798,7 +2087,7 @@ async function runSyncLocked(command, io) {
|
|
|
1798
2087
|
}, null, 2));
|
|
1799
2088
|
return run.ok ? 0 : 1;
|
|
1800
2089
|
}
|
|
1801
|
-
writeLine(run.ok ? io.stdout : io.stderr, `Cockpit parent sync ${
|
|
2090
|
+
writeLine(run.ok ? io.stdout : io.stderr, `Cockpit parent sync ${runStatus} ${run.outcomes.filter((outcome) => outcome.sync.status === "uploaded").length}/${run.outcomes.length} worktree(s).`);
|
|
1802
2091
|
for (const outcome of run.outcomes) {
|
|
1803
2092
|
const { worktree, sync } = outcome;
|
|
1804
2093
|
const uploaded = sync.status === "uploaded";
|
|
@@ -1814,12 +2103,19 @@ async function runSyncLocked(command, io) {
|
|
|
1814
2103
|
if (!result) {
|
|
1815
2104
|
throw new Error("Sync produced no result for the repo worktree.");
|
|
1816
2105
|
}
|
|
1817
|
-
const
|
|
2106
|
+
const runStatus = attributedSyncRunStatus(run);
|
|
2107
|
+
const gc = run.ok ? await runSyncRawEvidenceGc(command, io) : null;
|
|
1818
2108
|
if (command.json) {
|
|
1819
|
-
writeLine(io.stdout, JSON.stringify({
|
|
1820
|
-
|
|
2109
|
+
writeLine(io.stdout, JSON.stringify({
|
|
2110
|
+
...result,
|
|
2111
|
+
status: runStatus,
|
|
2112
|
+
collection_complete: run.ok,
|
|
2113
|
+
codex_sessions: run.summary,
|
|
2114
|
+
raw_evidence_gc: gc,
|
|
2115
|
+
}, null, 2));
|
|
2116
|
+
return run.ok ? 0 : 1;
|
|
1821
2117
|
}
|
|
1822
|
-
if (
|
|
2118
|
+
if (run.ok) {
|
|
1823
2119
|
writeLine(io.stdout, "Cockpit ambient envelope uploaded.");
|
|
1824
2120
|
writeLine(io.stdout, `Ticket: ${displayTicketId(result.ticket_id)}`);
|
|
1825
2121
|
writeLine(io.stdout, `Context: ${result.work_context_id}`);
|
|
@@ -1834,12 +2130,43 @@ async function runSyncLocked(command, io) {
|
|
|
1834
2130
|
writeLine(io.stdout, rawEvidenceGcSummary(gc));
|
|
1835
2131
|
return 0;
|
|
1836
2132
|
}
|
|
2133
|
+
if (result.status === "uploaded") {
|
|
2134
|
+
writeLine(io.stderr, "Cockpit ambient upload was accepted, but session collection is partial; retry `cockpit sync`.");
|
|
2135
|
+
writeAgentSessionSummary(io, run.summary);
|
|
2136
|
+
return 1;
|
|
2137
|
+
}
|
|
1837
2138
|
writeLine(io.stderr, "Cockpit ambient upload failed; safe retry metadata was spooled.");
|
|
1838
2139
|
writeLine(io.stderr, `Ticket: ${displayTicketId(result.ticket_id)}`);
|
|
1839
2140
|
writeLine(io.stderr, `Failure: ${result.failure_reason}`);
|
|
1840
2141
|
writeLine(io.stderr, `Retry: ${result.retry_command}`);
|
|
1841
2142
|
return 1;
|
|
1842
2143
|
}
|
|
2144
|
+
async function resolveSyncCollectionRoots(command) {
|
|
2145
|
+
const explicitRoots = normalizeCollectionRoots(command.repoRoot ? [command.repoRoot] : []);
|
|
2146
|
+
if (explicitRoots.length > 0) {
|
|
2147
|
+
return collectionRootConsentAliases(explicitRoots);
|
|
2148
|
+
}
|
|
2149
|
+
const config = await readLocalCollectorConfig(getCollectorRuntimePaths(command.homeDir)).catch(() => null);
|
|
2150
|
+
const savedRoots = normalizeCollectionRoots(config?.default_repo_paths ?? []);
|
|
2151
|
+
if (savedRoots.length > 0) {
|
|
2152
|
+
return collectionRootConsentAliases(savedRoots);
|
|
2153
|
+
}
|
|
2154
|
+
throw new Error(`${COLLECTION_ROOT_REQUIRED}: no explicit or saved collection root is available.`);
|
|
2155
|
+
}
|
|
2156
|
+
async function collectionRootConsentAliases(roots) {
|
|
2157
|
+
// Keep both spellings when an existing root is reached through a filesystem
|
|
2158
|
+
// alias such as macOS `/var` -> `/private/var`. Deleted child paths cannot be
|
|
2159
|
+
// realpathed later, so either transcript spelling must remain inside the same
|
|
2160
|
+
// operator-approved physical root.
|
|
2161
|
+
return normalizeCollectionRoots(await collectionRootPathAliases(roots));
|
|
2162
|
+
}
|
|
2163
|
+
function attributedSyncRunStatus(run) {
|
|
2164
|
+
if (run.ok)
|
|
2165
|
+
return "uploaded";
|
|
2166
|
+
return run.outcomes.every((outcome) => outcome.sync.status === "uploaded")
|
|
2167
|
+
? "partial"
|
|
2168
|
+
: "spooled";
|
|
2169
|
+
}
|
|
1843
2170
|
async function runAnalyze(command, io) {
|
|
1844
2171
|
const syncStdout = [];
|
|
1845
2172
|
const syncStderr = [];
|
|
@@ -2015,6 +2342,8 @@ async function runStatus(command, io) {
|
|
|
2015
2342
|
writeLine(io.stdout, `last_upload_success: ${status.last_upload_success_at ?? "never"}`);
|
|
2016
2343
|
writeLine(io.stdout, `last_upload_failure: ${status.last_upload_failure_reason ?? "none"}`);
|
|
2017
2344
|
writeLine(io.stdout, `pending_uploads: ${status.pending_upload_count}`);
|
|
2345
|
+
writeLine(io.stdout, `pending_health_receipts: ${status.pending_health_receipt_count}`);
|
|
2346
|
+
writeLine(io.stdout, `last_health_receipt_failure: ${status.last_health_receipt_failure_reason ?? "none"}`);
|
|
2018
2347
|
writeLine(io.stdout, `backfill: ${backfillCursorLine(backfillCursor)}`);
|
|
2019
2348
|
for (const detail of status.details)
|
|
2020
2349
|
writeLine(io.stdout, `- ${detail}`);
|
|
@@ -2028,39 +2357,20 @@ function displayWorkLabel(status) {
|
|
|
2028
2357
|
return `${status.work_label} (${status.work_id})`;
|
|
2029
2358
|
return status.work_label ?? status.work_id ?? "no active work context";
|
|
2030
2359
|
}
|
|
2031
|
-
async function fileExists(filePath) {
|
|
2032
|
-
try {
|
|
2033
|
-
await access(filePath);
|
|
2034
|
-
return true;
|
|
2035
|
-
}
|
|
2036
|
-
catch {
|
|
2037
|
-
return false;
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
async function onboardBackfillHint(homeDir) {
|
|
2041
|
-
const paths = getCollectorRuntimePaths(homeDir);
|
|
2042
|
-
if (await fileExists(backfillCompletionMarkerPath(paths)))
|
|
2043
|
-
return null;
|
|
2044
|
-
const hasArchivedSessions = await hasAnyJsonlFile([
|
|
2045
|
-
path.join(homeDir ?? os.homedir(), ".codex", "archived_sessions"),
|
|
2046
|
-
]);
|
|
2047
|
-
return hasArchivedSessions
|
|
2048
|
-
? "Backfill: archived sessions found. Run `cockpit backfill --all` to include older local history."
|
|
2049
|
-
: null;
|
|
2050
|
-
}
|
|
2051
2360
|
async function inspectBackfillCursor(homeDir) {
|
|
2052
2361
|
const paths = getCollectorRuntimePaths(homeDir);
|
|
2362
|
+
const roots = await currentBackfillRoots(homeDir);
|
|
2053
2363
|
const marker = await readBackfillCompletionMarker(paths);
|
|
2054
|
-
if (marker) {
|
|
2364
|
+
if (backfillCompletionCovers(marker, roots, ["codex", "claude_code"])) {
|
|
2055
2365
|
return {
|
|
2056
2366
|
state: "done",
|
|
2057
2367
|
remaining_count: 0,
|
|
2058
|
-
updated_at: marker
|
|
2059
|
-
completed_at: marker
|
|
2060
|
-
sources: summarizeBackfillCursorSources(marker
|
|
2368
|
+
updated_at: marker?.cursor.updated_at ?? null,
|
|
2369
|
+
completed_at: marker?.completed_at ?? null,
|
|
2370
|
+
sources: summarizeBackfillCursorSources(marker?.cursor ?? emptyBackfillCursorState()),
|
|
2061
2371
|
};
|
|
2062
2372
|
}
|
|
2063
|
-
const cursor = await readBackfillCursor(paths);
|
|
2373
|
+
const cursor = (prepareBackfillCursorForScope(await readBackfillCursor(paths), roots, ["codex", "claude_code"])).cursor;
|
|
2064
2374
|
if (!cursor.updated_at) {
|
|
2065
2375
|
return {
|
|
2066
2376
|
state: "never_run",
|
|
@@ -2078,60 +2388,9 @@ async function inspectBackfillCursor(homeDir) {
|
|
|
2078
2388
|
sources: summarizeBackfillCursorSources(cursor),
|
|
2079
2389
|
};
|
|
2080
2390
|
}
|
|
2081
|
-
async function
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
if (typeof raw.completed_at !== "string")
|
|
2085
|
-
return null;
|
|
2086
|
-
const cursor = raw.cursor && typeof raw.cursor === "object"
|
|
2087
|
-
? normalizeBackfillCursor(raw.cursor)
|
|
2088
|
-
: await readBackfillCursor(paths);
|
|
2089
|
-
return { completed_at: raw.completed_at, cursor };
|
|
2090
|
-
}
|
|
2091
|
-
catch {
|
|
2092
|
-
return null;
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
function normalizeBackfillCursor(value) {
|
|
2096
|
-
const record = value;
|
|
2097
|
-
return {
|
|
2098
|
-
schema_version: "cockpit-backfill-cursor.v1",
|
|
2099
|
-
updated_at: typeof record.updated_at === "string" && record.updated_at
|
|
2100
|
-
? record.updated_at
|
|
2101
|
-
: null,
|
|
2102
|
-
sources: {
|
|
2103
|
-
codex: {
|
|
2104
|
-
oldest_mtime_ms_processed: typeof record.sources?.codex?.oldest_mtime_ms_processed === "number"
|
|
2105
|
-
? record.sources.codex.oldest_mtime_ms_processed
|
|
2106
|
-
: null,
|
|
2107
|
-
oldest_mtime_processed: typeof record.sources?.codex?.oldest_mtime_processed === "string"
|
|
2108
|
-
? record.sources.codex.oldest_mtime_processed
|
|
2109
|
-
: null,
|
|
2110
|
-
state_counts: numberRecord(record.sources?.codex?.state_counts),
|
|
2111
|
-
reason_counts: numberRecord(record.sources?.codex?.reason_counts),
|
|
2112
|
-
},
|
|
2113
|
-
claude_code: {
|
|
2114
|
-
oldest_mtime_ms_processed: typeof record.sources?.claude_code?.oldest_mtime_ms_processed === "number"
|
|
2115
|
-
? record.sources.claude_code.oldest_mtime_ms_processed
|
|
2116
|
-
: null,
|
|
2117
|
-
oldest_mtime_processed: typeof record.sources?.claude_code?.oldest_mtime_processed === "string"
|
|
2118
|
-
? record.sources.claude_code.oldest_mtime_processed
|
|
2119
|
-
: null,
|
|
2120
|
-
state_counts: numberRecord(record.sources?.claude_code?.state_counts),
|
|
2121
|
-
reason_counts: numberRecord(record.sources?.claude_code?.reason_counts),
|
|
2122
|
-
},
|
|
2123
|
-
},
|
|
2124
|
-
};
|
|
2125
|
-
}
|
|
2126
|
-
function numberRecord(value) {
|
|
2127
|
-
if (!value || typeof value !== "object")
|
|
2128
|
-
return {};
|
|
2129
|
-
const out = {};
|
|
2130
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
2131
|
-
if (typeof raw === "number" && Number.isFinite(raw))
|
|
2132
|
-
out[key] = raw;
|
|
2133
|
-
}
|
|
2134
|
-
return out;
|
|
2391
|
+
async function currentBackfillRoots(homeDir) {
|
|
2392
|
+
const config = await readLocalCollectorConfig(getCollectorRuntimePaths(homeDir)).catch(() => null);
|
|
2393
|
+
return normalizeCollectionRoots(config?.default_repo_paths ?? []);
|
|
2135
2394
|
}
|
|
2136
2395
|
function summarizeBackfillCursorSources(cursor) {
|
|
2137
2396
|
return {
|
|
@@ -2190,14 +2449,6 @@ async function countClaudeMainFilesBeforeCursor(projectsDir, oldestProcessedMs)
|
|
|
2190
2449
|
});
|
|
2191
2450
|
return count;
|
|
2192
2451
|
}
|
|
2193
|
-
async function hasAnyJsonlFile(roots) {
|
|
2194
|
-
let found = false;
|
|
2195
|
-
await walkFiles(roots, async (_filePath, entryName) => {
|
|
2196
|
-
if (entryName.endsWith(".jsonl"))
|
|
2197
|
-
found = true;
|
|
2198
|
-
}, () => found);
|
|
2199
|
-
return found;
|
|
2200
|
-
}
|
|
2201
2452
|
async function walkFiles(roots, onFile, shouldStop = () => false) {
|
|
2202
2453
|
const stack = [...roots];
|
|
2203
2454
|
while (stack.length > 0 && !shouldStop()) {
|
|
@@ -2387,23 +2638,51 @@ async function runServe(command, io) {
|
|
|
2387
2638
|
}
|
|
2388
2639
|
async function runAutostart(command, io) {
|
|
2389
2640
|
const exec = io.exec ?? defaultExec();
|
|
2641
|
+
const repoRoots = command.action === "install" || command.action === "status"
|
|
2642
|
+
? await resolveAutostartRoots(command.homeDir, command.repoRoot)
|
|
2643
|
+
: [];
|
|
2390
2644
|
const result = command.action === "install"
|
|
2391
2645
|
? await installAutostartAgent({
|
|
2392
2646
|
homeDir: command.homeDir,
|
|
2393
|
-
repoRoot:
|
|
2647
|
+
repoRoot: repoRoots[0],
|
|
2648
|
+
repoRoots,
|
|
2394
2649
|
dashboardUrl: command.dashboardUrl,
|
|
2395
2650
|
intervalSeconds: command.intervalSeconds,
|
|
2396
2651
|
exec,
|
|
2397
2652
|
})
|
|
2398
2653
|
: command.action === "uninstall"
|
|
2399
2654
|
? await uninstallAutostartAgent({ homeDir: command.homeDir, exec })
|
|
2400
|
-
: await autostartStatus({
|
|
2655
|
+
: await autostartStatus({
|
|
2656
|
+
homeDir: command.homeDir,
|
|
2657
|
+
repoRoot: repoRoots[0],
|
|
2658
|
+
repoRoots,
|
|
2659
|
+
dashboardUrl: command.dashboardUrl,
|
|
2660
|
+
intervalSeconds: command.intervalSeconds,
|
|
2661
|
+
exec,
|
|
2662
|
+
});
|
|
2401
2663
|
if (command.json) {
|
|
2402
2664
|
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
2403
|
-
return
|
|
2665
|
+
return autostartExitCode(command.action, result);
|
|
2404
2666
|
}
|
|
2405
2667
|
writeAutostartResult(io, result);
|
|
2406
|
-
return
|
|
2668
|
+
return autostartExitCode(command.action, result);
|
|
2669
|
+
}
|
|
2670
|
+
function autostartExitCode(action, result) {
|
|
2671
|
+
if (result.status === "unsupported")
|
|
2672
|
+
return 1;
|
|
2673
|
+
if (action === "status")
|
|
2674
|
+
return result.status === "loaded" ? 0 : 1;
|
|
2675
|
+
if (action === "install" && result.loaded === false)
|
|
2676
|
+
return 1;
|
|
2677
|
+
if (action === "uninstall" && result.status === "not_loaded")
|
|
2678
|
+
return 1;
|
|
2679
|
+
return 0;
|
|
2680
|
+
}
|
|
2681
|
+
async function resolveAutostartRoots(homeDir, explicitRoot) {
|
|
2682
|
+
if (explicitRoot)
|
|
2683
|
+
return [explicitRoot];
|
|
2684
|
+
const config = await readLocalCollectorConfig(getCollectorRuntimePaths(homeDir)).catch(() => null);
|
|
2685
|
+
return config?.default_repo_paths ?? [];
|
|
2407
2686
|
}
|
|
2408
2687
|
async function runAgentRules(command, io) {
|
|
2409
2688
|
const hosts = agentRuleHosts(command.host);
|
|
@@ -2453,9 +2732,9 @@ function writeAutostartResult(io, result) {
|
|
|
2453
2732
|
case "installed":
|
|
2454
2733
|
writeLine(io.stdout, result.loaded
|
|
2455
2734
|
? "Cockpit autostart installed and loaded."
|
|
2456
|
-
: "Cockpit autostart installed (
|
|
2735
|
+
: "Cockpit autostart installed (the operating-system scheduler reported a problem).");
|
|
2457
2736
|
writeLine(io.stdout, `Label: ${result.label}`);
|
|
2458
|
-
writeLine(io.stdout,
|
|
2737
|
+
writeLine(io.stdout, autostartLocationLine(result));
|
|
2459
2738
|
writeLine(io.stdout, `Interval: every ${result.interval_seconds}s`);
|
|
2460
2739
|
writeLine(io.stdout, `Repo: ${result.work_dir}`);
|
|
2461
2740
|
writeLine(io.stdout, `Dashboard: ${result.dashboard_url}`);
|
|
@@ -2464,45 +2743,37 @@ function writeAutostartResult(io, result) {
|
|
|
2464
2743
|
return;
|
|
2465
2744
|
case "uninstalled":
|
|
2466
2745
|
writeLine(io.stdout, "Cockpit autostart removed.");
|
|
2467
|
-
writeLine(io.stdout,
|
|
2746
|
+
writeLine(io.stdout, autostartLocationLine(result));
|
|
2468
2747
|
return;
|
|
2469
2748
|
case "absent":
|
|
2470
2749
|
writeLine(io.stdout, "Cockpit autostart is not installed.");
|
|
2471
|
-
writeLine(io.stdout,
|
|
2750
|
+
writeLine(io.stdout, autostartLocationLine(result));
|
|
2472
2751
|
return;
|
|
2473
2752
|
case "loaded":
|
|
2474
2753
|
writeLine(io.stdout, "Cockpit autostart is installed and loaded.");
|
|
2475
|
-
writeLine(io.stdout,
|
|
2754
|
+
writeLine(io.stdout, autostartLocationLine(result));
|
|
2476
2755
|
return;
|
|
2477
2756
|
case "not_loaded":
|
|
2478
|
-
writeLine(io.stdout, "Cockpit autostart
|
|
2479
|
-
writeLine(io.stdout,
|
|
2757
|
+
writeLine(io.stdout, "Cockpit autostart exists but is disabled or not loaded; run `cockpit autostart install` to repair it.");
|
|
2758
|
+
writeLine(io.stdout, autostartLocationLine(result));
|
|
2759
|
+
if (result.message)
|
|
2760
|
+
writeLine(io.stderr, result.message);
|
|
2480
2761
|
return;
|
|
2481
2762
|
case "unsupported":
|
|
2482
2763
|
writeLine(io.stderr, `Autostart unsupported: ${result.message}`);
|
|
2483
2764
|
return;
|
|
2484
2765
|
}
|
|
2485
2766
|
}
|
|
2767
|
+
function autostartLocationLine(result) {
|
|
2768
|
+
return result.task_name
|
|
2769
|
+
? `Task: ${result.task_name}`
|
|
2770
|
+
: `Plist: ${result.plist_path}`;
|
|
2771
|
+
}
|
|
2486
2772
|
function defaultExec() {
|
|
2487
|
-
return (
|
|
2488
|
-
execFile(cmd, args, { encoding: "utf8" }, (err, stdout, stderr) => {
|
|
2489
|
-
// execFile's error `.code` is the exit code when numeric, but a string
|
|
2490
|
-
// (e.g. "ENOENT") for spawn failures — treat those as a generic 1.
|
|
2491
|
-
const code = err == null ? 0 : typeof err.code === "number" ? err.code : 1;
|
|
2492
|
-
resolve({ code, stdout: stdout ?? "", stderr: stderr ?? "" });
|
|
2493
|
-
});
|
|
2494
|
-
});
|
|
2773
|
+
return createCapturedExecRunner();
|
|
2495
2774
|
}
|
|
2496
2775
|
function defaultInteractiveExec() {
|
|
2497
|
-
return (
|
|
2498
|
-
const child = spawn(cmd, args, { stdio: "inherit" });
|
|
2499
|
-
child.on("error", (error) => {
|
|
2500
|
-
resolve({ code: 1, stdout: "", stderr: errorMessage(error) });
|
|
2501
|
-
});
|
|
2502
|
-
child.on("close", (code) => {
|
|
2503
|
-
resolve({ code: code ?? 1, stdout: "", stderr: "" });
|
|
2504
|
-
});
|
|
2505
|
-
});
|
|
2776
|
+
return createInteractiveExecRunner();
|
|
2506
2777
|
}
|
|
2507
2778
|
function defaultIo() {
|
|
2508
2779
|
if (!globalThis.fetch) {
|