@bli-cockpit/cli 0.1.19 → 0.1.21

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
@@ -46,19 +46,21 @@ On machines where Codex or Claude agents will do ticketed work, interactive
46
46
  `cockpit onboard` checks `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md` after
47
47
  harvest proof. If equivalent Cockpit ticket-binding guidance already exists, it
48
48
  leaves the files alone. If guidance is missing or clearly stale, it asks whether
49
- to install or replace it.
49
+ to install or replace it. The managed guidance is scoped to the workspace path
50
+ used for onboarding, so agents should ignore it in private chats or unrelated
51
+ repos.
50
52
 
51
53
  ```bash
52
54
  # Repair/manual path, or headless/json onboarding where Cockpit cannot prompt.
53
- cockpit agent-rules install
55
+ cockpit agent-rules install --workspace "$PWD"
54
56
  ```
55
57
 
56
58
  The direct command updates `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md` with
57
59
  the Cockpit rule to bind known Linear tickets before edits, or ask once when
58
- the ticket ID is missing. That binding starts attributing the session's work to
59
- the specific ticket in Cockpit. It checks for managed or equivalent guidance
60
- first, so current files are left unchanged and stale Cockpit ticket-binding
61
- sections are replaced.
60
+ the ticket ID is missing inside that workspace. That binding starts attributing
61
+ the session's work to the specific ticket in Cockpit. It checks for managed or
62
+ equivalent guidance first, so current files are left unchanged and stale Cockpit
63
+ ticket-binding sections are replaced.
62
64
 
