@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
- if (repoPathAbsentFromDisk([...signals.cwds, ...signals.workspaceRoots], options.collectionRoots ?? [])) {
76
- return skipped("repo_not_on_disk");
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",
@@ -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 repoMaterial = repoOriginUrl
36
- ? `origin:${repoOriginUrl}`
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 = `repo-${sha256(`local:${resolvedRoot}`).slice(0, 24)}`;
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {