@bli-cockpit/cli 0.2.94 → 0.2.95
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/commands/public-root.js +1 -1
- package/dist/commands/sync-followups-autostart.js +99 -0
- package/dist/commands/sync-followups-memory.js +123 -0
- package/dist/commands/sync-followups-self-update.js +127 -0
- package/dist/commands/sync-followups-staging.js +202 -0
- package/dist/commands/sync-followups.js +30 -511
- package/dist/commands/sync-heartbeat.js +36 -0
- package/dist/commands/sync-receipt.js +138 -0
- package/dist/commands/sync-report.js +153 -0
- package/dist/commands/sync-roots.js +28 -0
- package/dist/commands/sync-run.js +52 -0
- package/dist/commands/sync-types.js +1 -0
- package/dist/commands/sync.js +94 -357
- package/dist/disk-usage-classify.js +109 -0
- package/dist/disk-usage-facts.js +4 -0
- package/dist/disk-usage-files.js +99 -0
- package/dist/disk-usage-footprint.js +45 -0
- package/dist/disk-usage-ledger.js +49 -0
- package/dist/disk-usage-scan.js +80 -0
- package/dist/disk-usage-totals.js +83 -0
- package/dist/disk-usage.js +33 -396
- package/package.json +4 -4
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { writeLine } from "./cli-io.js";
|
|
2
|
+
import { classifySyncFailureRecords, redactedSyncErrorDetail, } from "./install-receipts.js";
|
|
3
|
+
import { runSyncLocked } from "./sync-run.js";
|
|
4
|
+
import { inspectBackfillLock } from "../backfill-lock.js";
|
|
5
|
+
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
6
|
+
import { acquireSyncLock } from "../sync-lock.js";
|
|
7
|
+
/**
|
|
8
|
+
* Run the tick behind both locks and come back with what to report.
|
|
9
|
+
*
|
|
10
|
+
* Standing aside is not failing: both skip arms exit 0 and say which lock they
|
|
11
|
+
* stood aside for, because a machine that runs its backfill for an hour is
|
|
12
|
+
* healthy and must not read as an hour of broken syncs.
|
|
13
|
+
*/
|
|
14
|
+
export async function runSyncWithHealthReceipt(command, io) {
|
|
15
|
+
const paths = getCollectorRuntimePaths(command.homeDir);
|
|
16
|
+
const backfillLock = await inspectBackfillLock(paths);
|
|
17
|
+
if (backfillLock.held) {
|
|
18
|
+
return standAsideForBackfill(command, io, backfillLock.held_since);
|
|
19
|
+
}
|
|
20
|
+
// Single-flight: a launchd timer and a manual sync must not interleave the
|
|
21
|
+
// cursor read-modify-write. A blocked invocation exits cleanly (B.4 §7).
|
|
22
|
+
const lock = await acquireSyncLock(paths);
|
|
23
|
+
if (!lock.acquired) {
|
|
24
|
+
return standAsideForRunningSync(command, io, lock.held_since);
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const run = await runSyncLocked(command, io);
|
|
28
|
+
return run.exitCode === 0 ? collectedReceipt(run) : failedReceipt(run);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
await lock.handle.release();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** The backfill owns the machine right now; say so and stand down. */
|
|
35
|
+
function standAsideForBackfill(command, io, heldSince) {
|
|
36
|
+
if (command.json) {
|
|
37
|
+
writeLine(io.stdout, JSON.stringify({
|
|
38
|
+
status: "live_sync_paused_during_backfill",
|
|
39
|
+
reason: "live sync paused during backfill",
|
|
40
|
+
held_since: heldSince,
|
|
41
|
+
collection_complete: false,
|
|
42
|
+
upload_state: "not_uploaded",
|
|
43
|
+
retryable: true,
|
|
44
|
+
}, null, 2));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
writeLine(io.stdout, "Pausing normal sync while it catches up on old sessions.");
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
exitCode: 0,
|
|
51
|
+
completion: {
|
|
52
|
+
step: "sync_complete",
|
|
53
|
+
status: "skipped",
|
|
54
|
+
error_code: "live_sync_paused_during_backfill",
|
|
55
|
+
},
|
|
56
|
+
heartbeat: {
|
|
57
|
+
status: "skipped",
|
|
58
|
+
reason: "live_sync_paused_during_backfill",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Another sync already holds the collection lock; say so and stand down. */
|
|
63
|
+
function standAsideForRunningSync(command, io, heldSince) {
|
|
64
|
+
if (command.json) {
|
|
65
|
+
writeLine(io.stdout, JSON.stringify({
|
|
66
|
+
status: "sync_already_running",
|
|
67
|
+
reason: "another sync owns the collection lock",
|
|
68
|
+
held_since: heldSince,
|
|
69
|
+
collection_complete: false,
|
|
70
|
+
upload_state: "not_uploaded",
|
|
71
|
+
retryable: true,
|
|
72
|
+
}, null, 2));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
writeLine(io.stdout, "Tower sync already running; skipping this run.");
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
exitCode: 0,
|
|
79
|
+
completion: {
|
|
80
|
+
step: "sync_complete",
|
|
81
|
+
status: "skipped",
|
|
82
|
+
error_code: "sync_already_running",
|
|
83
|
+
},
|
|
84
|
+
heartbeat: { status: "skipped", reason: "sync_already_running" },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The receipt for a run that came back clean.
|
|
89
|
+
*
|
|
90
|
+
* BLI-3551: an `ok` tick can still have something to say. `nothing_in_root`
|
|
91
|
+
* is the receipt that separates "this machine is alive and its operator
|
|
92
|
+
* works outside the approved roots" from "this machine is dead", which
|
|
93
|
+
* until now looked identical from the dashboard.
|
|
94
|
+
*/
|
|
95
|
+
function collectedReceipt(run) {
|
|
96
|
+
return {
|
|
97
|
+
exitCode: run.exitCode,
|
|
98
|
+
completion: {
|
|
99
|
+
step: "sync_complete",
|
|
100
|
+
status: "ok",
|
|
101
|
+
...(run.notice ? { error_detail: run.notice } : {}),
|
|
102
|
+
},
|
|
103
|
+
heartbeat: { status: "ok", reason: run.notice, ...heartbeatCounts(run) },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The receipt for a run that came back non-zero.
|
|
108
|
+
*
|
|
109
|
+
* A sync that fails by exit code says exactly as much as one that throws.
|
|
110
|
+
* It used to say `sync_failed` and nothing else, so 100% of recorded
|
|
111
|
+
* failure rows carried a null detail and the real reason was reachable only
|
|
112
|
+
* by running `cockpit status` on the machine itself (BLI-2526).
|
|
113
|
+
*/
|
|
114
|
+
function failedReceipt(run) {
|
|
115
|
+
const reasonText = run.failureReasons.join("; ");
|
|
116
|
+
// The bucket comes from the records the deciding branches wrote, not from
|
|
117
|
+
// this sentence (BLI-3551). The sentence is still the detail.
|
|
118
|
+
const errorCode = classifySyncFailureRecords(run.failureRecords);
|
|
119
|
+
return {
|
|
120
|
+
exitCode: run.exitCode,
|
|
121
|
+
completion: {
|
|
122
|
+
step: "sync_complete",
|
|
123
|
+
status: "fail",
|
|
124
|
+
error_code: errorCode,
|
|
125
|
+
error_detail: redactedSyncErrorDetail(reasonText),
|
|
126
|
+
},
|
|
127
|
+
heartbeat: { status: "fail", reason: errorCode, ...heartbeatCounts(run) },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** The counts the heartbeat carries so a quiet machine can explain itself. */
|
|
131
|
+
function heartbeatCounts(run) {
|
|
132
|
+
return {
|
|
133
|
+
sessionsObserved: run.sessionsObserved,
|
|
134
|
+
sessionsOutsideRoot: run.sessionsOutsideRoot,
|
|
135
|
+
sessionsNewThisTick: run.sessionsNewThisTick,
|
|
136
|
+
sessionsPendingUpload: run.sessionsPendingUpload,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { writeLine } from "./cli-io.js";
|
|
2
|
+
import { attributedSyncRunStatus, cursorStatusLine, displayTicketId, rawEvidenceSyncLine, shortSha, worktreeSyncRow, writeAgentSessionSummary, } from "./collection-report.js";
|
|
3
|
+
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
4
|
+
import { rawEvidenceGcSummary, runRawEvidenceLocalGc, } from "../raw-evidence-gc.js";
|
|
5
|
+
/**
|
|
6
|
+
* Report a run that touched more than one worktree.
|
|
7
|
+
*
|
|
8
|
+
* The per-worktree rows are the point: a parent sync is only as good as its
|
|
9
|
+
* weakest repo, and each row names its own failure reason.
|
|
10
|
+
*/
|
|
11
|
+
export async function reportMultiRepoSync(command, io, run, dedup) {
|
|
12
|
+
const rows = run.outcomes.map((outcome) => worktreeSyncRow(outcome, run));
|
|
13
|
+
const collectionRunStatus = attributedSyncRunStatus(run);
|
|
14
|
+
const gc = run.ok ? await runSyncRawEvidenceGc(command, io) : null;
|
|
15
|
+
if (command.json) {
|
|
16
|
+
writeLine(io.stdout, JSON.stringify({
|
|
17
|
+
mode: "multi_repo",
|
|
18
|
+
status: collectionRunStatus,
|
|
19
|
+
collection_complete: run.ok,
|
|
20
|
+
results: run.outcomes.map((outcome) => outcome.sync),
|
|
21
|
+
repos: rows,
|
|
22
|
+
codex_sessions: run.summary,
|
|
23
|
+
raw_evidence_gc: gc,
|
|
24
|
+
raw_evidence_dedup: dedup,
|
|
25
|
+
}, null, 2));
|
|
26
|
+
return syncResult(run);
|
|
27
|
+
}
|
|
28
|
+
writeLine(run.ok ? io.stdout : io.stderr, `Tower parent sync ${collectionRunStatus} ${run.outcomes.filter((outcome) => outcome.sync.status === "uploaded").length}/${run.outcomes.length} worktree(s).`);
|
|
29
|
+
writeWorktreeSyncRows(io, run);
|
|
30
|
+
writeAgentSessionSummary(io, run.summary);
|
|
31
|
+
if (gc && !gc.skipped)
|
|
32
|
+
writeLine(io.stdout, rawEvidenceGcSummary(gc));
|
|
33
|
+
return syncResult(run);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Report a run that found no git worktrees at all — a steady state, not a
|
|
37
|
+
* failure (the reasoning is at the branch that chooses this shape, in
|
|
38
|
+
* `sync-run.ts`). It still says what the session scan saw, and when everything
|
|
39
|
+
* it saw was outside the approved folders it says that in words.
|
|
40
|
+
*/
|
|
41
|
+
export async function reportNoWorktreeSync(command, io, run, dedup) {
|
|
42
|
+
const collectionRunStatus = attributedSyncRunStatus(run);
|
|
43
|
+
const gc = run.ok ? await runSyncRawEvidenceGc(command, io) : null;
|
|
44
|
+
if (command.json) {
|
|
45
|
+
writeLine(io.stdout, JSON.stringify({
|
|
46
|
+
mode: "no_worktrees",
|
|
47
|
+
status: collectionRunStatus,
|
|
48
|
+
collection_complete: run.ok,
|
|
49
|
+
...(run.notice ? { notice: run.notice } : {}),
|
|
50
|
+
codex_sessions: run.summary,
|
|
51
|
+
raw_evidence_gc: gc,
|
|
52
|
+
raw_evidence_dedup: dedup,
|
|
53
|
+
}, null, 2));
|
|
54
|
+
return syncResult(run);
|
|
55
|
+
}
|
|
56
|
+
writeLine(run.ok ? io.stdout : io.stderr, `Tower sync ${collectionRunStatus}: no git worktrees under this root; session scan ran.`);
|
|
57
|
+
if (run.notice) {
|
|
58
|
+
// Says out loud what the receipt now says to the dashboard: the sessions
|
|
59
|
+
// this machine ran were all outside the folders it is allowed to look at.
|
|
60
|
+
writeLine(io.stdout, `Every session seen this run was outside your approved folders (${run.notice}). Nothing was collected, and nothing is broken.`);
|
|
61
|
+
}
|
|
62
|
+
writeAgentSessionSummary(io, run.summary);
|
|
63
|
+
if (gc && !gc.skipped)
|
|
64
|
+
writeLine(io.stdout, rawEvidenceGcSummary(gc));
|
|
65
|
+
return syncResult(run);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Report the ordinary case: one worktree, three ways it can have gone.
|
|
69
|
+
*
|
|
70
|
+
* A clean run prints what landed, a partial one says to run the tick again,
|
|
71
|
+
* and a spooled one names the failure and the retry command — the operator's
|
|
72
|
+
* next move is on the screen in every branch.
|
|
73
|
+
*/
|
|
74
|
+
export async function reportSingleRepoSync(command, io, run, dedup) {
|
|
75
|
+
const result = run.outcomes[0]?.sync;
|
|
76
|
+
if (!result) {
|
|
77
|
+
throw new Error("Sync produced no result for the repo worktree.");
|
|
78
|
+
}
|
|
79
|
+
const collectionRunStatus = attributedSyncRunStatus(run);
|
|
80
|
+
const gc = run.ok ? await runSyncRawEvidenceGc(command, io) : null;
|
|
81
|
+
if (command.json) {
|
|
82
|
+
writeLine(io.stdout, JSON.stringify({
|
|
83
|
+
...result,
|
|
84
|
+
status: collectionRunStatus,
|
|
85
|
+
collection_complete: run.ok,
|
|
86
|
+
codex_sessions: run.summary,
|
|
87
|
+
raw_evidence_gc: gc,
|
|
88
|
+
raw_evidence_dedup: dedup,
|
|
89
|
+
}, null, 2));
|
|
90
|
+
return syncResult(run);
|
|
91
|
+
}
|
|
92
|
+
if (run.ok) {
|
|
93
|
+
writeUploadedSessionLines(io, result);
|
|
94
|
+
writeAgentSessionSummary(io, run.summary);
|
|
95
|
+
writeLine(io.stdout, cursorStatusLine(result));
|
|
96
|
+
if (gc && !gc.skipped)
|
|
97
|
+
writeLine(io.stdout, rawEvidenceGcSummary(gc));
|
|
98
|
+
return syncResult(run);
|
|
99
|
+
}
|
|
100
|
+
if (result.status === "uploaded") {
|
|
101
|
+
writeLine(io.stderr, "Tower uploaded, but some sessions did not make it. Run `cockpit sync` again.");
|
|
102
|
+
writeAgentSessionSummary(io, run.summary);
|
|
103
|
+
return syncResult(run);
|
|
104
|
+
}
|
|
105
|
+
writeLine(io.stderr, "Tower could not upload. It saved a note to retry and will try again on the next sync.");
|
|
106
|
+
writeLine(io.stderr, `Ticket: ${displayTicketId(result.ticket_id)}`);
|
|
107
|
+
writeLine(io.stderr, `Failure: ${result.failure_reason}`);
|
|
108
|
+
writeLine(io.stderr, `Retry: ${result.retry_command}`);
|
|
109
|
+
return syncResult(run);
|
|
110
|
+
}
|
|
111
|
+
/** One line per worktree, on stdout when it uploaded and stderr when it did not. */
|
|
112
|
+
function writeWorktreeSyncRows(io, run) {
|
|
113
|
+
for (const outcome of run.outcomes) {
|
|
114
|
+
const { worktree, sync } = outcome;
|
|
115
|
+
const uploaded = sync.status === "uploaded";
|
|
116
|
+
const failureSuffix = sync.status === "spooled" ? ` reason:${sync.failure_reason}` : "";
|
|
117
|
+
writeLine(uploaded ? io.stdout : io.stderr, `- ${worktree.repo_label}/${worktree.worktree_label} (${worktree.branch}) head:${shortSha(sync.head_sha ?? worktree.head_sha)} ${sync.status} objects:${sync.raw_evidence_uploaded_object_count} chunks:${sync.raw_evidence_uploaded_chunk_count} reused:${sync.raw_evidence_reused_count} failed:${sync.raw_evidence_failed_count} cursor:${sync.cursor_tracked_object_count}${failureSuffix}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** What one clean single-repo tick collected, in the order a person reads it. */
|
|
121
|
+
function writeUploadedSessionLines(io, result) {
|
|
122
|
+
writeLine(io.stdout, "Tower uploaded this session.");
|
|
123
|
+
writeLine(io.stdout, `Ticket: ${displayTicketId(result.ticket_id)}`);
|
|
124
|
+
writeLine(io.stdout, `Context: ${result.work_context_id}`);
|
|
125
|
+
writeLine(io.stdout, `Head: ${shortSha(result.head_sha)}`);
|
|
126
|
+
writeLine(io.stdout, `Things recorded: ${result.event_count}`);
|
|
127
|
+
writeLine(io.stdout, `Risk flags: ${result.risk_flag_count}`);
|
|
128
|
+
writeLine(io.stdout, `Raw evidence files: ${result.raw_evidence_file_count}`);
|
|
129
|
+
writeLine(io.stdout, rawEvidenceSyncLine(result));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Turn a finished run into an exit code and the reasons behind it.
|
|
133
|
+
*
|
|
134
|
+
* One place, so a future return path cannot reintroduce a code with no reason.
|
|
135
|
+
* The reasons come from the run itself — the code that decided `ok` is false is
|
|
136
|
+
* the only code that knows why.
|
|
137
|
+
*/
|
|
138
|
+
function syncResult(run) {
|
|
139
|
+
return {
|
|
140
|
+
exitCode: run.ok ? 0 : 1,
|
|
141
|
+
failureReasons: run.ok ? [] : run.failure_reasons,
|
|
142
|
+
failureRecords: run.ok ? [] : run.failure_records,
|
|
143
|
+
notice: run.notice,
|
|
144
|
+
sessionsObserved: run.sessions_observed,
|
|
145
|
+
sessionsOutsideRoot: run.sessions_outside_root,
|
|
146
|
+
sessionsNewThisTick: run.sessions_new_this_tick,
|
|
147
|
+
sessionsPendingUpload: run.sessions_pending_upload,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** The local raw-evidence sweep a successful run earns before it reports. */
|
|
151
|
+
async function runSyncRawEvidenceGc(command, io) {
|
|
152
|
+
return runRawEvidenceLocalGc(getCollectorRuntimePaths(command.homeDir), io.env);
|
|
153
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which approved roots this tick may collect (BLI-3982).
|
|
3
|
+
*
|
|
4
|
+
* Two steps ask the question — the run, which collects them, and the
|
|
5
|
+
* heartbeat, which labels the check-in with them — so it has one answer here
|
|
6
|
+
* rather than one each.
|
|
7
|
+
*/
|
|
8
|
+
import { collectionRootConsentAliases } from "./collection-roots.js";
|
|
9
|
+
import { getCollectorRuntimePaths, readLocalCollectorConfig, } from "../local-state.js";
|
|
10
|
+
import { CollectionRootRequiredError } from "../onboarding-roots.js";
|
|
11
|
+
import { normalizeCollectionRoots } from "../root-normalization.js";
|
|
12
|
+
/**
|
|
13
|
+
* The roots this run is allowed to look at: what the operator typed, else what
|
|
14
|
+
* they consented to when they onboarded, else nothing — and nothing is an
|
|
15
|
+
* error, because a sync with no boundary has no business reading a disk.
|
|
16
|
+
*/
|
|
17
|
+
export async function resolveSyncCollectionRoots(command) {
|
|
18
|
+
const explicitRoots = normalizeCollectionRoots(command.repoRoot ? [command.repoRoot] : []);
|
|
19
|
+
if (explicitRoots.length > 0) {
|
|
20
|
+
return collectionRootConsentAliases(explicitRoots);
|
|
21
|
+
}
|
|
22
|
+
const config = await readLocalCollectorConfig(getCollectorRuntimePaths(command.homeDir)).catch(() => null);
|
|
23
|
+
const savedRoots = normalizeCollectionRoots(config?.default_repo_paths ?? []);
|
|
24
|
+
if (savedRoots.length > 0) {
|
|
25
|
+
return collectionRootConsentAliases(savedRoots);
|
|
26
|
+
}
|
|
27
|
+
throw new CollectionRootRequiredError(`no explicit or saved collection root is available.`);
|
|
28
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { writeLine } from "./cli-io.js";
|
|
2
|
+
import { discoverCommandWorktrees } from "./local-discovery.js";
|
|
3
|
+
import { runAttributedWorktreeSync } from "./session-sync.js";
|
|
4
|
+
import { reportMultiRepoSync, reportNoWorktreeSync, reportSingleRepoSync, } from "./sync-report.js";
|
|
5
|
+
import { resolveSyncCollectionRoots } from "./sync-roots.js";
|
|
6
|
+
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
7
|
+
import { rawEvidenceDedupSummary, sweepDuplicateStagedRawEvidence, } from "../raw-evidence-gc.js";
|
|
8
|
+
/**
|
|
9
|
+
* Collect everything this tick may collect, then report it.
|
|
10
|
+
*
|
|
11
|
+
* The caller holds the collection lock for the whole of this function; nothing
|
|
12
|
+
* here acquires or releases one.
|
|
13
|
+
*/
|
|
14
|
+
export async function runSyncLocked(command, io) {
|
|
15
|
+
const collectionRoots = await resolveSyncCollectionRoots(command);
|
|
16
|
+
// Before anything is collected: collapse byte-identical staged packs. It runs
|
|
17
|
+
// first, unconditionally and unthrottled, because a machine that already
|
|
18
|
+
// holds 559 copies of one rollout needs the disk back before it stages
|
|
19
|
+
// anything else (BLI-3066). Safe by construction — a duplicate is identical
|
|
20
|
+
// by content hash to the survivor.
|
|
21
|
+
const dedup = await sweepDuplicateStagedRawEvidence(getCollectorRuntimePaths(command.homeDir), io.env);
|
|
22
|
+
if (!dedup.skipped && dedup.removed_dirs > 0) {
|
|
23
|
+
writeLine(io.stdout, rawEvidenceDedupSummary(dedup));
|
|
24
|
+
}
|
|
25
|
+
const worktrees = await discoverCommandWorktrees(collectionRoots, {
|
|
26
|
+
maxDepth: command.maxDepth,
|
|
27
|
+
maxRepos: command.maxRepos,
|
|
28
|
+
homeDir: command.homeDir,
|
|
29
|
+
allowEmpty: true,
|
|
30
|
+
}, io);
|
|
31
|
+
const run = await runAttributedWorktreeSync({
|
|
32
|
+
homeDir: command.homeDir,
|
|
33
|
+
dashboardUrl: command.dashboardUrl,
|
|
34
|
+
collectionRoots,
|
|
35
|
+
startContexts: false,
|
|
36
|
+
worktrees,
|
|
37
|
+
fetchImpl: io.fetch,
|
|
38
|
+
});
|
|
39
|
+
if (run.outcomes.length > 1) {
|
|
40
|
+
return reportMultiRepoSync(command, io, run, dedup);
|
|
41
|
+
}
|
|
42
|
+
// Zero worktrees is a legitimate steady state, not a failure: an approved
|
|
43
|
+
// root can hold no git repos, and sessions upload independently of
|
|
44
|
+
// worktrees (session-first, BLI-2581). This used to throw "Sync produced no
|
|
45
|
+
// result", which painted ~90 false-red sync_failed receipts per day on one
|
|
46
|
+
// fleet machine with a single empty root and taught people to ignore
|
|
47
|
+
// sync_failed (BLI-2722). A genuinely broken run still fails via run.ok.
|
|
48
|
+
if (run.outcomes.length === 0) {
|
|
49
|
+
return reportNoWorktreeSync(command, io, run, dedup);
|
|
50
|
+
}
|
|
51
|
+
return reportSingleRepoSync(command, io, run, dedup);
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|