@bli-cockpit/cli 0.1.32 → 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,6 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { normalizeGitOrigin, repoFingerprintFromOrigin, repoLabelFromOrigin, stableWorktreeFingerprint, } from "../repo-identity.js";
|
|
3
|
+
import { normalizeGitOrigin, repoFingerprintFromLocalRoot, repoFingerprintFromOrigin, repoLabelFromOrigin, stableWorktreeFingerprint, } from "../repo-identity.js";
|
|
4
4
|
/**
|
|
5
5
|
* Shared deterministic attribution core for agent session transcripts (Codex
|
|
6
6
|
* and Claude Code). Both adapters extract source-shaped signals, normalize them
|
|
@@ -73,14 +73,21 @@ export function scoreSignalsAgainstWorktrees(signals, worktrees, options = {}) {
|
|
|
73
73
|
const best = scored[0];
|
|
74
74
|
const secondBestScore = scored[1]?.score ?? 0;
|
|
75
75
|
if (!best || best.score === 0) {
|
|
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;
|
|
77
84
|
const transcriptFallback = fallbackToTranscriptOriginForDeletedRepo({
|
|
78
85
|
originLabel,
|
|
79
86
|
signals,
|
|
80
87
|
});
|
|
81
88
|
if (transcriptFallback)
|
|
82
89
|
return transcriptFallback;
|
|
83
|
-
return skipped(
|
|
90
|
+
return skipped(terminalReasonForRecordedPaths(recordedPaths, options.pathExists));
|
|
84
91
|
}
|
|
85
92
|
const reason = signals.cwds.length > 0 || signals.workspaceRoots.length > 0
|
|
86
93
|
? "cwd_outside_scanned_worktrees"
|
|
@@ -142,6 +149,63 @@ function skipped(reason) {
|
|
|
142
149
|
worktree: null,
|
|
143
150
|
};
|
|
144
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
|
+
}
|
|
145
209
|
function fallbackToKnownRepoPrimaryWorkContext(options) {
|
|
146
210
|
const originCandidates = options.worktrees.filter((worktree) => worktree.repo_origin_url &&
|
|
147
211
|
options.signals.originUrls.includes(worktree.repo_origin_url));
|
|
@@ -223,6 +287,23 @@ function fallbackToTranscriptOriginForDeletedRepo(options) {
|
|
|
223
287
|
},
|
|
224
288
|
};
|
|
225
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
|
+
}
|
|
226
307
|
/**
|
|
227
308
|
* For each path signal, the single worktree whose root most specifically
|
|
228
309
|
* contains it (the longest containing root). Returns the set of worktree
|
|
@@ -254,12 +335,26 @@ export function isPathWithin(candidate, root) {
|
|
|
254
335
|
return (normalizedCandidate === normalizedRoot ||
|
|
255
336
|
normalizedCandidate.startsWith(normalizedRoot + path.sep));
|
|
256
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
|
+
}
|
|
257
344
|
function repoPathAbsentFromDisk(paths, collectionRoots) {
|
|
258
345
|
if (paths.length === 0 || collectionRoots.length === 0)
|
|
259
346
|
return false;
|
|
260
347
|
const roots = collectionRoots.map((root) => path.resolve(root));
|
|
261
348
|
return paths.some((value) => roots.some((root) => isPathWithin(value, root)));
|
|
262
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
|
+
}
|
|
263
358
|
export function clampScore(value) {
|
|
264
359
|
return Math.min(1, Math.max(0, Number(value.toFixed(4))));
|
|
265
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
|
@@ -140,7 +140,7 @@ async function hasGitMarker(dir) {
|
|
|
140
140
|
async function fallbackFilesystemIdentity(repoRoot) {
|
|
141
141
|
const resolvedRoot = await stableWorktreeRoot(repoRoot);
|
|
142
142
|
const repoLabel = path.basename(resolvedRoot) || "repo";
|
|
143
|
-
const repoFingerprint =
|
|
143
|
+
const repoFingerprint = repoFingerprintFromLocalRoot(resolvedRoot);
|
|
144
144
|
return {
|
|
145
145
|
requested_path: resolvedRoot,
|
|
146
146
|
repo_root: resolvedRoot,
|
|
@@ -161,6 +161,9 @@ export async function stableWorktreeRoot(repoRoot) {
|
|
|
161
161
|
export function stableWorktreeFingerprint(repoRoot) {
|
|
162
162
|
return `wt-${sha256(`worktree:${normalizeFingerprintPath(repoRoot)}`).slice(0, 24)}`;
|
|
163
163
|
}
|
|
164
|
+
export function repoFingerprintFromLocalRoot(root) {
|
|
165
|
+
return `repo-${sha256(`local:${path.resolve(root)}`).slice(0, 24)}`;
|
|
166
|
+
}
|
|
164
167
|
export function repoFingerprintFromOrigin(origin) {
|
|
165
168
|
const normalizedOrigin = normalizeGitOrigin(origin);
|
|
166
169
|
return `repo-${sha256(`origin:${normalizedOrigin}`).slice(0, 24)}`;
|