63
65
  Parent mode scans child git repos/worktrees (3 folder levels deep, up to 50
64
66
  repos by default — tune with `--max-depth` / `--max-repos`; a warning prints
@@ -113,6 +115,30 @@ cockpit sync \
113
115
  No ticket is required for setup, chatting, planning, or general ambient capture.
114
116
  Only pass `--ticket` when the work really belongs to a visible ticket.
115
117
 
118
+ When the work is important but ticketless, label it explicitly before syncing so
119
+ later analysis does not have to guess the topic:
120
+
121
+ ```bash
122
+ cockpit start \
123
+ --workspace "$PWD" \
124
+ --topic "lead ingestion rewrite planning" \
125
+ --intent planning \
126
+ --phase discovery \
127
+ --intent-confidence 0.9
128
+
129
+ cockpit sync \
130
+ --workspace "$PWD" \
131
+ --json
132
+ ```
133
+
134
+ Supported `--intent` values are `implementation`, `bug_fix`,
135
+ `root_cause_analysis`, `planning`, `discovery`, `review`, `testing`,
136
+ `documentation`, `release`, `learning`, `coordination`, `maintenance`,
137
+ `analysis`, `unknown`, and `other`. Supported `--phase` values are `planning`,
138
+ `discovery`, `implementation`, `debugging`, `review`, `testing`,
139
+ `documentation`, `release`, `handoff`, `analysis`, `unknown`, and `other`.
140
+ Use `--topic-summary` only for short redacted summaries, not transcript text.
141
+
116
142
  ## What gets saved
117
143
 
118
144
  Local files:
@@ -180,3 +206,5 @@ This public package intentionally excludes Cockpit admin bootstrap commands,
180
206
  service-role credential handling, source maps, tests, and internal runbooks.
181
207
  Clean `npm pack` and `npm publish` run the public CLI build before packaging so
182
208
  `dist/cli.js` is present in emergency releases.
209
+ When collector changes depend on new telemetry-core exports, publish
210
+ `@bli-cockpit/telemetry-core` first, then publish `@bli-cockpit/cli`.
@@ -54,8 +54,19 @@ export async function scanAndAttributeClaudeSessions(options) {
54
54
  const countState = (state) => results.filter((result) => result.state === state).length;
55
55
  return {
56
56
  results,
57
+ discovered_session_count: discovery.sessions.length,
57
58
  scanned_session_count: sessions.length,
59
+ since_minutes: sinceMinutes,
60
+ session_limit: limit,
61
+ session_limit_applied: discovery.sessions.length > limit,
62
+ max_file_bytes: CLAUDE_SESSION_MAX_FILE_BYTES,
63
+ max_sidecar_files: CLAUDE_SESSION_MAX_SIDECAR_FILES,
64
+ max_line_buffer_bytes: MAX_LINE_BUFFER_BYTES,
58
65
  project_dirs_skipped: discovery.projectDirsSkipped,
66
+ project_dir_read_failed_count: discovery.projectDirReadFailedCount,
67
+ session_stat_failed_count: discovery.sessionStatFailedCount,
68
+ sidecar_dir_read_failed_count: discovery.sidecarDirReadFailedCount,
69
+ sidecar_stat_failed_count: discovery.sidecarStatFailedCount,
59
70
  counts: {
60
71
  attributed: countState("attributed"),
61
72
  ambiguous: countState("ambiguous"),
@@ -72,12 +83,23 @@ export async function scanAndAttributeClaudeSessions(options) {
72
83
  async function discoverClaudeSessions(projectsDir, cutoffMs) {
73
84
  const sessions = [];
74
85
  let projectDirsSkipped = 0;
86
+ let projectDirReadFailedCount = 0;
87
+ let sessionStatFailedCount = 0;
88
+ let sidecarDirReadFailedCount = 0;
89
+ let sidecarStatFailedCount = 0;
75
90
  let projectEntries;
76
91
  try {
77
92
  projectEntries = await fs.readdir(projectsDir, { withFileTypes: true });
78
93
  }
79
94
  catch {
80
- return { sessions, projectDirsSkipped };
95
+ return {
96
+ sessions,
97
+ projectDirsSkipped,
98
+ projectDirReadFailedCount: 1,
99
+ sessionStatFailedCount,
100
+ sidecarDirReadFailedCount,
101
+ sidecarStatFailedCount,
102
+ };
81
103
  }
82
104
  for (const projectEntry of projectEntries) {
83
105
  if (!projectEntry.isDirectory())
@@ -96,6 +118,7 @@ async function discoverClaudeSessions(projectsDir, cutoffMs) {
96
118
  sessionEntries = await fs.readdir(projectDir, { withFileTypes: true });
97
119
  }
98
120
  catch {
121
+ projectDirReadFailedCount += 1;
99
122
  continue;
100
123
  }
101
124
  for (const sessionEntry of sessionEntries) {
@@ -109,10 +132,14 @@ async function discoverClaudeSessions(projectsDir, cutoffMs) {
109
132
  mainStat = await fs.stat(mainFile);
110
133
  }
111
134
  catch {
135
+ sessionStatFailedCount += 1;
112
136
  continue;
113
137
  }
114
138
  const sessionUuid = sessionEntry.name.replace(/\.jsonl$/i, "");
115
- const { sidecars, sidecarsCapped } = await discoverSidecars(path.join(projectDir, sessionUuid, "subagents"));
139
+ const sidecarDiscovery = await discoverSidecars(path.join(projectDir, sessionUuid, "subagents"));
140
+ const { sidecars, sidecarsCapped } = sidecarDiscovery;
141
+ sidecarDirReadFailedCount += sidecarDiscovery.dirReadFailedCount;
142
+ sidecarStatFailedCount += sidecarDiscovery.statFailedCount;
116
143
  const recencyMs = Math.max(mainStat.mtimeMs, ...sidecars.map((sidecar) => sidecar.mtimeMs));
117
144
  // D10: a session is recent if its main file OR any sidecar is in window.
118
145
  if (recencyMs < cutoffMs)
@@ -127,7 +154,14 @@ async function discoverClaudeSessions(projectsDir, cutoffMs) {
127
154
  });
128
155
  }
129
156
  }
130
- return { sessions, projectDirsSkipped };
157
+ return {
158
+ sessions,
159
+ projectDirsSkipped,
160
+ projectDirReadFailedCount,
161
+ sessionStatFailedCount,
162
+ sidecarDirReadFailedCount,
163
+ sidecarStatFailedCount,
164
+ };
131
165
  }
132
166
  async function discoverSidecars(subagentsDir) {
133
167
  let entries;
@@ -135,9 +169,15 @@ async function discoverSidecars(subagentsDir) {
135
169
  entries = await fs.readdir(subagentsDir, { withFileTypes: true });
136
170
  }
137
171
  catch {
138
- return { sidecars: [], sidecarsCapped: 0 };
172
+ return {
173
+ sidecars: [],
174
+ sidecarsCapped: 0,
175
+ dirReadFailedCount: 1,
176
+ statFailedCount: 0,
177
+ };
139
178
  }
140
179
  const discovered = [];
180
+ let statFailedCount = 0;
141
181
  for (const entry of entries) {
142
182
  // Only agent transcripts. `*.meta.json` carry operator-authored
143
183
  // descriptions and are never harvested (D3).
@@ -152,6 +192,7 @@ async function discoverSidecars(subagentsDir) {
152
192
  stat = await fs.stat(local_path);
153
193
  }
154
194
  catch {
195
+ statFailedCount += 1;
155
196
  continue;
156
197
  }
157
198
  discovered.push({
@@ -166,6 +207,8 @@ async function discoverSidecars(subagentsDir) {
166
207
  return {
167
208
  sidecars: discovered.slice(0, CLAUDE_SESSION_MAX_SIDECAR_FILES),
168
209
  sidecarsCapped: capped,
210
+ dirReadFailedCount: 0,
211
+ statFailedCount,
169
212
  };
170
213
  }
171
214
  async function attributeOneSession(session, worktrees) {
@@ -23,14 +23,23 @@ export async function scanAndAttributeCodexSessions(options) {
23
23
  const sinceMinutes = options.sinceMinutes ?? CODEX_ATTRIBUTION_DEFAULT_SINCE_MINUTES;
24
24
  const limit = options.limit ?? CODEX_ATTRIBUTION_DEFAULT_SESSION_LIMIT;
25
25
  const cutoffMs = options.now.getTime() - sinceMinutes * 60 * 1000;
26
- const files = (await walkCodexJsonlFiles(options.sessionsDir, cutoffMs)).slice(0, limit);
26
+ const discovery = await discoverCodexJsonlFiles(options.sessionsDir, cutoffMs);
27
+ const files = discovery.files.slice(0, limit);
27
28
  const results = [];
28
29
  for (const file of files) {
29
30
  results.push(await attributeOneSession(file, options.worktrees));
30
31
  }
31
32
  return {
32
33
  results,
34
+ discovered_file_count: discovery.files.length,
33
35
  scanned_file_count: files.length,
36
+ since_minutes: sinceMinutes,
37
+ session_limit: limit,
38
+ session_limit_applied: discovery.files.length > limit,
39
+ max_file_bytes: CODEX_SESSION_MAX_FILE_BYTES,
40
+ directory_read_failed_count: discovery.directoryReadFailedCount,
41
+ stat_failed_count: discovery.statFailedCount,
42
+ secret_path_skipped_count: discovery.secretPathSkippedCount,
34
43
  counts: {
35
44
  attributed: results.filter((entry) => entry.state === "attributed").length,
36
45
  ambiguous: results.filter((entry) => entry.state === "ambiguous").length,
@@ -41,25 +50,40 @@ export async function scanAndAttributeCodexSessions(options) {
41
50
  };
42
51
  }
43
52
  export async function walkCodexJsonlFiles(dir, cutoffMs) {
53
+ return (await discoverCodexJsonlFiles(dir, cutoffMs)).files;
54
+ }
55
+ async function discoverCodexJsonlFiles(dir, cutoffMs) {
44
56
  const out = [];
57
+ let directoryReadFailedCount = 0;
58
+ let statFailedCount = 0;
59
+ let secretPathSkippedCount = 0;
45
60
  const stack = [dir];
46
61
  while (stack.length > 0) {
47
62
  const current = stack.pop();
48
- if (!current || isSecretLikePath(current))
63
+ if (!current)
64
+ continue;
65
+ if (isSecretLikePath(current)) {
66
+ secretPathSkippedCount += 1;
49
67
  continue;
68
+ }
50
69
  let entries;
51
70
  try {
52
71
  entries = await fs.readdir(current, { withFileTypes: true });
53
72
  }
54
73
  catch {
74
+ directoryReadFailedCount += 1;
55
75
  continue;
56
76
  }
57
77
  for (const entry of entries) {
58
78
  const full = path.join(current, entry.name);
59
79
  if (entry.isDirectory()) {
60
80
  // Never descend into secret-like directories.
61
- if (!isSecretLikePath(full))
81
+ if (isSecretLikePath(full)) {
82
+ secretPathSkippedCount += 1;
83
+ }
84
+ else {
62
85
  stack.push(full);
86
+ }
63
87
  continue;
64
88
  }
65
89
  // Secret-like file NAMES stay in the list so attribution can record a
@@ -67,14 +91,26 @@ export async function walkCodexJsonlFiles(dir, cutoffMs) {
67
91
  // instead of silently dropping the session.
68
92
  if (!entry.isFile() || !entry.name.endsWith(".jsonl"))
69
93
  continue;
70
- const stat = await fs.stat(full);
94
+ let stat;
95
+ try {
96
+ stat = await fs.stat(full);
97
+ }
98
+ catch {
99
+ statFailedCount += 1;
100
+ continue;
101
+ }
71
102
  if (stat.mtimeMs >= cutoffMs) {
72
103
  out.push({ file: full, mtimeMs: stat.mtimeMs, byteSize: stat.size });
73
104
  }
74
105
  }
75
106
  }
76
107
  out.sort((a, b) => b.mtimeMs - a.mtimeMs);
77
- return out;
108
+ return {
109
+ files: out,
110
+ directoryReadFailedCount,
111
+ statFailedCount,
112
+ secretPathSkippedCount,
113
+ };
78
114
  }
79
115
  async function attributeOneSession(file, worktrees) {
80
116
  const fileName = path.basename(file.file);
@@ -28,7 +28,9 @@ export async function runLocalSourceCollectors(options) {
28
28
  includeCodexJsonl: options.rawEvidenceIncludeCodexJsonl,
29
29
  includeClaudeJsonl: options.rawEvidenceIncludeClaudeJsonl,
30
30
  codexSessionFiles: options.rawEvidenceCodexSessionFiles,
31
+ codexAttributionScan: options.rawEvidenceCodexAttributionScan,
31
32
  claudeSessionFiles: options.rawEvidenceClaudeSessionFiles,
33
+ claudeAttributionScan: options.rawEvidenceClaudeAttributionScan,
32
34
  skipContentHashes: options.rawEvidenceSkipContentHashes,
33
35
  byteBudget: options.rawEvidenceByteBudget,
34
36
  objectBudget: options.rawEvidenceObjectBudget,