@bli-cockpit/cli 0.2.237 → 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.
- package/dist/adapters/attribution-core-fallbacks.js +15 -11
- package/dist/adapters/attribution-core-score.js +111 -87
- package/dist/adapters/car-state.js +38 -35
- package/dist/adapters/claude-attribution-discovery.js +81 -56
- package/dist/adapters/claude-attribution-score.js +108 -94
- package/dist/adapters/claude-attribution-signals.js +52 -42
- package/dist/adapters/codex-attribution.js +42 -33
- package/dist/adapters/codex-session-signals.js +11 -9
- package/dist/adapters/git-state.js +56 -50
- package/dist/adapters/local-sources.js +52 -43
- package/dist/adapters/raw-evidence-git-diff.js +4 -2
- package/dist/adapters/raw-evidence-sanitize.js +15 -12
- package/dist/adapters/raw-evidence-transcript-file.js +32 -31
- package/dist/autostart-self-heal.js +18 -2
- package/dist/commands/agent-session-report.js +51 -59
- package/dist/commands/agent-setup-observed.js +30 -23
- package/dist/commands/agent-setup.js +96 -45
- package/dist/commands/autostart-heal.js +8 -2
- package/dist/commands/autostart-reading.js +3 -0
- package/dist/commands/backfill-evidence-outcomes.js +9 -6
- package/dist/commands/backfill-issues.js +3 -0
- package/dist/commands/backfill-scan.js +27 -22
- package/dist/commands/backfill-upload.js +69 -60
- package/dist/commands/backfill.js +26 -21
- package/dist/commands/brief-done.js +4 -1
- package/dist/commands/brief-edit.js +71 -46
- package/dist/commands/cal-detail.js +35 -0
- package/dist/commands/cal-overlay.js +73 -30
- package/dist/commands/cal.js +36 -23
- package/dist/commands/clean.js +4 -1
- package/dist/commands/docs-read.js +5 -2
- package/dist/commands/doctor-backfill.js +88 -71
- package/dist/commands/doctor-pipeline.js +59 -46
- package/dist/commands/heartbeat-post.js +116 -0
- package/dist/commands/heartbeat-token-lock.js +46 -33
- package/dist/commands/heartbeat.js +43 -125
- package/dist/commands/install-update.js +17 -15
- package/dist/commands/issue-show.js +27 -28
- package/dist/commands/issue-write.js +70 -23
- package/dist/commands/issue.js +68 -37
- package/dist/commands/jarvis-live-answer.js +99 -0
- package/dist/commands/jarvis-render.js +1 -94
- package/dist/commands/jarvis-trace.js +30 -22
- package/dist/commands/jarvis-turn-report.js +124 -0
- package/dist/commands/jarvis-turn.js +108 -182
- package/dist/commands/jarvis.js +26 -17
- package/dist/commands/local-args-collector-setup.js +82 -84
- package/dist/commands/local-args-collector-work.js +37 -38
- package/dist/commands/local-args-tower-admin.js +155 -142
- package/dist/commands/local-args-tower-cal.js +52 -50
- package/dist/commands/local-args-tower-careers.js +73 -54
- package/dist/commands/local-args-tower-docs-msg.js +55 -45
- package/dist/commands/local-args-tower-github.js +26 -20
- package/dist/commands/local-args-tower-hits.js +32 -20
- package/dist/commands/local-args-tower-manager.js +85 -4
- package/dist/commands/local-args-tower-memory.js +139 -117
- package/dist/commands/local-args-tower-notes.js +346 -0
- package/dist/commands/local-args-tower-pages.js +100 -378
- package/dist/commands/local-args-tower-settings.js +28 -29
- package/dist/commands/local-args-tower-work.js +52 -33
- package/dist/commands/local-args.js +80 -129
- package/dist/commands/local-help-commands-tower-ops.js +15 -4
- package/dist/commands/local-help-commands-tower.js +6 -2
- package/dist/commands/local-help-commands.js +342 -341
- package/dist/commands/local-help.js +87 -83
- package/dist/commands/local-run-guards.js +100 -0
- package/dist/commands/local.js +137 -206
- package/dist/commands/login.js +13 -9
- package/dist/commands/logs.js +55 -38
- package/dist/commands/mail-attachments.js +44 -26
- package/dist/commands/manager-render-home.js +208 -0
- package/dist/commands/manager-render.js +7 -3
- package/dist/commands/manager-requests.js +33 -0
- package/dist/commands/manager.js +39 -5
- package/dist/commands/memory-consolidation.js +18 -9
- package/dist/commands/memory-daemon-probe.js +37 -28
- package/dist/commands/memory-install-config.js +64 -60
- package/dist/commands/memory-install.js +21 -41
- package/dist/commands/memory-requeue.js +26 -19
- package/dist/commands/memory-rules.js +26 -18
- package/dist/commands/memory-search-print.js +26 -0
- package/dist/commands/memory-store.js +14 -30
- package/dist/commands/memory-verb-list.js +49 -48
- package/dist/commands/notes-accounts.js +49 -32
- package/dist/commands/notes-sync-loop.js +115 -89
- package/dist/commands/notes-wisprflow-login.js +44 -23
- package/dist/commands/notes-writes.js +115 -100
- package/dist/commands/notes.js +33 -55
- package/dist/commands/onboard-flows.js +69 -51
- package/dist/commands/onboard.js +11 -9
- package/dist/commands/ops-render-memory.js +10 -7
- package/dist/commands/ops-render.js +33 -30
- package/dist/commands/ops.js +3 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-outside-roots.js +40 -36
- package/dist/commands/session-sync-plan.js +14 -10
- package/dist/commands/session-sync-upload.js +24 -22
- package/dist/commands/session-sync.js +19 -8
- package/dist/commands/slack.js +25 -16
- package/dist/commands/sources-check.js +13 -7
- package/dist/commands/sources-discovery.js +25 -17
- package/dist/commands/sources-verdict.js +19 -18
- package/dist/commands/speed.js +23 -19
- package/dist/commands/start.js +20 -16
- package/dist/commands/sync-doctor.js +52 -48
- package/dist/commands/sync.js +22 -16
- package/dist/commands/usage-telemetry-toml.js +71 -45
- package/dist/commands/usage.js +91 -67
- package/dist/commands/workbook-render.js +10 -7
- package/dist/evidence-reconcile-client.js +75 -64
- package/dist/evidence-redelivery-plan.js +37 -34
- package/dist/evidence-redelivery.js +32 -29
- package/dist/evidence-transcripts-client.js +20 -20
- package/dist/evidence-upload-object.js +86 -89
- package/dist/evidence-upload-plan.js +94 -83
- package/dist/evidence-upload-terminal.js +45 -50
- package/dist/image-description-session.js +156 -159
- package/dist/local-state-pairing-code.js +39 -31
- package/dist/local-state-status.js +9 -2
- package/dist/onboarding-roots.js +5 -8
- package/dist/process-runner.js +8 -8
- package/dist/raw-evidence-gc.js +33 -38
- package/dist/raw-evidence-stuck-summary.js +7 -14
- package/dist/repo-identity.js +26 -23
- package/dist/scheduled-self-update.js +11 -15
- package/dist/second-install.js +3 -0
- package/dist/slim/codex-rules.js +23 -36
- package/dist/sync-lock.js +5 -15
- package/dist/tower-client.js +36 -53
- package/dist/tower-stream.js +7 -24
- package/dist/upload-agent-artifacts.js +58 -64
- package/dist/upload-envelope-build.js +60 -59
- package/dist/upload-evidence-delivery-reconcile.js +58 -56
- package/dist/upload-evidence-delivery-summary.js +32 -34
- package/dist/upload-ingest-receipt.js +20 -23
- package/dist/upload-session-reports-queue.js +4 -1
- package/dist/upload-session-reports-wire.js +64 -68
- package/dist/upload-sync.js +171 -157
- 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
|
-
|
|
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
|
-
|
|
27
|
-
signals
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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:
|
|
105
|
+
outcome: transcriptFallback,
|
|
99
106
|
signals,
|
|
100
107
|
collectionRoots,
|
|
101
108
|
approvedRecordedPaths,
|
|
102
109
|
});
|
|
103
110
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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 &&
|