@bli-cockpit/cli 0.1.9 → 0.1.11
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/README.md +7 -3
- package/dist/adapters/agent-image-evidence.js +97 -0
- package/dist/adapters/agent-image-records.js +147 -0
- package/dist/adapters/agent-image-validation.js +88 -0
- package/dist/adapters/raw-evidence.js +145 -16
- package/dist/autostart.js +43 -0
- package/dist/commands/local-args.js +383 -0
- package/dist/commands/local.js +7 -877
- package/dist/commands/session-sync.js +504 -0
- package/dist/evidence-upload-client.js +7 -0
- package/dist/local-state.js +1 -1
- package/dist/upload.js +152 -3
- package/package.json +2 -2
package/dist/commands/local.js
CHANGED
|
@@ -2,16 +2,14 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createCollectorServer } from "../server.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { scanAndAttributeCodexSessions
|
|
9
|
-
import { scanAndAttributeClaudeSessions
|
|
10
|
-
import { RAW_EVIDENCE_DEFAULT_BYTE_BUDGET, RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET, } from "../adapters/raw-evidence.js";
|
|
11
|
-
import { CLAUDE_CURSOR_FILENAME, countStaleSessions, emptyRawEvidenceCursorState, readRawEvidenceCursor, recordSessionObservation, writeRawEvidenceCursor, } from "../cursors/raw-evidence-cursor.js";
|
|
12
|
-
import { readLocalCollectorConfig } from "../local-state.js";
|
|
5
|
+
import { parseLocalArgs, normalizeUrl } from "./local-args.js";
|
|
6
|
+
import { autostartStatus, installAutostartAgent, uninstallAutostartAgent } from "../autostart.js";
|
|
7
|
+
import { getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext } from "../local-state.js";
|
|
8
|
+
import { scanAndAttributeCodexSessions } from "../adapters/codex-attribution.js";
|
|
9
|
+
import { scanAndAttributeClaudeSessions } from "../adapters/claude-attribution.js";
|
|
13
10
|
import { acquireSyncLock } from "../sync-lock.js";
|
|
14
|
-
import { discoverGitWorktrees
|
|
11
|
+
import { discoverGitWorktrees } from "../repo-identity.js";
|
|
12
|
+
import { runAttributedWorktreeSync } from "./session-sync.js";
|
|
15
13
|
export const rootCommandNames = new Set([
|
|
16
14
|
"onboard",
|
|
17
15
|
"install",
|
|
@@ -195,314 +193,6 @@ function isLocalHelpRequest(argv) {
|
|
|
195
193
|
return false;
|
|
196
194
|
return argv.length === 2 && (argv[1] === "--help" || argv[1] === "-h");
|
|
197
195
|
}
|
|
198
|
-
function parseLocalArgs(argv) {
|
|
199
|
-
const command = argv[0];
|
|
200
|
-
switch (command) {
|
|
201
|
-
case "onboard":
|
|
202
|
-
return parseOnboardArgs(argv.slice(1));
|
|
203
|
-
case "install":
|
|
204
|
-
return parseInstallArgs(argv.slice(1));
|
|
205
|
-
case "login":
|
|
206
|
-
case "pair":
|
|
207
|
-
return parseLoginArgs(argv.slice(1));
|
|
208
|
-
case "logout":
|
|
209
|
-
return parseLogoutArgs(argv.slice(1));
|
|
210
|
-
case "start":
|
|
211
|
-
return parseStartArgs(argv.slice(1));
|
|
212
|
-
case "sync":
|
|
213
|
-
return parseSyncArgs(argv.slice(1));
|
|
214
|
-
case "status":
|
|
215
|
-
return parseStatusArgs(argv.slice(1));
|
|
216
|
-
case "sessions":
|
|
217
|
-
return parseSessionsArgs(argv.slice(1));
|
|
218
|
-
case "serve":
|
|
219
|
-
return parseServeArgs(argv.slice(1));
|
|
220
|
-
case "autostart":
|
|
221
|
-
return parseAutostartArgs(argv.slice(1));
|
|
222
|
-
default:
|
|
223
|
-
throw new Error(`Unknown local command: ${command ?? ""}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
function parseOnboardArgs(args) {
|
|
227
|
-
const values = parseNamedArgs(args, {
|
|
228
|
-
allowedFlags: [
|
|
229
|
-
"--home",
|
|
230
|
-
"--repo",
|
|
231
|
-
"--dashboard-url",
|
|
232
|
-
"--email",
|
|
233
|
-
"--device-name",
|
|
234
|
-
"--ticket",
|
|
235
|
-
"--branch",
|
|
236
|
-
"--json",
|
|
237
|
-
"--poll-interval-ms",
|
|
238
|
-
"--timeout-ms",
|
|
239
|
-
"--max-depth",
|
|
240
|
-
"--max-repos",
|
|
241
|
-
],
|
|
242
|
-
valueFlags: [
|
|
243
|
-
"--home",
|
|
244
|
-
"--repo",
|
|
245
|
-
"--dashboard-url",
|
|
246
|
-
"--email",
|
|
247
|
-
"--device-name",
|
|
248
|
-
"--ticket",
|
|
249
|
-
"--branch",
|
|
250
|
-
"--poll-interval-ms",
|
|
251
|
-
"--timeout-ms",
|
|
252
|
-
"--max-depth",
|
|
253
|
-
"--max-repos",
|
|
254
|
-
],
|
|
255
|
-
});
|
|
256
|
-
assertNoPositionals(values.positionals, "onboard");
|
|
257
|
-
return {
|
|
258
|
-
kind: "onboard",
|
|
259
|
-
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
260
|
-
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
261
|
-
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
262
|
-
claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
|
|
263
|
-
deviceName: optionalNonEmpty(values.flags.get("--device-name")),
|
|
264
|
-
activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
|
|
265
|
-
branch: optionalNonEmpty(values.flags.get("--branch")),
|
|
266
|
-
json: values.booleans.has("--json"),
|
|
267
|
-
pollIntervalMs: optionalPositiveInteger(values.flags.get("--poll-interval-ms"), "--poll-interval-ms"),
|
|
268
|
-
timeoutMs: optionalPositiveInteger(values.flags.get("--timeout-ms"), "--timeout-ms"),
|
|
269
|
-
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
270
|
-
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
function parseInstallArgs(args) {
|
|
274
|
-
const values = parseNamedArgs(args, {
|
|
275
|
-
allowedFlags: [
|
|
276
|
-
"--home",
|
|
277
|
-
"--repo",
|
|
278
|
-
"--dashboard-url",
|
|
279
|
-
"--supabase-url",
|
|
280
|
-
"--json",
|
|
281
|
-
],
|
|
282
|
-
valueFlags: ["--home", "--repo", "--dashboard-url", "--supabase-url"],
|
|
283
|
-
});
|
|
284
|
-
assertNoPositionals(values.positionals, "install");
|
|
285
|
-
return {
|
|
286
|
-
kind: "install",
|
|
287
|
-
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
288
|
-
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
289
|
-
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
290
|
-
supabaseUrl: optionalNonEmpty(values.flags.get("--supabase-url")),
|
|
291
|
-
json: values.booleans.has("--json"),
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
function parseLoginArgs(args) {
|
|
295
|
-
const values = parseNamedArgs(args, {
|
|
296
|
-
allowedFlags: [
|
|
297
|
-
"--home",
|
|
298
|
-
"--dashboard-url",
|
|
299
|
-
"--email",
|
|
300
|
-
"--device-name",
|
|
301
|
-
"--json",
|
|
302
|
-
"--poll-interval-ms",
|
|
303
|
-
"--timeout-ms",
|
|
304
|
-
],
|
|
305
|
-
valueFlags: [
|
|
306
|
-
"--home",
|
|
307
|
-
"--dashboard-url",
|
|
308
|
-
"--email",
|
|
309
|
-
"--device-name",
|
|
310
|
-
"--poll-interval-ms",
|
|
311
|
-
"--timeout-ms",
|
|
312
|
-
],
|
|
313
|
-
});
|
|
314
|
-
assertNoPositionals(values.positionals, "login");
|
|
315
|
-
return {
|
|
316
|
-
kind: "login",
|
|
317
|
-
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
318
|
-
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
319
|
-
claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
|
|
320
|
-
deviceName: optionalNonEmpty(values.flags.get("--device-name")),
|
|
321
|
-
json: values.booleans.has("--json"),
|
|
322
|
-
pollIntervalMs: optionalPositiveInteger(values.flags.get("--poll-interval-ms"), "--poll-interval-ms"),
|
|
323
|
-
timeoutMs: optionalPositiveInteger(values.flags.get("--timeout-ms"), "--timeout-ms"),
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
function parseLogoutArgs(args) {
|
|
327
|
-
const values = parseNamedArgs(args, {
|
|
328
|
-
allowedFlags: ["--home", "--json"],
|
|
329
|
-
valueFlags: ["--home"],
|
|
330
|
-
});
|
|
331
|
-
assertNoPositionals(values.positionals, "logout");
|
|
332
|
-
return {
|
|
333
|
-
kind: "logout",
|
|
334
|
-
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
335
|
-
json: values.booleans.has("--json"),
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
function parseStartArgs(args) {
|
|
339
|
-
const values = parseNamedArgs(args, {
|
|
340
|
-
allowedFlags: [
|
|
341
|
-
"--home",
|
|
342
|
-
"--repo",
|
|
343
|
-
"--branch",
|
|
344
|
-
"--ticket",
|
|
345
|
-
"--operator-id",
|
|
346
|
-
"--session-id",
|
|
347
|
-
"--json",
|
|
348
|
-
"--max-depth",
|
|
349
|
-
"--max-repos",
|
|
350
|
-
],
|
|
351
|
-
valueFlags: [
|
|
352
|
-
"--home",
|
|
353
|
-
"--repo",
|
|
354
|
-
"--branch",
|
|
355
|
-
"--ticket",
|
|
356
|
-
"--operator-id",
|
|
357
|
-
"--session-id",
|
|
358
|
-
"--max-depth",
|
|
359
|
-
"--max-repos",
|
|
360
|
-
],
|
|
361
|
-
});
|
|
362
|
-
assertNoPositionals(values.positionals, "start");
|
|
363
|
-
return {
|
|
364
|
-
kind: "start",
|
|
365
|
-
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
366
|
-
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
367
|
-
branch: optionalNonEmpty(values.flags.get("--branch")),
|
|
368
|
-
activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
|
|
369
|
-
operatorId: optionalNonEmpty(values.flags.get("--operator-id")),
|
|
370
|
-
sessionId: optionalNonEmpty(values.flags.get("--session-id")),
|
|
371
|
-
json: values.booleans.has("--json"),
|
|
372
|
-
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
373
|
-
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
function parseSyncArgs(args) {
|
|
377
|
-
const values = parseNamedArgs(args, {
|
|
378
|
-
allowedFlags: ["--home", "--repo", "--dashboard-url", "--json", "--max-depth", "--max-repos"],
|
|
379
|
-
valueFlags: ["--home", "--repo", "--dashboard-url", "--max-depth", "--max-repos"],
|
|
380
|
-
});
|
|
381
|
-
assertNoPositionals(values.positionals, "sync");
|
|
382
|
-
return {
|
|
383
|
-
kind: "sync",
|
|
384
|
-
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
385
|
-
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
386
|
-
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
387
|
-
json: values.booleans.has("--json"),
|
|
388
|
-
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
389
|
-
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
function parseStatusArgs(args) {
|
|
393
|
-
const values = parseNamedArgs(args, {
|
|
394
|
-
allowedFlags: ["--home", "--repo", "--json", "--max-depth", "--max-repos"],
|
|
395
|
-
valueFlags: ["--home", "--repo", "--max-depth", "--max-repos"],
|
|
396
|
-
});
|
|
397
|
-
assertNoPositionals(values.positionals, "status");
|
|
398
|
-
return {
|
|
399
|
-
kind: "status",
|
|
400
|
-
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
401
|
-
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
402
|
-
json: values.booleans.has("--json"),
|
|
403
|
-
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
404
|
-
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
function parseSessionsArgs(args) {
|
|
408
|
-
const values = parseNamedArgs(args, {
|
|
409
|
-
allowedFlags: ["--home", "--repo", "--source", "--json", "--max-depth", "--max-repos"],
|
|
410
|
-
valueFlags: ["--home", "--repo", "--source", "--max-depth", "--max-repos"],
|
|
411
|
-
});
|
|
412
|
-
assertNoPositionals(values.positionals, "sessions");
|
|
413
|
-
const source = values.flags.get("--source");
|
|
414
|
-
if (source !== undefined && source !== "codex" && source !== "claude") {
|
|
415
|
-
throw new Error("--source must be 'codex' or 'claude'.");
|
|
416
|
-
}
|
|
417
|
-
return {
|
|
418
|
-
kind: "sessions",
|
|
419
|
-
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
420
|
-
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
421
|
-
source,
|
|
422
|
-
json: values.booleans.has("--json"),
|
|
423
|
-
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
424
|
-
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
function parseServeArgs(args) {
|
|
428
|
-
const values = parseNamedArgs(args, {
|
|
429
|
-
allowedFlags: ["--home", "--repo", "--port"],
|
|
430
|
-
valueFlags: ["--home", "--repo", "--port"],
|
|
431
|
-
});
|
|
432
|
-
assertNoPositionals(values.positionals, "serve");
|
|
433
|
-
const port = Number(values.flags.get("--port") ?? "4174");
|
|
434
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
435
|
-
throw new Error("--port must be a TCP port between 1 and 65535.");
|
|
436
|
-
}
|
|
437
|
-
return {
|
|
438
|
-
kind: "serve",
|
|
439
|
-
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
440
|
-
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
441
|
-
port,
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
function parseAutostartArgs(args) {
|
|
445
|
-
const values = parseNamedArgs(args, {
|
|
446
|
-
allowedFlags: [
|
|
447
|
-
"--home",
|
|
448
|
-
"--repo",
|
|
449
|
-
"--dashboard-url",
|
|
450
|
-
"--interval-seconds",
|
|
451
|
-
"--json",
|
|
452
|
-
],
|
|
453
|
-
valueFlags: ["--home", "--repo", "--dashboard-url", "--interval-seconds"],
|
|
454
|
-
});
|
|
455
|
-
if (values.positionals.length > 1) {
|
|
456
|
-
throw new Error("autostart accepts at most one action (install|uninstall|status).");
|
|
457
|
-
}
|
|
458
|
-
const action = values.positionals[0] ?? "install";
|
|
459
|
-
if (action !== "install" && action !== "uninstall" && action !== "status") {
|
|
460
|
-
throw new Error("autostart action must be install, uninstall, or status.");
|
|
461
|
-
}
|
|
462
|
-
return {
|
|
463
|
-
kind: "autostart",
|
|
464
|
-
action,
|
|
465
|
-
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
466
|
-
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
467
|
-
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
468
|
-
intervalSeconds: optionalPositiveInteger(values.flags.get("--interval-seconds"), "--interval-seconds") ?? 1800,
|
|
469
|
-
json: values.booleans.has("--json"),
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
function parseNamedArgs(args, options) {
|
|
473
|
-
const allowed = new Set(options.allowedFlags);
|
|
474
|
-
const valueFlags = new Set(options.valueFlags);
|
|
475
|
-
const flags = new Map();
|
|
476
|
-
const booleans = new Set();
|
|
477
|
-
const positionals = [];
|
|
478
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
479
|
-
const arg = args[index] ?? "";
|
|
480
|
-
rejectServiceRoleLikeArgument(arg);
|
|
481
|
-
if (!arg.startsWith("--")) {
|
|
482
|
-
positionals.push(arg);
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
const [flag, inlineValue] = arg.split("=", 2);
|
|
486
|
-
if (!allowed.has(flag))
|
|
487
|
-
throw new Error(`Unknown flag: ${flag}`);
|
|
488
|
-
if (valueFlags.has(flag)) {
|
|
489
|
-
const value = inlineValue ?? args[index + 1];
|
|
490
|
-
if (!value || value.startsWith("--")) {
|
|
491
|
-
throw new Error(`${flag} requires a value.`);
|
|
492
|
-
}
|
|
493
|
-
rejectServiceRoleLikeArgument(value);
|
|
494
|
-
flags.set(flag, value);
|
|
495
|
-
if (inlineValue === undefined)
|
|
496
|
-
index += 1;
|
|
497
|
-
}
|
|
498
|
-
else {
|
|
499
|
-
if (inlineValue !== undefined)
|
|
500
|
-
throw new Error(`${flag} does not accept a value.`);
|
|
501
|
-
booleans.add(flag);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
return { flags, booleans, positionals };
|
|
505
|
-
}
|
|
506
196
|
async function runInstall(command, io) {
|
|
507
197
|
const result = await installLocalCollector(command);
|
|
508
198
|
if (command.json) {
|
|
@@ -782,497 +472,6 @@ async function readOnboardSessionReuseCandidate(homeDir) {
|
|
|
782
472
|
function normalizeUrlForComparison(value) {
|
|
783
473
|
return value ? normalizeUrl(value) : null;
|
|
784
474
|
}
|
|
785
|
-
const CLAUDE_FIRST_RUN_BACKFILL_MINUTES = 14 * 24 * 60;
|
|
786
|
-
const CLAUDE_DAMP_GROWTH_BYTES = 256 * 1024;
|
|
787
|
-
const CLAUDE_DAMP_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
|
788
|
-
/**
|
|
789
|
-
* Shared dual-source sync orchestration for single-repo and parent-folder
|
|
790
|
-
* modes. Codex AND Claude Code sessions are scanned and attributed once across
|
|
791
|
-
* every discovered worktree; each worktree syncs with only its own attributed
|
|
792
|
-
* transcripts (codex + claude main + sidecars), and the
|
|
793
|
-
* ambiguous/unattributed/skipped remainder is reported with reason labels and a
|
|
794
|
-
* `source` discriminator instead of being duplicated into every repo or
|
|
795
|
-
* silently dropped. The session row's upload state maps ONLY from the main-file
|
|
796
|
-
* outcome (D3); sidecar outcomes aggregate into CLI counts.
|
|
797
|
-
*/
|
|
798
|
-
async function runAttributedWorktreeSync(options) {
|
|
799
|
-
const now = new Date();
|
|
800
|
-
const homeDir = options.homeDir ?? os.homedir();
|
|
801
|
-
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
802
|
-
const claudeEnabled = await isClaudeCollectionEnabled(paths);
|
|
803
|
-
const codexAttribution = await scanAndAttributeCodexSessions({
|
|
804
|
-
sessionsDir: path.join(homeDir, ".codex", "sessions"),
|
|
805
|
-
worktrees: options.worktrees,
|
|
806
|
-
now,
|
|
807
|
-
});
|
|
808
|
-
// First run (D B.4 §8): without a Claude cursor yet, widen the window to 14
|
|
809
|
-
// days so the first sync captures retroactive history instead of only 24h.
|
|
810
|
-
const claudeCursorExists = await fileExists(path.join(paths.cursors_dir, CLAUDE_CURSOR_FILENAME));
|
|
811
|
-
const firstRunBackfill = claudeEnabled && !claudeCursorExists;
|
|
812
|
-
const claudeAttribution = claudeEnabled
|
|
813
|
-
? await scanAndAttributeClaudeSessions({
|
|
814
|
-
projectsDir: path.join(homeDir, ".claude", "projects"),
|
|
815
|
-
worktrees: options.worktrees,
|
|
816
|
-
now,
|
|
817
|
-
sinceMinutes: firstRunBackfill
|
|
818
|
-
? CLAUDE_FIRST_RUN_BACKFILL_MINUTES
|
|
819
|
-
: undefined,
|
|
820
|
-
})
|
|
821
|
-
: emptyClaudeScan();
|
|
822
|
-
// Read the Claude cursor up front: damping decisions need prior upload state.
|
|
823
|
-
const claudeCursorBefore = claudeEnabled
|
|
824
|
-
? await readRawEvidenceCursor(paths, {
|
|
825
|
-
filename: CLAUDE_CURSOR_FILENAME,
|
|
826
|
-
}).catch(() => emptyRawEvidenceCursorState())
|
|
827
|
-
: emptyRawEvidenceCursorState();
|
|
828
|
-
// sessionId -> prior durable pointer for damped sessions (drives the
|
|
829
|
-
// growth_damped count + the skip_main decision).
|
|
830
|
-
const dampedClaudePointers = new Map();
|
|
831
|
-
// sessionId -> prior durable pointer for EVERY already-durable Claude session
|
|
832
|
-
// (superset of damped). A session that was durable before but had no fresh
|
|
833
|
-
// upload this sync (damped, spooled, budget-deferred) reports reused_existing
|
|
834
|
-
// with this pointer instead of not_uploaded, so the store row never flips.
|
|
835
|
-
const claudePriorDurablePointers = new Map();
|
|
836
|
-
for (const [sessionId, entry] of Object.entries(claudeCursorBefore.sessions)) {
|
|
837
|
-
if (entry.uploaded_object_key) {
|
|
838
|
-
claudePriorDurablePointers.set(sessionId, entry.uploaded_object_key);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
// One shared budget for the whole sync (D7b is per-sync): a parent-folder
|
|
842
|
-
// sync over many worktrees honors a single byte/object cap rather than N×.
|
|
843
|
-
const rawEvidenceBudget = {
|
|
844
|
-
remainingBytes: RAW_EVIDENCE_DEFAULT_BYTE_BUDGET,
|
|
845
|
-
remainingObjects: RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET,
|
|
846
|
-
};
|
|
847
|
-
const outcomes = [];
|
|
848
|
-
let ok = true;
|
|
849
|
-
for (const worktree of options.worktrees) {
|
|
850
|
-
let context = null;
|
|
851
|
-
if (options.startContexts) {
|
|
852
|
-
context = await startLocalWorkContext({
|
|
853
|
-
homeDir: options.homeDir,
|
|
854
|
-
repoRoot: worktree.repo_root,
|
|
855
|
-
branch: options.branch,
|
|
856
|
-
activeTicketId: options.activeTicketId,
|
|
857
|
-
operatorId: options.operatorId,
|
|
858
|
-
sessionId: options.sessionId,
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
const claudeSessionFiles = claudeAttribution.results
|
|
862
|
-
.filter((result) => result.state === "attributed" &&
|
|
863
|
-
result.worktree?.worktree_fingerprint ===
|
|
864
|
-
worktree.worktree_fingerprint)
|
|
865
|
-
.map((result) => {
|
|
866
|
-
const damped = !result.main_file_oversized &&
|
|
867
|
-
shouldDampClaudeMain(result, claudeCursorBefore, now);
|
|
868
|
-
if (damped) {
|
|
869
|
-
dampedClaudePointers.set(result.claude_session_id, claudeCursorBefore.sessions[result.claude_session_id]
|
|
870
|
-
?.uploaded_object_key ?? null);
|
|
871
|
-
}
|
|
872
|
-
return {
|
|
873
|
-
local_path: result.file_path,
|
|
874
|
-
claude_session_id: result.claude_session_id,
|
|
875
|
-
main_file_oversized: result.main_file_oversized,
|
|
876
|
-
skip_main: damped,
|
|
877
|
-
sidecar_files: result.sidecar_files
|
|
878
|
-
.filter((sidecar) => !sidecar.skipped_reason)
|
|
879
|
-
.map((sidecar) => ({ local_path: sidecar.local_path })),
|
|
880
|
-
};
|
|
881
|
-
});
|
|
882
|
-
const syncOptions = {
|
|
883
|
-
homeDir: options.homeDir,
|
|
884
|
-
repoRoot: worktree.repo_root,
|
|
885
|
-
dashboardUrl: options.dashboardUrl,
|
|
886
|
-
codexSessionFiles: codexAttribution.results
|
|
887
|
-
.filter((result) => result.state === "attributed" &&
|
|
888
|
-
result.worktree?.worktree_fingerprint ===
|
|
889
|
-
worktree.worktree_fingerprint)
|
|
890
|
-
.map((result) => ({
|
|
891
|
-
local_path: result.file_path,
|
|
892
|
-
codex_session_id: result.codex_session_id,
|
|
893
|
-
})),
|
|
894
|
-
claudeSessionFiles,
|
|
895
|
-
rawEvidenceBudget,
|
|
896
|
-
fetch: options.fetchImpl,
|
|
897
|
-
};
|
|
898
|
-
let sync;
|
|
899
|
-
try {
|
|
900
|
-
sync = await syncLocalAmbientEnvelope(syncOptions);
|
|
901
|
-
}
|
|
902
|
-
catch (error) {
|
|
903
|
-
// A newly cloned repo has no work context yet. Capture is permissive
|
|
904
|
-
// and ticket binding comes later, so start general ambient capture for
|
|
905
|
-
// it instead of blocking every other repo's sync until someone runs
|
|
906
|
-
// `cockpit start` by hand.
|
|
907
|
-
if (error instanceof LocalUploadBlockedError &&
|
|
908
|
-
error.blocker === "missing_context") {
|
|
909
|
-
context = await startLocalWorkContext({
|
|
910
|
-
homeDir: options.homeDir,
|
|
911
|
-
repoRoot: worktree.repo_root,
|
|
912
|
-
branch: options.branch,
|
|
913
|
-
});
|
|
914
|
-
sync = await syncLocalAmbientEnvelope(syncOptions);
|
|
915
|
-
}
|
|
916
|
-
else {
|
|
917
|
-
throw error;
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
ok = ok && sync.status === "uploaded";
|
|
921
|
-
outcomes.push({ worktree, context, sync });
|
|
922
|
-
}
|
|
923
|
-
const sessions = buildAgentSessionReport({
|
|
924
|
-
codexResults: codexAttribution.results,
|
|
925
|
-
claudeResults: claudeAttribution.results,
|
|
926
|
-
outcomes,
|
|
927
|
-
now,
|
|
928
|
-
claudePriorDurablePointers,
|
|
929
|
-
});
|
|
930
|
-
// The sessions cursor is an optimization; a broken local state dir must not
|
|
931
|
-
// turn already-completed syncs into a CLI crash.
|
|
932
|
-
let codexStaleCount = 0;
|
|
933
|
-
let claudeStaleCount = 0;
|
|
934
|
-
try {
|
|
935
|
-
const codexCursor = await readRawEvidenceCursor(paths);
|
|
936
|
-
codexStaleCount = recordSourceObservations({
|
|
937
|
-
cursor: codexCursor,
|
|
938
|
-
results: codexAttribution.results,
|
|
939
|
-
sessions,
|
|
940
|
-
source: "codex",
|
|
941
|
-
sessionIdOf: (result) => result.codex_session_id,
|
|
942
|
-
now,
|
|
943
|
-
});
|
|
944
|
-
codexCursor.updated_at = now.toISOString();
|
|
945
|
-
await writeRawEvidenceCursor(paths, codexCursor);
|
|
946
|
-
}
|
|
947
|
-
catch {
|
|
948
|
-
// Best-effort: stale counts read 0 and observations re-record next sync.
|
|
949
|
-
}
|
|
950
|
-
if (claudeEnabled) {
|
|
951
|
-
try {
|
|
952
|
-
claudeStaleCount = recordSourceObservations({
|
|
953
|
-
cursor: claudeCursorBefore,
|
|
954
|
-
results: claudeAttribution.results,
|
|
955
|
-
sessions,
|
|
956
|
-
source: "claude_code",
|
|
957
|
-
sessionIdOf: (result) => result.claude_session_id,
|
|
958
|
-
now,
|
|
959
|
-
priorCursor: claudeCursorBefore,
|
|
960
|
-
});
|
|
961
|
-
claudeCursorBefore.updated_at = now.toISOString();
|
|
962
|
-
await writeRawEvidenceCursor(paths, claudeCursorBefore, {
|
|
963
|
-
filename: CLAUDE_CURSOR_FILENAME,
|
|
964
|
-
sessionsOnly: true,
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
|
-
catch {
|
|
968
|
-
// Best-effort: a broken Claude cursor must not fail the sync.
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
const firstUploaded = outcomes.find((outcome) => outcome.sync.status === "uploaded");
|
|
972
|
-
const report = firstUploaded
|
|
973
|
-
? await postCodexSessionReport({
|
|
974
|
-
homeDir: options.homeDir,
|
|
975
|
-
repoRoot: firstUploaded.worktree.repo_root,
|
|
976
|
-
dashboardUrl: options.dashboardUrl,
|
|
977
|
-
sessions,
|
|
978
|
-
fetch: options.fetchImpl,
|
|
979
|
-
now,
|
|
980
|
-
})
|
|
981
|
-
: {
|
|
982
|
-
posted: false,
|
|
983
|
-
reason: sessions.length === 0 ? "no_sessions_observed" : "no_successful_sync",
|
|
984
|
-
};
|
|
985
|
-
const summary = buildAgentSessionSummary({
|
|
986
|
-
codexAttribution,
|
|
987
|
-
claudeAttribution,
|
|
988
|
-
outcomes,
|
|
989
|
-
codexStaleCount,
|
|
990
|
-
claudeStaleCount,
|
|
991
|
-
firstRunBackfill,
|
|
992
|
-
growthDamped: dampedClaudePointers.size,
|
|
993
|
-
report,
|
|
994
|
-
});
|
|
995
|
-
return { ok, outcomes, codexAttribution, claudeAttribution, summary };
|
|
996
|
-
}
|
|
997
|
-
const ATTRIBUTION_STATE_RANK = {
|
|
998
|
-
attributed: 3,
|
|
999
|
-
ambiguous: 2,
|
|
1000
|
-
unattributed: 1,
|
|
1001
|
-
skipped: 0,
|
|
1002
|
-
};
|
|
1003
|
-
function normalizeCodexResult(result) {
|
|
1004
|
-
return {
|
|
1005
|
-
source: "codex",
|
|
1006
|
-
session_id: result.codex_session_id,
|
|
1007
|
-
state: result.state,
|
|
1008
|
-
reason: result.reason,
|
|
1009
|
-
signals: result.signals,
|
|
1010
|
-
attribution_score: result.attribution_score,
|
|
1011
|
-
path_score: result.path_score,
|
|
1012
|
-
content_hash_sha256: result.content_hash_sha256,
|
|
1013
|
-
byte_size: result.byte_size,
|
|
1014
|
-
session_file_mtime: result.session_file_mtime,
|
|
1015
|
-
session_file_mtime_ms: result.session_file_mtime_ms,
|
|
1016
|
-
worktree: result.worktree,
|
|
1017
|
-
cwd_basename: result.cwd_basename,
|
|
1018
|
-
cwd_hash: result.cwd_hash,
|
|
1019
|
-
};
|
|
1020
|
-
}
|
|
1021
|
-
function normalizeClaudeResult(result) {
|
|
1022
|
-
return {
|
|
1023
|
-
source: "claude_code",
|
|
1024
|
-
session_id: result.claude_session_id,
|
|
1025
|
-
state: result.state,
|
|
1026
|
-
reason: result.reason,
|
|
1027
|
-
signals: result.signals,
|
|
1028
|
-
attribution_score: result.attribution_score,
|
|
1029
|
-
path_score: result.path_score,
|
|
1030
|
-
content_hash_sha256: result.content_hash_sha256,
|
|
1031
|
-
byte_size: result.byte_size,
|
|
1032
|
-
session_file_mtime: result.session_file_mtime,
|
|
1033
|
-
session_file_mtime_ms: result.session_file_mtime_ms,
|
|
1034
|
-
worktree: result.worktree,
|
|
1035
|
-
cwd_basename: result.cwd_basename,
|
|
1036
|
-
cwd_hash: result.cwd_hash,
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
-
/**
|
|
1040
|
-
* Generalizes the per-session report across sources. Dedupe is per
|
|
1041
|
-
* `(source, session_id)` so a Codex session and a Claude session that happen to
|
|
1042
|
-
* share an id are never collapsed. Upload state maps ONLY from the main-file
|
|
1043
|
-
* outcome (kind `codex_jsonl` / `claude_jsonl`); sidecar outcomes never set a
|
|
1044
|
-
* session's upload state (D3). Damped Claude sessions report `reused_existing`
|
|
1045
|
-
* carrying their prior durable pointer.
|
|
1046
|
-
*/
|
|
1047
|
-
function buildAgentSessionReport(options) {
|
|
1048
|
-
const normalized = [
|
|
1049
|
-
...options.codexResults.map(normalizeCodexResult),
|
|
1050
|
-
...options.claudeResults.map(normalizeClaudeResult),
|
|
1051
|
-
];
|
|
1052
|
-
const bestByKey = new Map();
|
|
1053
|
-
for (const result of normalized) {
|
|
1054
|
-
const key = `${result.source}:${result.session_id}`;
|
|
1055
|
-
const existing = bestByKey.get(key);
|
|
1056
|
-
if (!existing ||
|
|
1057
|
-
(ATTRIBUTION_STATE_RANK[result.state] ?? 0) >
|
|
1058
|
-
(ATTRIBUTION_STATE_RANK[existing.state] ?? 0) ||
|
|
1059
|
-
((ATTRIBUTION_STATE_RANK[result.state] ?? 0) ===
|
|
1060
|
-
(ATTRIBUTION_STATE_RANK[existing.state] ?? 0) &&
|
|
1061
|
-
result.session_file_mtime_ms > existing.session_file_mtime_ms)) {
|
|
1062
|
-
bestByKey.set(key, result);
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
// Main-file outcomes only (D3): a sidecar making it must never mark a session
|
|
1066
|
-
// uploaded when the main did not.
|
|
1067
|
-
const uploadByKey = new Map();
|
|
1068
|
-
for (const outcome of options.outcomes) {
|
|
1069
|
-
if (outcome.sync.status !== "uploaded")
|
|
1070
|
-
continue;
|
|
1071
|
-
for (const upload of outcome.sync.raw_evidence_outcomes) {
|
|
1072
|
-
if (!upload.codex_session_id || !upload.raw_evidence_pointer_id)
|
|
1073
|
-
continue;
|
|
1074
|
-
const source = upload.kind === "claude_jsonl"
|
|
1075
|
-
? "claude_code"
|
|
1076
|
-
: upload.kind === "codex_jsonl"
|
|
1077
|
-
? "codex"
|
|
1078
|
-
: null;
|
|
1079
|
-
if (!source)
|
|
1080
|
-
continue; // sidecars and other kinds do not set session state
|
|
1081
|
-
uploadByKey.set(`${source}:${upload.codex_session_id}`, upload);
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
return [...bestByKey.values()].map((result) => {
|
|
1085
|
-
const key = `${result.source}:${result.session_id}`;
|
|
1086
|
-
const upload = uploadByKey.get(key);
|
|
1087
|
-
// A previously-durable Claude session with no fresh main upload this sync
|
|
1088
|
-
// (damped / spooled / budget-deferred) reports reused_existing + its prior
|
|
1089
|
-
// pointer rather than not_uploaded, so the store row never flips.
|
|
1090
|
-
const priorDurablePointer = result.source === "claude_code"
|
|
1091
|
-
? (options.claudePriorDurablePointers.get(result.session_id) ?? null)
|
|
1092
|
-
: null;
|
|
1093
|
-
return {
|
|
1094
|
-
codex_session_id: result.session_id,
|
|
1095
|
-
source: result.source,
|
|
1096
|
-
observed_at: options.now.toISOString(),
|
|
1097
|
-
attribution_state: result.state,
|
|
1098
|
-
attribution_reason: result.reason,
|
|
1099
|
-
attribution_score: result.attribution_score,
|
|
1100
|
-
path_score: result.path_score,
|
|
1101
|
-
signals: result.signals,
|
|
1102
|
-
...(result.content_hash_sha256
|
|
1103
|
-
? { session_file_hash_sha256: result.content_hash_sha256 }
|
|
1104
|
-
: {}),
|
|
1105
|
-
session_file_byte_size: result.byte_size,
|
|
1106
|
-
session_file_mtime: result.session_file_mtime,
|
|
1107
|
-
...(result.worktree
|
|
1108
|
-
? {
|
|
1109
|
-
repo_fingerprint: result.worktree.repo_fingerprint,
|
|
1110
|
-
worktree_fingerprint: result.worktree.worktree_fingerprint,
|
|
1111
|
-
repo_label: result.worktree.repo_label,
|
|
1112
|
-
branch: result.worktree.branch,
|
|
1113
|
-
}
|
|
1114
|
-
: {}),
|
|
1115
|
-
...(result.cwd_basename ? { cwd_basename: result.cwd_basename } : {}),
|
|
1116
|
-
...(result.cwd_hash ? { cwd_hash: result.cwd_hash } : {}),
|
|
1117
|
-
...(upload
|
|
1118
|
-
? {
|
|
1119
|
-
raw_evidence_pointer_id: upload.raw_evidence_pointer_id,
|
|
1120
|
-
upload_state: upload.upload_state,
|
|
1121
|
-
}
|
|
1122
|
-
: priorDurablePointer
|
|
1123
|
-
? {
|
|
1124
|
-
raw_evidence_pointer_id: priorDurablePointer,
|
|
1125
|
-
upload_state: "reused_existing",
|
|
1126
|
-
}
|
|
1127
|
-
: result.state === "attributed"
|
|
1128
|
-
? { upload_state: "not_uploaded" }
|
|
1129
|
-
: {}),
|
|
1130
|
-
};
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
/**
|
|
1134
|
-
* Records per-source session observations into its cursor and returns the stale
|
|
1135
|
-
* count. Damped/reused Claude sessions carry forward their prior upload
|
|
1136
|
-
* timestamp + byte size so the 6h damping window keeps counting from the real
|
|
1137
|
-
* last upload (otherwise a slowly-growing file would never re-upload — D21).
|
|
1138
|
-
*/
|
|
1139
|
-
function recordSourceObservations(options) {
|
|
1140
|
-
const seen = new Set(options.results.map((result) => options.sessionIdOf(result)));
|
|
1141
|
-
const stale = countStaleSessions(options.cursor, seen);
|
|
1142
|
-
for (const result of options.results) {
|
|
1143
|
-
const sessionId = options.sessionIdOf(result);
|
|
1144
|
-
const reported = options.sessions.find((session) => session.source === options.source &&
|
|
1145
|
-
session.codex_session_id === sessionId);
|
|
1146
|
-
const uploadedThisSync = reported?.upload_state === "uploaded";
|
|
1147
|
-
const durableThisSync = reported?.upload_state === "uploaded" ||
|
|
1148
|
-
reported?.upload_state === "reused_existing";
|
|
1149
|
-
const prior = options.priorCursor?.sessions[sessionId];
|
|
1150
|
-
// D21 / no-flip-flop: a sync that is spooled (offline), budget-deferred, or
|
|
1151
|
-
// upload-failed for a session that was ALREADY durable must NOT wipe the
|
|
1152
|
-
// prior durable state — otherwise damping is forfeited forever and the
|
|
1153
|
-
// store row oscillates uploaded -> not_uploaded hourly. Carry the prior
|
|
1154
|
-
// durable pointer/timestamp/size forward unless we durably uploaded anew.
|
|
1155
|
-
const uploadedObjectKey = durableThisSync
|
|
1156
|
-
? (reported?.raw_evidence_pointer_id ?? prior?.uploaded_object_key ?? null)
|
|
1157
|
-
: (prior?.uploaded_object_key ?? null);
|
|
1158
|
-
const uploadedAt = uploadedThisSync
|
|
1159
|
-
? options.now.toISOString()
|
|
1160
|
-
: (prior?.uploaded_at ?? (durableThisSync ? options.now.toISOString() : null));
|
|
1161
|
-
const uploadedByteSize = uploadedThisSync
|
|
1162
|
-
? result.byte_size
|
|
1163
|
-
: (prior?.uploaded_byte_size ??
|
|
1164
|
-
(durableThisSync ? result.byte_size : null));
|
|
1165
|
-
const entry = {
|
|
1166
|
-
file_hash_sha256: result.content_hash_sha256,
|
|
1167
|
-
file_mtime_ms: result.session_file_mtime_ms,
|
|
1168
|
-
byte_size: result.byte_size,
|
|
1169
|
-
// Durable byte offset reflects how many bytes are durable remotely (the
|
|
1170
|
-
// last uploaded size), not the current file size.
|
|
1171
|
-
byte_offset: uploadedByteSize ?? 0,
|
|
1172
|
-
state: result.state,
|
|
1173
|
-
reason: result.reason,
|
|
1174
|
-
worktree_fingerprint: result.worktree?.worktree_fingerprint ?? null,
|
|
1175
|
-
uploaded_object_key: uploadedObjectKey,
|
|
1176
|
-
uploaded_at: uploadedAt,
|
|
1177
|
-
uploaded_byte_size: uploadedByteSize,
|
|
1178
|
-
last_seen_at: options.now.toISOString(),
|
|
1179
|
-
};
|
|
1180
|
-
recordSessionObservation(options.cursor, sessionId, entry);
|
|
1181
|
-
}
|
|
1182
|
-
return stale;
|
|
1183
|
-
}
|
|
1184
|
-
function shouldDampClaudeMain(result, cursor, now) {
|
|
1185
|
-
const entry = cursor.sessions[result.claude_session_id];
|
|
1186
|
-
if (!entry ||
|
|
1187
|
-
!entry.uploaded_object_key ||
|
|
1188
|
-
!entry.uploaded_at ||
|
|
1189
|
-
entry.uploaded_byte_size == null) {
|
|
1190
|
-
return false;
|
|
1191
|
-
}
|
|
1192
|
-
const grew = result.byte_size > entry.uploaded_byte_size;
|
|
1193
|
-
if (!grew)
|
|
1194
|
-
return false; // unchanged content reuses via the object cursor
|
|
1195
|
-
const growth = result.byte_size - entry.uploaded_byte_size;
|
|
1196
|
-
const ageMs = now.getTime() - Date.parse(entry.uploaded_at);
|
|
1197
|
-
return (growth <= CLAUDE_DAMP_GROWTH_BYTES &&
|
|
1198
|
-
Number.isFinite(ageMs) &&
|
|
1199
|
-
ageMs <= CLAUDE_DAMP_MAX_AGE_MS);
|
|
1200
|
-
}
|
|
1201
|
-
function buildAgentSessionSummary(options) {
|
|
1202
|
-
const sidecarOutcomes = options.outcomes.flatMap((outcome) => outcome.sync.raw_evidence_outcomes.filter((upload) => upload.kind === "claude_jsonl_sidecar"));
|
|
1203
|
-
const attributedClaude = options.claudeAttribution.results.filter((result) => result.state === "attributed");
|
|
1204
|
-
const sidecarsCollected = attributedClaude.reduce((total, result) => total +
|
|
1205
|
-
result.sidecar_files.filter((sidecar) => !sidecar.skipped_reason).length, 0);
|
|
1206
|
-
const sidecarsSkipped = options.claudeAttribution.results.reduce((total, result) => total +
|
|
1207
|
-
result.sidecar_files.filter((sidecar) => sidecar.skipped_reason).length, 0);
|
|
1208
|
-
const codex = {
|
|
1209
|
-
scanned: options.codexAttribution.scanned_file_count,
|
|
1210
|
-
attributed: options.codexAttribution.counts.attributed,
|
|
1211
|
-
ambiguous: options.codexAttribution.counts.ambiguous,
|
|
1212
|
-
unattributed: options.codexAttribution.counts.unattributed,
|
|
1213
|
-
skipped: options.codexAttribution.counts.skipped,
|
|
1214
|
-
stale: options.codexStaleCount,
|
|
1215
|
-
};
|
|
1216
|
-
const claude = {
|
|
1217
|
-
scanned: options.claudeAttribution.scanned_session_count,
|
|
1218
|
-
attributed: options.claudeAttribution.counts.attributed,
|
|
1219
|
-
ambiguous: options.claudeAttribution.counts.ambiguous,
|
|
1220
|
-
unattributed: options.claudeAttribution.counts.unattributed,
|
|
1221
|
-
skipped: options.claudeAttribution.counts.skipped,
|
|
1222
|
-
stale: options.claudeStaleCount,
|
|
1223
|
-
sidecars_collected: sidecarsCollected,
|
|
1224
|
-
sidecars_uploaded: sidecarOutcomes.filter((upload) => upload.upload_state === "uploaded" ||
|
|
1225
|
-
upload.upload_state === "reused_existing").length,
|
|
1226
|
-
sidecars_skipped: sidecarsSkipped,
|
|
1227
|
-
sidecars_capped: options.claudeAttribution.counts.sidecars_capped,
|
|
1228
|
-
sidecars_failed: sidecarOutcomes.filter((upload) => upload.upload_state === "upload_failed").length,
|
|
1229
|
-
mains_oversized: options.claudeAttribution.counts.mains_oversized,
|
|
1230
|
-
oversized_lines_skipped: options.claudeAttribution.counts.oversized_lines_skipped,
|
|
1231
|
-
project_dirs_skipped: options.claudeAttribution.project_dirs_skipped,
|
|
1232
|
-
sessions_schema_drift: options.claudeAttribution.counts.sessions_schema_drift,
|
|
1233
|
-
growth_damped: options.growthDamped,
|
|
1234
|
-
first_run_backfill: options.firstRunBackfill,
|
|
1235
|
-
};
|
|
1236
|
-
return {
|
|
1237
|
-
scanned: codex.scanned,
|
|
1238
|
-
attributed: codex.attributed,
|
|
1239
|
-
ambiguous: codex.ambiguous,
|
|
1240
|
-
unattributed: codex.unattributed,
|
|
1241
|
-
skipped: codex.skipped,
|
|
1242
|
-
stale: codex.stale,
|
|
1243
|
-
report_posted: options.report.posted,
|
|
1244
|
-
report_reason: options.report.reason,
|
|
1245
|
-
codex,
|
|
1246
|
-
claude,
|
|
1247
|
-
files_deferred_byte_budget: options.outcomes.reduce((total, outcome) => total + outcome.sync.raw_evidence_deferred_byte_budget, 0),
|
|
1248
|
-
files_deferred_object_budget: options.outcomes.reduce((total, outcome) => total + outcome.sync.raw_evidence_deferred_object_budget, 0),
|
|
1249
|
-
};
|
|
1250
|
-
}
|
|
1251
|
-
function emptyClaudeScan() {
|
|
1252
|
-
return {
|
|
1253
|
-
results: [],
|
|
1254
|
-
scanned_session_count: 0,
|
|
1255
|
-
project_dirs_skipped: 0,
|
|
1256
|
-
counts: {
|
|
1257
|
-
attributed: 0,
|
|
1258
|
-
ambiguous: 0,
|
|
1259
|
-
unattributed: 0,
|
|
1260
|
-
skipped: 0,
|
|
1261
|
-
mains_oversized: 0,
|
|
1262
|
-
oversized_lines_skipped: 0,
|
|
1263
|
-
sessions_schema_drift: 0,
|
|
1264
|
-
sidecars_capped: 0,
|
|
1265
|
-
},
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
async function isClaudeCollectionEnabled(paths) {
|
|
1269
|
-
const config = await readLocalCollectorConfig(paths).catch(() => null);
|
|
1270
|
-
return config?.collect_claude_jsonl !== false;
|
|
1271
|
-
}
|
|
1272
|
-
async function fileExists(filePath) {
|
|
1273
|
-
const { stat } = await import("node:fs/promises");
|
|
1274
|
-
return stat(filePath).then(() => true, () => false);
|
|
1275
|
-
}
|
|
1276
475
|
function shortSha(value) {
|
|
1277
476
|
return value ? value.slice(0, 12) : "unknown";
|
|
1278
477
|
}
|
|
@@ -1828,75 +1027,6 @@ function writeAutostartResult(io, result) {
|
|
|
1828
1027
|
return;
|
|
1829
1028
|
}
|
|
1830
1029
|
}
|
|
1831
|
-
function assertNoPositionals(positionals, command) {
|
|
1832
|
-
if (positionals.length > 0) {
|
|
1833
|
-
throw new Error(`${command} does not accept positional arguments.`);
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
function optionalNonEmpty(value) {
|
|
1837
|
-
const trimmed = value?.trim();
|
|
1838
|
-
return trimmed ? trimmed : undefined;
|
|
1839
|
-
}
|
|
1840
|
-
function optionalUrl(value) {
|
|
1841
|
-
return value === undefined ? undefined : normalizeUrl(value);
|
|
1842
|
-
}
|
|
1843
|
-
function optionalEmail(value) {
|
|
1844
|
-
const trimmed = value?.trim().toLowerCase();
|
|
1845
|
-
if (!trimmed)
|
|
1846
|
-
return undefined;
|
|
1847
|
-
if (!trimmed.includes("@")) {
|
|
1848
|
-
throw new Error("--email must be a valid email address.");
|
|
1849
|
-
}
|
|
1850
|
-
return trimmed;
|
|
1851
|
-
}
|
|
1852
|
-
function optionalPositiveInteger(value, flag) {
|
|
1853
|
-
if (value === undefined)
|
|
1854
|
-
return undefined;
|
|
1855
|
-
const parsed = Number(value);
|
|
1856
|
-
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
1857
|
-
throw new Error(`${flag} must be a positive integer.`);
|
|
1858
|
-
}
|
|
1859
|
-
return parsed;
|
|
1860
|
-
}
|
|
1861
|
-
function normalizeUrl(value) {
|
|
1862
|
-
const trimmed = value.trim().replace(/\/+$/, "");
|
|
1863
|
-
if (!trimmed)
|
|
1864
|
-
throw new Error("URL value cannot be empty.");
|
|
1865
|
-
return trimmed;
|
|
1866
|
-
}
|
|
1867
|
-
function rejectServiceRoleLikeArgument(value) {
|
|
1868
|
-
if (!looksLikeServiceRoleSecret(value))
|
|
1869
|
-
return;
|
|
1870
|
-
throw new Error("Service-role credentials are not accepted by local collector commands.");
|
|
1871
|
-
}
|
|
1872
|
-
function looksLikeServiceRoleSecret(value) {
|
|
1873
|
-
if (serviceCredentialNamePattern().test(value))
|
|
1874
|
-
return true;
|
|
1875
|
-
const parts = value.split(".");
|
|
1876
|
-
if (parts.length !== 3)
|
|
1877
|
-
return false;
|
|
1878
|
-
try {
|
|
1879
|
-
const payload = Buffer.from(base64UrlToBase64(parts[1] ?? ""), "base64").toString("utf8");
|
|
1880
|
-
return serviceCredentialPayloadPattern().test(payload);
|
|
1881
|
-
}
|
|
1882
|
-
catch {
|
|
1883
|
-
return false;
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
function serviceCredentialNamePattern() {
|
|
1887
|
-
return new RegExp([
|
|
1888
|
-
["SUPABASE", "SERVICE", "ROLE", "KEY"].join("[_-]?"),
|
|
1889
|
-
["service", "role"].join("[_-]?"),
|
|
1890
|
-
].join("|"), "i");
|
|
1891
|
-
}
|
|
1892
|
-
function serviceCredentialPayloadPattern() {
|
|
1893
|
-
const privilegedRole = ["service", "role"].join("_");
|
|
1894
|
-
return new RegExp(`"role"\\s*:\\s*"${privilegedRole}"`);
|
|
1895
|
-
}
|
|
1896
|
-
function base64UrlToBase64(value) {
|
|
1897
|
-
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
1898
|
-
return `${normalized}${"=".repeat((4 - (normalized.length % 4)) % 4)}`;
|
|
1899
|
-
}
|
|
1900
1030
|
function defaultExec() {
|
|
1901
1031
|
return (cmd, args) => new Promise((resolve) => {
|
|
1902
1032
|
execFile(cmd, args, { encoding: "utf8" }, (err, stdout, stderr) => {
|