@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.
@@ -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
- const reasons = result.incomplete_reasons.join(",");
1798
- if (io) {
1799
- writeLine(io.stderr, `BLOCKED: repo discovery is incomplete (${reasons}); no collection cursor was advanced.`);
1800
- }
1801
- throw new Error(`repo discovery incomplete: ${reasons}. Raise --max-depth or --max-repos until every approved-root repo is included.`);
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.10");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.11");
19
19
  return 0;
20
20
  }
21
21
 
@@ -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 ?? 50);
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 ?? 2;
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 ?? 50);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {