@bli-cockpit/cli 0.2.236 → 0.2.238

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.
Files changed (143) hide show
  1. package/dist/adapters/attribution-core-fallbacks.js +15 -11
  2. package/dist/adapters/attribution-core-score.js +111 -87
  3. package/dist/adapters/car-state.js +38 -35
  4. package/dist/adapters/claude-attribution-discovery.js +81 -56
  5. package/dist/adapters/claude-attribution-score.js +108 -94
  6. package/dist/adapters/claude-attribution-signals.js +52 -42
  7. package/dist/adapters/codex-attribution.js +42 -33
  8. package/dist/adapters/codex-session-signals.js +11 -9
  9. package/dist/adapters/git-state.js +56 -50
  10. package/dist/adapters/local-sources.js +52 -43
  11. package/dist/adapters/raw-evidence-git-diff.js +4 -2
  12. package/dist/adapters/raw-evidence-sanitize.js +15 -12
  13. package/dist/adapters/raw-evidence-transcript-file.js +32 -31
  14. package/dist/autostart-self-heal.js +18 -2
  15. package/dist/commands/agent-session-report.js +51 -59
  16. package/dist/commands/agent-setup-observed.js +30 -23
  17. package/dist/commands/agent-setup.js +96 -45
  18. package/dist/commands/autostart-heal.js +8 -2
  19. package/dist/commands/autostart-reading.js +3 -0
  20. package/dist/commands/backfill-evidence-outcomes.js +9 -6
  21. package/dist/commands/backfill-issues.js +3 -0
  22. package/dist/commands/backfill-scan.js +27 -22
  23. package/dist/commands/backfill-upload.js +69 -60
  24. package/dist/commands/backfill.js +26 -21
  25. package/dist/commands/brief-done.js +4 -1
  26. package/dist/commands/brief-edit.js +71 -46
  27. package/dist/commands/cal-detail.js +35 -0
  28. package/dist/commands/cal-overlay.js +106 -0
  29. package/dist/commands/cal.js +40 -23
  30. package/dist/commands/clean.js +4 -1
  31. package/dist/commands/docs-read.js +5 -2
  32. package/dist/commands/doctor-backfill.js +88 -71
  33. package/dist/commands/doctor-pipeline.js +59 -46
  34. package/dist/commands/doctor-usage-telemetry.js +2 -1
  35. package/dist/commands/heartbeat-post.js +116 -0
  36. package/dist/commands/heartbeat-token-lock.js +46 -33
  37. package/dist/commands/heartbeat.js +43 -125
  38. package/dist/commands/install-update.js +17 -15
  39. package/dist/commands/issue-show.js +27 -28
  40. package/dist/commands/issue-write.js +70 -23
  41. package/dist/commands/issue.js +68 -37
  42. package/dist/commands/jarvis-live-answer.js +99 -0
  43. package/dist/commands/jarvis-render.js +1 -94
  44. package/dist/commands/jarvis-trace.js +30 -22
  45. package/dist/commands/jarvis-turn-report.js +124 -0
  46. package/dist/commands/jarvis-turn.js +108 -182
  47. package/dist/commands/jarvis.js +26 -17
  48. package/dist/commands/local-args-collector-setup.js +82 -84
  49. package/dist/commands/local-args-collector-work.js +37 -38
  50. package/dist/commands/local-args-tower-admin.js +155 -142
  51. package/dist/commands/local-args-tower-cal.js +54 -48
  52. package/dist/commands/local-args-tower-careers.js +73 -54
  53. package/dist/commands/local-args-tower-docs-msg.js +56 -188
  54. package/dist/commands/local-args-tower-github.js +26 -20
  55. package/dist/commands/local-args-tower-hits.js +32 -20
  56. package/dist/commands/local-args-tower-manager.js +85 -4
  57. package/dist/commands/local-args-tower-memory.js +139 -117
  58. package/dist/commands/local-args-tower-msg.js +149 -0
  59. package/dist/commands/local-args-tower-notes.js +346 -0
  60. package/dist/commands/local-args-tower-pages.js +100 -378
  61. package/dist/commands/local-args-tower-settings.js +28 -29
  62. package/dist/commands/local-args-tower-work.js +52 -33
  63. package/dist/commands/local-args.js +80 -129
  64. package/dist/commands/local-help-commands-tower-ops.js +15 -4
  65. package/dist/commands/local-help-commands-tower.js +11 -4
  66. package/dist/commands/local-help-commands.js +342 -341
  67. package/dist/commands/local-help.js +87 -83
  68. package/dist/commands/local-run-guards.js +100 -0
  69. package/dist/commands/local.js +137 -206
  70. package/dist/commands/login.js +13 -9
  71. package/dist/commands/logs.js +55 -38
  72. package/dist/commands/mail-attachments.js +44 -26
  73. package/dist/commands/mail.js +6 -4
  74. package/dist/commands/manager-render-home.js +208 -0
  75. package/dist/commands/manager-render.js +7 -3
  76. package/dist/commands/manager-requests.js +33 -0
  77. package/dist/commands/manager.js +39 -5
  78. package/dist/commands/memory-consolidation.js +18 -9
  79. package/dist/commands/memory-daemon-probe.js +37 -28
  80. package/dist/commands/memory-install-config.js +64 -60
  81. package/dist/commands/memory-install.js +21 -41
  82. package/dist/commands/memory-requeue.js +26 -19
  83. package/dist/commands/memory-rules.js +26 -18
  84. package/dist/commands/memory-search-print.js +26 -0
  85. package/dist/commands/memory-store.js +14 -30
  86. package/dist/commands/memory-verb-list.js +49 -48
  87. package/dist/commands/msg.js +51 -9
  88. package/dist/commands/notes-accounts.js +49 -32
  89. package/dist/commands/notes-sync-loop.js +115 -89
  90. package/dist/commands/notes-wisprflow-login.js +44 -23
  91. package/dist/commands/notes-writes.js +115 -100
  92. package/dist/commands/notes.js +33 -55
  93. package/dist/commands/onboard-flows.js +69 -51
  94. package/dist/commands/onboard.js +11 -9
  95. package/dist/commands/ops-render-memory.js +10 -7
  96. package/dist/commands/ops-render.js +33 -30
  97. package/dist/commands/ops.js +3 -0
  98. package/dist/commands/public-root.js +1 -1
  99. package/dist/commands/session-sync-outside-roots.js +40 -36
  100. package/dist/commands/session-sync-plan.js +14 -10
  101. package/dist/commands/session-sync-upload.js +24 -22
  102. package/dist/commands/session-sync.js +19 -8
  103. package/dist/commands/slack.js +25 -16
  104. package/dist/commands/sources-check.js +13 -7
  105. package/dist/commands/sources-discovery.js +25 -17
  106. package/dist/commands/sources-verdict.js +19 -18
  107. package/dist/commands/speed.js +23 -19
  108. package/dist/commands/start.js +20 -16
  109. package/dist/commands/sync-doctor.js +52 -48
  110. package/dist/commands/sync.js +22 -16
  111. package/dist/commands/usage-telemetry-toml.js +71 -45
  112. package/dist/commands/usage.js +91 -67
  113. package/dist/commands/workbook-render.js +10 -7
  114. package/dist/evidence-reconcile-client.js +75 -64
  115. package/dist/evidence-redelivery-plan.js +37 -34
  116. package/dist/evidence-redelivery.js +32 -29
  117. package/dist/evidence-transcripts-client.js +20 -20
  118. package/dist/evidence-upload-object.js +86 -89
  119. package/dist/evidence-upload-plan.js +94 -83
  120. package/dist/evidence-upload-terminal.js +45 -50
  121. package/dist/image-description-session.js +156 -159
  122. package/dist/local-state-pairing-code.js +39 -31
  123. package/dist/local-state-status.js +9 -2
  124. package/dist/onboarding-roots.js +5 -8
  125. package/dist/process-runner.js +8 -8
  126. package/dist/raw-evidence-gc.js +33 -38
  127. package/dist/raw-evidence-stuck-summary.js +7 -14
  128. package/dist/repo-identity.js +26 -23
  129. package/dist/scheduled-self-update.js +11 -15
  130. package/dist/second-install.js +3 -0
  131. package/dist/slim/codex-rules.js +23 -36
  132. package/dist/sync-lock.js +5 -15
  133. package/dist/tower-client.js +36 -53
  134. package/dist/tower-stream.js +7 -24
  135. package/dist/upload-agent-artifacts.js +58 -64
  136. package/dist/upload-envelope-build.js +60 -59
  137. package/dist/upload-evidence-delivery-reconcile.js +58 -56
  138. package/dist/upload-evidence-delivery-summary.js +32 -34
  139. package/dist/upload-ingest-receipt.js +20 -23
  140. package/dist/upload-session-reports-queue.js +4 -1
  141. package/dist/upload-session-reports-wire.js +64 -68
  142. package/dist/upload-sync.js +171 -157
  143. package/package.json +2 -2
@@ -75,17 +75,7 @@ function deepestApprovedRootForRecordedPaths(recordedPaths, collectionRoots) {
75
75
  * root.
76
76
  */
77
77
  export function fallbackToFolderWorkspaceForContainerCwd(options) {
78
- let bestFolder = null;
79
- for (const recordedPath of pathComparisonAliases(options.approvedRecordedPaths)) {
80
- const resolvedPath = resolveAttributionPath(recordedPath);
81
- const containedWorktrees = options.worktrees.filter((worktree) => isPathStrictlyWithin(worktree.repo_root, resolvedPath));
82
- if (containedWorktrees.length === 0)
83
- continue;
84
- const depth = pathDepth(resolvedPath);
85
- if (!bestFolder || depth > bestFolder.depth) {
86
- bestFolder = { resolvedPath, depth, containedWorktrees };
87
- }
88
- }
78
+ const bestFolder = findDeepestContainerFolder(options);
89
79
  if (!bestFolder)
90
80
  return null;
91
81
  const repoFingerprints = new Set(bestFolder.containedWorktrees.map((worktree) => worktree.repo_fingerprint));
@@ -123,6 +113,20 @@ export function fallbackToFolderWorkspaceForContainerCwd(options) {
123
113
  },
124
114
  };
125
115
  }
