@bli-cockpit/cli 0.1.31 → 0.1.33
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.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { normalizeGitOrigin, repoFingerprintFromLocalRoot, repoFingerprintFromOrigin, repoLabelFromOrigin, stableWorktreeFingerprint, } from "../repo-identity.js";
|
|
3
4
|
/**
|
|
4
5
|
* Shared deterministic attribution core for agent session transcripts (Codex
|
|
5
6
|
* and Claude Code). Both adapters extract source-shaped signals, normalize them
|
|
@@ -72,8 +73,21 @@ export function scoreSignalsAgainstWorktrees(signals, worktrees, options = {}) {
|
|
|
72
73
|
const best = scored[0];
|
|
73
74
|
const secondBestScore = scored[1]?.score ?? 0;
|
|
74
75
|
if (!best || best.score === 0) {
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
const recordedPaths = [...signals.cwds, ...signals.workspaceRoots];
|
|
77
|
+
if (repoPathAbsentFromDisk(recordedPaths, options.collectionRoots ?? [])) {
|
|
78
|
+
const folderFallback = fallbackToFolderWorkspaceForContainerCwd({
|
|
79
|
+
signals,
|
|
80
|
+
worktrees,
|
|
81
|
+
});
|
|
82
|
+
if (folderFallback)
|
|
83
|
+
return folderFallback;
|
|
84
|
+
const transcriptFallback = fallbackToTranscriptOriginForDeletedRepo({
|
|
85
|
+
originLabel,
|
|
86
|
+
signals,
|
|
87
|
+
});
|
|
88
|
+
if (transcriptFallback)
|
|
89
|
+
return transcriptFallback;
|
|
90
|
+
return skipped(terminalReasonForRecordedPaths(recordedPaths, options.pathExists));
|
|
77
91
|
}
|
|
78
92
|
const reason = signals.cwds.length > 0 || signals.workspaceRoots.length > 0
|
|
79
93
|
? "cwd_outside_scanned_worktrees"
|
|
@@ -135,6 +149,63 @@ function skipped(reason) {
|
|
|
135
149
|
worktree: null,
|
|
136
150
|
};
|
|
137
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Handles sessions launched from wrapper folders that intentionally contain
|
|
154
|
+
* several repos. The wrapper path is the user's working context even though it
|
|
155
|
+
* is not itself a git repo, and the consent boundary is unchanged because this
|
|
156
|
+
* only runs after the recorded path was proven under an approved collection
|
|
157
|
+
* root.
|
|
158
|
+
*/
|
|
159
|
+
function fallbackToFolderWorkspaceForContainerCwd(options) {
|
|
160
|
+
const recordedPaths = [...options.signals.cwds, ...options.signals.workspaceRoots];
|
|
161
|
+
let bestFolder = null;
|
|
162
|
+
for (const recordedPath of recordedPaths) {
|
|
163
|
+
const resolvedPath = path.resolve(recordedPath);
|
|
164
|
+
const containedWorktrees = options.worktrees.filter((worktree) => isPathStrictlyWithin(worktree.repo_root, resolvedPath));
|
|
165
|
+
if (containedWorktrees.length === 0)
|
|
166
|
+
continue;
|
|
167
|
+
const depth = pathDepth(resolvedPath);
|
|
168
|
+
if (!bestFolder || depth > bestFolder.depth) {
|
|
169
|
+
bestFolder = { resolvedPath, depth, containedWorktrees };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!bestFolder)
|
|
173
|
+
return null;
|
|
174
|
+
const repoFingerprints = new Set(bestFolder.containedWorktrees.map((worktree) => worktree.repo_fingerprint));
|
|
175
|
+
if (repoFingerprints.size === 1) {
|
|
176
|
+
const primary = sortWorktreesForFolderFallback(bestFolder.containedWorktrees)[0];
|
|
177
|
+
if (!primary)
|
|
178
|
+
return null;
|
|
179
|
+
return {
|
|
180
|
+
state: "attributed_fallback",
|
|
181
|
+
reason: "single_repo_folder_fallback",
|
|
182
|
+
signals: ["single_repo_folder_fallback"],
|
|
183
|
+
attribution_score: clampScore(Math.min(SCORE_CWD_MATCH, ATTRIBUTED_FALLBACK_MAX_SCORE)),
|
|
184
|
+
path_score: 0,
|
|
185
|
+
worktree: primary,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const repoLabel = path.basename(bestFolder.resolvedPath);
|
|
189
|
+
return {
|
|
190
|
+
state: "attributed_fallback",
|
|
191
|
+
reason: "multi_repo_folder_workspace",
|
|
192
|
+
signals: ["multi_repo_folder_workspace"],
|
|
193
|
+
attribution_score: clampScore(ATTRIBUTED_FALLBACK_MAX_SCORE),
|
|
194
|
+
path_score: 0,
|
|
195
|
+
worktree: {
|
|
196
|
+
requested_path: bestFolder.resolvedPath,
|
|
197
|
+
repo_root: bestFolder.resolvedPath,
|
|
198
|
+
repo_label: repoLabel,
|
|
199
|
+
repo_fingerprint: repoFingerprintFromLocalRoot(bestFolder.resolvedPath),
|
|
200
|
+
repo_origin_url: null,
|
|
201
|
+
branch: options.signals.branches[0] ?? "unknown",
|
|
202
|
+
head_sha: options.signals.headShas[0] ?? null,
|
|
203
|
+
worktree_label: repoLabel,
|
|
204
|
+
worktree_fingerprint: stableWorktreeFingerprint(bestFolder.resolvedPath),
|
|
205
|
+
worktree_is_primary: true,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
138
209
|
function fallbackToKnownRepoPrimaryWorkContext(options) {
|
|
139
210
|
const originCandidates = options.worktrees.filter((worktree) => worktree.repo_origin_url &&
|
|
140
211
|
options.signals.originUrls.includes(worktree.repo_origin_url));
|
|
@@ -173,6 +244,66 @@ function fallbackToKnownRepoPrimaryWorkContext(options) {
|
|
|
173
244
|
worktree: primary,
|
|
174
245
|
};
|
|
175
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Reconstructs a repo/worktree identity from transcript origin metadata only
|
|
249
|
+
* after the caller has already proven a recorded cwd/workspace root sits under
|
|
250
|
+
* an operator-approved collection root. The origin is never allowed to widen
|
|
251
|
+
* consent by itself: it only rescues sessions for repos that were once inside
|
|
252
|
+
* the approved scan boundary but have since been deleted from disk, where the
|
|
253
|
+
* transcript is still durable enough to carry the normalized remote identity.
|
|
254
|
+
*/
|
|
255
|
+
function fallbackToTranscriptOriginForDeletedRepo(options) {
|
|
256
|
+
const origins = [
|
|
257
|
+
...new Set(options.signals.originUrls
|
|
258
|
+
.map((origin) => normalizeGitOrigin(origin))
|
|
259
|
+
.filter(Boolean)),
|
|
260
|
+
];
|
|
261
|
+
if (origins.length === 0)
|
|
262
|
+
return null;
|
|
263
|
+
if (origins.length > 1)
|
|
264
|
+
return skipped("multiple_transcript_origins");
|
|
265
|
+
const recordedPath = options.signals.workspaceRoots[0] ?? options.signals.cwds[0] ?? null;
|
|
266
|
+
if (!recordedPath)
|
|
267
|
+
return null;
|
|
268
|
+
const resolvedPath = path.resolve(recordedPath);
|
|
269
|
+
const origin = origins[0] ?? "";
|
|
270
|
+
return {
|
|
271
|
+
state: "attributed_fallback",
|
|
272
|
+
reason: "transcript_origin_fallback",
|
|
273
|
+
signals: ["transcript_origin_fallback", options.originLabel],
|
|
274
|
+
attribution_score: clampScore(SCORE_ORIGIN_MATCH),
|
|
275
|
+
path_score: 0,
|
|
276
|
+
worktree: {
|
|
277
|
+
requested_path: resolvedPath,
|
|
278
|
+
repo_root: resolvedPath,
|
|
279
|
+
repo_label: repoLabelFromOrigin(origin),
|
|
280
|
+
repo_fingerprint: repoFingerprintFromOrigin(origin),
|
|
281
|
+
repo_origin_url: origin,
|
|
282
|
+
branch: options.signals.branches[0] ?? "unknown",
|
|
283
|
+
head_sha: options.signals.headShas[0] ?? null,
|
|
284
|
+
worktree_label: path.basename(resolvedPath),
|
|
285
|
+
worktree_fingerprint: stableWorktreeFingerprint(resolvedPath),
|
|
286
|
+
worktree_is_primary: false,
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function terminalReasonForRecordedPaths(paths, pathExists) {
|
|
291
|
+
// A recorded path under a collection root can mean two different things:
|
|
292
|
+
// deleted/missing repo (retryable) or an existing non-repo folder. The latter
|
|
293
|
+
// is common with wrapper-folder workflows, so adapters inject existence only
|
|
294
|
+
// at the boundary and the pure scorer keeps the old label when no hook exists.
|
|
295
|
+
if (!pathExists)
|
|
296
|
+
return "repo_not_on_disk";
|
|
297
|
+
const anyRecordedPathExists = paths.some((value) => {
|
|
298
|
+
try {
|
|
299
|
+
return pathExists(value);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
return anyRecordedPathExists ? "cwd_not_a_repo" : "repo_not_on_disk";
|
|
306
|
+
}
|
|
176
307
|
/**
|
|
177
308
|
* For each path signal, the single worktree whose root most specifically
|
|
178
309
|
* contains it (the longest containing root). Returns the set of worktree
|
|
@@ -204,12 +335,26 @@ export function isPathWithin(candidate, root) {
|
|
|
204
335
|
return (normalizedCandidate === normalizedRoot ||
|
|
205
336
|
normalizedCandidate.startsWith(normalizedRoot + path.sep));
|
|
206
337
|
}
|
|
338
|
+
function isPathStrictlyWithin(candidate, root) {
|
|
339
|
+
const normalizedCandidate = path.resolve(candidate);
|
|
340
|
+
const normalizedRoot = path.resolve(root);
|
|
341
|
+
return (normalizedCandidate !== normalizedRoot &&
|
|
342
|
+
normalizedCandidate.startsWith(normalizedRoot + path.sep));
|
|
343
|
+
}
|
|
207
344
|
function repoPathAbsentFromDisk(paths, collectionRoots) {
|
|
208
345
|
if (paths.length === 0 || collectionRoots.length === 0)
|
|
209
346
|
return false;
|
|
210
347
|
const roots = collectionRoots.map((root) => path.resolve(root));
|
|
211
348
|
return paths.some((value) => roots.some((root) => isPathWithin(value, root)));
|
|
212
349
|
}
|
|
350
|
+
function sortWorktreesForFolderFallback(worktrees) {
|
|
351
|
+
return [...worktrees].sort((a, b) => Number(b.worktree_is_primary) - Number(a.worktree_is_primary) ||
|
|
352
|
+
path.resolve(a.repo_root).localeCompare(path.resolve(b.repo_root)) ||
|
|
353
|
+
a.worktree_fingerprint.localeCompare(b.worktree_fingerprint));
|
|
354
|
+
}
|
|
355
|
+
function pathDepth(value) {
|
|
356
|
+
return path.resolve(value).split(path.sep).filter(Boolean).length;
|
|
357
|
+
}
|
|
213
358
|
export function clampScore(value) {
|
|
214
359
|
return Math.min(1, Math.max(0, Number(value.toFixed(4))));
|
|
215
360
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RAW_EVIDENCE_UPLOAD_MAX_FILE_BYTES, SECRET_FILE_SEGMENT_PATTERN, containsSecretLikeContent, } from "@bli-cockpit/telemetry-core";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
|
-
import { createReadStream } from "node:fs";
|
|
4
|
+
import { createReadStream, existsSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { StringDecoder } from "node:string_decoder";
|
|
7
7
|
import { SESSION_FILE_UUID_PATTERN, isPathWithin, sanitizeSessionId, scoreSignalsAgainstWorktrees, sessionIdFromFileName, shortHash, } from "./attribution-core.js";
|
|
@@ -304,7 +304,7 @@ async function attributeOneSession(session, worktrees, collectionRoots) {
|
|
|
304
304
|
originUrls: signals.repository_urls,
|
|
305
305
|
branches: signals.branches,
|
|
306
306
|
headShas: [],
|
|
307
|
-
}, worktrees, { collectionRoots, originLabel: "pr_repo_match" });
|
|
307
|
+
}, worktrees, { collectionRoots, originLabel: "pr_repo_match", pathExists: existsSync });
|
|
308
308
|
const extraSignals = [];
|
|
309
309
|
if (base.main_file_oversized)
|
|
310
310
|
extraSignals.push("main_file_oversized");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SECRET_FILE_SEGMENT_PATTERN, } from "@bli-cockpit/telemetry-core";
|
|
2
|
-
import { createReadStream } from "node:fs";
|
|
2
|
+
import { createReadStream, existsSync } from "node:fs";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
@@ -201,7 +201,7 @@ async function attributeOneSession(file, worktrees, collectionRoots) {
|
|
|
201
201
|
originUrls: signals.repository_urls,
|
|
202
202
|
branches: signals.branches,
|
|
203
203
|
headShas: signals.commit_hashes,
|
|
204
|
-
}, worktrees, { collectionRoots });
|
|
204
|
+
}, worktrees, { collectionRoots, pathExists: existsSync });
|
|
205
205
|
return { ...base, ...outcome };
|
|
206
206
|
}
|
|
207
207
|
async function readCodexMetadataSignals(filePath) {
|
|
@@ -566,6 +566,30 @@ function reasonClassification(reason) {
|
|
|
566
566
|
note: "repo must exist on disk",
|
|
567
567
|
};
|
|
568
568
|
}
|
|
569
|
+
if (reason === "cwd_not_a_repo") {
|
|
570
|
+
return {
|
|
571
|
+
classification: "permanent",
|
|
572
|
+
note: "cwd exists but is not a repo or folder workspace",
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (reason === "multiple_transcript_origins") {
|
|
576
|
+
return {
|
|
577
|
+
classification: "permanent",
|
|
578
|
+
note: "multiple transcript origins; attribution is ambiguous",
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
if (reason === "single_repo_folder_fallback") {
|
|
582
|
+
return {
|
|
583
|
+
classification: "permanent",
|
|
584
|
+
note: "uploadable single-repo folder workspace fallback",
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
if (reason === "multi_repo_folder_workspace") {
|
|
588
|
+
return {
|
|
589
|
+
classification: "permanent",
|
|
590
|
+
note: "uploadable multi-repo folder workspace fallback",
|
|
591
|
+
};
|
|
592
|
+
}
|
|
569
593
|
if (reason.startsWith("deferred_")) {
|
|
570
594
|
return {
|
|
571
595
|
classification: "retryable",
|
package/dist/repo-identity.js
CHANGED
|
@@ -32,10 +32,9 @@ export async function resolveRepoWorktreeIdentity(repoRoot) {
|
|
|
32
32
|
const repoLabel = repoOriginUrl
|
|
33
33
|
? repoLabelFromOrigin(repoOriginUrl)
|
|
34
34
|
: path.basename(resolvedRoot);
|
|
35
|
-
const
|
|
36
|
-
?
|
|
37
|
-
: `local:${sha256(commonGitDir ?? resolvedRoot)}`;
|
|
38
|
-
const repoFingerprint = `repo-${sha256(repoMaterial).slice(0, 24)}`;
|
|
35
|
+
const repoFingerprint = repoOriginUrl
|
|
36
|
+
? repoFingerprintFromOrigin(repoOriginUrl)
|
|
37
|
+
: `repo-${sha256(`local:${sha256(commonGitDir ?? resolvedRoot)}`).slice(0, 24)}`;
|
|
39
38
|
const gitFilePath = path.join(resolvedRoot, ".git");
|
|
40
39
|
const worktreeIsPrimary = await fs.stat(gitFilePath).then((stat) => stat.isDirectory(), () => false);
|
|
41
40
|
return {
|
|
@@ -141,7 +140,7 @@ async function hasGitMarker(dir) {
|
|
|
141
140
|
async function fallbackFilesystemIdentity(repoRoot) {
|
|
142
141
|
const resolvedRoot = await stableWorktreeRoot(repoRoot);
|
|
143
142
|
const repoLabel = path.basename(resolvedRoot) || "repo";
|
|
144
|
-
const repoFingerprint =
|
|
143
|
+
const repoFingerprint = repoFingerprintFromLocalRoot(resolvedRoot);
|
|
145
144
|
return {
|
|
146
145
|
requested_path: resolvedRoot,
|
|
147
146
|
repo_root: resolvedRoot,
|
|
@@ -162,6 +161,13 @@ export async function stableWorktreeRoot(repoRoot) {
|
|
|
162
161
|
export function stableWorktreeFingerprint(repoRoot) {
|
|
163
162
|
return `wt-${sha256(`worktree:${normalizeFingerprintPath(repoRoot)}`).slice(0, 24)}`;
|
|
164
163
|
}
|
|
164
|
+
export function repoFingerprintFromLocalRoot(root) {
|
|
165
|
+
return `repo-${sha256(`local:${path.resolve(root)}`).slice(0, 24)}`;
|
|
166
|
+
}
|
|
167
|
+
export function repoFingerprintFromOrigin(origin) {
|
|
168
|
+
const normalizedOrigin = normalizeGitOrigin(origin);
|
|
169
|
+
return `repo-${sha256(`origin:${normalizedOrigin}`).slice(0, 24)}`;
|
|
170
|
+
}
|
|
165
171
|
async function resolveBranchFromHead(repoRoot) {
|
|
166
172
|
try {
|
|
167
173
|
const gitPath = path.join(repoRoot, ".git");
|