@bli-cockpit/cli 0.2.236 → 0.2.238
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/attribution-core-fallbacks.js +15 -11
- package/dist/adapters/attribution-core-score.js +111 -87
- package/dist/adapters/car-state.js +38 -35
- package/dist/adapters/claude-attribution-discovery.js +81 -56
- package/dist/adapters/claude-attribution-score.js +108 -94
- package/dist/adapters/claude-attribution-signals.js +52 -42
- package/dist/adapters/codex-attribution.js +42 -33
- package/dist/adapters/codex-session-signals.js +11 -9
- package/dist/adapters/git-state.js +56 -50
- package/dist/adapters/local-sources.js +52 -43
- package/dist/adapters/raw-evidence-git-diff.js +4 -2
- package/dist/adapters/raw-evidence-sanitize.js +15 -12
- package/dist/adapters/raw-evidence-transcript-file.js +32 -31
- package/dist/autostart-self-heal.js +18 -2
- package/dist/commands/agent-session-report.js +51 -59
- package/dist/commands/agent-setup-observed.js +30 -23
- package/dist/commands/agent-setup.js +96 -45
- package/dist/commands/autostart-heal.js +8 -2
- package/dist/commands/autostart-reading.js +3 -0
- package/dist/commands/backfill-evidence-outcomes.js +9 -6
- package/dist/commands/backfill-issues.js +3 -0
- package/dist/commands/backfill-scan.js +27 -22
- package/dist/commands/backfill-upload.js +69 -60
- package/dist/commands/backfill.js +26 -21
- package/dist/commands/brief-done.js +4 -1
- package/dist/commands/brief-edit.js +71 -46
- package/dist/commands/cal-detail.js +35 -0
- package/dist/commands/cal-overlay.js +106 -0
- package/dist/commands/cal.js +40 -23
- package/dist/commands/clean.js +4 -1
- package/dist/commands/docs-read.js +5 -2
- package/dist/commands/doctor-backfill.js +88 -71
- package/dist/commands/doctor-pipeline.js +59 -46
- package/dist/commands/doctor-usage-telemetry.js +2 -1
- package/dist/commands/heartbeat-post.js +116 -0
- package/dist/commands/heartbeat-token-lock.js +46 -33
- package/dist/commands/heartbeat.js +43 -125
- package/dist/commands/install-update.js +17 -15
- package/dist/commands/issue-show.js +27 -28
- package/dist/commands/issue-write.js +70 -23
- package/dist/commands/issue.js +68 -37
- package/dist/commands/jarvis-live-answer.js +99 -0
- package/dist/commands/jarvis-render.js +1 -94
- package/dist/commands/jarvis-trace.js +30 -22
- package/dist/commands/jarvis-turn-report.js +124 -0
- package/dist/commands/jarvis-turn.js +108 -182
- package/dist/commands/jarvis.js +26 -17
- package/dist/commands/local-args-collector-setup.js +82 -84
- package/dist/commands/local-args-collector-work.js +37 -38
- package/dist/commands/local-args-tower-admin.js +155 -142
- package/dist/commands/local-args-tower-cal.js +54 -48
- package/dist/commands/local-args-tower-careers.js +73 -54
- package/dist/commands/local-args-tower-docs-msg.js +56 -188
- 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 +149 -0
- package/dist/commands/local-args-tower-notes.js +346 -0
- package/dist/commands/local-args-tower-pages.js +100 -378
- package/dist/commands/local-args-tower-settings.js +28 -29
- package/dist/commands/local-args-tower-work.js +52 -33
- package/dist/commands/local-args.js +80 -129
- package/dist/commands/local-help-commands-tower-ops.js +15 -4
- package/dist/commands/local-help-commands-tower.js +11 -4
- 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/mail.js +6 -4
- 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.js +51 -9
- package/dist/commands/notes-accounts.js +49 -32
- package/dist/commands/notes-sync-loop.js +115 -89
- package/dist/commands/notes-wisprflow-login.js +44 -23
- package/dist/commands/notes-writes.js +115 -100
- package/dist/commands/notes.js +33 -55
- package/dist/commands/onboard-flows.js +69 -51
- package/dist/commands/onboard.js +11 -9
- package/dist/commands/ops-render-memory.js +10 -7
- package/dist/commands/ops-render.js +33 -30
- package/dist/commands/ops.js +3 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-outside-roots.js +40 -36
- package/dist/commands/session-sync-plan.js +14 -10
- package/dist/commands/session-sync-upload.js +24 -22
- package/dist/commands/session-sync.js +19 -8
- package/dist/commands/slack.js +25 -16
- package/dist/commands/sources-check.js +13 -7
- package/dist/commands/sources-discovery.js +25 -17
- package/dist/commands/sources-verdict.js +19 -18
- package/dist/commands/speed.js +23 -19
- package/dist/commands/start.js +20 -16
- package/dist/commands/sync-doctor.js +52 -48
- package/dist/commands/sync.js +22 -16
- package/dist/commands/usage-telemetry-toml.js +71 -45
- package/dist/commands/usage.js +91 -67
- package/dist/commands/workbook-render.js +10 -7
- package/dist/evidence-reconcile-client.js +75 -64
- package/dist/evidence-redelivery-plan.js +37 -34
- package/dist/evidence-redelivery.js +32 -29
- package/dist/evidence-transcripts-client.js +20 -20
- package/dist/evidence-upload-object.js +86 -89
- package/dist/evidence-upload-plan.js +94 -83
- package/dist/evidence-upload-terminal.js +45 -50
- package/dist/image-description-session.js +156 -159
- package/dist/local-state-pairing-code.js +39 -31
- package/dist/local-state-status.js +9 -2
- package/dist/onboarding-roots.js +5 -8
- package/dist/process-runner.js +8 -8
- package/dist/raw-evidence-gc.js +33 -38
- package/dist/raw-evidence-stuck-summary.js +7 -14
- package/dist/repo-identity.js +26 -23
- package/dist/scheduled-self-update.js +11 -15
- package/dist/second-install.js +3 -0
- package/dist/slim/codex-rules.js +23 -36
- package/dist/sync-lock.js +5 -15
- package/dist/tower-client.js +36 -53
- package/dist/tower-stream.js +7 -24
- package/dist/upload-agent-artifacts.js +58 -64
- package/dist/upload-envelope-build.js +60 -59
- package/dist/upload-evidence-delivery-reconcile.js +58 -56
- package/dist/upload-evidence-delivery-summary.js +32 -34
- package/dist/upload-ingest-receipt.js +20 -23
- package/dist/upload-session-reports-queue.js +4 -1
- package/dist/upload-session-reports-wire.js +64 -68
- package/dist/upload-sync.js +171 -157
- package/package.json +2 -2
|
@@ -7,7 +7,52 @@ import { collectRawEvidencePack, } from "./raw-evidence.js";
|
|
|
7
7
|
import { generateDumbRiskFlags } from "./risk-flags.js";
|
|
8
8
|
import { resolveTicketBinding, } from "./ticket-binding.js";
|
|
9
9
|
export async function runLocalSourceCollectors(options) {
|
|
10
|
-
const context =
|
|
10
|
+
const context = makeCollectionContext(options);
|
|
11
|
+
const git = await collectGitState(context);
|
|
12
|
+
const car = await collectCarState(context);
|
|
13
|
+
const rawEvidence = await collectConfiguredRawEvidence(context, options);
|
|
14
|
+
const unavailableScans = makeUnconfiguredSourceScans(context);
|
|
15
|
+
// claude_jsonl availability (B.4): "ok" when ~/.claude/projects exists, else
|
|
16
|
+
// skipped with the reason it could not be read — absent, permission-denied,
|
|
17
|
+
// not a directory, or unreadable (BLI-3483) — so an operator sees whether
|
|
18
|
+
// the empty claude funnel is normal or a wall they can take down. Collection
|
|
19
|
+
// itself is gated separately by collect_claude_jsonl in the sync
|
|
20
|
+
// orchestrator.
|
|
21
|
+
const claudeJsonlScan = await makeClaudeJsonlScan(context, options.claudeProjectsDir);
|
|
22
|
+
const candidates = collectBindingCandidates([
|
|
23
|
+
git.scan,
|
|
24
|
+
car.scan,
|
|
25
|
+
rawEvidence.scan,
|
|
26
|
+
claudeJsonlScan,
|
|
27
|
+
...unavailableScans,
|
|
28
|
+
]);
|
|
29
|
+
const binding = resolveTicketBinding({
|
|
30
|
+
activeWorkContext: options.activeWorkContext,
|
|
31
|
+
candidates,
|
|
32
|
+
});
|
|
33
|
+
const gitFacts = isGitStateFacts(git.facts) ? git.facts : null;
|
|
34
|
+
const carFacts = isCarStateFacts(car.facts) ? car.facts : null;
|
|
35
|
+
const rawEvidenceFacts = isRawEvidenceFacts(rawEvidence.facts)
|
|
36
|
+
? rawEvidence.facts
|
|
37
|
+
: null;
|
|
38
|
+
const riskFlags = generateDumbRiskFlags({
|
|
39
|
+
context,
|
|
40
|
+
git: gitFacts,
|
|
41
|
+
binding,
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
scans: [git.scan, car.scan, rawEvidence.scan, claudeJsonlScan, ...unavailableScans],
|
|
45
|
+
facts: {
|
|
46
|
+
git: gitFacts,
|
|
47
|
+
car: carFacts,
|
|
48
|
+
raw_evidence: rawEvidenceFacts,
|
|
49
|
+
},
|
|
50
|
+
binding,
|
|
51
|
+
risk_flags: riskFlags,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function makeCollectionContext(options) {
|
|
55
|
+
return {
|
|
11
56
|
repoRoot: options.repoRoot,
|
|
12
57
|
repoLabel: options.activeWorkContext?.repo_label,
|
|
13
58
|
worktreeLabel: options.activeWorkContext?.worktree_label,
|
|
@@ -19,9 +64,9 @@ export async function runLocalSourceCollectors(options) {
|
|
|
19
64
|
activeTicketId: options.activeWorkContext?.active_ticket_id ?? null,
|
|
20
65
|
now: options.now ?? new Date(),
|
|
21
66
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
67
|
+
}
|
|
68
|
+
async function collectConfiguredRawEvidence(context, options) {
|
|
69
|
+
return options.rawEvidenceStateDir
|
|
25
70
|
? await collectRawEvidencePack(context, {
|
|
26
71
|
stateDir: options.rawEvidenceStateDir,
|
|
27
72
|
repoRoot: options.repoRoot,
|
|
@@ -46,51 +91,15 @@ export async function runLocalSourceCollectors(options) {
|
|
|
46
91
|
scan: makeUnavailableScan(context, "codex_jsonl", "codex-jsonl", "raw_evidence_state_dir_not_configured"),
|
|
47
92
|
facts: null,
|
|
48
93
|
};
|
|
49
|
-
|
|
94
|
+
}
|
|
95
|
+
function makeUnconfiguredSourceScans(context) {
|
|
96
|
+
return [
|
|
50
97
|
makeUnavailableScan(context, "codex_otel", "codex-otel", "codex_otel_export_not_configured"),
|
|
51
98
|
makeUnavailableScan(context, "github_state", "github-state", "github_auth_not_configured"),
|
|
52
99
|
makeUnavailableScan(context, "linear_state", "linear-state", "linear_auth_not_configured"),
|
|
53
100
|
makeUnavailableScan(context, "mcp_local", "mcp-local", "mcp_activity_not_configured"),
|
|
54
101
|
makeUnavailableScan(context, "claude_hooks", "claude-hooks", "claude_hooks_not_present"),
|
|
55
102
|
];
|
|
56
|
-
// claude_jsonl availability (B.4): "ok" when ~/.claude/projects exists, else
|
|
57
|
-
// skipped with the reason it could not be read — absent, permission-denied,
|
|
58
|
-
// not a directory, or unreadable (BLI-3483) — so an operator sees whether
|
|
59
|
-
// the empty claude funnel is normal or a wall they can take down. Collection
|
|
60
|
-
// itself is gated separately by collect_claude_jsonl in the sync
|
|
61
|
-
// orchestrator.
|
|
62
|
-
const claudeJsonlScan = await makeClaudeJsonlScan(context, options.claudeProjectsDir);
|
|
63
|
-
const candidates = collectBindingCandidates([
|
|
64
|
-
git.scan,
|
|
65
|
-
car.scan,
|
|
66
|
-
rawEvidence.scan,
|
|
67
|
-
claudeJsonlScan,
|
|
68
|
-
...unavailableScans,
|
|
69
|
-
]);
|
|
70
|
-
const binding = resolveTicketBinding({
|
|
71
|
-
activeWorkContext: options.activeWorkContext,
|
|
72
|
-
candidates,
|
|
73
|
-
});
|
|
74
|
-
const gitFacts = isGitStateFacts(git.facts) ? git.facts : null;
|
|
75
|
-
const carFacts = isCarStateFacts(car.facts) ? car.facts : null;
|
|
76
|
-
const rawEvidenceFacts = isRawEvidenceFacts(rawEvidence.facts)
|
|
77
|
-
? rawEvidence.facts
|
|
78
|
-
: null;
|
|
79
|
-
const riskFlags = generateDumbRiskFlags({
|
|
80
|
-
context,
|
|
81
|
-
git: gitFacts,
|
|
82
|
-
binding,
|
|
83
|
-
});
|
|
84
|
-
return {
|
|
85
|
-
scans: [git.scan, car.scan, rawEvidence.scan, claudeJsonlScan, ...unavailableScans],
|
|
86
|
-
facts: {
|
|
87
|
-
git: gitFacts,
|
|
88
|
-
car: carFacts,
|
|
89
|
-
raw_evidence: rawEvidenceFacts,
|
|
90
|
-
},
|
|
91
|
-
binding,
|
|
92
|
-
risk_flags: riskFlags,
|
|
93
|
-
};
|
|
94
103
|
}
|
|
95
104
|
function collectBindingCandidates(scans) {
|
|
96
105
|
return scans.flatMap((scan) => scan.ticket_binding_candidates);
|
|
@@ -79,6 +79,9 @@ export async function runGitDiff(args, repoRoot) {
|
|
|
79
79
|
if (!(await repoRootExists(repoRoot))) {
|
|
80
80
|
throw new RepoRootMissingError();
|
|
81
81
|
}
|
|
82
|
+
return captureGitDiff(args, repoRoot);
|
|
83
|
+
}
|
|
84
|
+
function captureGitDiff(args, repoRoot) {
|
|
82
85
|
return new Promise((resolve, reject) => {
|
|
83
86
|
const child = spawn("git", [...args, ...SECRET_EXCLUDING_PATHSPEC], {
|
|
84
87
|
cwd: repoRoot,
|
|
@@ -119,8 +122,7 @@ export async function runGitDiff(args, repoRoot) {
|
|
|
119
122
|
});
|
|
120
123
|
child.on("close", (code, signal) => {
|
|
121
124
|
clearTimeout(timeout);
|
|
122
|
-
//
|
|
123
|
-
// non-zero code are both success here, not failure.
|
|
125
|
+
// Caps deliberately kill the child; SIGTERM and non-zero codes then count as success.
|
|
124
126
|
if (code === 0 || truncated || signal === "SIGTERM") {
|
|
125
127
|
resolve({
|
|
126
128
|
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
@@ -56,18 +56,7 @@ export function sanitizeTextEvidenceForUpload(options) {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
catch (error) {
|
|
59
|
-
|
|
60
|
-
// placeholder goes up instead of unredacted bytes — but it is also
|
|
61
|
-
// indistinguishable downstream from a file that was legitimately masked,
|
|
62
|
-
// so a systematic redactor bug would look like a machine that simply
|
|
63
|
-
// writes a lot of secrets. Never the text, never the field values: name,
|
|
64
|
-
// code and size only (BLI-3238).
|
|
65
|
-
console.error("[raw-evidence-sanitize] redaction crashed; uploading a stub instead of the content", JSON.stringify({
|
|
66
|
-
reason: "redaction_crashed",
|
|
67
|
-
byte_size: originalBytes.byteLength,
|
|
68
|
-
redacted_field_count: options.redactedFields.length,
|
|
69
|
-
...describeError(error),
|
|
70
|
-
}));
|
|
59
|
+
logRedactionCrash(error, originalBytes.byteLength, options.redactedFields.length);
|
|
71
60
|
return stubForCrashedRedaction({
|
|
72
61
|
text: options.text,
|
|
73
62
|
originalBytes,
|
|
@@ -75,6 +64,20 @@ export function sanitizeTextEvidenceForUpload(options) {
|
|
|
75
64
|
});
|
|
76
65
|
}
|
|
77
66
|
}
|
|
67
|
+
function logRedactionCrash(error, byteSize, redactedFieldCount) {
|
|
68
|
+
// The redactor itself crashed. The stub below is the safe answer — a
|
|
69
|
+
// placeholder goes up instead of unredacted bytes — but it is also
|
|
70
|
+
// indistinguishable downstream from a file that was legitimately masked,
|
|
71
|
+
// so a systematic redactor bug would look like a machine that simply
|
|
72
|
+
// writes a lot of secrets. Never the text, never the field values: name,
|
|
73
|
+
// code and size only (BLI-3238).
|
|
74
|
+
console.error("[raw-evidence-sanitize] redaction crashed; uploading a stub instead of the content", JSON.stringify({
|
|
75
|
+
reason: "redaction_crashed",
|
|
76
|
+
byte_size: byteSize,
|
|
77
|
+
redacted_field_count: redactedFieldCount,
|
|
78
|
+
...describeError(error),
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
78
81
|
/**
|
|
79
82
|
* The name itself (`.env.production`, `id_rsa`) is the signal, so nothing in
|
|
80
83
|
* this file is trusted: whatever the redactor left behind is re-checked, and a
|
|
@@ -33,14 +33,7 @@ import { admitEvidence, stageEvidenceBytes, } from "./raw-evidence-collection-st
|
|
|
33
33
|
*/
|
|
34
34
|
export async function collectOneEvidenceFile(collection, options) {
|
|
35
35
|
const names = nameEvidenceFile(options);
|
|
36
|
-
const skip = (reason) =>
|
|
37
|
-
collection.skipped.push({
|
|
38
|
-
kind: options.kind,
|
|
39
|
-
label: names.evidenceLabel,
|
|
40
|
-
reason,
|
|
41
|
-
});
|
|
42
|
-
return false;
|
|
43
|
-
};
|
|
36
|
+
const skip = (reason) => recordEvidenceSkip(collection, options.kind, names.evidenceLabel, reason);
|
|
44
37
|
// Delivery backoff is checked BEFORE the file is read. An object whose commit
|
|
45
38
|
// has failed repeatedly costs nothing at all this cycle — no read, no hash,
|
|
46
39
|
// no copy, no request — and the hold is a named, retryable gap so a held
|
|
@@ -58,29 +51,7 @@ export async function collectOneEvidenceFile(collection, options) {
|
|
|
58
51
|
markCapApplied(collection, options.kind, "max_file_bytes");
|
|
59
52
|
return skip("file_too_large");
|
|
60
53
|
}
|
|
61
|
-
|
|
62
|
-
// base64 for a short marker BEFORE masking, so the masker and the content hash see the final bytes.
|
|
63
|
-
const imagesPrepared = await prepareEmbeddedImages(collection, {
|
|
64
|
-
bytes: read.bytes,
|
|
65
|
-
kind: options.kind,
|
|
66
|
-
sessionId: options.sessionId,
|
|
67
|
-
});
|
|
68
|
-
const sanitized = maskSecretsInTranscript(collection, {
|
|
69
|
-
raw: imagesPrepared,
|
|
70
|
-
kind: options.kind,
|
|
71
|
-
evidenceLabel: names.evidenceLabel,
|
|
72
|
-
secretLikeFileName: names.secretLikeFileName,
|
|
73
|
-
});
|
|
74
|
-
const contentHash = sha256(sanitized.bytes);
|
|
75
|
-
// BLI-11102: a session the server named a redaction stub is re-offered even
|
|
76
|
-
// when these bytes are already durable, because the stub is not a delivery.
|
|
77
|
-
const stubClaim = stubReplacementClaim({
|
|
78
|
-
kind: options.kind,
|
|
79
|
-
stubPointerId: collection.stubReoffers.get(options.sessionId),
|
|
80
|
-
completenessLabel: sanitized.completenessLabel,
|
|
81
|
-
byteSize: sanitized.bytes.byteLength,
|
|
82
|
-
alreadyDurable: collection.skipContentHashes.has(contentHash),
|
|
83
|
-
});
|
|
54
|
+
const { sanitized, contentHash, stubClaim } = await prepareTranscriptForUpload(collection, options, names, read.bytes);
|
|
84
55
|
if (collection.skipContentHashes.has(contentHash) && !stubClaim) {
|
|
85
56
|
collection.reused.push({
|
|
86
57
|
kind: options.kind,
|
|
@@ -104,6 +75,36 @@ export async function collectOneEvidenceFile(collection, options) {
|
|
|
104
75
|
});
|
|
105
76
|
return true;
|
|
106
77
|
}
|
|
78
|
+
function recordEvidenceSkip(collection, kind, label, reason) {
|
|
79
|
+
collection.skipped.push({ kind, label, reason });
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
async function prepareTranscriptForUpload(collection, options, names, bytes) {
|
|
83
|
+
// BLI-16977: describe each distinct embedded image once and, when the one switch says so, swap the
|
|
84
|
+
// base64 for a short marker BEFORE masking, so the masker and the content hash see the final bytes.
|
|
85
|
+
const imagesPrepared = await prepareEmbeddedImages(collection, {
|
|
86
|
+
bytes,
|
|
87
|
+
kind: options.kind,
|
|
88
|
+
sessionId: options.sessionId,
|
|
89
|
+
});
|
|
90
|
+
const sanitized = maskSecretsInTranscript(collection, {
|
|
91
|
+
raw: imagesPrepared,
|
|
92
|
+
kind: options.kind,
|
|
93
|
+
evidenceLabel: names.evidenceLabel,
|
|
94
|
+
secretLikeFileName: names.secretLikeFileName,
|
|
95
|
+
});
|
|
96
|
+
const contentHash = sha256(sanitized.bytes);
|
|
97
|
+
// BLI-11102: a session the server named a redaction stub is re-offered even
|
|
98
|
+
// when these bytes are already durable, because the stub is not a delivery.
|
|
99
|
+
const stubClaim = stubReplacementClaim({
|
|
100
|
+
kind: options.kind,
|
|
101
|
+
stubPointerId: collection.stubReoffers.get(options.sessionId),
|
|
102
|
+
completenessLabel: sanitized.completenessLabel,
|
|
103
|
+
byteSize: sanitized.bytes.byteLength,
|
|
104
|
+
alreadyDurable: collection.skipContentHashes.has(contentHash),
|
|
105
|
+
});
|
|
106
|
+
return { sanitized, contentHash, stubClaim };
|
|
107
|
+
}
|
|
107
108
|
/**
|
|
108
109
|
* The transcript bytes after the image step. Unchanged (the very same buffer) unless this sync has
|
|
109
110
|
* an image session AND the switch is `describe_and_drop` (images become markers where a description
|
|
@@ -16,6 +16,17 @@ export async function runAutostartSelfHeal(paths, options) {
|
|
|
16
16
|
return null;
|
|
17
17
|
if (options.repoRoots.length === 0)
|
|
18
18
|
return null;
|
|
19
|
+
const problem = await readAutostartProblem(paths, options, platform);
|
|
20
|
+
if (!problem)
|
|
21
|
+
return null;
|
|
22
|
+
const guarded = await prepareRepairAttempt(paths, options, platform, problem.detail);
|
|
23
|
+
if (guarded)
|
|
24
|
+
return guarded;
|
|
25
|
+
if (platform === "darwin")
|
|
26
|
+
return scheduleDarwinHeal(options, problem.detail);
|
|
27
|
+
return repairWindowsAutostart(options, platform, problem.absent, problem.detail);
|
|
28
|
+
}
|
|
29
|
+
async function readAutostartProblem(paths, options, platform) {
|
|
19
30
|
const status = await autostartStatus({
|
|
20
31
|
homeDir: options.homeDir,
|
|
21
32
|
repoRoot: options.repoRoots[0],
|
|
@@ -56,6 +67,10 @@ export async function runAutostartSelfHeal(paths, options) {
|
|
|
56
67
|
]
|
|
57
68
|
.join("; ")
|
|
58
69
|
.slice(0, DETAIL_MAX_CHARS) || "registration not loaded";
|
|
70
|
+
return { absent, detail: problem };
|
|
71
|
+
}
|
|
72
|
+
async function prepareRepairAttempt(paths, options, platform, problem) {
|
|
73
|
+
const marker = path.join(paths.state_dir, AUTOSTART_REPAIR_THROTTLE_MARKER);
|
|
59
74
|
// Two installs fighting over one registration re-register each other every
|
|
60
75
|
// tick. Naming that is the repair; rewriting the plist would BE the flapping.
|
|
61
76
|
const secondInstall = await detectSecondCockpitInstall({
|
|
@@ -85,8 +100,9 @@ export async function runAutostartSelfHeal(paths, options) {
|
|
|
85
100
|
}
|
|
86
101
|
await fs.mkdir(paths.state_dir, { recursive: true }).catch(() => undefined);
|
|
87
102
|
await fs.writeFile(marker, now.toISOString()).catch(() => undefined);
|
|
88
|
-
|
|
89
|
-
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
async function repairWindowsAutostart(options, platform, absent, problem) {
|
|
90
106
|
const repaired = await installAutostartAgent({
|
|
91
107
|
homeDir: options.homeDir,
|
|
92
108
|
repoRoot: options.repoRoots[0],
|
|
@@ -172,49 +172,34 @@ function sessionReportEntry(result, context) {
|
|
|
172
172
|
: {}),
|
|
173
173
|
...(result.cwd_basename ? { cwd_basename: result.cwd_basename } : {}),
|
|
174
174
|
...(result.cwd_hash ? { cwd_hash: result.cwd_hash } : {}),
|
|
175
|
-
...(upload
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
? "sync_incomplete_this_pass"
|
|
204
|
-
: NO_UPLOAD_ATTEMPT_RECORDED),
|
|
205
|
-
}
|
|
206
|
-
: {
|
|
207
|
-
// BLI-3272: the upload policy refused this session's
|
|
208
|
-
// attribution state. That refusal used to spread `{}` here, so
|
|
209
|
-
// the row landed with upload_state NULL and upload_reason NULL
|
|
210
|
-
// — a withhold that named nothing, on 1,256 production
|
|
211
|
-
// sessions. It is a decision like any other and it says so.
|
|
212
|
-
// Any reason the pipeline did record still wins: it is the more
|
|
213
|
-
// specific answer.
|
|
214
|
-
upload_state: "not_uploaded",
|
|
215
|
-
upload_reason: context.noUploadReasonByKey.get(key) ??
|
|
216
|
-
notUploadableAttributionStateReason(result.reason),
|
|
217
|
-
}),
|
|
175
|
+
...describeSessionUpload(result, context, key, upload, priorDurablePointer),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function describeSessionUpload(result, context, key, upload, priorDurablePointer) {
|
|
179
|
+
if (upload) {
|
|
180
|
+
return {
|
|
181
|
+
raw_evidence_pointer_id: upload.raw_evidence_pointer_id,
|
|
182
|
+
upload_state: upload.upload_state,
|
|
183
|
+
...(upload.upload_state === "upload_failed"
|
|
184
|
+
? { upload_reason: upload.reason ?? NO_UPLOAD_ATTEMPT_RECORDED }
|
|
185
|
+
: {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (priorDurablePointer) {
|
|
189
|
+
return { raw_evidence_pointer_id: priorDurablePointer, upload_state: "reused_existing" };
|
|
190
|
+
}
|
|
191
|
+
if (isRawEvidenceUploadableAttributionState(result.state, result.worktree !== null)) {
|
|
192
|
+
// No upload attempt still needs a reason, even when no more specific cause was recorded.
|
|
193
|
+
return {
|
|
194
|
+
upload_state: "not_uploaded",
|
|
195
|
+
upload_reason: context.noUploadReasonByKey.get(key) ??
|
|
196
|
+
(context.anySyncIncomplete ? "sync_incomplete_this_pass" : NO_UPLOAD_ATTEMPT_RECORDED),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// The attribution policy's refusal is an outcome; preserve any more specific recorded reason.
|
|
200
|
+
return {
|
|
201
|
+
upload_state: "not_uploaded",
|
|
202
|
+
upload_reason: context.noUploadReasonByKey.get(key) ?? notUploadableAttributionStateReason(result.reason),
|
|
218
203
|
};
|
|
219
204
|
}
|
|
220
205
|
/**
|
|
@@ -245,22 +230,8 @@ export function buildAgentSessionReport(options) {
|
|
|
245
230
|
* damping counts, and the two budget totals.
|
|
246
231
|
*/
|
|
247
232
|
export function buildAgentSessionSummary(options) {
|
|
248
|
-
const sidecarOutcomes
|
|
249
|
-
const
|
|
250
|
-
const sidecarsCollected = attributedClaude.reduce((total, result) => total +
|
|
251
|
-
result.sidecar_files.filter((sidecar) => !sidecar.skipped_reason).length, 0);
|
|
252
|
-
const sidecarsSkipped = options.claudeAttribution.results.reduce((total, result) => total +
|
|
253
|
-
result.sidecar_files.filter((sidecar) => sidecar.skipped_reason).length, 0);
|
|
254
|
-
const codex = {
|
|
255
|
-
scanned: options.codexAttribution.scanned_file_count,
|
|
256
|
-
attributed: options.codexAttribution.counts.attributed,
|
|
257
|
-
attributed_fallback: options.codexAttribution.counts.attributed_fallback,
|
|
258
|
-
ambiguous: options.codexAttribution.counts.ambiguous,
|
|
259
|
-
unattributed: options.codexAttribution.counts.unattributed,
|
|
260
|
-
skipped: options.codexAttribution.counts.skipped,
|
|
261
|
-
stale: options.codexStaleCount,
|
|
262
|
-
read_failures: codexAttributionReadFailureCount(options.codexAttribution),
|
|
263
|
-
};
|
|
233
|
+
const { sidecarOutcomes, sidecarsCollected, sidecarsSkipped } = countClaudeSidecars(options);
|
|
234
|
+
const codex = makeCodexFunnel(options);
|
|
264
235
|
const claude = {
|
|
265
236
|
scanned: options.claudeAttribution.scanned_session_count,
|
|
266
237
|
attributed: options.claudeAttribution.counts.attributed,
|
|
@@ -298,4 +269,25 @@ export function buildAgentSessionSummary(options) {
|
|
|
298
269
|
files_deferred_byte_budget: options.outcomes.reduce((total, outcome) => total + outcome.sync.raw_evidence_deferred_byte_budget, 0),
|
|
299
270
|
files_deferred_object_budget: options.outcomes.reduce((total, outcome) => total + outcome.sync.raw_evidence_deferred_object_budget, 0),
|
|
300
271
|
};
|
|
272
|
+
}
|
|
273
|
+
function countClaudeSidecars(options) {
|
|
274
|
+
const sidecarOutcomes = options.outcomes.flatMap((outcome) => outcome.sync.raw_evidence_outcomes.filter((upload) => upload.kind === "claude_jsonl_sidecar"));
|
|
275
|
+
const attributedClaude = options.claudeAttribution.results.filter((result) => isLiveRawEvidenceSyncAttribution(result.state, result.reason, result.worktree !== null));
|
|
276
|
+
const sidecarsCollected = attributedClaude.reduce((total, result) => total +
|
|
277
|
+
result.sidecar_files.filter((sidecar) => !sidecar.skipped_reason).length, 0);
|
|
278
|
+
const sidecarsSkipped = options.claudeAttribution.results.reduce((total, result) => total +
|
|
279
|
+
result.sidecar_files.filter((sidecar) => sidecar.skipped_reason).length, 0);
|
|
280
|
+
return { sidecarOutcomes, sidecarsCollected, sidecarsSkipped };
|
|
281
|
+
}
|
|
282
|
+
function makeCodexFunnel(options) {
|
|
283
|
+
return {
|
|
284
|
+
scanned: options.codexAttribution.scanned_file_count,
|
|
285
|
+
attributed: options.codexAttribution.counts.attributed,
|
|
286
|
+
attributed_fallback: options.codexAttribution.counts.attributed_fallback,
|
|
287
|
+
ambiguous: options.codexAttribution.counts.ambiguous,
|
|
288
|
+
unattributed: options.codexAttribution.counts.unattributed,
|
|
289
|
+
skipped: options.codexAttribution.counts.skipped,
|
|
290
|
+
stale: options.codexStaleCount,
|
|
291
|
+
read_failures: codexAttributionReadFailureCount(options.codexAttribution),
|
|
292
|
+
};
|
|
301
293
|
}
|
|
@@ -31,10 +31,6 @@ function top(counts) {
|
|
|
31
31
|
export function observeTranscripts(tool, samples) {
|
|
32
32
|
const models = new Map(), efforts = new Map();
|
|
33
33
|
let version = null;
|
|
34
|
-
const bump = (map, value) => {
|
|
35
|
-
if (typeof value === "string" && value && !value.startsWith("<"))
|
|
36
|
-
map.set(value, (map.get(value) ?? 0) + 1);
|
|
37
|
-
};
|
|
38
34
|
for (const sample of samples)
|
|
39
35
|
for (const line of sample.split("\n")) {
|
|
40
36
|
if (!line.startsWith("{"))
|
|
@@ -46,25 +42,33 @@ export function observeTranscripts(tool, samples) {
|
|
|
46
42
|
catch {
|
|
47
43
|
continue;
|
|
48
44
|
} // a chunk edge can cut a line
|
|
49
|
-
|
|
50
|
-
if (tool === "claude") {
|
|
51
|
-
if (typeof entry.version === "string")
|
|
52
|
-
version = newerVersion(version, entry.version);
|
|
53
|
-
const message = entry.message && typeof entry.message === "object" ? entry.message : {};
|
|
54
|
-
if (entry.type === "assistant")
|
|
55
|
-
bump(models, message.model);
|
|
56
|
-
}
|
|
57
|
-
else if (entry.type === "session_meta") {
|
|
58
|
-
if (typeof payload.cli_version === "string")
|
|
59
|
-
version = newerVersion(version, payload.cli_version);
|
|
60
|
-
}
|
|
61
|
-
else if (entry.type === "turn_context") {
|
|
62
|
-
bump(models, payload.model);
|
|
63
|
-
bump(efforts, payload.effort);
|
|
64
|
-
}
|
|
45
|
+
version = observeTranscriptEntry(tool, entry, version, models, efforts);
|
|
65
46
|
}
|
|
66
47
|
return { version, model: top(models), effort: top(efforts), files: samples.length };
|
|
67
48
|
}
|
|
49
|
+
function observeTranscriptEntry(tool, entry, version, models, efforts) {
|
|
50
|
+
const payload = entry.payload && typeof entry.payload === "object" ? entry.payload : {};
|
|
51
|
+
if (tool === "claude") {
|
|
52
|
+
if (typeof entry.version === "string")
|
|
53
|
+
version = newerVersion(version, entry.version);
|
|
54
|
+
const message = entry.message && typeof entry.message === "object" ? entry.message : {};
|
|
55
|
+
if (entry.type === "assistant")
|
|
56
|
+
countObservedValue(models, message.model);
|
|
57
|
+
}
|
|
58
|
+
else if (entry.type === "session_meta") {
|
|
59
|
+
if (typeof payload.cli_version === "string")
|
|
60
|
+
version = newerVersion(version, payload.cli_version);
|
|
61
|
+
}
|
|
62
|
+
else if (entry.type === "turn_context") {
|
|
63
|
+
countObservedValue(models, payload.model);
|
|
64
|
+
countObservedValue(efforts, payload.effort);
|
|
65
|
+
}
|
|
66
|
+
return version;
|
|
67
|
+
}
|
|
68
|
+
function countObservedValue(map, value) {
|
|
69
|
+
if (typeof value === "string" && value && !value.startsWith("<"))
|
|
70
|
+
map.set(value, (map.get(value) ?? 0) + 1);
|
|
71
|
+
}
|
|
68
72
|
async function sampleFile(file) {
|
|
69
73
|
let handle;
|
|
70
74
|
try {
|
|
@@ -110,14 +114,17 @@ async function newestCodexFiles(home) {
|
|
|
110
114
|
for (const year of await newestFirst(root))
|
|
111
115
|
for (const month of await newestFirst(path.join(root, year)))
|
|
112
116
|
for (const day of await newestFirst(path.join(root, year, month))) {
|
|
113
|
-
|
|
114
|
-
if (name.endsWith(".jsonl") && files.length < TRANSCRIPT_FILES)
|
|
115
|
-
files.push(path.join(root, year, month, day, name));
|
|
117
|
+
await collectCodexDayFiles(path.join(root, year, month, day), files);
|
|
116
118
|
if (files.length >= TRANSCRIPT_FILES)
|
|
117
119
|
return files;
|
|
118
120
|
}
|
|
119
121
|
return files;
|
|
120
122
|
}
|
|
123
|
+
async function collectCodexDayFiles(directory, files) {
|
|
124
|
+
for (const name of await (await entries(directory)).sort().reverse())
|
|
125
|
+
if (name.endsWith(".jsonl") && files.length < TRANSCRIPT_FILES)
|
|
126
|
+
files.push(path.join(directory, name));
|
|
127
|
+
}
|
|
121
128
|
/** Text samples of the newest transcripts of one tool in the last 30 days. Never throws. */
|
|
122
129
|
export async function readTranscriptSamples(tool, home, now = Date.now()) {
|
|
123
130
|
try {
|