@bli-cockpit/cli 0.1.9 → 0.1.12
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 +25 -14
- 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 +47 -1
- package/dist/commands/local-args.js +383 -0
- package/dist/commands/local.js +15 -877
- package/dist/commands/public-root.js +3 -0
- 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 +153 -4
- 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 { DEFAULT_DASHBOARD_URL, 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",
|
|
@@ -84,6 +82,8 @@ export function localCommandHelp(command) {
|
|
|
84
82
|
" cockpit sessions [--source codex|claude] [--repo <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
85
83
|
" cockpit serve [--port <port>] [--repo <path>]",
|
|
86
84
|
" cockpit autostart [install|uninstall|status] [--repo <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
85
|
+
"",
|
|
86
|
+
`Default dashboard: ${DEFAULT_DASHBOARD_URL}. Omit --dashboard-url for normal production use; pass it only for staging/custom dashboards or to force a different pairing.`,
|
|
87
87
|
].join("\n");
|
|
88
88
|
}
|
|
89
89
|
function localSubcommandHelp(command) {
|
|
@@ -95,6 +95,8 @@ function localSubcommandHelp(command) {
|
|
|
95
95
|
"",
|
|
96
96
|
"Installs, pairs, starts work context(s), syncs once, and prints readiness proof.",
|
|
97
97
|
"If --repo is a parent folder, scans child git repos/worktrees and rolls them up by repo.",
|
|
98
|
+
`Omit --dashboard-url for normal production setup (${DEFAULT_DASHBOARD_URL}).`,
|
|
99
|
+
"Pass --dashboard-url only for staging/custom dashboards or to force a different pairing.",
|
|
98
100
|
"Run with no flags in a terminal and it prompts for the dashboard email; pass --email to skip the prompt (and on shared/reused machines, where mismatched sessions are re-paired).",
|
|
99
101
|
],
|
|
100
102
|
],
|
|
@@ -104,6 +106,7 @@ function localSubcommandHelp(command) {
|
|
|
104
106
|
"Usage: cockpit install [--dashboard-url <url>] [--repo <path>] [--json]",
|
|
105
107
|
"",
|
|
106
108
|
"Writes local collector config. Pair with `cockpit login`, then run `cockpit start` when work begins.",
|
|
109
|
+
"Omit --dashboard-url for the production dashboard; pass it only for staging/custom dashboards.",
|
|
107
110
|
],
|
|
108
111
|
],
|
|
109
112
|
[
|
|
@@ -112,6 +115,7 @@ function localSubcommandHelp(command) {
|
|
|
112
115
|
"Usage: cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
113
116
|
"",
|
|
114
117
|
"Starts dashboard device pairing and stores the approved local session.",
|
|
118
|
+
"Omit --dashboard-url for the production dashboard; pass it only for staging/custom dashboards.",
|
|
115
119
|
],
|
|
116
120
|
],
|
|
117
121
|
[
|
|
@@ -138,6 +142,7 @@ function localSubcommandHelp(command) {
|
|
|
138
142
|
"Usage: cockpit sync [--repo <path>] [--dashboard-url <url>] [--json]",
|
|
139
143
|
"",
|
|
140
144
|
"Uploads latest local ambient envelope(s), or spools safe retries if blocked.",
|
|
145
|
+
"Omit --dashboard-url for normal production sync; pass it only for staging/custom dashboards or forced re-pairing.",
|
|
141
146
|
"Parent folders sync each child git worktree; Codex AND Claude Code JSONL",
|
|
142
147
|
"transcripts (and Claude subagent sidecars) are attributed to repos",
|
|
143
148
|
"deterministically and ambiguous transcripts are retained as unattributed",
|
|
@@ -183,6 +188,7 @@ function localSubcommandHelp(command) {
|
|
|
183
188
|
"Installs a macOS launchd LaunchAgent that runs `cockpit sync` at login and",
|
|
184
189
|
"every 30 min (default), surviving reboots — so machines never drift to Stale.",
|
|
185
190
|
"Action defaults to `install`. `--repo` is the parent work folder to sync.",
|
|
191
|
+
"Omit --dashboard-url for production; pass it only for staging/custom dashboards.",
|
|
186
192
|
"macOS-only for now; see docs/runbooks/cockpit-launchd-sync.md.",
|
|
187
193
|
],
|
|
188
194
|
],
|
|
@@ -195,314 +201,6 @@ function isLocalHelpRequest(argv) {
|
|
|
195
201
|
return false;
|
|
196
202
|
return argv.length === 2 && (argv[1] === "--help" || argv[1] === "-h");
|
|
197
203
|
}
|
|
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
204
|
async function runInstall(command, io) {
|
|
507
205
|
const result = await installLocalCollector(command);
|
|
508
206
|
if (command.json) {
|
|
@@ -782,497 +480,6 @@ async function readOnboardSessionReuseCandidate(homeDir) {
|
|
|
782
480
|
function normalizeUrlForComparison(value) {
|
|
783
481
|
return value ? normalizeUrl(value) : null;
|
|
784
482
|
}
|
|
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
483
|
function shortSha(value) {
|
|
1277
484
|
return value ? value.slice(0, 12) : "unknown";
|
|
1278
485
|
}
|
|
@@ -1828,75 +1035,6 @@ function writeAutostartResult(io, result) {
|
|
|
1828
1035
|
return;
|
|
1829
1036
|
}
|
|
1830
1037
|
}
|
|
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
1038
|
function defaultExec() {
|
|
1901
1039
|
return (cmd, args) => new Promise((resolve) => {
|
|
1902
1040
|
execFile(cmd, args, { encoding: "utf8" }, (err, stdout, stderr) => {
|