116
+ function findDeepestContainerFolder(options) {
117
+ let bestFolder = null;
118
+ for (const recordedPath of pathComparisonAliases(options.approvedRecordedPaths)) {
119
+ const resolvedPath = resolveAttributionPath(recordedPath);
120
+ const containedWorktrees = options.worktrees.filter((worktree) => isPathStrictlyWithin(worktree.repo_root, resolvedPath));
121
+ if (containedWorktrees.length === 0)
122
+ continue;
123
+ const depth = pathDepth(resolvedPath);
124
+ if (!bestFolder || depth > bestFolder.depth) {
125
+ bestFolder = { resolvedPath, depth, containedWorktrees };
126
+ }
127
+ }
128
+ return bestFolder;
129
+ }
126
130
  export function fallbackToKnownRepoPrimaryWorkContext(options) {
127
131
  const originCandidates = options.worktrees.filter((worktree) => worktree.repo_origin_url &&
128
132
  options.signals.originUrls.includes(worktree.repo_origin_url));
@@ -16,6 +16,17 @@ import { attachApprovedRootWorkspace, fallbackToFolderWorkspaceForContainerCwd,
16
16
  * origin standing in for a deleted repo.
17
17
  */
18
18
  export function scoreSignalsAgainstWorktrees(signals, worktrees, options = {}) {
19
+ const context = prepareScoringContext(signals, worktrees, options);
20
+ if (!hasAnyAttributionSignal(signals))
21
+ return unattributed("no_repo_signals");
22
+ const scored = scoreCandidateWorktrees(context);
23
+ scored.sort((a, b) => b.score - a.score);
24
+ const best = scored[0];
25
+ if (!best || best.score === 0)
26
+ return resolveUnmatchedSignals(context);
27
+ return resolveBestMatch(context, scored, best);
28
+ }
29
+ function prepareScoringContext(signals, worktrees, options) {
19
30
  const originLabel = options.originLabel ?? "origin_url_match";
20
31
  const collectionRoots = collectionRootComparisonAliases(options.collectionRoots ?? []);
21
32
  const recordedPaths = [...signals.cwds, ...signals.workspaceRoots];
@@ -23,14 +34,17 @@ export function scoreSignalsAgainstWorktrees(signals, worktrees, options = {}) {
23
34
  const scopedWorktrees = worktreesWithinCollectionScope(worktrees, collectionRoots);
24
35
  const cwdComparisonPaths = pathComparisonAliases(signals.cwds);
25
36
  const workspaceRootComparisonPaths = pathComparisonAliases(signals.workspaceRoots);
26
- const hasAnySignal = signals.cwds.length > 0 ||
27
- signals.workspaceRoots.length > 0 ||
28
- signals.originUrls.length > 0 ||
29
- signals.branches.length > 0 ||
30
- signals.headShas.length > 0;
31
- if (!hasAnySignal) {
32
- return unattributed("no_repo_signals");
33
- }
37
+ return {
38
+ signals, options, originLabel, collectionRoots, recordedPaths,
39
+ approvedRecordedPaths, scopedWorktrees, cwdComparisonPaths, workspaceRootComparisonPaths
40
+ };
41
+ }
42
+ function hasAnyAttributionSignal(signals) {
43
+ return signals.cwds.length > 0 || signals.workspaceRoots.length > 0 ||
44
+ signals.originUrls.length > 0 || signals.branches.length > 0 || signals.headShas.length > 0;
45
+ }
46
+ function scoreCandidateWorktrees(context) {
47
+ const { cwdComparisonPaths, workspaceRootComparisonPaths, scopedWorktrees } = context;
34
48
  // D6 deepest-root tie-break: a path signal can sit inside several candidate
35
49
  // roots when one root is nested inside another (e.g. Claude's
36
50
  // `<repo>/.claude/worktrees/<name>` linked worktrees, or any monorepo with a
@@ -39,73 +53,76 @@ export function scoreSignalsAgainstWorktrees(signals, worktrees, options = {}) {
39
53
  // root earns the path score, which is also the most specific (correct) match.
40
54
  const cwdWinners = deepestContainers(cwdComparisonPaths, scopedWorktrees);
41
55
  const workspaceRootWinners = deepestContainers(workspaceRootComparisonPaths, scopedWorktrees);
42
- const scored = scopedWorktrees.map((worktree) => {
43
- const matched = [];
44
- let score = 0;
45
- let pathScore = 0;
46
- if (cwdWinners.has(worktree.worktree_fingerprint)) {
47
- score += SCORE_CWD_MATCH;
48
- pathScore += SCORE_CWD_MATCH;
49
- matched.push("cwd_match");
50
- }
51
- if (workspaceRootWinners.has(worktree.worktree_fingerprint)) {
52
- score += SCORE_WORKSPACE_ROOT_MATCH;
53
- pathScore += SCORE_WORKSPACE_ROOT_MATCH;
54
- matched.push("workspace_root_match");
55
- }
56
- if (worktree.repo_origin_url &&
57
- signals.originUrls.includes(worktree.repo_origin_url)) {
58
- score += SCORE_ORIGIN_MATCH;
59
- matched.push(originLabel);
60
- }
61
- if (signals.branches.includes(worktree.branch)) {
62
- score += SCORE_BRANCH_MATCH;
63
- matched.push("branch_match");
64
- }
65
- if (worktree.head_sha && signals.headShas.includes(worktree.head_sha)) {
66
- score += SCORE_HEAD_SHA_MATCH;
67
- matched.push("head_sha_match");
68
- }
69
- return { worktree, score, pathScore, matched };
70
- });
71
- scored.sort((a, b) => b.score - a.score);
72
- const best = scored[0];
73
- const secondBestScore = scored[1]?.score ?? 0;
74
- if (!best || best.score === 0) {
75
- if (approvedRecordedPaths.length > 0) {
76
- const folderFallback = fallbackToFolderWorkspaceForContainerCwd({
77
- signals,
78
- worktrees: scopedWorktrees,
79
- approvedRecordedPaths,
80
- });
81
- if (folderFallback)
82
- return folderFallback;
83
- const transcriptFallback = fallbackToTranscriptOriginForDeletedRepo({
84
- originLabel,
85
- signals,
86
- collectionRoots,
87
- pathExists: options.pathExists,
88
- });
89
- if (transcriptFallback) {
90
- return attachApprovedRootWorkspace({
91
- outcome: transcriptFallback,
92
- signals,
93
- collectionRoots,
94
- approvedRecordedPaths,
95
- });
96
- }
56
+ return scopedWorktrees.map(worktree => scoreOneWorktree(worktree, context.signals, context.originLabel, cwdWinners, workspaceRootWinners));
57
+ }
58
+ function scoreOneWorktree(worktree, signals, originLabel, cwdWinners, workspaceRootWinners) {
59
+ const matched = [];
60
+ let score = 0;
61
+ let pathScore = 0;
62
+ if (cwdWinners.has(worktree.worktree_fingerprint)) {
63
+ score += SCORE_CWD_MATCH;
64
+ pathScore += SCORE_CWD_MATCH;
65
+ matched.push("cwd_match");
66
+ }
67
+ if (workspaceRootWinners.has(worktree.worktree_fingerprint)) {
68
+ score += SCORE_WORKSPACE_ROOT_MATCH;
69
+ pathScore += SCORE_WORKSPACE_ROOT_MATCH;
70
+ matched.push("workspace_root_match");
71
+ }
72
+ if (worktree.repo_origin_url &&
73
+ signals.originUrls.includes(worktree.repo_origin_url)) {
74
+ score += SCORE_ORIGIN_MATCH;
75
+ matched.push(originLabel);
76
+ }
77
+ if (signals.branches.includes(worktree.branch)) {
78
+ score += SCORE_BRANCH_MATCH;
79
+ matched.push("branch_match");
80
+ }
81
+ if (worktree.head_sha && signals.headShas.includes(worktree.head_sha)) {
82
+ score += SCORE_HEAD_SHA_MATCH;
83
+ matched.push("head_sha_match");
84
+ }
85
+ return { worktree, score, pathScore, matched };
86
+ }
87
+ function resolveUnmatchedSignals(context) {
88
+ const { signals, options, originLabel, collectionRoots, approvedRecordedPaths, scopedWorktrees } = context;
89
+ if (approvedRecordedPaths.length > 0) {
90
+ const folderFallback = fallbackToFolderWorkspaceForContainerCwd({
91
+ signals,
92
+ worktrees: scopedWorktrees,
93
+ approvedRecordedPaths,
94
+ });
95
+ if (folderFallback)
96
+ return folderFallback;
97
+ const transcriptFallback = fallbackToTranscriptOriginForDeletedRepo({
98
+ originLabel,
99
+ signals,
100
+ collectionRoots,
101
+ pathExists: options.pathExists,
102
+ });
103
+ if (transcriptFallback) {
97
104
  return attachApprovedRootWorkspace({
98
- outcome: skipped(terminalReasonForRecordedPaths(approvedRecordedPaths, options.pathExists)),
105
+ outcome: transcriptFallback,
99
106
  signals,
100
107
  collectionRoots,
101
108
  approvedRecordedPaths,
102
109
  });
103
110
  }
104
- const reason = signals.cwds.length > 0 || signals.workspaceRoots.length > 0
105
- ? "cwd_outside_scanned_worktrees"
106
- : "no_matching_worktree_signals";
107
- return unattributed(reason);
111
+ return attachApprovedRootWorkspace({
112
+ outcome: skipped(terminalReasonForRecordedPaths(approvedRecordedPaths, options.pathExists)),
113
+ signals,
114
+ collectionRoots,
115
+ approvedRecordedPaths,
116
+ });
108
117
  }
118
+ const reason = signals.cwds.length > 0 || signals.workspaceRoots.length > 0
119
+ ? "cwd_outside_scanned_worktrees"
120
+ : "no_matching_worktree_signals";
121
+ return unattributed(reason);
122
+ }
123
+ function resolveBestMatch(context, scored, best) {
124
+ const { signals, collectionRoots, approvedRecordedPaths } = context;
125
+ const secondBestScore = scored[1]?.score ?? 0;
109
126
  const score = clampScore(best.score);
110
127
  const pathScore = clampScore(best.pathScore);
111
128
  if (best.pathScore > 0 &&
@@ -121,25 +138,9 @@ export function scoreSignalsAgainstWorktrees(signals, worktrees, options = {}) {
121
138
  };
122
139
  }
123
140
  if (best.pathScore === 0) {
124
- if (collectionRoots.length > 0 && approvedRecordedPaths.length === 0) {
125
- return unattributed(recordedPaths.length > 0
126
- ? "cwd_outside_scanned_worktrees"
127
- : "no_matching_worktree_signals");
128
- }
129
- const fallback = fallbackToKnownRepoPrimaryWorkContext({
130
- originLabel,
131
- scored,
132
- signals,
133
- worktrees: scopedWorktrees,
134
- });
135
- if (fallback) {
136
- return attachApprovedRootWorkspace({
137
- outcome: fallback,
138
- signals,
139
- collectionRoots,
140
- approvedRecordedPaths,
141
- });
142
- }
141
+ const fallback = resolveOriginOnlyMatch(context, scored);
142
+ if (fallback)
143
+ return fallback;
143
144
  }
144
145
  return attachApprovedRootWorkspace({
145
146
  outcome: {
@@ -156,4 +157,27 @@ export function scoreSignalsAgainstWorktrees(signals, worktrees, options = {}) {
156
157
  collectionRoots,
157
158
  approvedRecordedPaths,
158
159
  });
160
+ }
161
+ function resolveOriginOnlyMatch(context, scored) {
162
+ const { signals, originLabel, collectionRoots, recordedPaths, approvedRecordedPaths, scopedWorktrees } = context;
163
+ if (collectionRoots.length > 0 && approvedRecordedPaths.length === 0) {
164
+ return unattributed(recordedPaths.length > 0
165
+ ? "cwd_outside_scanned_worktrees"
166
+ : "no_matching_worktree_signals");
167
+ }
168
+ const fallback = fallbackToKnownRepoPrimaryWorkContext({
169
+ originLabel,
170
+ scored,
171
+ signals,
172
+ worktrees: scopedWorktrees,
173
+ });
174
+ if (fallback) {
175
+ return attachApprovedRootWorkspace({
176
+ outcome: fallback,
177
+ signals,
178
+ collectionRoots,
179
+ approvedRecordedPaths,
180
+ });
181
+ }
182
+ return null;
159
183
  }
@@ -6,41 +6,7 @@ import { describeError, isMissingFileFailure } from "../health-detail.js";
6
6
  export async function collectCarState(context) {
7
7
  const ticketsDir = path.join(context.repoRoot, ".codex-autorunner", "tickets");
8
8
  try {
9
- const filenames = (await fs.readdir(ticketsDir))
10
- .filter(isCarTicketFilename)
11
- .sort();
12
- const tickets = await Promise.all(filenames.map(async (filename) => parseCarTicketFile(path.join(ticketsDir, filename))));
13
- const openTickets = tickets.filter((ticket) => !ticket.done);
14
- const currentTicket = openTickets[0] ?? null;
15
- const ticketId = currentTicket && parseTicketIdFromText(`${currentTicket.ticket_id} ${currentTicket.title}`);
16
- const ticketBindingCandidates = ticketId
17
- ? [
18
- {
19
- ticket_id: ticketId,
20
- binding_source: "car_ticket",
21
- confidence: 0.7,
22
- evidence_labels: [`car_ticket:${currentTicket.file}`],
23
- },
24
- ]
25
- : [];
26
- const facts = {
27
- present: true,
28
- current_ticket: currentTicket,
29
- open_ticket_count: openTickets.length,
30
- };
31
- return {
32
- scan: makeCarScan({
33
- context,
34
- status: "ok",
35
- facts,
36
- ticketBindingCandidates,
37
- diagnosticLabels: [
38
- `car_open_tickets:${facts.open_ticket_count}`,
39
- currentTicket ? `car_current_ticket:${currentTicket.file}` : "car_no_open_ticket",
40
- ],
41
- }),
42
- facts,
43
- };
9
+ return await collectPresentCarState(context, ticketsDir);
44
10
  }
45
11
  catch (error) {
46
12
  // `car_not_present` is honest for a repo with no `car/` folder, which is
@@ -70,6 +36,43 @@ export async function collectCarState(context) {
70
36
  };
71
37
  }
72
38
  }
39
+ async function collectPresentCarState(context, ticketsDir) {
40
+ const filenames = (await fs.readdir(ticketsDir))
41
+ .filter(isCarTicketFilename)
42
+ .sort();
43
+ const tickets = await Promise.all(filenames.map(async (filename) => parseCarTicketFile(path.join(ticketsDir, filename))));
44
+ const openTickets = tickets.filter((ticket) => !ticket.done);
45
+ const currentTicket = openTickets[0] ?? null;
46
+ const ticketId = currentTicket && parseTicketIdFromText(`${currentTicket.ticket_id} ${currentTicket.title}`);
47
+ const ticketBindingCandidates = ticketId
48
+ ? [
49
+ {
50
+ ticket_id: ticketId,
51
+ binding_source: "car_ticket",
52
+ confidence: 0.7,
53
+ evidence_labels: [`car_ticket:${currentTicket.file}`],
54
+ },
55
+ ]
56
+ : [];
57
+ const facts = {
58
+ present: true,
59
+ current_ticket: currentTicket,
60
+ open_ticket_count: openTickets.length,
61
+ };
62
+ return {
63
+ scan: makeCarScan({
64
+ context,
65
+ status: "ok",
66
+ facts,
67
+ ticketBindingCandidates,
68
+ diagnosticLabels: [
69
+ `car_open_tickets:${facts.open_ticket_count}`,
70
+ currentTicket ? `car_current_ticket:${currentTicket.file}` : "car_no_open_ticket",
71
+ ],
72
+ }),
73
+ facts,
74
+ };
75
+ }
73
76
  export async function parseCarTicketFile(filePath) {
74
77
  const raw = await fs.readFile(filePath, "utf8");
75
78
  return {
@@ -11,14 +11,9 @@ import { CLAUDE_SESSION_MAX_SIDECAR_FILES, } from "./claude-attribution-types.js
11
11
  export async function discoverClaudeSessions(projectsDir, cutoffMs) {
12
12
  const sessions = [];
13
13
  const projectDirsSkipped = 0;
14
- let projectDirReadFailedCount = 0;
15
- let sessionStatFailedCount = 0;
16
- let sidecarDirReadFailedCount = 0;
17
- let sidecarStatFailedCount = 0;
18
- // Same shape as the Codex walk: first reason plus counts, once. Per-file
19
- // lines across a store of thousands would be their own silence (BLI-3238).
20
- let firstDirectoryFailure = null;
21
- let firstStatFailure = null;
14
+ const sessionStatFailedCount = 0;
15
+ const sidecarDirReadFailedCount = 0;
16
+ const sidecarStatFailedCount = 0;
22
17
  let projectEntries;
23
18
  try {
24
19
  projectEntries = await fs.readdir(projectsDir, { withFileTypes: true });
@@ -42,6 +37,19 @@ export async function discoverClaudeSessions(projectsDir, cutoffMs) {
42
37
  sidecarStatFailedCount,
43
38
  };
44
39
  }
40
+ return walkProjectSessions(projectEntries, projectsDir, cutoffMs);
41
+ }
42
+ async function walkProjectSessions(projectEntries, projectsDir, cutoffMs) {
43
+ const sessions = [];
44
+ const projectDirsSkipped = 0;
45
+ let projectDirReadFailedCount = 0;
46
+ let sessionStatFailedCount = 0;
47
+ let sidecarDirReadFailedCount = 0;
48
+ let sidecarStatFailedCount = 0;
49
+ // Same shape as the Codex walk: first reason plus counts, once. Per-file
50
+ // lines across a store of thousands would be their own silence (BLI-3238).
51
+ let firstDirectoryFailure = null;
52
+ let firstStatFailure = null;
45
53
  for (const projectEntry of projectEntries) {
46
54
  if (!projectEntry.isDirectory())
47
55
  continue;
@@ -57,40 +65,61 @@ export async function discoverClaudeSessions(projectsDir, cutoffMs) {
57
65
  }
58
66
  continue;
59
67
  }
60
- for (const sessionEntry of sessionEntries) {
61
- if (!sessionEntry.isFile())
62
- continue;
63
- if (!SESSION_FILE_UUID_PATTERN.test(sessionEntry.name))
64
- continue;
65
- const mainFile = path.join(projectDir, sessionEntry.name);
66
- let mainStat;
67
- try {
68
- mainStat = await fs.stat(mainFile);
69
- }
70
- catch (error) {
71
- sessionStatFailedCount += 1;
72
- firstStatFailure ??= describeError(error);
73
- continue;
74
- }
75
- const sessionUuid = sessionEntry.name.replace(/\.jsonl$/i, "");
76
- const sidecarDiscovery = await discoverSidecars(path.join(projectDir, sessionUuid, "subagents"));
77
- const { sidecars, sidecarsCapped } = sidecarDiscovery;
78
- sidecarDirReadFailedCount += sidecarDiscovery.dirReadFailedCount;
79
- sidecarStatFailedCount += sidecarDiscovery.statFailedCount;
80
- const recencyMs = Math.max(mainStat.mtimeMs, ...sidecars.map((sidecar) => sidecar.mtimeMs));
81
- // D10: a session is recent if its main file OR any sidecar is in window.
82
- if (recencyMs < cutoffMs)
83
- continue;
84
- sessions.push({
85
- mainFile,
86
- mainMtimeMs: mainStat.mtimeMs,
87
- mainByteSize: mainStat.size,
88
- recencyMs,
89
- sidecars,
90
- sidecarsCapped,
91
- });
68
+ const project = await collectProjectSessions(sessionEntries, projectDir, cutoffMs, sessions);
69
+ sessionStatFailedCount += project.sessionStatFailedCount;
70
+ sidecarDirReadFailedCount += project.sidecarDirReadFailedCount;
71
+ sidecarStatFailedCount += project.sidecarStatFailedCount;
72
+ firstStatFailure ??= project.firstStatFailure;
73
+ }
74
+ logSessionDiscoveryFailures(sessions, projectDirReadFailedCount, sessionStatFailedCount, firstDirectoryFailure, firstStatFailure);
75
+ return {
76
+ sessions,
77
+ projectDirsSkipped,
78
+ projectDirReadFailedCount,
79
+ sessionStatFailedCount,
80
+ sidecarDirReadFailedCount,
81
+ sidecarStatFailedCount,
82
+ };
83
+ }
84
+ async function collectProjectSessions(sessionEntries, projectDir, cutoffMs, sessions) {
85
+ let sessionStatFailedCount = 0, sidecarDirReadFailedCount = 0, sidecarStatFailedCount = 0;
86
+ let firstStatFailure = null;
87
+ for (const sessionEntry of sessionEntries) {
88
+ if (!sessionEntry.isFile())
89
+ continue;
90
+ if (!SESSION_FILE_UUID_PATTERN.test(sessionEntry.name))
91
+ continue;
92
+ const mainFile = path.join(projectDir, sessionEntry.name);
93
+ let mainStat;
94
+ try {
95
+ mainStat = await fs.stat(mainFile);
96
+ }
97
+ catch (error) {
98
+ sessionStatFailedCount += 1;
99
+ firstStatFailure ??= describeError(error);
100
+ continue;
92
101
  }
102
+ const sessionUuid = sessionEntry.name.replace(/\.jsonl$/i, "");
103
+ const sidecarDiscovery = await discoverSidecars(path.join(projectDir, sessionUuid, "subagents"));
104
+ const { sidecars, sidecarsCapped } = sidecarDiscovery;
105
+ sidecarDirReadFailedCount += sidecarDiscovery.dirReadFailedCount;
106
+ sidecarStatFailedCount += sidecarDiscovery.statFailedCount;
107
+ const recencyMs = Math.max(mainStat.mtimeMs, ...sidecars.map((sidecar) => sidecar.mtimeMs));
108
+ // D10: a session is recent if its main file OR any sidecar is in window.
109
+ if (recencyMs < cutoffMs)
110
+ continue;
111
+ sessions.push({
112
+ mainFile,
113
+ mainMtimeMs: mainStat.mtimeMs,
114
+ mainByteSize: mainStat.size,
115
+ recencyMs,
116
+ sidecars,
117
+ sidecarsCapped,
118
+ });
93
119
  }
120
+ return { sessionStatFailedCount, sidecarDirReadFailedCount, sidecarStatFailedCount, firstStatFailure };
121
+ }
122
+ function logSessionDiscoveryFailures(sessions, projectDirReadFailedCount, sessionStatFailedCount, firstDirectoryFailure, firstStatFailure) {
94
123
  if (projectDirReadFailedCount > 0 || sessionStatFailedCount > 0) {
95
124
  console.error("[claude-attribution] Claude sessions were invisible to the walk", JSON.stringify({
96
125
  reason: "session_discovery_incomplete",
@@ -103,14 +132,6 @@ export async function discoverClaudeSessions(projectsDir, cutoffMs) {
103
132
  ...(firstStatFailure ? { first_stat_failure: firstStatFailure } : {}),
104
133
  }));
105
134
  }
106
- return {
107
- sessions,
108
- projectDirsSkipped,
109
- projectDirReadFailedCount,
110
- sessionStatFailedCount,
111
- sidecarDirReadFailedCount,
112
- sidecarStatFailedCount,
113
- };
114
135
  }
115
136
  async function discoverSidecars(subagentsDir) {
116
137
  let entries;
@@ -134,6 +155,17 @@ async function discoverSidecars(subagentsDir) {
134
155
  statFailedCount: 0,
135
156
  };
136
157
  }
158
+ const { discovered, statFailedCount } = await statSidecars(entries, subagentsDir);
159
+ discovered.sort((a, b) => b.mtimeMs - a.mtimeMs);
160
+ const capped = Math.max(0, discovered.length - CLAUDE_SESSION_MAX_SIDECAR_FILES);
161
+ return {
162
+ sidecars: discovered.slice(0, CLAUDE_SESSION_MAX_SIDECAR_FILES),
163
+ sidecarsCapped: capped,
164
+ dirReadFailedCount: 0,
165
+ statFailedCount,
166
+ };
167
+ }
168
+ async function statSidecars(entries, subagentsDir) {
137
169
  const discovered = [];
138
170
  let statFailedCount = 0;
139
171
  let firstSidecarStatFailure = null;
@@ -170,14 +202,7 @@ async function discoverSidecars(subagentsDir) {
170
202
  ...firstSidecarStatFailure,
171
203
  }));
172
204
  }
173
- discovered.sort((a, b) => b.mtimeMs - a.mtimeMs);
174
- const capped = Math.max(0, discovered.length - CLAUDE_SESSION_MAX_SIDECAR_FILES);
175
- return {
176
- sidecars: discovered.slice(0, CLAUDE_SESSION_MAX_SIDECAR_FILES),
177
- sidecarsCapped: capped,
178
- dirReadFailedCount: 0,
179
- statFailedCount,
180
- };
205
+ return { discovered, statFailedCount };
181
206
  }
182
207
  function isMissingPathError(error) {
183
208
  return (error instanceof Error &&