@bli-cockpit/cli 0.1.5 → 0.1.6

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 CHANGED
@@ -33,9 +33,12 @@ npm exec --yes --package=@bli-cockpit/cli@latest -- cockpit onboard \
33
33
  --repo "$PWD"
34
34
  ```
35
35
 
36
- Parent mode scans child git repos/worktrees, creates one stable work context per
36
+ Parent mode scans child git repos/worktrees (3 folder levels deep, up to 50
37
+ repos by default — tune with `--max-depth` / `--max-repos`; a warning prints
38
+ if the repo cap truncates discovery), creates one stable work context per
37
39
  worktree, and the dashboard rolls them up under one repo row with worktree
38
- drilldown. Device approval is still one time per laptop/dashboard/email. Codex
40
+ drilldown. Repos cloned later are picked up automatically: `cockpit sync`
41
+ starts a general ambient work context for newly discovered repos on its own. Device approval is still one time per laptop/dashboard/email. Codex
39
42
  JSONL transcripts are attributed to a repo/worktree deterministically (session
40
43
  cwd, workspace roots, git origin/branch/commit signals) and upload only into
41
44
  that repo's evidence lane; transcripts that cannot be attributed to exactly one
@@ -2,7 +2,7 @@ import os from "node:os";
2
2
  import path from "node:path";
3
3
  import { createCollectorServer } from "../server.js";
4
4
  import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext, } from "../local-state.js";
5
- import { postCodexSessionReport, syncLocalAmbientEnvelope, } from "../upload.js";
5
+ import { LocalUploadBlockedError, postCodexSessionReport, syncLocalAmbientEnvelope, } from "../upload.js";
6
6
  import { scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
7
7
  import { countStaleSessions, readRawEvidenceCursor, recordSessionObservation, writeRawEvidenceCursor, } from "../cursors/raw-evidence-cursor.js";
8
8
  import { discoverGitWorktrees, } from "../repo-identity.js";
@@ -61,14 +61,14 @@ export function localCommandHelp(command) {
61
61
  if (command)
62
62
  return localSubcommandHelp(command);
63
63
  return [
64
- " cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--repo <path>] [--branch <name>] [--json]",
64
+ " cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--repo <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
65
65
  " cockpit install [--dashboard-url <url>] [--repo <path>] [--json]",
66
66
  " cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
67
67
  " cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
68
68
  " cockpit logout",
69
- " cockpit start [--ticket <id>] [--repo <path>] [--branch <name>] [--json]",
70
- " cockpit sync [--repo <path>] [--dashboard-url <url>] [--json]",
71
- " cockpit status [--repo <path>] [--json]",
69
+ " cockpit start [--ticket <id>] [--repo <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
70
+ " cockpit sync [--repo <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
71
+ " cockpit status [--repo <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
72
72
  " cockpit serve [--port <port>] [--repo <path>]",
73
73
  ].join("\n");
74
74
  }
@@ -127,6 +127,9 @@ function localSubcommandHelp(command) {
127
127
  "Parent folders sync each child git worktree; Codex JSONL transcripts are attributed",
128
128
  "to repos deterministically and ambiguous transcripts are retained as unattributed",
129
129
  "instead of being duplicated across repos.",
130
+ "Newly discovered repos get a general ambient work context automatically.",
131
+ "Discovery scans 3 folder levels and up to 50 repos by default; tune with",
132
+ "--max-depth and --max-repos.",
130
133
  ],
131
134
  ],
132
135
  [
@@ -191,6 +194,8 @@ function parseOnboardArgs(args) {
191
194
  "--json",
192
195
  "--poll-interval-ms",
193
196
  "--timeout-ms",
197
+ "--max-depth",
198
+ "--max-repos",
194
199
  ],
195
200
  valueFlags: [
196
201
  "--home",
@@ -202,6 +207,8 @@ function parseOnboardArgs(args) {
202
207
  "--branch",
203
208
  "--poll-interval-ms",
204
209
  "--timeout-ms",
210
+ "--max-depth",
211
+ "--max-repos",
205
212
  ],
206
213
  });
207
214
  assertNoPositionals(values.positionals, "onboard");
@@ -217,6 +224,8 @@ function parseOnboardArgs(args) {
217
224
  json: values.booleans.has("--json"),
218
225
  pollIntervalMs: optionalPositiveInteger(values.flags.get("--poll-interval-ms"), "--poll-interval-ms"),
219
226
  timeoutMs: optionalPositiveInteger(values.flags.get("--timeout-ms"), "--timeout-ms"),
227
+ maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
228
+ maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
220
229
  };
221
230
  }
222
231
  function parseInstallArgs(args) {
@@ -294,6 +303,8 @@ function parseStartArgs(args) {
294
303
  "--operator-id",
295
304
  "--session-id",
296
305
  "--json",
306
+ "--max-depth",
307
+ "--max-repos",
297
308
  ],
298
309
  valueFlags: [
299
310
  "--home",
@@ -302,6 +313,8 @@ function parseStartArgs(args) {
302
313
  "--ticket",
303
314
  "--operator-id",
304
315
  "--session-id",
316
+ "--max-depth",
317
+ "--max-repos",
305
318
  ],
306
319
  });
307
320
  assertNoPositionals(values.positionals, "start");
@@ -314,12 +327,14 @@ function parseStartArgs(args) {
314
327
  operatorId: optionalNonEmpty(values.flags.get("--operator-id")),
315
328
  sessionId: optionalNonEmpty(values.flags.get("--session-id")),
316
329
  json: values.booleans.has("--json"),
330
+ maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
331
+ maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
317
332
  };
318
333
  }
319
334
  function parseSyncArgs(args) {
320
335
  const values = parseNamedArgs(args, {
321
- allowedFlags: ["--home", "--repo", "--dashboard-url", "--json"],
322
- valueFlags: ["--home", "--repo", "--dashboard-url"],
336
+ allowedFlags: ["--home", "--repo", "--dashboard-url", "--json", "--max-depth", "--max-repos"],
337
+ valueFlags: ["--home", "--repo", "--dashboard-url", "--max-depth", "--max-repos"],
323
338
  });
324
339
  assertNoPositionals(values.positionals, "sync");
325
340
  return {
@@ -328,12 +343,14 @@ function parseSyncArgs(args) {
328
343
  repoRoot: optionalNonEmpty(values.flags.get("--repo")),
329
344
  dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
330
345
  json: values.booleans.has("--json"),
346
+ maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
347
+ maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
331
348
  };
332
349
  }
333
350
  function parseStatusArgs(args) {
334
351
  const values = parseNamedArgs(args, {
335
- allowedFlags: ["--home", "--repo", "--json"],
336
- valueFlags: ["--home", "--repo"],
352
+ allowedFlags: ["--home", "--repo", "--json", "--max-depth", "--max-repos"],
353
+ valueFlags: ["--home", "--repo", "--max-depth", "--max-repos"],
337
354
  });
338
355
  assertNoPositionals(values.positionals, "status");
339
356
  return {
@@ -341,6 +358,8 @@ function parseStatusArgs(args) {
341
358
  homeDir: optionalNonEmpty(values.flags.get("--home")),
342
359
  repoRoot: optionalNonEmpty(values.flags.get("--repo")),
343
360
  json: values.booleans.has("--json"),
361
+ maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
362
+ maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
344
363
  };
345
364
  }
346
365
  function parseServeArgs(args) {
@@ -465,7 +484,7 @@ async function runOnboard(command, io) {
465
484
  writeLine(io.stdout, `Device: ${pair.session.device_name ?? pair.session.device_id ?? "unknown"}`);
466
485
  }
467
486
  }
468
- const worktrees = await discoverCommandWorktrees(command.repoRoot);
487
+ const worktrees = await discoverCommandWorktrees(command.repoRoot, { maxDepth: command.maxDepth, maxRepos: command.maxRepos }, io);
469
488
  if (worktrees.length > 1) {
470
489
  const multi = await runMultiRepoOnboard(command, io, worktrees);
471
490
  if (command.json) {
@@ -626,7 +645,7 @@ async function runAttributedWorktreeSync(options) {
626
645
  sessionId: options.sessionId,
627
646
  });
628
647
  }
629
- const sync = await syncLocalAmbientEnvelope({
648
+ const syncOptions = {
630
649
  homeDir: options.homeDir,
631
650
  repoRoot: worktree.repo_root,
632
651
  dashboardUrl: options.dashboardUrl,
@@ -639,7 +658,29 @@ async function runAttributedWorktreeSync(options) {
639
658
  codex_session_id: result.codex_session_id,
640
659
  })),
641
660
  fetch: options.fetchImpl,
642
- });
661
+ };
662
+ let sync;
663
+ try {
664
+ sync = await syncLocalAmbientEnvelope(syncOptions);
665
+ }
666
+ catch (error) {
667
+ // A newly cloned repo has no work context yet. Capture is permissive
668
+ // and ticket binding comes later, so start general ambient capture for
669
+ // it instead of blocking every other repo's sync until someone runs
670
+ // `cockpit start` by hand.
671
+ if (error instanceof LocalUploadBlockedError &&
672
+ error.blocker === "missing_context") {
673
+ context = await startLocalWorkContext({
674
+ homeDir: options.homeDir,
675
+ repoRoot: worktree.repo_root,
676
+ branch: options.branch,
677
+ });
678
+ sync = await syncLocalAmbientEnvelope(syncOptions);
679
+ }
680
+ else {
681
+ throw error;
682
+ }
683
+ }
643
684
  ok = ok && sync.status === "uploaded";
644
685
  outcomes.push({ worktree, context, sync });
645
686
  }
@@ -795,11 +836,20 @@ function rawEvidenceSyncLine(sync) {
795
836
  function cursorStatusLine(sync) {
796
837
  return `Cursor: ${sync.cursor_tracked_object_count} durable object(s) tracked`;
797
838
  }
798
- async function discoverCommandWorktrees(repoRoot) {
799
- const worktrees = await discoverGitWorktrees(repoRoot ?? process.cwd());
839
+ const DEFAULT_DISCOVERY_MAX_DEPTH = 3;
840
+ const DEFAULT_DISCOVERY_MAX_REPOS = 50;
841
+ async function discoverCommandWorktrees(repoRoot, discovery = {}, io) {
842
+ const maxWorktrees = discovery.maxRepos ?? DEFAULT_DISCOVERY_MAX_REPOS;
843
+ const worktrees = await discoverGitWorktrees(repoRoot ?? process.cwd(), {
844
+ maxDepth: discovery.maxDepth ?? DEFAULT_DISCOVERY_MAX_DEPTH,
845
+ maxWorktrees,
846
+ });
800
847
  if (worktrees.length === 0) {
801
848
  throw new Error("No git repos found. Run from a git repo, or from a parent folder containing git repos.");
802
849
  }
850
+ if (io && worktrees.length >= maxWorktrees) {
851
+ writeLine(io.stderr, `Warning: repo discovery hit the cap of ${maxWorktrees}; additional repos may be excluded. Raise --max-repos if this folder really holds more.`);
852
+ }
803
853
  return worktrees;
804
854
  }
805
855
  async function runMultiRepoOnboard(command, io, worktrees) {
@@ -959,7 +1009,7 @@ async function runLogout(command, io) {
959
1009
  return 0;
960
1010
  }
961
1011
  async function runStart(command, io) {
962
- const worktrees = await discoverCommandWorktrees(command.repoRoot);
1012
+ const worktrees = await discoverCommandWorktrees(command.repoRoot, { maxDepth: command.maxDepth, maxRepos: command.maxRepos }, io);
963
1013
  if (worktrees.length > 1) {
964
1014
  const contexts = await Promise.all(worktrees.map((worktree) => startLocalWorkContext({
965
1015
  homeDir: command.homeDir,
@@ -992,7 +1042,7 @@ async function runStart(command, io) {
992
1042
  return 0;
993
1043
  }
994
1044
  async function runSync(command, io) {
995
- const worktrees = await discoverCommandWorktrees(command.repoRoot);
1045
+ const worktrees = await discoverCommandWorktrees(command.repoRoot, { maxDepth: command.maxDepth, maxRepos: command.maxRepos }, io);
996
1046
  const run = await runAttributedWorktreeSync({
997
1047
  homeDir: command.homeDir,
998
1048
  dashboardUrl: command.dashboardUrl,
@@ -1052,7 +1102,7 @@ async function runSync(command, io) {
1052
1102
  return 1;
1053
1103
  }
1054
1104
  async function runStatus(command, io) {
1055
- const worktrees = await discoverCommandWorktrees(command.repoRoot);
1105
+ const worktrees = await discoverCommandWorktrees(command.repoRoot, { maxDepth: command.maxDepth, maxRepos: command.maxRepos }, io);
1056
1106
  if (worktrees.length > 1) {
1057
1107
  const statuses = await Promise.all(worktrees.map(async (worktree) => ({
1058
1108
  ...(await inspectLocalCollectorStatus({
@@ -5,7 +5,7 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { resolveRepoWorktreeIdentity, } from "./repo-identity.js";
7
7
  import { summarizeLocalUploadSpool } from "./spool/local-spool.js";
8
- export const LOCAL_COLLECTOR_VERSION = "0.1.5";
8
+ export const LOCAL_COLLECTOR_VERSION = "0.1.6";
9
9
  export const DEFAULT_DASHBOARD_URL = "http://127.0.0.1:3100";
10
10
  export function getCollectorRuntimePaths(homeDir = os.homedir()) {
11
11
  const paths = getUserLocalCockpitPaths(homeDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {