@bli-cockpit/cli 0.1.20 → 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.
|
@@ -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 {
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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,
|
|
@@ -28,6 +28,7 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
28
28
|
const entries = [];
|
|
29
29
|
const skipped = [];
|
|
30
30
|
const truncated = [];
|
|
31
|
+
const failed = [];
|
|
31
32
|
const reused = [];
|
|
32
33
|
const sinceMinutes = options.sinceMinutes ?? DEFAULT_SINCE_MINUTES;
|
|
33
34
|
const sessionLimit = options.sessionLimit ?? DEFAULT_SESSION_LIMIT;
|
|
@@ -40,6 +41,7 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
40
41
|
entries,
|
|
41
42
|
skipped,
|
|
42
43
|
truncated,
|
|
44
|
+
failed,
|
|
43
45
|
reused,
|
|
44
46
|
scanned: new Map(),
|
|
45
47
|
caps: [
|
|
@@ -92,6 +94,10 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
92
94
|
try {
|
|
93
95
|
await ensurePrivateDir(evidenceDir);
|
|
94
96
|
await ensurePrivateDir(filesDir);
|
|
97
|
+
recordAttributionCompleteness(collection, {
|
|
98
|
+
codex: options.codexAttributionScan,
|
|
99
|
+
claude: options.claudeAttributionScan,
|
|
100
|
+
});
|
|
95
101
|
if (options.includeCodexJsonl !== false) {
|
|
96
102
|
await collectCodexJsonlFiles(collection, {
|
|
97
103
|
codexSessionFiles: options.codexSessionFiles,
|
|
@@ -104,8 +110,8 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
104
110
|
await collectClaudeJsonlFiles(collection, options.claudeSessionFiles);
|
|
105
111
|
}
|
|
106
112
|
await collectGitDiffFiles(collection, options.repoRoot);
|
|
107
|
-
const deferredByteBudgetCount = skipped.filter((entry) => entry.reason === "deferred_byte_budget").
|
|
108
|
-
const deferredObjectBudgetCount = skipped.filter((entry) => entry.reason === "deferred_object_budget").
|
|
113
|
+
const deferredByteBudgetCount = skipped.filter((entry) => entry.reason === "deferred_byte_budget").reduce((sum, entry) => sum + evidenceEntryCount(entry), 0);
|
|
114
|
+
const deferredObjectBudgetCount = skipped.filter((entry) => entry.reason === "deferred_object_budget").reduce((sum, entry) => sum + evidenceEntryCount(entry), 0);
|
|
109
115
|
if (entries.length === 0) {
|
|
110
116
|
const evidenceCompleteness = makeEvidenceCompleteness(collection, {
|
|
111
117
|
startedAt,
|
|
@@ -119,7 +125,7 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
119
125
|
storage_bucket: RAW_EVIDENCE_BUCKET,
|
|
120
126
|
file_count: 0,
|
|
121
127
|
byte_size: 0,
|
|
122
|
-
skipped_count: skipped
|
|
128
|
+
skipped_count: countEvidenceEntries(skipped),
|
|
123
129
|
reused_count: reused.length,
|
|
124
130
|
deferred_byte_budget_count: deferredByteBudgetCount,
|
|
125
131
|
deferred_object_budget_count: deferredObjectBudgetCount,
|
|
@@ -173,7 +179,7 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
173
179
|
storage_bucket: RAW_EVIDENCE_BUCKET,
|
|
174
180
|
file_count: entries.length,
|
|
175
181
|
byte_size: entries.reduce((sum, entry) => sum + entry.byte_size, 0),
|
|
176
|
-
skipped_count: skipped
|
|
182
|
+
skipped_count: countEvidenceEntries(skipped),
|
|
177
183
|
reused_count: reused.length,
|
|
178
184
|
deferred_byte_budget_count: deferredByteBudgetCount,
|
|
179
185
|
deferred_object_budget_count: deferredObjectBudgetCount,
|
|
@@ -201,18 +207,46 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
201
207
|
}),
|
|
202
208
|
};
|
|
203
209
|
}
|
|
204
|
-
catch
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
],
|
|
210
|
+
catch {
|
|
211
|
+
failed.push({
|
|
212
|
+
kind: "raw_evidence",
|
|
213
|
+
reason: "collection_failed",
|
|
214
|
+
});
|
|
215
|
+
const evidenceCompleteness = makeEvidenceCompleteness(collection, {
|
|
216
|
+
startedAt,
|
|
217
|
+
finishedAt: context.now.toISOString(),
|
|
218
|
+
sinceMinutes,
|
|
214
219
|
});
|
|
215
|
-
|
|
220
|
+
const facts = {
|
|
221
|
+
pack_id: packId,
|
|
222
|
+
manifest_path: path.join(evidenceDir, "manifest.json"),
|
|
223
|
+
evidence_dir: evidenceDir,
|
|
224
|
+
storage_bucket: RAW_EVIDENCE_BUCKET,
|
|
225
|
+
file_count: 0,
|
|
226
|
+
byte_size: 0,
|
|
227
|
+
skipped_count: countEvidenceEntries(skipped),
|
|
228
|
+
reused_count: reused.length,
|
|
229
|
+
deferred_byte_budget_count: skipped
|
|
230
|
+
.filter((entry) => entry.reason === "deferred_byte_budget")
|
|
231
|
+
.reduce((sum, entry) => sum + evidenceEntryCount(entry), 0),
|
|
232
|
+
deferred_object_budget_count: skipped
|
|
233
|
+
.filter((entry) => entry.reason === "deferred_object_budget")
|
|
234
|
+
.reduce((sum, entry) => sum + evidenceEntryCount(entry), 0),
|
|
235
|
+
content_kinds: [],
|
|
236
|
+
evidence_completeness: evidenceCompleteness,
|
|
237
|
+
pointers: [],
|
|
238
|
+
upload_files: [],
|
|
239
|
+
reused,
|
|
240
|
+
};
|
|
241
|
+
return {
|
|
242
|
+
facts,
|
|
243
|
+
scan: makeRawEvidenceScan({
|
|
244
|
+
context,
|
|
245
|
+
startedAt,
|
|
246
|
+
status: "failed",
|
|
247
|
+
facts,
|
|
248
|
+
}),
|
|
249
|
+
};
|
|
216
250
|
}
|
|
217
251
|
}
|
|
218
252
|
function makeRawEvidenceScan(options) {
|
|
@@ -246,10 +280,105 @@ function makeRawEvidenceScan(options) {
|
|
|
246
280
|
`completeness:${options.facts.evidence_completeness.status}`,
|
|
247
281
|
`truncated:${options.facts.evidence_completeness.totals.truncated_count}`,
|
|
248
282
|
`deferred:${options.facts.evidence_completeness.totals.deferred_count}`,
|
|
283
|
+
`failed:${options.facts.evidence_completeness.totals.failed_count}`,
|
|
249
284
|
...options.facts.content_kinds.map((kind) => `kind:${kind}`),
|
|
250
285
|
],
|
|
251
286
|
});
|
|
252
287
|
}
|
|
288
|
+
function recordAttributionCompleteness(collection, scans) {
|
|
289
|
+
if (scans.codex)
|
|
290
|
+
recordCodexAttributionCompleteness(collection, scans.codex);
|
|
291
|
+
if (scans.claude)
|
|
292
|
+
recordClaudeAttributionCompleteness(collection, scans.claude);
|
|
293
|
+
}
|
|
294
|
+
function recordCodexAttributionCompleteness(collection, scan) {
|
|
295
|
+
recordScanned(collection, "codex_attribution", scan.scanned_file_count);
|
|
296
|
+
collection.caps.push({
|
|
297
|
+
source: "codex_attribution",
|
|
298
|
+
cap_type: "scan_window_minutes",
|
|
299
|
+
limit: scan.since_minutes,
|
|
300
|
+
observed: scan.since_minutes,
|
|
301
|
+
applied: false,
|
|
302
|
+
}, {
|
|
303
|
+
source: "codex_attribution",
|
|
304
|
+
cap_type: "session_limit",
|
|
305
|
+
limit: scan.session_limit,
|
|
306
|
+
observed: scan.discovered_file_count,
|
|
307
|
+
applied: scan.session_limit_applied,
|
|
308
|
+
}, {
|
|
309
|
+
source: "codex_attribution",
|
|
310
|
+
cap_type: "max_file_bytes",
|
|
311
|
+
limit: scan.max_file_bytes,
|
|
312
|
+
applied: scan.results.some((result) => result.reason === "file_too_large"),
|
|
313
|
+
});
|
|
314
|
+
recordSkipCount(collection, "codex_attribution", "session_limit_overflow", Math.max(0, scan.discovered_file_count - scan.scanned_file_count));
|
|
315
|
+
recordSkipCount(collection, "codex_attribution", "directory_read_failed", scan.directory_read_failed_count);
|
|
316
|
+
recordSkipCount(collection, "codex_attribution", "file_stat_failed", scan.stat_failed_count);
|
|
317
|
+
recordSkipCount(collection, "codex_attribution", "secret_like_directory", scan.secret_path_skipped_count);
|
|
318
|
+
recordAttributionResultSkips(collection, "codex_attribution", scan.results);
|
|
319
|
+
}
|
|
320
|
+
function recordClaudeAttributionCompleteness(collection, scan) {
|
|
321
|
+
recordScanned(collection, "claude_attribution", scan.scanned_session_count);
|
|
322
|
+
if (scan.disabled_reason) {
|
|
323
|
+
recordSkipCount(collection, "claude_attribution", scan.disabled_reason, 1);
|
|
324
|
+
}
|
|
325
|
+
collection.caps.push({
|
|
326
|
+
source: "claude_attribution",
|
|
327
|
+
cap_type: "scan_window_minutes",
|
|
328
|
+
limit: scan.since_minutes,
|
|
329
|
+
observed: scan.since_minutes,
|
|
330
|
+
applied: false,
|
|
331
|
+
}, {
|
|
332
|
+
source: "claude_attribution",
|
|
333
|
+
cap_type: "session_limit",
|
|
334
|
+
limit: scan.session_limit,
|
|
335
|
+
observed: scan.discovered_session_count,
|
|
336
|
+
applied: scan.session_limit_applied,
|
|
337
|
+
}, {
|
|
338
|
+
source: "claude_attribution",
|
|
339
|
+
cap_type: "max_file_bytes",
|
|
340
|
+
limit: scan.max_file_bytes,
|
|
341
|
+
applied: scan.counts.mains_oversized > 0 ||
|
|
342
|
+
scan.results.some((result) => result.reason === "file_too_large"),
|
|
343
|
+
}, {
|
|
344
|
+
source: "claude_attribution",
|
|
345
|
+
cap_type: "max_sidecar_files",
|
|
346
|
+
limit: scan.max_sidecar_files,
|
|
347
|
+
observed: scan.max_sidecar_files + scan.counts.sidecars_capped,
|
|
348
|
+
applied: scan.counts.sidecars_capped > 0,
|
|
349
|
+
}, {
|
|
350
|
+
source: "claude_attribution",
|
|
351
|
+
cap_type: "max_line_buffer_bytes",
|
|
352
|
+
limit: scan.max_line_buffer_bytes,
|
|
353
|
+
applied: scan.counts.oversized_lines_skipped > 0,
|
|
354
|
+
});
|
|
355
|
+
recordSkipCount(collection, "claude_attribution", "session_limit_overflow", Math.max(0, scan.discovered_session_count - scan.scanned_session_count));
|
|
356
|
+
recordSkipCount(collection, "claude_attribution", "secret_like_project_dir", scan.project_dirs_skipped);
|
|
357
|
+
recordSkipCount(collection, "claude_attribution", "project_dir_read_failed", scan.project_dir_read_failed_count);
|
|
358
|
+
recordSkipCount(collection, "claude_attribution", "session_stat_failed", scan.session_stat_failed_count);
|
|
359
|
+
recordSkipCount(collection, "claude_attribution", "sidecar_dir_read_failed", scan.sidecar_dir_read_failed_count);
|
|
360
|
+
recordSkipCount(collection, "claude_attribution", "sidecar_stat_failed", scan.sidecar_stat_failed_count);
|
|
361
|
+
recordSkipCount(collection, "claude_attribution", "sidecar_limit_overflow", scan.counts.sidecars_capped);
|
|
362
|
+
recordTruncationCount(collection, "claude_attribution", "oversized_jsonl_line", scan.counts.oversized_lines_skipped, { max_bytes: scan.max_line_buffer_bytes });
|
|
363
|
+
recordAttributionResultSkips(collection, "claude_attribution", scan.results);
|
|
364
|
+
for (const result of scan.results) {
|
|
365
|
+
for (const sidecar of result.sidecar_files) {
|
|
366
|
+
if (!sidecar.skipped_reason)
|
|
367
|
+
continue;
|
|
368
|
+
recordSkipCount(collection, "claude_attribution", `sidecar_${sidecar.skipped_reason}`, 1);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function recordAttributionResultSkips(collection, source, results) {
|
|
373
|
+
for (const result of results) {
|
|
374
|
+
if (result.state === "attributed")
|
|
375
|
+
continue;
|
|
376
|
+
const reason = result.state === "skipped"
|
|
377
|
+
? result.reason
|
|
378
|
+
: `attribution_${result.state}:${result.reason}`;
|
|
379
|
+
recordSkipCount(collection, source, reason, 1);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
253
382
|
async function collectCodexJsonlFiles(collection, options) {
|
|
254
383
|
const resolvedCandidates = options.codexSessionFiles
|
|
255
384
|
? options.codexSessionFiles.map((file) => ({
|
|
@@ -542,7 +671,18 @@ async function collectGitDiffFiles(collection, repoRoot) {
|
|
|
542
671
|
];
|
|
543
672
|
for (const target of diffTargets) {
|
|
544
673
|
recordScanned(collection, "git_diff");
|
|
545
|
-
|
|
674
|
+
let diff;
|
|
675
|
+
try {
|
|
676
|
+
diff = await runGitDiff(target.args, repoRoot);
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
collection.skipped.push({
|
|
680
|
+
kind: "git_diff",
|
|
681
|
+
label: target.label,
|
|
682
|
+
reason: "git_diff_failed",
|
|
683
|
+
});
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
546
686
|
if (diff.truncated) {
|
|
547
687
|
markCapApplied(collection, "git_diff", diff.truncationCapType);
|
|
548
688
|
collection.truncated.push({
|
|
@@ -686,6 +826,32 @@ function markCapApplied(collection, source, capType) {
|
|
|
686
826
|
if (cap)
|
|
687
827
|
cap.applied = true;
|
|
688
828
|
}
|
|
829
|
+
function recordSkipCount(collection, source, reason, count) {
|
|
830
|
+
if (count <= 0)
|
|
831
|
+
return;
|
|
832
|
+
collection.skipped.push({
|
|
833
|
+
kind: source,
|
|
834
|
+
label: reason,
|
|
835
|
+
reason,
|
|
836
|
+
count,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
function recordTruncationCount(collection, source, reason, count, details = {}) {
|
|
840
|
+
if (count <= 0)
|
|
841
|
+
return;
|
|
842
|
+
collection.truncated.push({
|
|
843
|
+
kind: source,
|
|
844
|
+
reason,
|
|
845
|
+
count,
|
|
846
|
+
...details,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
function evidenceEntryCount(entry) {
|
|
850
|
+
return entry.count ?? 1;
|
|
851
|
+
}
|
|
852
|
+
function countEvidenceEntries(entries, predicate = () => true) {
|
|
853
|
+
return entries.reduce((sum, entry) => sum + (predicate(entry) ? evidenceEntryCount(entry) : 0), 0);
|
|
854
|
+
}
|
|
689
855
|
function makeEvidenceCompleteness(collection, options) {
|
|
690
856
|
const sources = new Set(collection.scanned.keys());
|
|
691
857
|
for (const entry of collection.entries)
|
|
@@ -696,16 +862,19 @@ function makeEvidenceCompleteness(collection, options) {
|
|
|
696
862
|
sources.add(entry.kind);
|
|
697
863
|
for (const entry of collection.truncated)
|
|
698
864
|
sources.add(entry.kind);
|
|
865
|
+
for (const entry of collection.failed)
|
|
866
|
+
sources.add(entry.kind);
|
|
699
867
|
const sourceCounts = [...sources].sort().map((source) => {
|
|
700
868
|
const skipped = collection.skipped.filter((entry) => entry.kind === source);
|
|
701
869
|
return {
|
|
702
870
|
source,
|
|
703
871
|
scanned_count: collection.scanned.get(source) ?? 0,
|
|
704
872
|
included_count: collection.entries.filter((entry) => entry.kind === source).length,
|
|
705
|
-
skipped_count: skipped
|
|
706
|
-
truncated_count: collection.truncated
|
|
707
|
-
deferred_count: skipped
|
|
873
|
+
skipped_count: countEvidenceEntries(skipped),
|
|
874
|
+
truncated_count: countEvidenceEntries(collection.truncated, (entry) => entry.kind === source),
|
|
875
|
+
deferred_count: countEvidenceEntries(skipped, (entry) => entry.reason.startsWith("deferred_")),
|
|
708
876
|
reused_count: collection.reused.filter((entry) => entry.kind === source).length,
|
|
877
|
+
failed_count: countEvidenceEntries(collection.failed, (entry) => entry.kind === source),
|
|
709
878
|
};
|
|
710
879
|
});
|
|
711
880
|
const totals = sourceCounts.reduce((sum, count) => ({
|
|
@@ -715,6 +884,7 @@ function makeEvidenceCompleteness(collection, options) {
|
|
|
715
884
|
truncated_count: sum.truncated_count + count.truncated_count,
|
|
716
885
|
deferred_count: sum.deferred_count + count.deferred_count,
|
|
717
886
|
reused_count: sum.reused_count + count.reused_count,
|
|
887
|
+
failed_count: sum.failed_count + count.failed_count,
|
|
718
888
|
}), {
|
|
719
889
|
scanned_count: 0,
|
|
720
890
|
included_count: 0,
|
|
@@ -722,19 +892,35 @@ function makeEvidenceCompleteness(collection, options) {
|
|
|
722
892
|
truncated_count: 0,
|
|
723
893
|
deferred_count: 0,
|
|
724
894
|
reused_count: 0,
|
|
895
|
+
failed_count: 0,
|
|
725
896
|
});
|
|
726
897
|
const skipReasonCounts = new Map();
|
|
727
898
|
for (const skipped of collection.skipped) {
|
|
728
899
|
const key = `${skipped.kind}:${skipped.reason}`;
|
|
729
900
|
const existing = skipReasonCounts.get(key);
|
|
730
901
|
if (existing) {
|
|
731
|
-
existing.count +=
|
|
902
|
+
existing.count += evidenceEntryCount(skipped);
|
|
732
903
|
}
|
|
733
904
|
else {
|
|
734
905
|
skipReasonCounts.set(key, {
|
|
735
906
|
source: skipped.kind,
|
|
736
907
|
reason: skipped.reason,
|
|
737
|
-
count:
|
|
908
|
+
count: evidenceEntryCount(skipped),
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const failureReasonCounts = new Map();
|
|
913
|
+
for (const failed of collection.failed) {
|
|
914
|
+
const key = `${failed.kind}:${failed.reason}`;
|
|
915
|
+
const existing = failureReasonCounts.get(key);
|
|
916
|
+
if (existing) {
|
|
917
|
+
existing.count += evidenceEntryCount(failed);
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
failureReasonCounts.set(key, {
|
|
921
|
+
source: failed.kind,
|
|
922
|
+
reason: failed.reason,
|
|
923
|
+
count: evidenceEntryCount(failed),
|
|
738
924
|
});
|
|
739
925
|
}
|
|
740
926
|
}
|
|
@@ -743,7 +929,7 @@ function makeEvidenceCompleteness(collection, options) {
|
|
|
743
929
|
const key = `${truncated.kind}:${truncated.reason}`;
|
|
744
930
|
const existing = truncationCounts.get(key);
|
|
745
931
|
if (existing) {
|
|
746
|
-
existing.count +=
|
|
932
|
+
existing.count += evidenceEntryCount(truncated);
|
|
747
933
|
existing.observed_bytes = Math.max(existing.observed_bytes ?? 0, truncated.observed_bytes ?? 0);
|
|
748
934
|
existing.included_bytes = Math.max(existing.included_bytes ?? 0, truncated.included_bytes ?? 0);
|
|
749
935
|
}
|
|
@@ -751,7 +937,7 @@ function makeEvidenceCompleteness(collection, options) {
|
|
|
751
937
|
truncationCounts.set(key, {
|
|
752
938
|
source: truncated.kind,
|
|
753
939
|
reason: truncated.reason,
|
|
754
|
-
count:
|
|
940
|
+
count: evidenceEntryCount(truncated),
|
|
755
941
|
...(truncated.max_bytes !== undefined
|
|
756
942
|
? { max_bytes: truncated.max_bytes }
|
|
757
943
|
: {}),
|
|
@@ -767,12 +953,16 @@ function makeEvidenceCompleteness(collection, options) {
|
|
|
767
953
|
const hasGaps = totals.skipped_count > 0 ||
|
|
768
954
|
totals.truncated_count > 0 ||
|
|
769
955
|
totals.deferred_count > 0 ||
|
|
956
|
+
totals.failed_count > 0 ||
|
|
770
957
|
collection.caps.some((cap) => cap.applied);
|
|
771
|
-
const status = totals.
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
958
|
+
const status = totals.failed_count > 0 &&
|
|
959
|
+
totals.included_count + totals.reused_count === 0
|
|
960
|
+
? "failed"
|
|
961
|
+
: totals.included_count + totals.reused_count === 0 && !hasGaps
|
|
962
|
+
? "empty"
|
|
963
|
+
: hasGaps
|
|
964
|
+
? "partial"
|
|
965
|
+
: "complete";
|
|
776
966
|
return EvidenceCompletenessPayloadSchema.parse({
|
|
777
967
|
schema_version: "evidence-completeness.v1",
|
|
778
968
|
status,
|
|
@@ -786,10 +976,13 @@ function makeEvidenceCompleteness(collection, options) {
|
|
|
786
976
|
totals,
|
|
787
977
|
caps: collection.caps,
|
|
788
978
|
skip_reasons: [...skipReasonCounts.values()].sort((a, b) => `${a.source}:${a.reason}`.localeCompare(`${b.source}:${b.reason}`)),
|
|
979
|
+
failure_reasons: [...failureReasonCounts.values()].sort((a, b) => `${a.source}:${a.reason}`.localeCompare(`${b.source}:${b.reason}`)),
|
|
789
980
|
truncation_markers: [...truncationCounts.values()].sort((a, b) => `${a.source}:${a.reason}`.localeCompare(`${b.source}:${b.reason}`)),
|
|
790
|
-
notes:
|
|
791
|
-
? ["Evidence
|
|
792
|
-
:
|
|
981
|
+
notes: totals.failed_count > 0
|
|
982
|
+
? ["Evidence collection failed; downstream analysis should not infer confidence."]
|
|
983
|
+
: hasGaps
|
|
984
|
+
? ["Evidence is incomplete; downstream analysis should lower confidence."]
|
|
985
|
+
: [],
|
|
793
986
|
});
|
|
794
987
|
}
|
|
795
988
|
async function walkJsonlFiles(dir, cutoffMs) {
|
|
@@ -120,7 +120,9 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
120
120
|
local_path: result.file_path,
|
|
121
121
|
codex_session_id: result.codex_session_id,
|
|
122
122
|
})),
|
|
123
|
+
codexAttributionScan: codexAttribution,
|
|
123
124
|
claudeSessionFiles,
|
|
125
|
+
claudeAttributionScan: claudeAttribution,
|
|
124
126
|
rawEvidenceBudget,
|
|
125
127
|
fetch: options.fetchImpl,
|
|
126
128
|
};
|
|
@@ -480,8 +482,20 @@ function buildAgentSessionSummary(options) {
|
|
|
480
482
|
function emptyClaudeScan() {
|
|
481
483
|
return {
|
|
482
484
|
results: [],
|
|
485
|
+
discovered_session_count: 0,
|
|
483
486
|
scanned_session_count: 0,
|
|
487
|
+
since_minutes: 0,
|
|
488
|
+
session_limit: 0,
|
|
489
|
+
session_limit_applied: false,
|
|
490
|
+
max_file_bytes: 0,
|
|
491
|
+
max_sidecar_files: 0,
|
|
492
|
+
max_line_buffer_bytes: 0,
|
|
484
493
|
project_dirs_skipped: 0,
|
|
494
|
+
project_dir_read_failed_count: 0,
|
|
495
|
+
session_stat_failed_count: 0,
|
|
496
|
+
sidecar_dir_read_failed_count: 0,
|
|
497
|
+
sidecar_stat_failed_count: 0,
|
|
498
|
+
disabled_reason: "claude_collection_disabled_by_config",
|
|
485
499
|
counts: {
|
|
486
500
|
attributed: 0,
|
|
487
501
|
ambiguous: 0,
|
package/dist/upload.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AgentImageArtifactReportRequestSchema, TelemetryIngestEnvelopeSchema, TelemetryIngestEventDtoSchema, } from "@bli-cockpit/telemetry-core";
|
|
1
|
+
import { AgentImageArtifactReportRequestSchema, EvidenceCompletenessPayloadSchema, TelemetryIngestEnvelopeSchema, TelemetryIngestEventDtoSchema, } from "@bli-cockpit/telemetry-core";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getCollectorRuntimePaths, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, readLocalWorkContextForRepo, } from "./local-state.js";
|
|
4
4
|
import { runLocalSourceCollectors } from "./adapters/local-sources.js";
|
|
@@ -62,7 +62,9 @@ export async function buildLocalAmbientEnvelope(options = {}) {
|
|
|
62
62
|
rawEvidenceIncludeCodexJsonl: options.rawEvidenceIncludeCodexJsonl,
|
|
63
63
|
rawEvidenceIncludeClaudeJsonl: options.rawEvidenceIncludeClaudeJsonl,
|
|
64
64
|
rawEvidenceCodexSessionFiles: options.codexSessionFiles,
|
|
65
|
+
rawEvidenceCodexAttributionScan: options.codexAttributionScan,
|
|
65
66
|
rawEvidenceClaudeSessionFiles: options.claudeSessionFiles,
|
|
67
|
+
rawEvidenceClaudeAttributionScan: options.claudeAttributionScan,
|
|
66
68
|
rawEvidenceSkipContentHashes: skipContentHashes,
|
|
67
69
|
rawEvidenceByteBudget: options.rawEvidenceByteBudget,
|
|
68
70
|
rawEvidenceObjectBudget: options.rawEvidenceObjectBudget,
|
|
@@ -534,12 +536,29 @@ function pruneUndurablePointers(envelope, outcomes) {
|
|
|
534
536
|
.filter((outcome) => outcome.upload_state === "upload_failed" &&
|
|
535
537
|
!durablePointerIds.has(outcome.pointer.raw_evidence_pointer_id))
|
|
536
538
|
.map((outcome) => outcome.pointer.raw_evidence_pointer_id));
|
|
539
|
+
const failedOutcomes = outcomes.filter((outcome) => failedPointerIds.has(outcome.pointer.raw_evidence_pointer_id));
|
|
537
540
|
if (failedPointerIds.size === 0)
|
|
538
541
|
return envelope;
|
|
539
542
|
return {
|
|
540
543
|
...envelope,
|
|
541
544
|
events: envelope.events.map((event) => ({
|
|
542
545
|
...event,
|
|
546
|
+
metrics: {
|
|
547
|
+
...event.metrics,
|
|
548
|
+
evidence_failed_count: (event.metrics["evidence_failed_count"] ?? 0) +
|
|
549
|
+
failedOutcomes.length,
|
|
550
|
+
},
|
|
551
|
+
attributes: event.evidence_completeness
|
|
552
|
+
? {
|
|
553
|
+
...event.attributes,
|
|
554
|
+
evidence_completeness_schema_version: event.evidence_completeness.schema_version,
|
|
555
|
+
evidence_completeness_status: "partial",
|
|
556
|
+
evidence_incomplete: true,
|
|
557
|
+
}
|
|
558
|
+
: event.attributes,
|
|
559
|
+
evidence_completeness: event.evidence_completeness
|
|
560
|
+
? markCompletenessUploadFailures(event.evidence_completeness, failedOutcomes)
|
|
561
|
+
: event.evidence_completeness,
|
|
543
562
|
raw_evidence_pointers: event.raw_evidence_pointers.filter((pointer) => !failedPointerIds.has(pointer.raw_evidence_pointer_id)),
|
|
544
563
|
redaction: {
|
|
545
564
|
...event.redaction,
|
|
@@ -548,6 +567,71 @@ function pruneUndurablePointers(envelope, outcomes) {
|
|
|
548
567
|
})),
|
|
549
568
|
};
|
|
550
569
|
}
|
|
570
|
+
function markCompletenessUploadFailures(completeness, failedOutcomes) {
|
|
571
|
+
const failureCounts = new Map();
|
|
572
|
+
for (const outcome of failedOutcomes) {
|
|
573
|
+
const source = outcome.kind ?? "raw_evidence";
|
|
574
|
+
failureCounts.set(source, (failureCounts.get(source) ?? 0) + 1);
|
|
575
|
+
}
|
|
576
|
+
const totalFailures = [...failureCounts.values()].reduce((sum, count) => sum + count, 0);
|
|
577
|
+
const sourceCounts = [...completeness.source_counts];
|
|
578
|
+
for (const [source, count] of failureCounts) {
|
|
579
|
+
const existingIndex = sourceCounts.findIndex((entry) => entry.source === source);
|
|
580
|
+
if (existingIndex === -1) {
|
|
581
|
+
sourceCounts.push({
|
|
582
|
+
source,
|
|
583
|
+
scanned_count: 0,
|
|
584
|
+
included_count: 0,
|
|
585
|
+
skipped_count: 0,
|
|
586
|
+
truncated_count: 0,
|
|
587
|
+
deferred_count: 0,
|
|
588
|
+
reused_count: 0,
|
|
589
|
+
failed_count: count,
|
|
590
|
+
});
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const existing = sourceCounts[existingIndex];
|
|
594
|
+
if (!existing)
|
|
595
|
+
continue;
|
|
596
|
+
sourceCounts[existingIndex] = {
|
|
597
|
+
...existing,
|
|
598
|
+
failed_count: existing.failed_count + count,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
const failureReasons = [...completeness.failure_reasons];
|
|
602
|
+
for (const [source, count] of failureCounts) {
|
|
603
|
+
const reason = "upload_failed";
|
|
604
|
+
const existingIndex = failureReasons.findIndex((entry) => entry.source === source && entry.reason === reason);
|
|
605
|
+
if (existingIndex === -1) {
|
|
606
|
+
failureReasons.push({ source, reason, count });
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
const existing = failureReasons[existingIndex];
|
|
610
|
+
if (existing) {
|
|
611
|
+
failureReasons[existingIndex] = {
|
|
612
|
+
...existing,
|
|
613
|
+
count: existing.count + count,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return EvidenceCompletenessPayloadSchema.parse({
|
|
619
|
+
...completeness,
|
|
620
|
+
status: "partial",
|
|
621
|
+
source_counts: sourceCounts,
|
|
622
|
+
totals: {
|
|
623
|
+
...completeness.totals,
|
|
624
|
+
failed_count: completeness.totals.failed_count + totalFailures,
|
|
625
|
+
},
|
|
626
|
+
failure_reasons: failureReasons.sort((a, b) => `${a.source}:${a.reason}`.localeCompare(`${b.source}:${b.reason}`)),
|
|
627
|
+
notes: [
|
|
628
|
+
...new Set([
|
|
629
|
+
...completeness.notes,
|
|
630
|
+
"Some collected evidence did not become durable; downstream analysis should lower confidence.",
|
|
631
|
+
]),
|
|
632
|
+
],
|
|
633
|
+
});
|
|
634
|
+
}
|
|
551
635
|
function makeUploadWorkContext(options) {
|
|
552
636
|
const provenance = makeCollectorProvenance({
|
|
553
637
|
context: options.activeContext,
|
|
@@ -624,6 +708,7 @@ function makeSourceScanCompletedEvent(options) {
|
|
|
624
708
|
evidence_truncated_count: evidenceCompleteness?.totals.truncated_count ?? 0,
|
|
625
709
|
evidence_deferred_count: evidenceCompleteness?.totals.deferred_count ?? 0,
|
|
626
710
|
evidence_reused_count: evidenceCompleteness?.totals.reused_count ?? 0,
|
|
711
|
+
evidence_failed_count: evidenceCompleteness?.totals.failed_count ?? 0,
|
|
627
712
|
},
|
|
628
713
|
attributes: {
|
|
629
714
|
repo_label: options.context.repo,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,6 @@
|
|
|
26
26
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
29
|
+
"@bli-cockpit/telemetry-core": "0.1.7"
|
|
30
30
|
}
|
|
31
31
|
}
|