@bli-cockpit/cli 0.2.29 → 0.2.31

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.
Files changed (47) hide show
  1. package/README.md +22 -15
  2. package/dist/adapters/agent-image-evidence.js +4 -0
  3. package/dist/adapters/attribution-core.js +12 -0
  4. package/dist/adapters/car-state.js +12 -1
  5. package/dist/adapters/claude-attribution.js +81 -7
  6. package/dist/adapters/codex-attribution.js +37 -3
  7. package/dist/adapters/raw-evidence-manifest.js +12 -1
  8. package/dist/adapters/raw-evidence-pack-store.js +28 -3
  9. package/dist/adapters/raw-evidence-sanitize.js +46 -2
  10. package/dist/adapters/raw-evidence.js +51 -6
  11. package/dist/agent-rules.js +34 -3
  12. package/dist/autostart.js +3 -0
  13. package/dist/backfill-lock.js +22 -1
  14. package/dist/commands/backfill.js +41 -7
  15. package/dist/commands/cli-io.js +3 -0
  16. package/dist/commands/collection-report.js +25 -21
  17. package/dist/commands/doctor.js +54 -21
  18. package/dist/commands/install-receipts.js +43 -6
  19. package/dist/commands/install-update.js +3 -1
  20. package/dist/commands/local-args.js +4 -0
  21. package/dist/commands/local-auth.js +14 -0
  22. package/dist/commands/local-help.js +9 -9
  23. package/dist/commands/local.js +34 -15
  24. package/dist/commands/public-root.js +1 -1
  25. package/dist/commands/session-sync.js +35 -6
  26. package/dist/commands/status.js +82 -27
  27. package/dist/cursors/backfill-cursor.js +23 -2
  28. package/dist/cursors/raw-evidence-cursor.js +14 -1
  29. package/dist/discovery-limits.js +12 -1
  30. package/dist/evidence-upload-client.js +41 -4
  31. package/dist/health-detail.js +111 -2
  32. package/dist/local-state.js +98 -11
  33. package/dist/onboarding-roots.js +3 -0
  34. package/dist/raw-evidence-attribution-policy.js +7 -0
  35. package/dist/raw-evidence-gc.js +6 -1
  36. package/dist/raw-evidence-staging.js +12 -1
  37. package/dist/repo-identity.js +15 -1
  38. package/dist/scheduled-self-update.js +14 -1
  39. package/dist/spool/install-event-outbox.js +11 -1
  40. package/dist/spool/local-spool.js +3 -0
  41. package/dist/sync-lock.js +24 -1
  42. package/dist/upload-agent-artifacts.js +11 -1
  43. package/dist/upload-envelope.js +30 -3
  44. package/dist/upload-http.js +10 -0
  45. package/dist/upload-session-reports.js +36 -3
  46. package/dist/upload.js +16 -5
  47. package/package.json +2 -2
@@ -11,6 +11,7 @@
11
11
  * throwing.
12
12
  */
13
13
  import { AgentImageArtifactReportRequestSchema, } from "@bli-cockpit/telemetry-core";
14
+ import { describeError } from "./health-detail.js";
14
15
  import { readResponseJson } from "./upload-http.js";
15
16
  export async function reportAgentImageArtifacts(options) {
16
17
  const artifacts = agentArtifactsFromEvidence(options);
@@ -60,7 +61,16 @@ export async function reportAgentImageArtifacts(options) {
60
61
  recorded_count: Number.isFinite(recordedCount) ? recordedCount : 0,
61
62
  };
62
63
  }
63
- catch {
64
+ catch (error) {
65
+ // The label stays `report_network_error` — it is the wire contract. What
66
+ // it never carried is WHICH network error, and DNS failure, TLS refusal,
67
+ // a proxy reset and a body that would not serialize are four different
68
+ // repairs wearing one name (BLI-3238).
69
+ console.error("[agent-artifacts] report request failed before a status came back", JSON.stringify({
70
+ reason: "report_network_error",
71
+ artifact_count: artifacts.length,
72
+ ...describeError(error),
73
+ }));
64
74
  return { posted: false, reason: "report_network_error", recorded_count: 0 };
65
75
  }
66
76
  }
