@bli-cockpit/cli 0.2.237 → 0.2.239
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-direct.js +119 -0
- package/dist/commands/cal-overlay.js +73 -30
- package/dist/commands/cal.js +62 -25
- package/dist/commands/clean.js +4 -1
- package/dist/commands/docs-read.js +5 -2
- package/dist/commands/docs.js +38 -6
- 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-versions.js +93 -0
- package/dist/commands/issue-write.js +70 -23
- package/dist/commands/issue.js +73 -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 +69 -54
- package/dist/commands/local-args-tower-careers.js +73 -54
- package/dist/commands/local-args-tower-docs-msg.js +79 -51
- 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-msg.js +24 -6
- 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-setup.js +11 -4
- package/dist/commands/local-args-tower-work.js +74 -37
- 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 +20 -7
- 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/msg-read-line.js +32 -0
- package/dist/commands/msg.js +45 -3
- 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/setup.js +13 -0
- 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 +94 -68
- 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/spool/local-spool-parse.js +13 -12
- 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 +4 -4
|
@@ -19,95 +19,18 @@ import { CLAUDE_SESSION_MAX_FILE_BYTES, } from "./claude-attribution-types.js";
|
|
|
19
19
|
* work and must not inflate this session's numbers.
|
|
20
20
|
*/
|
|
21
21
|
export async function attributeOneSession(session, worktrees, collectionRoots) {
|
|
22
|
-
const fileName = path.basename(session.mainFile);
|
|
23
22
|
const factsAcc = newSessionFactsAccumulator("claude_code");
|
|
24
|
-
const base =
|
|
25
|
-
file_path: session.mainFile,
|
|
26
|
-
file_name: fileName,
|
|
27
|
-
claude_session_id: sessionIdFromFileName(fileName) ?? shortHash(session.mainFile),
|
|
28
|
-
cwd_basename: null,
|
|
29
|
-
cwd_hash: null,
|
|
30
|
-
session_file_mtime: new Date(session.mainMtimeMs).toISOString(),
|
|
31
|
-
session_file_mtime_ms: session.mainMtimeMs,
|
|
32
|
-
session_recency_ms: session.recencyMs,
|
|
33
|
-
byte_size: session.mainByteSize,
|
|
34
|
-
content_hash_sha256: null,
|
|
35
|
-
main_file_oversized: false,
|
|
36
|
-
oversized_lines_skipped: 0,
|
|
37
|
-
sidecars_capped: session.sidecarsCapped,
|
|
38
|
-
sidecar_files: [],
|
|
39
|
-
session_facts: null,
|
|
40
|
-
};
|
|
23
|
+
const base = createSessionBase(session);
|
|
41
24
|
if (session.mainByteSize === 0) {
|
|
42
25
|
return skippedResult(base, "empty_file");
|
|
43
26
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
streamed = await streamMainSignals(session.mainFile, factsAcc);
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
54
|
-
// A read race on one oversized main must not abort the whole scan; honor
|
|
55
|
-
// the per-file skip contract. Per session, so worth a line each time:
|
|
56
|
-
// this is a whole session nobody will ever coach on (BLI-3238).
|
|
57
|
-
console.error("[claude-attribution] oversized session could not be streamed, skipping it", JSON.stringify({
|
|
58
|
-
reason: "file_read_failed",
|
|
59
|
-
stage: "stream_oversized_main",
|
|
60
|
-
byte_size: session.mainByteSize,
|
|
61
|
-
...describeError(error),
|
|
62
|
-
}));
|
|
63
|
-
return skippedResult(base, "file_read_failed");
|
|
64
|
-
}
|
|
65
|
-
signals = streamed.signals;
|
|
66
|
-
observeSessionFactsFile(factsAcc, {
|
|
67
|
-
bytes: streamed.byteSize,
|
|
68
|
-
lines: streamed.signals.line_count,
|
|
69
|
-
});
|
|
70
|
-
base.content_hash_sha256 = streamed.contentHash;
|
|
71
|
-
base.byte_size = streamed.byteSize;
|
|
72
|
-
base.main_file_oversized = true;
|
|
73
|
-
base.oversized_lines_skipped = streamed.oversizedLinesSkipped;
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
let raw;
|
|
77
|
-
try {
|
|
78
|
-
raw = await fs.readFile(session.mainFile);
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
console.error("[claude-attribution] session file unreadable, skipping it", JSON.stringify({
|
|
82
|
-
reason: "file_read_failed",
|
|
83
|
-
stage: "read_main",
|
|
84
|
-
byte_size: session.mainByteSize,
|
|
85
|
-
...describeError(error),
|
|
86
|
-
}));
|
|
87
|
-
return skippedResult(base, "file_read_failed");
|
|
88
|
-
}
|
|
89
|
-
const content = raw.toString("utf8");
|
|
90
|
-
base.content_hash_sha256 = sha256(raw);
|
|
91
|
-
base.byte_size = raw.byteLength;
|
|
92
|
-
if (content.trim() === "") {
|
|
93
|
-
return skippedResult(base, "empty_file");
|
|
94
|
-
}
|
|
95
|
-
signals = extractClaudeSessionSignals(content, factsAcc);
|
|
96
|
-
observeSessionFactsFile(factsAcc, {
|
|
97
|
-
bytes: raw.byteLength,
|
|
98
|
-
lines: signals.line_count,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
const metaSessionId = sanitizeSessionId(signals.session_ids[0]);
|
|
102
|
-
if (metaSessionId) {
|
|
103
|
-
base.claude_session_id = metaSessionId;
|
|
104
|
-
}
|
|
105
|
-
const primaryCwd = signals.cwds[0] ?? null;
|
|
106
|
-
if (primaryCwd) {
|
|
107
|
-
base.local_cwd = primaryCwd;
|
|
108
|
-
base.cwd_basename = (path.win32.isAbsolute(primaryCwd) ? path.win32 : path.posix).basename(primaryCwd) || null;
|
|
109
|
-
base.cwd_hash = shortHash(primaryCwd);
|
|
110
|
-
}
|
|
27
|
+
const read = session.mainByteSize > CLAUDE_SESSION_MAX_FILE_BYTES
|
|
28
|
+
? await streamOversizedMain(session, base, factsAcc)
|
|
29
|
+
: await readMainSignals(session, base, factsAcc);
|
|
30
|
+
if (typeof read === "string")
|
|
31
|
+
return skippedResult(base, read);
|
|
32
|
+
const signals = read;
|
|
33
|
+
applySessionMetadata(base, signals);
|
|
111
34
|
if (signals.line_count === 0 || signals.parse_error_count === signals.line_count) {
|
|
112
35
|
return {
|
|
113
36
|
...base,
|
|
@@ -131,15 +54,7 @@ export async function attributeOneSession(session, worktrees, collectionRoots) {
|
|
|
131
54
|
extraSignals.push("main_file_oversized");
|
|
132
55
|
if (isSchemaDriftSuspected(signals))
|
|
133
56
|
extraSignals.push("schema_drift_suspected");
|
|
134
|
-
const sidecarFiles =
|
|
135
|
-
? await collectSidecarDiagnostics(session.sidecars, outcome.worktree, factsAcc)
|
|
136
|
-
: session.sidecars.map((sidecar) => ({
|
|
137
|
-
local_path: sidecar.local_path,
|
|
138
|
-
file_name: sidecar.file_name,
|
|
139
|
-
byte_size: sidecar.byteSize,
|
|
140
|
-
content_hash_sha256: null,
|
|
141
|
-
skipped_reason: null,
|
|
142
|
-
}));
|
|
57
|
+
const sidecarFiles = await collectSessionSidecars(session, outcome, factsAcc);
|
|
143
58
|
return {
|
|
144
59
|
...base,
|
|
145
60
|
session_facts: finalizeSessionFacts(factsAcc),
|
|
@@ -152,6 +67,105 @@ export async function attributeOneSession(session, worktrees, collectionRoots) {
|
|
|
152
67
|
sidecar_files: sidecarFiles,
|
|
153
68
|
};
|
|
154
69
|
}
|
|
70
|
+
function createSessionBase(session) {
|
|
71
|
+
const fileName = path.basename(session.mainFile);
|
|
72
|
+
return {
|
|
73
|
+
file_path: session.mainFile,
|
|
74
|
+
file_name: fileName,
|
|
75
|
+
claude_session_id: sessionIdFromFileName(fileName) ?? shortHash(session.mainFile),
|
|
76
|
+
cwd_basename: null,
|
|
77
|
+
cwd_hash: null,
|
|
78
|
+
session_file_mtime: new Date(session.mainMtimeMs).toISOString(),
|
|
79
|
+
session_file_mtime_ms: session.mainMtimeMs,
|
|
80
|
+
session_recency_ms: session.recencyMs,
|
|
81
|
+
byte_size: session.mainByteSize,
|
|
82
|
+
content_hash_sha256: null,
|
|
83
|
+
main_file_oversized: false,
|
|
84
|
+
oversized_lines_skipped: 0,
|
|
85
|
+
sidecars_capped: session.sidecarsCapped,
|
|
86
|
+
sidecar_files: [],
|
|
87
|
+
session_facts: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function streamOversizedMain(session, base, factsAcc) {
|
|
91
|
+
// D7: stream the oversized main so metadata signals remain available. The
|
|
92
|
+
// bytes themselves are not uploaded until the collection ceiling changes.
|
|
93
|
+
let streamed;
|
|
94
|
+
try {
|
|
95
|
+
streamed = await streamMainSignals(session.mainFile, factsAcc);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
// A read race on one oversized main must not abort the whole scan; honor
|
|
99
|
+
// the per-file skip contract. Per session, so worth a line each time:
|
|
100
|
+
// this is a whole session nobody will ever coach on (BLI-3238).
|
|
101
|
+
console.error("[claude-attribution] oversized session could not be streamed, skipping it", JSON.stringify({
|
|
102
|
+
reason: "file_read_failed",
|
|
103
|
+
stage: "stream_oversized_main",
|
|
104
|
+
byte_size: session.mainByteSize,
|
|
105
|
+
...describeError(error),
|
|
106
|
+
}));
|
|
107
|
+
return "file_read_failed";
|
|
108
|
+
}
|
|
109
|
+
observeSessionFactsFile(factsAcc, {
|
|
110
|
+
bytes: streamed.byteSize,
|
|
111
|
+
lines: streamed.signals.line_count,
|
|
112
|
+
});
|
|
113
|
+
base.content_hash_sha256 = streamed.contentHash;
|
|
114
|
+
base.byte_size = streamed.byteSize;
|
|
115
|
+
base.main_file_oversized = true;
|
|
116
|
+
base.oversized_lines_skipped = streamed.oversizedLinesSkipped;
|
|
117
|
+
return streamed.signals;
|
|
118
|
+
}
|
|
119
|
+
async function readMainSignals(session, base, factsAcc) {
|
|
120
|
+
let raw;
|
|
121
|
+
try {
|
|
122
|
+
raw = await fs.readFile(session.mainFile);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
console.error("[claude-attribution] session file unreadable, skipping it", JSON.stringify({
|
|
126
|
+
reason: "file_read_failed",
|
|
127
|
+
stage: "read_main",
|
|
128
|
+
byte_size: session.mainByteSize,
|
|
129
|
+
...describeError(error),
|
|
130
|
+
}));
|
|
131
|
+
return "file_read_failed";
|
|
132
|
+
}
|
|
133
|
+
const content = raw.toString("utf8");
|
|
134
|
+
base.content_hash_sha256 = sha256(raw);
|
|
135
|
+
base.byte_size = raw.byteLength;
|
|
136
|
+
if (content.trim() === "") {
|
|
137
|
+
return "empty_file";
|
|
138
|
+
}
|
|
139
|
+
const signals = extractClaudeSessionSignals(content, factsAcc);
|
|
140
|
+
observeSessionFactsFile(factsAcc, {
|
|
141
|
+
bytes: raw.byteLength,
|
|
142
|
+
lines: signals.line_count,
|
|
143
|
+
});
|
|
144
|
+
return signals;
|
|
145
|
+
}
|
|
146
|
+
function applySessionMetadata(base, signals) {
|
|
147
|
+
const metaSessionId = sanitizeSessionId(signals.session_ids[0]);
|
|
148
|
+
if (metaSessionId) {
|
|
149
|
+
base.claude_session_id = metaSessionId;
|
|
150
|
+
}
|
|
151
|
+
const primaryCwd = signals.cwds[0] ?? null;
|
|
152
|
+
if (primaryCwd) {
|
|
153
|
+
base.local_cwd = primaryCwd;
|
|
154
|
+
base.cwd_basename = (path.win32.isAbsolute(primaryCwd) ? path.win32 : path.posix).basename(primaryCwd) || null;
|
|
155
|
+
base.cwd_hash = shortHash(primaryCwd);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function collectSessionSidecars(session, outcome, factsAcc) {
|
|
159
|
+
return isRawEvidenceUploadableAttributionState(outcome.state, outcome.worktree !== null) && outcome.worktree
|
|
160
|
+
? await collectSidecarDiagnostics(session.sidecars, outcome.worktree, factsAcc)
|
|
161
|
+
: session.sidecars.map((sidecar) => ({
|
|
162
|
+
local_path: sidecar.local_path,
|
|
163
|
+
file_name: sidecar.file_name,
|
|
164
|
+
byte_size: sidecar.byteSize,
|
|
165
|
+
content_hash_sha256: null,
|
|
166
|
+
skipped_reason: null,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
155
169
|
/**
|
|
156
170
|
* Validates and content-addresses an attributed session's sidecars. Each
|
|
157
171
|
* sidecar's envelope cwd must resolve within the attributed worktree root, or
|
|
@@ -75,20 +75,21 @@ function createSignalAccumulator(facts) {
|
|
|
75
75
|
contentRecordsWithEnvelope += 1;
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
|
-
finalize()
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
78
|
+
finalize: () => finalizeSignals(sessionIds, cwds, branches, repositoryUrls, lineCount, parseErrorCount, contentRecordCount, contentRecordsWithEnvelope),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function finalizeSignals(sessionIds, cwds, branches, repositoryUrls, lineCount, parseErrorCount, contentRecordCount, contentRecordsWithEnvelope) {
|
|
82
|
+
return {
|
|
83
|
+
session_ids: [...sessionIds],
|
|
84
|
+
cwds: [...cwds],
|
|
85
|
+
branches: [...branches],
|
|
86
|
+
repository_urls: [...repositoryUrls],
|
|
87
|
+
line_count: lineCount,
|
|
88
|
+
parse_error_count: parseErrorCount,
|
|
89
|
+
content_record_count: contentRecordCount,
|
|
90
|
+
envelope_field_hit_rate: contentRecordCount === 0
|
|
91
|
+
? 1
|
|
92
|
+
: contentRecordsWithEnvelope / contentRecordCount,
|
|
92
93
|
};
|
|
93
94
|
}
|
|
94
95
|
export function isSchemaDriftSuspected(signals) {
|
|
@@ -106,8 +107,40 @@ export function isSchemaDriftSuspected(signals) {
|
|
|
106
107
|
export async function streamMainSignals(filePath, facts) {
|
|
107
108
|
const accumulator = createSignalAccumulator(facts);
|
|
108
109
|
const hash = crypto.createHash("sha256");
|
|
109
|
-
let oversizedLinesSkipped = 0;
|
|
110
110
|
let byteSize = 0;
|
|
111
|
+
const lines = bufferSignalLines(accumulator);
|
|
112
|
+
const { consumeText } = lines;
|
|
113
|
+
// StringDecoder buffers an incomplete multibyte char across chunk boundaries
|
|
114
|
+
// so a UTF-8 character split by the read boundary never decodes to a fragment
|
|
115
|
+
// that could weaken the secret guard or corrupt a cwd signal. The content
|
|
116
|
+
// hash is over raw bytes (below) and is unaffected either way.
|
|
117
|
+
const decoder = new StringDecoder("utf8");
|
|
118
|
+
await new Promise((resolve, reject) => {
|
|
119
|
+
const stream = createReadStream(filePath);
|
|
120
|
+
stream.on("data", (chunk) => {
|
|
121
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
122
|
+
hash.update(buffer);
|
|
123
|
+
byteSize += buffer.byteLength;
|
|
124
|
+
consumeText(decoder.write(buffer));
|
|
125
|
+
});
|
|
126
|
+
stream.on("end", () => {
|
|
127
|
+
const tail = decoder.end();
|
|
128
|
+
if (tail)
|
|
129
|
+
consumeText(tail);
|
|
130
|
+
lines.finish();
|
|
131
|
+
resolve();
|
|
132
|
+
});
|
|
133
|
+
stream.on("error", reject);
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
signals: accumulator.finalize(),
|
|
137
|
+
oversizedLinesSkipped: lines.skippedCount(),
|
|
138
|
+
contentHash: hash.digest("hex"),
|
|
139
|
+
byteSize,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function bufferSignalLines(accumulator) {
|
|
143
|
+
let oversizedLinesSkipped = 0;
|
|
111
144
|
let pending = "";
|
|
112
145
|
let pendingTruncated = false;
|
|
113
146
|
const flushCompletedLine = (lineText) => {
|
|
@@ -123,11 +156,6 @@ export async function streamMainSignals(filePath, facts) {
|
|
|
123
156
|
pending = "";
|
|
124
157
|
accumulator.processLine(full);
|
|
125
158
|
};
|
|
126
|
-
// StringDecoder buffers an incomplete multibyte char across chunk boundaries
|
|
127
|
-
// so a UTF-8 character split by the read boundary never decodes to a fragment
|
|
128
|
-
// that could weaken the secret guard or corrupt a cwd signal. The content
|
|
129
|
-
// hash is over raw bytes (below) and is unaffected either way.
|
|
130
|
-
const decoder = new StringDecoder("utf8");
|
|
131
159
|
const consumeText = (text) => {
|
|
132
160
|
let remaining = text;
|
|
133
161
|
let newlineIndex = remaining.indexOf("\n");
|
|
@@ -146,29 +174,11 @@ export async function streamMainSignals(filePath, facts) {
|
|
|
146
174
|
pending += remaining;
|
|
147
175
|
}
|
|
148
176
|
};
|
|
149
|
-
await new Promise((resolve, reject) => {
|
|
150
|
-
const stream = createReadStream(filePath);
|
|
151
|
-
stream.on("data", (chunk) => {
|
|
152
|
-
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
153
|
-
hash.update(buffer);
|
|
154
|
-
byteSize += buffer.byteLength;
|
|
155
|
-
consumeText(decoder.write(buffer));
|
|
156
|
-
});
|
|
157
|
-
stream.on("end", () => {
|
|
158
|
-
const tail = decoder.end();
|
|
159
|
-
if (tail)
|
|
160
|
-
consumeText(tail);
|
|
161
|
-
if (pending.length > 0 || pendingTruncated)
|
|
162
|
-
flushCompletedLine("");
|
|
163
|
-
resolve();
|
|
164
|
-
});
|
|
165
|
-
stream.on("error", reject);
|
|
166
|
-
});
|
|
167
177
|
return {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
178
|
+
consumeText, flushCompletedLine,
|
|
179
|
+
finish: () => { if (pending.length > 0 || pendingTruncated)
|
|
180
|
+
flushCompletedLine(""); },
|
|
181
|
+
skippedCount: () => oversizedLinesSkipped
|
|
172
182
|
};
|
|
173
183
|
}
|
|
174
184
|
function normalizePrRepository(repository) {
|
|
@@ -120,25 +120,28 @@ async function discoverCodexJsonlFiles(dir, cutoffMs) {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
+
logDiscoveryFailures(directoryReadFailedCount, statFailedCount, out.length, firstDirectoryFailure, firstStatFailure);
|
|
124
|
+
out.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
125
|
+
return {
|
|
126
|
+
files: out,
|
|
127
|
+
directoryReadFailedCount,
|
|
128
|
+
statFailedCount,
|
|
129
|
+
secretPathSkippedCount,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function logDiscoveryFailures(directoryReadFailedCount, statFailedCount, foundFileCount, firstDirectoryFailure, firstStatFailure) {
|
|
123
133
|
if (directoryReadFailedCount > 0 || statFailedCount > 0) {
|
|
124
134
|
console.error("[codex-attribution] Codex sessions were invisible to the walk", JSON.stringify({
|
|
125
135
|
reason: "session_discovery_incomplete",
|
|
126
136
|
directory_read_failed_count: directoryReadFailedCount,
|
|
127
137
|
stat_failed_count: statFailedCount,
|
|
128
|
-
found_file_count:
|
|
138
|
+
found_file_count: foundFileCount,
|
|
129
139
|
...(firstDirectoryFailure
|
|
130
140
|
? { first_directory_failure: firstDirectoryFailure }
|
|
131
141
|
: {}),
|
|
132
142
|
...(firstStatFailure ? { first_stat_failure: firstStatFailure } : {}),
|
|
133
143
|
}));
|
|
134
144
|
}
|
|
135
|
-
out.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
136
|
-
return {
|
|
137
|
-
files: out,
|
|
138
|
-
directoryReadFailedCount,
|
|
139
|
-
statFailedCount,
|
|
140
|
-
secretPathSkippedCount,
|
|
141
|
-
};
|
|
142
145
|
}
|
|
143
146
|
function isMissingPathError(error) {
|
|
144
147
|
return (error instanceof Error &&
|
|
@@ -162,20 +165,7 @@ function dedupePaths(values) {
|
|
|
162
165
|
return out;
|
|
163
166
|
}
|
|
164
167
|
async function attributeOneSession(file, worktrees, collectionRoots) {
|
|
165
|
-
const
|
|
166
|
-
const base = {
|
|
167
|
-
file_path: file.file,
|
|
168
|
-
file_name: fileName,
|
|
169
|
-
codex_session_id: sessionIdFromFileName(fileName) ?? shortHash(file.file),
|
|
170
|
-
session_id_source: sessionIdFromFileName(fileName) ? "file_name" : "path_hash",
|
|
171
|
-
cwd_basename: null,
|
|
172
|
-
cwd_hash: null,
|
|
173
|
-
session_file_mtime: new Date(file.mtimeMs).toISOString(),
|
|
174
|
-
session_file_mtime_ms: file.mtimeMs,
|
|
175
|
-
byte_size: file.byteSize,
|
|
176
|
-
content_hash_sha256: null,
|
|
177
|
-
session_facts: null,
|
|
178
|
-
};
|
|
168
|
+
const base = createSessionBase(file);
|
|
179
169
|
let read;
|
|
180
170
|
try {
|
|
181
171
|
read = await readCodexMetadataSignals(file.file);
|
|
@@ -195,17 +185,7 @@ async function attributeOneSession(file, worktrees, collectionRoots) {
|
|
|
195
185
|
base.byte_size = read.byteSize;
|
|
196
186
|
base.session_facts = read.facts;
|
|
197
187
|
const signals = read.signals;
|
|
198
|
-
|
|
199
|
-
if (metaSessionId) {
|
|
200
|
-
base.codex_session_id = metaSessionId;
|
|
201
|
-
base.session_id_source = "session_meta";
|
|
202
|
-
}
|
|
203
|
-
const primaryCwd = signals.cwds[0] ?? null;
|
|
204
|
-
if (primaryCwd) {
|
|
205
|
-
base.local_cwd = primaryCwd;
|
|
206
|
-
base.cwd_basename = (path.win32.isAbsolute(primaryCwd) ? path.win32 : path.posix).basename(primaryCwd) || null;
|
|
207
|
-
base.cwd_hash = shortHash(primaryCwd);
|
|
208
|
-
}
|
|
188
|
+
applySessionMetadata(base, signals);
|
|
209
189
|
if (signals.line_count === 0 || signals.parse_error_count === signals.line_count) {
|
|
210
190
|
return {
|
|
211
191
|
...base,
|
|
@@ -226,6 +206,35 @@ async function attributeOneSession(file, worktrees, collectionRoots) {
|
|
|
226
206
|
}, worktrees, { collectionRoots, pathExists: existsSync });
|
|
227
207
|
return { ...base, ...outcome };
|
|
228
208
|
}
|
|
209
|
+
function createSessionBase(file) {
|
|
210
|
+
const fileName = path.basename(file.file);
|
|
211
|
+
return {
|
|
212
|
+
file_path: file.file,
|
|
213
|
+
file_name: fileName,
|
|
214
|
+
codex_session_id: sessionIdFromFileName(fileName) ?? shortHash(file.file),
|
|
215
|
+
session_id_source: sessionIdFromFileName(fileName) ? "file_name" : "path_hash",
|
|
216
|
+
cwd_basename: null,
|
|
217
|
+
cwd_hash: null,
|
|
218
|
+
session_file_mtime: new Date(file.mtimeMs).toISOString(),
|
|
219
|
+
session_file_mtime_ms: file.mtimeMs,
|
|
220
|
+
byte_size: file.byteSize,
|
|
221
|
+
content_hash_sha256: null,
|
|
222
|
+
session_facts: null,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function applySessionMetadata(base, signals) {
|
|
226
|
+
const metaSessionId = sanitizeSessionId(signals.session_ids[0]);
|
|
227
|
+
if (metaSessionId) {
|
|
228
|
+
base.codex_session_id = metaSessionId;
|
|
229
|
+
base.session_id_source = "session_meta";
|
|
230
|
+
}
|
|
231
|
+
const primaryCwd = signals.cwds[0] ?? null;
|
|
232
|
+
if (primaryCwd) {
|
|
233
|
+
base.local_cwd = primaryCwd;
|
|
234
|
+
base.cwd_basename = (path.win32.isAbsolute(primaryCwd) ? path.win32 : path.posix).basename(primaryCwd) || null;
|
|
235
|
+
base.cwd_hash = shortHash(primaryCwd);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
229
238
|
function skippedResult(base, reason) {
|
|
230
239
|
return {
|
|
231
240
|
...base,
|
|
@@ -142,15 +142,17 @@ function absorbCodexSessionLine(state, line) {
|
|
|
142
142
|
}
|
|
143
143
|
else if (type === "turn_context") {
|
|
144
144
|
addString(state.cwds, payloadRecord["cwd"]);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
145
|
+
absorbWorkspaceRoots(state, payloadRecord["workspace_roots"]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function absorbWorkspaceRoots(state, roots) {
|
|
149
|
+
if (Array.isArray(roots)) {
|
|
150
|
+
for (const root of roots) {
|
|
151
|
+
if (typeof root === "string") {
|
|
152
|
+
addString(state.workspaceRoots, root);
|
|
153
|
+
}
|
|
154
|
+
else if (root && typeof root === "object") {
|
|
155
|
+
addString(state.workspaceRoots, root["path"]);
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
158
|
}
|
|
@@ -6,56 +6,7 @@ const execFileAsync = promisify(execFile);
|
|
|
6
6
|
export async function collectGitState(context, runner = defaultGitCommandRunner()) {
|
|
7
7
|
const startedAt = context.now.toISOString();
|
|
8
8
|
try {
|
|
9
|
-
|
|
10
|
-
context.repoRoot;
|
|
11
|
-
const branch = (await runner.runGit(["rev-parse", "--abbrev-ref", "HEAD"], repoRoot, 2_000)).trim() ||
|
|
12
|
-
context.branch;
|
|
13
|
-
const porcelain = await runner.runGit(["status", "--porcelain=v1"], repoRoot, 2_000);
|
|
14
|
-
const unstagedNumstat = await runner.runGit(["diff", "--numstat"], repoRoot, 2_000);
|
|
15
|
-
const stagedNumstat = await runner.runGit(["diff", "--cached", "--numstat"], repoRoot, 2_000);
|
|
16
|
-
const fileStats = [...parseNumstat(unstagedNumstat), ...parseNumstat(stagedNumstat)];
|
|
17
|
-
const addedLines = fileStats.reduce((sum, file) => sum + file.added_lines, 0);
|
|
18
|
-
const deletedLines = fileStats.reduce((sum, file) => sum + file.deleted_lines, 0);
|
|
19
|
-
const changedPaths = [
|
|
20
|
-
...new Set([
|
|
21
|
-
...parsePorcelainPaths(porcelain),
|
|
22
|
-
...fileStats.map((file) => file.path),
|
|
23
|
-
]),
|
|
24
|
-
].sort();
|
|
25
|
-
const ticketIdFromBranch = parseTicketIdFromBranch(branch);
|
|
26
|
-
const ticketBindingCandidates = ticketIdFromBranch
|
|
27
|
-
? [
|
|
28
|
-
{
|
|
29
|
-
ticket_id: ticketIdFromBranch,
|
|
30
|
-
binding_source: "branch_name",
|
|
31
|
-
confidence: 0.85,
|
|
32
|
-
evidence_labels: [`branch:${branch}`],
|
|
33
|
-
},
|
|
34
|
-
]
|
|
35
|
-
: [];
|
|
36
|
-
const facts = {
|
|
37
|
-
repo_root: repoRoot,
|
|
38
|
-
branch,
|
|
39
|
-
changed_file_count: changedPaths.length,
|
|
40
|
-
added_lines: addedLines,
|
|
41
|
-
deleted_lines: deletedLines,
|
|
42
|
-
changed_paths: changedPaths,
|
|
43
|
-
ticket_id_from_branch: ticketIdFromBranch,
|
|
44
|
-
};
|
|
45
|
-
const scan = makeGitScan({
|
|
46
|
-
context,
|
|
47
|
-
startedAt,
|
|
48
|
-
status: "ok",
|
|
49
|
-
branch,
|
|
50
|
-
facts,
|
|
51
|
-
ticketBindingCandidates,
|
|
52
|
-
diagnosticLabels: [
|
|
53
|
-
`changed_files:${facts.changed_file_count}`,
|
|
54
|
-
`added_lines:${facts.added_lines}`,
|
|
55
|
-
`deleted_lines:${facts.deleted_lines}`,
|
|
56
|
-
],
|
|
57
|
-
});
|
|
58
|
-
return { scan, facts };
|
|
9
|
+
return await collectAvailableGitState(context, runner, startedAt);
|
|
59
10
|
}
|
|
60
11
|
catch (error) {
|
|
61
12
|
const scan = makeGitScan({
|
|
@@ -72,6 +23,61 @@ export async function collectGitState(context, runner = defaultGitCommandRunner(
|
|
|
72
23
|
return { scan, facts: { git_unavailable: true } };
|
|
73
24
|
}
|
|
74
25
|
}
|
|
26
|
+
async function collectAvailableGitState(context, runner, startedAt) {
|
|
27
|
+
const repoRoot = (await runner.runGit(["rev-parse", "--show-toplevel"], context.repoRoot, 2_000)).trim() ||
|
|
28
|
+
context.repoRoot;
|
|
29
|
+
const branch = (await runner.runGit(["rev-parse", "--abbrev-ref", "HEAD"], repoRoot, 2_000)).trim() ||
|
|
30
|
+
context.branch;
|
|
31
|
+
const porcelain = await runner.runGit(["status", "--porcelain=v1"], repoRoot, 2_000);
|
|
32
|
+
const unstagedNumstat = await runner.runGit(["diff", "--numstat"], repoRoot, 2_000);
|
|
33
|
+
const stagedNumstat = await runner.runGit(["diff", "--cached", "--numstat"], repoRoot, 2_000);
|
|
34
|
+
const fileStats = [...parseNumstat(unstagedNumstat), ...parseNumstat(stagedNumstat)];
|
|
35
|
+
const addedLines = fileStats.reduce((sum, file) => sum + file.added_lines, 0);
|
|
36
|
+
const deletedLines = fileStats.reduce((sum, file) => sum + file.deleted_lines, 0);
|
|
37
|
+
const changedPaths = [
|
|
38
|
+
...new Set([
|
|
39
|
+
...parsePorcelainPaths(porcelain),
|
|
40
|
+
...fileStats.map((file) => file.path),
|
|
41
|
+
]),
|
|
42
|
+
].sort();
|
|
43
|
+
const ticketIdFromBranch = parseTicketIdFromBranch(branch);
|
|
44
|
+
const ticketBindingCandidates = bindBranchTicket(ticketIdFromBranch, branch);
|
|
45
|
+
const facts = {
|
|
46
|
+
repo_root: repoRoot,
|
|
47
|
+
branch,
|
|
48
|
+
changed_file_count: changedPaths.length,
|
|
49
|
+
added_lines: addedLines,
|
|
50
|
+
deleted_lines: deletedLines,
|
|
51
|
+
changed_paths: changedPaths,
|
|
52
|
+
ticket_id_from_branch: ticketIdFromBranch,
|
|
53
|
+
};
|
|
54
|
+
const scan = makeGitScan({
|
|
55
|
+
context,
|
|
56
|
+
startedAt,
|
|
57
|
+
status: "ok",
|
|
58
|
+
branch,
|
|
59
|
+
facts,
|
|
60
|
+
ticketBindingCandidates,
|
|
61
|
+
diagnosticLabels: [
|
|
62
|
+
`changed_files:${facts.changed_file_count}`,
|
|
63
|
+
`added_lines:${facts.added_lines}`,
|
|
64
|
+
`deleted_lines:${facts.deleted_lines}`,
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
return { scan, facts };
|
|
68
|
+
}
|
|
69
|
+
function bindBranchTicket(ticketIdFromBranch, branch) {
|
|
70
|
+
return ticketIdFromBranch
|
|
71
|
+
? [
|
|
72
|
+
{
|
|
73
|
+
ticket_id: ticketIdFromBranch,
|
|
74
|
+
binding_source: "branch_name",
|
|
75
|
+
confidence: 0.85,
|
|
76
|
+
evidence_labels: [`branch:${branch}`],
|
|
77
|
+
},
|
|
78
|
+
]
|
|
79
|
+
: [];
|
|
80
|
+
}
|
|
75
81
|
export function parseNumstat(raw) {
|
|
76
82
|
return raw
|
|
77
83
|
.split("\n")
|