@bli-cockpit/cli 0.2.10 → 0.2.11
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.
|
@@ -9,7 +9,7 @@ import { RAW_EVIDENCE_DEFAULT_BYTE_BUDGET, RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET }
|
|
|
9
9
|
import { acquireBackfillLock } from "../backfill-lock.js";
|
|
10
10
|
import { BACKFILL_COMPLETION_RECHECK_MS, BACKFILL_COVERAGE_VERSION, emptyBackfillCursorState, prepareBackfillCursorForScope, readBackfillCursor, recordBackfillCursorObservations, recordBackfillScanCoverage, writeBackfillCompletionMarker, writeBackfillCursor, } from "../cursors/backfill-cursor.js";
|
|
11
11
|
import { getCollectorRuntimePaths, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, readLocalWorkContextForRepo, startLocalWorkContext } from "../local-state.js";
|
|
12
|
-
import { collectionRootPathAliases, discoverGitWorktreesInRootsWithStatus, } from "../repo-identity.js";
|
|
12
|
+
import { DEFAULT_DISCOVERY_MAX_DEPTH, DEFAULT_DISCOVERY_MAX_REPOS, collectionRootPathAliases, discoverGitWorktreesInRootsWithStatus, } from "../repo-identity.js";
|
|
13
13
|
import { isRawEvidenceUploadableAttributionState } from "../raw-evidence-attribution-policy.js";
|
|
14
14
|
import { normalizeCollectionRoots } from "../root-normalization.js";
|
|
15
15
|
import { acquireSyncLock } from "../sync-lock.js";
|
|
@@ -17,8 +17,6 @@ import { LocalUploadBlockedError, postCodexSessionReport, syncLocalAmbientEnvelo
|
|
|
17
17
|
const BACKFILL_UPLOAD_BATCH_SESSIONS = 25;
|
|
18
18
|
const BACKFILL_MAX_CONSECUTIVE_FAILURES = 3;
|
|
19
19
|
const ALL_BACKFILL_SINCE_MINUTES = 20 * 365 * 24 * 60;
|
|
20
|
-
const DEFAULT_DISCOVERY_MAX_DEPTH = 3;
|
|
21
|
-
const DEFAULT_DISCOVERY_MAX_REPOS = 50;
|
|
22
20
|
// Window a bare `cockpit backfill` uses. Wide enough to cover a new machine's
|
|
23
21
|
// recent history and an intern who went quiet for a few weeks, narrow enough
|
|
24
22
|
// that it is not the whole-history scan `--all` deliberately gates.
|
package/dist/commands/local.js
CHANGED
|
@@ -15,7 +15,7 @@ import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WIN
|
|
|
15
15
|
import { scanAndAttributeClaudeSessions } from "../adapters/claude-attribution.js";
|
|
16
16
|
import { backfillCompletionCovers, emptyBackfillCursorState, prepareBackfillCursorForScope, readBackfillCompletionMarker, readBackfillCursor, } from "../cursors/backfill-cursor.js";
|
|
17
17
|
import { acquireSyncLock } from "../sync-lock.js";
|
|
18
|
-
import { collectionRootPathAliases, discoverGitWorktreesInRootsWithStatus, } from "../repo-identity.js";
|
|
18
|
+
import { DEFAULT_DISCOVERY_MAX_DEPTH, DEFAULT_DISCOVERY_MAX_REPOS, collectionRootPathAliases, discoverGitWorktreesInRootsWithStatus, } from "../repo-identity.js";
|
|
19
19
|
import { runAttributedWorktreeSync, matchesLiveSyncWorktree, } from "./session-sync.js";
|
|
20
20
|
import { COLLECTION_ROOT_REQUIRED, missingCollectionRootMessage, normalizeRootsDetailed, resolveOnboardingRoots, rootRejectionExplanation, } from "../onboarding-roots.js";
|
|
21
21
|
import { rawEvidenceGcSummary, runRawEvidenceLocalGc, } from "../raw-evidence-gc.js";
|
|
@@ -1779,8 +1779,6 @@ function rawEvidenceSyncLine(sync) {
|
|
|
1779
1779
|
function cursorStatusLine(sync) {
|
|
1780
1780
|
return `Cursor: ${sync.cursor_tracked_object_count} durable object(s) tracked`;
|
|
1781
1781
|
}
|
|
1782
|
-
const DEFAULT_DISCOVERY_MAX_DEPTH = 3;
|
|
1783
|
-
const DEFAULT_DISCOVERY_MAX_REPOS = 50;
|
|
1784
1782
|
const ALL_SESSION_SCAN_WINDOW_MINUTES = 20 * 365 * 24 * 60;
|
|
1785
1783
|
const SESSION_SCAN_OVERRIDE_LIMIT = 10_000;
|
|
1786
1784
|
async function discoverCommandWorktrees(repoRoot, discovery = {}, io) {
|
|
@@ -1794,17 +1792,59 @@ async function discoverCommandWorktrees(repoRoot, discovery = {}, io) {
|
|
|
1794
1792
|
});
|
|
1795
1793
|
const worktrees = result.worktrees;
|
|
1796
1794
|
if (!result.complete) {
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1795
|
+
// Sync fails closed here ON PURPOSE, and that is not the bug. Advancing a
|
|
1796
|
+
// cursor after a partial scan would mark the run as covering repos it
|
|
1797
|
+
// never saw, permanently skipping their sessions — backfill can tolerate
|
|
1798
|
+
// partial only because it keeps per-scope completion markers, and sync
|
|
1799
|
+
// does not. The bug (BLI-2362) was that the refusal named no roots and
|
|
1800
|
+
// gave no runnable command, so a big workspace just stayed red forever.
|
|
1801
|
+
const message = incompleteDiscoveryMessage({
|
|
1802
|
+
result,
|
|
1803
|
+
roots,
|
|
1804
|
+
maxDepth: discovery.maxDepth ?? DEFAULT_DISCOVERY_MAX_DEPTH,
|
|
1805
|
+
maxRepos: maxWorktrees,
|
|
1806
|
+
found: worktrees.length,
|
|
1807
|
+
});
|
|
1808
|
+
if (io)
|
|
1809
|
+
writeLine(io.stderr, message);
|
|
1810
|
+
throw new Error(message);
|
|
1802
1811
|
}
|
|
1803
1812
|
if (worktrees.length === 0 && !discovery.allowEmpty) {
|
|
1804
1813
|
throw new Error("No git repos found. Run from a git repo, or from a parent folder containing git repos.");
|
|
1805
1814
|
}
|
|
1806
1815
|
return worktrees;
|
|
1807
1816
|
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Names the roots that could not be covered and hands back a command that
|
|
1819
|
+
* actually fixes it, with this machine's numbers already filled in.
|
|
1820
|
+
*/
|
|
1821
|
+
function incompleteDiscoveryMessage(input) {
|
|
1822
|
+
const { result, roots, maxDepth, maxRepos, found } = input;
|
|
1823
|
+
const blocked = result.incomplete_roots.length > 0 ? result.incomplete_roots : roots;
|
|
1824
|
+
const hitRepoCap = result.incomplete_reasons.includes("max_worktrees_reached");
|
|
1825
|
+
const nextDepth = maxDepth + 3;
|
|
1826
|
+
const nextRepos = Math.max(maxRepos * 2, found + 50);
|
|
1827
|
+
const retry = [
|
|
1828
|
+
"cockpit do-everything",
|
|
1829
|
+
...roots.map((root) => `--workspace ${root}`),
|
|
1830
|
+
`--max-depth ${hitRepoCap ? maxDepth : nextDepth}`,
|
|
1831
|
+
`--max-repos ${nextRepos}`,
|
|
1832
|
+
].join(" ");
|
|
1833
|
+
return [
|
|
1834
|
+
`Cockpit could not finish scanning for repos, so it stopped instead of collecting a partial picture (${result.incomplete_reasons.join(", ")}).`,
|
|
1835
|
+
"It stops rather than continuing because a partial scan would mark these repos as already checked and skip them from now on.",
|
|
1836
|
+
"",
|
|
1837
|
+
"Could not fully scan:",
|
|
1838
|
+
...blocked.map((root) => ` ${root}`),
|
|
1839
|
+
"",
|
|
1840
|
+
`Found ${found} repo(s) before stopping, with --max-depth ${maxDepth} and --max-repos ${maxRepos}.`,
|
|
1841
|
+
"",
|
|
1842
|
+
"Run this to raise the limits and try again:",
|
|
1843
|
+
` ${retry}`,
|
|
1844
|
+
"",
|
|
1845
|
+
"If that still stops, the folder is deeper or larger than expected — raise the numbers again, or point --workspace at the specific project folders instead of a parent.",
|
|
1846
|
+
].join("\n");
|
|
1847
|
+
}
|
|
1808
1848
|
async function runMultiRepoOnboard(command, io, worktrees) {
|
|
1809
1849
|
if (!command.json) {
|
|
1810
1850
|
writeLine(io.stdout, `3/5 Parent folder mode: discovered ${worktrees.length} git worktree(s).`);
|
|
@@ -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.11");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
package/dist/repo-identity.js
CHANGED
|
@@ -16,6 +16,21 @@ const SKIPPED_DIR_NAMES = new Set([
|
|
|
16
16
|
"node_modules",
|
|
17
17
|
"out",
|
|
18
18
|
]);
|
|
19
|
+
/**
|
|
20
|
+
* How deep discovery walks, and how many repos it will hold, when the caller
|
|
21
|
+
* names no limit. Both are the ONLY defaults — commands import these rather
|
|
22
|
+
* than declaring their own, because a second copy silently drifts.
|
|
23
|
+
*
|
|
24
|
+
* These are generous on purpose. Discovery stops descending the moment it sees
|
|
25
|
+
* a git marker and skips dot-dirs, `node_modules`, `dist` and friends, so the
|
|
26
|
+
* walk is bounded by plain folders rather than by repo contents: on a real
|
|
27
|
+
* workspace of 38 repos, depth 20 visited the same 308 directories in the same
|
|
28
|
+
* ~110ms as depth 6, because the tree simply ran out first. A limit that is too
|
|
29
|
+
* low is not merely slow — discovery fails closed, so every extra level costs
|
|
30
|
+
* nothing while every missing level costs the whole machine's collection.
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_DISCOVERY_MAX_DEPTH = 20;
|
|
33
|
+
export const DEFAULT_DISCOVERY_MAX_REPOS = 200;
|
|
19
34
|
export async function resolveRepoWorktreeIdentity(repoRoot) {
|
|
20
35
|
const requestedPath = path.resolve(repoRoot);
|
|
21
36
|
const gitRoot = await runGit(["rev-parse", "--show-toplevel"], requestedPath);
|
|
@@ -62,14 +77,14 @@ export async function discoverGitWorktrees(root, options = {}) {
|
|
|
62
77
|
export async function discoverGitWorktreesWithStatus(root, options = {}) {
|
|
63
78
|
const resolvedRoot = path.resolve(root);
|
|
64
79
|
const allowedRoots = [await stableWorktreeRoot(resolvedRoot)];
|
|
65
|
-
const maxWorktrees = Math.max(1, options.maxWorktrees ??
|
|
80
|
+
const maxWorktrees = Math.max(1, options.maxWorktrees ?? DEFAULT_DISCOVERY_MAX_REPOS);
|
|
66
81
|
const direct = await resolveRepoWorktreeIdentity(resolvedRoot).catch(() => null);
|
|
67
82
|
if (direct)
|
|
68
83
|
return expandLinkedWorktrees([direct], maxWorktrees, allowedRoots);
|
|
69
84
|
if (await hasGitMarker(resolvedRoot)) {
|
|
70
85
|
return expandLinkedWorktrees([await fallbackFilesystemIdentity(resolvedRoot)], maxWorktrees, allowedRoots);
|
|
71
86
|
}
|
|
72
|
-
const maxDepth = options.maxDepth ??
|
|
87
|
+
const maxDepth = options.maxDepth ?? DEFAULT_DISCOVERY_MAX_DEPTH;
|
|
73
88
|
const discovered = new Map();
|
|
74
89
|
const stack = [{ dir: resolvedRoot, depth: 0 }];
|
|
75
90
|
const visited = new Set();
|
|
@@ -113,6 +128,8 @@ export async function discoverGitWorktreesWithStatus(root, options = {}) {
|
|
|
113
128
|
worktrees: expanded.worktrees,
|
|
114
129
|
complete: incompleteReasons.size === 0,
|
|
115
130
|
incomplete_reasons: [...incompleteReasons].sort(),
|
|
131
|
+
// Single-root scanner: the only root in play is the one it was handed.
|
|
132
|
+
incomplete_roots: incompleteReasons.size === 0 ? [] : [path.resolve(root)],
|
|
116
133
|
};
|
|
117
134
|
}
|
|
118
135
|
/**
|
|
@@ -126,9 +143,10 @@ export async function discoverGitWorktreesInRoots(roots, options = {}) {
|
|
|
126
143
|
return (await discoverGitWorktreesInRootsWithStatus(roots, options)).worktrees;
|
|
127
144
|
}
|
|
128
145
|
export async function discoverGitWorktreesInRootsWithStatus(roots, options = {}) {
|
|
129
|
-
const maxWorktrees = Math.max(1, options.maxWorktrees ??
|
|
146
|
+
const maxWorktrees = Math.max(1, options.maxWorktrees ?? DEFAULT_DISCOVERY_MAX_REPOS);
|
|
130
147
|
const discovered = new Map();
|
|
131
148
|
const incompleteReasons = new Set();
|
|
149
|
+
const incompleteRoots = new Set();
|
|
132
150
|
for (const root of roots) {
|
|
133
151
|
const result = await discoverGitWorktreesWithStatus(root, {
|
|
134
152
|
...options,
|
|
@@ -136,11 +154,15 @@ export async function discoverGitWorktreesInRootsWithStatus(roots, options = {})
|
|
|
136
154
|
});
|
|
137
155
|
for (const reason of result.incomplete_reasons) {
|
|
138
156
|
incompleteReasons.add(reason);
|
|
157
|
+
incompleteRoots.add(root);
|
|
139
158
|
}
|
|
140
159
|
for (const worktree of result.worktrees) {
|
|
141
160
|
if (discovered.size >= maxWorktrees) {
|
|
142
161
|
if (!discovered.has(worktree.worktree_fingerprint)) {
|
|
143
162
|
incompleteReasons.add("max_worktrees_reached");
|
|
163
|
+
// The cap is global, so the root being read when it filled up is the
|
|
164
|
+
// one whose repos got dropped.
|
|
165
|
+
incompleteRoots.add(root);
|
|
144
166
|
}
|
|
145
167
|
continue;
|
|
146
168
|
}
|
|
@@ -151,6 +173,7 @@ export async function discoverGitWorktreesInRootsWithStatus(roots, options = {})
|
|
|
151
173
|
worktrees: [...discovered.values()].sort(compareIdentity),
|
|
152
174
|
complete: incompleteReasons.size === 0,
|
|
153
175
|
incomplete_reasons: [...incompleteReasons].sort(),
|
|
176
|
+
incomplete_roots: [...incompleteRoots].sort(),
|
|
154
177
|
};
|
|
155
178
|
}
|
|
156
179
|
/**
|
|
@@ -219,6 +242,9 @@ async function expandLinkedWorktrees(identities, maxWorktrees, allowedRoots) {
|
|
|
219
242
|
worktrees,
|
|
220
243
|
complete: incompleteReasons.length === 0,
|
|
221
244
|
incomplete_reasons: incompleteReasons,
|
|
245
|
+
// Linked-worktree expansion is not scoped to one root; callers merge this
|
|
246
|
+
// into a result that already knows which roots were involved.
|
|
247
|
+
incomplete_roots: [],
|
|
222
248
|
};
|
|
223
249
|
}
|
|
224
250
|
function isLinkedWorktreeWithinCollectionScope(linked, discoveredFromApprovedRoots, allowedRoots) {
|