@@ -24,6 +24,7 @@ import { getCollectorRuntimePaths, LOCAL_COLLECTOR_VERSION, readLocalCollectorCo
24
24
  import { runLocalSourceCollectors } from "./adapters/local-sources.js";
25
25
  import { defaultCodexSessionDirs, } from "./adapters/codex-attribution.js";
26
26
  import { normalizeDashboardUrl } from "./upload-http.js";
27
+ import { describeError, isMissingFileFailure } from "./health-detail.js";
27
28
  export class LocalUploadBlockedError extends Error {
28
29
  blocker;
29
30
  retry_hint;
@@ -40,7 +41,16 @@ export async function buildLocalAmbientEnvelope(options = {}) {
40
41
  const collector = await readPairedCollector(paths);
41
42
  const { config, sessionFile, session } = collector;
42
43
  const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
43
- const activeContext = await readLocalWorkContextForRepo(paths, repoRoot).catch(() => {
44
+ const activeContext = await readLocalWorkContextForRepo(paths, repoRoot).catch((error) => {
45
+ // The operator is told "run `cockpit start`", which is right when the file
46
+ // is simply absent and wrong when it exists and will not parse — the same
47
+ // advice, forever, on a machine that has already run it (BLI-3238).
48
+ if (!isMissingFileFailure(error)) {
49
+ console.error("[upload-envelope] work context present but unreadable, reporting it as missing", JSON.stringify({
50
+ reason: "missing_context",
51
+ ...describeError(error),
52
+ }));
53
+ }
44
54
  throw new LocalUploadBlockedError("missing_context", "Active work context missing. Run `cockpit start --workspace \"$PWD\"` before `cockpit sync`.", "cockpit start --workspace \"$PWD\"");
45
55
  });
46
56
  const repoLabel = safeRepoLabel(activeContext.repo_label ?? repoRoot);
@@ -132,10 +142,27 @@ export async function buildLocalAmbientEnvelope(options = {}) {
132
142
  * with no next step is what turns a five-second fix into a support thread.
133
143
  */
134
144
  async function readPairedCollector(paths) {
135
- const config = await readLocalCollectorConfig(paths).catch(() => {
145
+ const config = await readLocalCollectorConfig(paths).catch((error) => {
146
+ // `not_installed` tells the operator to reinstall the CLI. That is the
147
+ // wrong instruction for a config that exists and is corrupt, and there was
148
+ // no way to tell which one this machine hit.
149
+ if (!isMissingFileFailure(error)) {
150
+ console.error("[upload-envelope] collector config present but unreadable, reporting it as not installed", JSON.stringify({
151
+ reason: "not_installed",
152
+ ...describeError(error),
153
+ }));
154
+ }
136
155
  throw new LocalUploadBlockedError("not_installed", "Local collector config missing. Install/update the CLI, then run `cockpit do-everything` before `cockpit sync`.", "npm install -g @bli-cockpit/cli@latest && cockpit do-everything");
137
156
  });
138
- const sessionFile = await readLocalCollectorSessionFile(paths).catch(() => {
157
+ const sessionFile = await readLocalCollectorSessionFile(paths).catch((error) => {
158
+ // Same trap on the pairing half: `unpaired` sends the operator to
159
+ // `cockpit login`, which does not fix an unreadable session file.
160
+ if (!isMissingFileFailure(error)) {
161
+ console.error("[upload-envelope] session file present but unreadable, reporting the machine as unpaired", JSON.stringify({
162
+ reason: "unpaired",
163
+ ...describeError(error),
164
+ }));
165
+ }
139
166
  throw new LocalUploadBlockedError("unpaired", "No paired collector session found. Run `cockpit login` or `cockpit pair` before `cockpit sync`.", "cockpit login");
140
167
  });
141
168
  const session = await readLocalSessionReference(paths);
@@ -21,6 +21,16 @@ export async function readResponseJson(response) {
21
21
  return JSON.parse(text);
22
22
  }
23
23
  catch {
24
+ // The text itself is never logged: a proxy error page is unbounded and can
25
+ // carry anything. The shape is the useful part — an HTML body under a 200
26
+ // means something in front of the dashboard answered instead of it, which
27
+ // reads downstream as an ordinary upload failure (BLI-3238).
28
+ console.error("[cockpit-http] reply was not JSON", JSON.stringify({
29
+ reason: "response_body_not_json",
30
+ http_status: response.status,
31
+ byte_size: text.length,
32
+ content_type: response.headers.get("content-type") ?? "none",
33
+ }));
24
34
  return { message: text };
25
35
  }
26
36
  }
@@ -27,6 +27,7 @@ import { getCollectorRuntimePaths, LOCAL_COLLECTOR_VERSION, readLocalCollectorCo
27
27
  import { readLocalUploadSpoolState, recordPendingSessionReport, recordSessionReportFailure, recordSessionReportSuccess, } from "./spool/local-spool.js";
28
28
  import { makeCollectorProvenance, makeUploadWorkContext, safeRepoLabel, } from "./upload-envelope.js";
29
29
  import { normalizeDashboardUrl, readResponseJson } from "./upload-http.js";
30
+ import { describeError } from "./health-detail.js";
30
31
  /** Attempts per chunk, and the linear 250ms-per-attempt backoff between them. */
31
32
  const DEFAULT_REPORT_ATTEMPTS = 3;
32
33
  const REPORT_RETRY_BACKOFF_STEP_MS = 250;
@@ -73,7 +74,19 @@ export async function postCodexSessionReport(options) {
73
74
  sessions: options.sessions,
74
75
  });
75
76
  }
76
- catch {
77
+ catch (error) {
78
+ // `collector_not_ready` covers this whole block — config read, session
79
+ // read, work-context read AND the report request itself. The label is
80
+ // kept (it is what the receipt contract carries) but it is broader than
81
+ // its name suggests: a network failure inside `reportCodexSessionAttributions`
82
+ // also lands here and reads as "this machine is not set up". The detail is
83
+ // now the only way to tell those apart. Flagged, not reclassified —
84
+ // narrowing the label is a contract change (BLI-3238).
85
+ console.error("[session-reports] could not report session attributions", JSON.stringify({
86
+ reason: "collector_not_ready",
87
+ session_count: options.sessions.length,
88
+ ...describeError(error),
89
+ }));
77
90
  return emptyCodexSessionReportResult("collector_not_ready");
78
91
  }
79
92
  }
@@ -125,7 +138,15 @@ export async function flushPendingCodexSessionReports(options) {
125
138
  readLocalSessionReference(paths),
126
139
  ]);
127
140
  }
128
- catch {
141
+ catch (error) {
142
+ // This marks every queued report as failed. The count is what makes it
143
+ // worth a line: an unreadable session file here strands N reports at once
144
+ // and the only visible trace is a spool that stops draining (BLI-3238).
145
+ console.error("[session-reports] session file unreadable, failing the pending reports", JSON.stringify({
146
+ reason: "collector_not_ready",
147
+ pending_report_count: state.pending_session_reports.length,
148
+ ...describeError(error),
149
+ }));
129
150
  return await failPendingSessionReports(paths, state.pending_session_reports, attemptedAt, "collector_not_ready");
130
151
  }
131
152
  if (session.session_state !== "valid") {
@@ -311,7 +332,19 @@ async function postCodexSessionAttributionChunk(options) {
311
332
  }
312
333
  }
313
334
  }
314
- catch {
335
+ catch (error) {
336
+ // Logged per attempt, on purpose: the interesting failure is the one
337
+ // that repeats. Three identical ECONNRESETs and one DNS failure followed
338
+ // by two resets are different stories, and `report_network_error` alone
339
+ // tells neither (BLI-3238).
340
+ console.error("[session-reports] report attempt failed before a status came back", JSON.stringify({
341
+ reason: "report_network_error",
342
+ attempt,
343
+ max_attempts: maxAttempts,
344
+ batch_index: options.batchIndex,
345
+ session_count: options.sessions.length,
346
+ ...describeError(error),
347
+ }));
315
348
  lastStatus = null;
316
349
  lastFailureReason = "report_network_error";
317
350
  }
package/dist/upload.js CHANGED
@@ -6,6 +6,7 @@ import { recordUploadBlocked, recordUploadFailure, recordUploadSuccess, } from "
6
6
  import { buildLocalAmbientEnvelope, LocalUploadBlockedError, } from "./upload-envelope.js";
7
7
  import { applyRawEvidenceUploadOutcomes, hasRetryableEvidenceGap, logEvidenceBackoffBypassed, logEvidenceHeldByBackoff, logPermanentlyRejectedEvidence, partitionHeldEvidenceFiles, permanentEvidenceFailureReason, persistDeliveryAttempts, retrySourcesForFailedSync, retryableEvidenceGapReason, summarizeRawEvidenceDelivery, } from "./upload-evidence-delivery.js";
8
8
  import { reportAgentImageArtifacts } from "./upload-agent-artifacts.js";
9
+ import { describeError } from "./health-detail.js";
9
10
  import { isNonEmptyString, readResponseJson, responseErrorMessage, } from "./upload-http.js";
10
11
  export { LocalUploadBlockedError, buildLocalAmbientEnvelope, } from "./upload-envelope.js";
11
12
  export { partitionHeldEvidenceFiles } from "./upload-evidence-delivery.js";
@@ -76,11 +77,21 @@ export async function syncLocalAmbientEnvelope(options = {}) {
76
77
  outcomes: uploadOutcomes,
77
78
  reused: built.raw_evidence_facts?.reused ?? [],
78
79
  cursorObjects: cursor.objects,
79
- }).catch(() => ({
80
- posted: false,
81
- reason: "agent_artifact_report_local_error",
82
- recorded_count: 0,
83
- }));
80
+ }).catch((error) => {
81
+ // The whole point of `agent_artifact_report_local_error` is that it is
82
+ // NOT the network error the reporter already labels itself — it is a
83
+ // throw from our own code on the way in. That distinction is only
84
+ // useful if the throw says what it was (BLI-3238).
85
+ console.error("[cockpit-sync] agent image artifact report threw locally", JSON.stringify({
86
+ reason: "agent_artifact_report_local_error",
87
+ ...describeError(error),
88
+ }));
89
+ return {
90
+ posted: false,
91
+ reason: "agent_artifact_report_local_error",
92
+ recorded_count: 0,
93
+ };
94
+ });
84
95
  await recordIngestedSyncOutcome({
85
96
  paths,
86
97
  options,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.29",
3
+ "version": "0.2.31",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,6 @@
26
26
  "test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
27
27
  },
28
28
  "dependencies": {
29
- "@bli-cockpit/telemetry-core": "0.1.22"
29
+ "@bli-cockpit/telemetry-core": "0.1.23"
30
30
  }
31
31
  }