@bli-cockpit/cli 0.2.54 → 0.2.56

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 (60) hide show
  1. package/dist/adapters/attribution-core-fallbacks.js +247 -0
  2. package/dist/adapters/attribution-core-paths.js +182 -0
  3. package/dist/adapters/attribution-core-score.js +159 -0
  4. package/dist/adapters/attribution-core-types.js +13 -0
  5. package/dist/adapters/attribution-core.js +13 -565
  6. package/dist/adapters/claude-attribution-discovery.js +186 -0
  7. package/dist/adapters/claude-attribution-score.js +204 -0
  8. package/dist/adapters/claude-attribution-signals.js +180 -0
  9. package/dist/adapters/claude-attribution-types.js +25 -0
  10. package/dist/adapters/claude-attribution.js +14 -569
  11. package/dist/commands/doctor-access.js +129 -0
  12. package/dist/commands/doctor-pipeline.js +326 -0
  13. package/dist/commands/doctor-registration.js +105 -0
  14. package/dist/commands/doctor-report.js +111 -0
  15. package/dist/commands/doctor-update.js +120 -0
  16. package/dist/commands/doctor.js +8 -753
  17. package/dist/commands/heartbeat.js +8 -0
  18. package/dist/commands/jarvis-contracts.js +8 -0
  19. package/dist/commands/jarvis-render.js +413 -0
  20. package/dist/commands/jarvis-turn.js +305 -0
  21. package/dist/commands/jarvis.js +23 -698
  22. package/dist/commands/local-args-collector-setup.js +250 -0
  23. package/dist/commands/local-args-collector-status.js +227 -0
  24. package/dist/commands/local-args-collector-work.js +175 -0
  25. package/dist/commands/local-args-collector.js +19 -624
  26. package/dist/commands/local-args-tower-admin.js +456 -0
  27. package/dist/commands/local-args-tower-chat.js +194 -0
  28. package/dist/commands/local-args-tower-pages.js +314 -0
  29. package/dist/commands/local-args-tower.js +13 -880
  30. package/dist/commands/local-help.js +10 -2
  31. package/dist/commands/onboard-completion.js +136 -0
  32. package/dist/commands/onboard-flows.js +165 -0
  33. package/dist/commands/onboard-setup.js +102 -0
  34. package/dist/commands/onboard.js +5 -392
  35. package/dist/commands/public-root.js +1 -1
  36. package/dist/commands/session-sync-counters.js +55 -0
  37. package/dist/commands/session-sync-health.js +8 -1
  38. package/dist/commands/session-sync-plan.js +47 -7
  39. package/dist/commands/session-sync-scan.js +4 -4
  40. package/dist/commands/session-sync.js +6 -0
  41. package/dist/commands/settings-render.js +27 -0
  42. package/dist/commands/sync-followups.js +5 -1
  43. package/dist/commands/sync.js +5 -1
  44. package/dist/commands/team-device-reasons.js +16 -0
  45. package/dist/commands/team.js +87 -7
  46. package/dist/evidence-upload-client.js +14 -763
  47. package/dist/evidence-upload-object.js +181 -0
  48. package/dist/evidence-upload-plan.js +233 -0
  49. package/dist/evidence-upload-terminal.js +309 -0
  50. package/dist/evidence-upload-transport.js +104 -0
  51. package/dist/spool/local-spool-io.js +122 -0
  52. package/dist/spool/local-spool-mutations.js +174 -0
  53. package/dist/spool/local-spool-parse.js +143 -0
  54. package/dist/spool/local-spool-types.js +22 -0
  55. package/dist/spool/local-spool.js +20 -426
  56. package/dist/upload-evidence-delivery-offer.js +144 -0
  57. package/dist/upload-evidence-delivery-reconcile.js +134 -0
  58. package/dist/upload-evidence-delivery-summary.js +205 -0
  59. package/dist/upload-evidence-delivery.js +12 -482
  60. package/package.json +3 -3
@@ -0,0 +1,134 @@
1
+ import { EvidenceCompletenessPayloadSchema, } from "@bli-cockpit/telemetry-core";
2
+ /**
3
+ * 4. Reconciling the envelope with what actually became durable.
4
+ *
5
+ * Ingest refuses pointers whose objects never became durable, so failed
6
+ * uploads are pruned from the envelope instead of failing the whole sync.
7
+ * Successful upload responses can also carry server-side sanitized hash and
8
+ * redaction metadata; apply those before ingest so refs describe the bytes
9
+ * actually stored in the durable bucket.
10
+ */
11
+ export function applyRawEvidenceUploadOutcomes(envelope, outcomes) {
12
+ // Content-addressed keys mean one pointer id can carry several outcomes
13
+ // (byte-identical files); the pointer is durable if ANY outcome succeeded.
14
+ const durablePointers = new Map();
15
+ for (const outcome of outcomes) {
16
+ if (outcome.upload_state === "upload_failed")
17
+ continue;
18
+ durablePointers.set(outcome.pointer.raw_evidence_pointer_id, outcome.pointer);
19
+ }
20
+ const durablePointerIds = new Set([...durablePointers.keys()]);
21
+ const failedPointerIds = new Set(outcomes
22
+ .filter((outcome) => outcome.upload_state === "upload_failed" &&
23
+ !durablePointerIds.has(outcome.pointer.raw_evidence_pointer_id))
24
+ .map((outcome) => outcome.pointer.raw_evidence_pointer_id));
25
+ const failedOutcomes = outcomes.filter((outcome) => failedPointerIds.has(outcome.pointer.raw_evidence_pointer_id));
26
+ if (failedPointerIds.size === 0 && durablePointers.size === 0)
27
+ return envelope;
28
+ return {
29
+ ...envelope,
30
+ events: envelope.events.map((event) => ({
31
+ ...event,
32
+ metrics: failedPointerIds.size > 0
33
+ ? {
34
+ ...event.metrics,
35
+ evidence_failed_count: (event.metrics["evidence_failed_count"] ?? 0) +
36
+ failedOutcomes.length,
37
+ }
38
+ : event.metrics,
39
+ attributes: failedPointerIds.size > 0 && event.evidence_completeness
40
+ ? {
41
+ ...event.attributes,
42
+ evidence_completeness_schema_version: event.evidence_completeness.schema_version,
43
+ evidence_completeness_status: "partial",
44
+ evidence_incomplete: true,
45
+ }
46
+ : event.attributes,
47
+ evidence_completeness: failedPointerIds.size > 0 && event.evidence_completeness
48
+ ? markCompletenessUploadFailures(event.evidence_completeness, failedOutcomes)
49
+ : event.evidence_completeness,
50
+ raw_evidence_pointers: event.raw_evidence_pointers.flatMap((pointer) => {
51
+ const pointerId = pointer.raw_evidence_pointer_id;
52
+ if (failedPointerIds.has(pointerId))
53
+ return [];
54
+ return [durablePointers.get(pointerId) ?? pointer];
55
+ }),
56
+ redaction: failedPointerIds.size > 0
57
+ ? {
58
+ ...event.redaction,
59
+ raw_evidence_pointer_ids: event.redaction.raw_evidence_pointer_ids.filter((pointerId) => !failedPointerIds.has(pointerId)),
60
+ }
61
+ : event.redaction,
62
+ })),
63
+ };
64
+ }
65
+ /**
66
+ * Fold upload failures back into the completeness payload the event carries, so
67
+ * a downstream reader sees `partial` and the per-source failure counts rather
68
+ * than a complete-looking payload with fewer pointers than it claims.
69
+ */
70
+ function markCompletenessUploadFailures(completeness, failedOutcomes) {
71
+ const failureCounts = new Map();
72
+ for (const outcome of failedOutcomes) {
73
+ const source = outcome.kind ?? "raw_evidence";
74
+ failureCounts.set(source, (failureCounts.get(source) ?? 0) + 1);
75
+ }
76
+ const totalFailures = [...failureCounts.values()].reduce((sum, count) => sum + count, 0);
77
+ const sourceCounts = [...completeness.source_counts];
78
+ for (const [source, count] of failureCounts) {
79
+ const existingIndex = sourceCounts.findIndex((entry) => entry.source === source);
80
+ if (existingIndex === -1) {
81
+ sourceCounts.push({
82
+ source,
83
+ scanned_count: 0,
84
+ included_count: 0,
85
+ skipped_count: 0,
86
+ truncated_count: 0,
87
+ deferred_count: 0,
88
+ reused_count: 0,
89
+ failed_count: count,
90
+ });
91
+ continue;
92
+ }
93
+ const existing = sourceCounts[existingIndex];
94
+ if (!existing)
95
+ continue;
96
+ sourceCounts[existingIndex] = {
97
+ ...existing,
98
+ failed_count: existing.failed_count + count,
99
+ };
100
+ }
101
+ const failureReasons = [...completeness.failure_reasons];
102
+ for (const [source, count] of failureCounts) {
103
+ const reason = "upload_failed";
104
+ const existingIndex = failureReasons.findIndex((entry) => entry.source === source && entry.reason === reason);
105
+ if (existingIndex === -1) {
106
+ failureReasons.push({ source, reason, count });
107
+ }
108
+ else {
109
+ const existing = failureReasons[existingIndex];
110
+ if (existing) {
111
+ failureReasons[existingIndex] = {
112
+ ...existing,
113
+ count: existing.count + count,
114
+ };
115
+ }
116
+ }
117
+ }
118
+ return EvidenceCompletenessPayloadSchema.parse({
119
+ ...completeness,
120
+ status: "partial",
121
+ source_counts: sourceCounts,
122
+ totals: {
123
+ ...completeness.totals,
124
+ failed_count: completeness.totals.failed_count + totalFailures,
125
+ },
126
+ failure_reasons: failureReasons.sort((a, b) => `${a.source}:${a.reason}`.localeCompare(`${b.source}:${b.reason}`)),
127
+ notes: [
128
+ ...new Set([
129
+ ...completeness.notes,
130
+ "Some collected evidence did not become durable; downstream analysis should lower confidence.",
131
+ ]),
132
+ ],
133
+ });
134
+ }
@@ -0,0 +1,205 @@
1
+ import { isPermanentUploadFailure } from "@bli-cockpit/telemetry-core";
2
+ import { DELIVERY_BACKOFF_HOLDING_REASON, summarizeStuckEvidence, } from "./raw-evidence-staging.js";
3
+ /**
4
+ * Which adapters a queued retry has to re-run.
5
+ *
6
+ * Anything this sync touched or tried to touch counts, including a directory it
7
+ * could not read — a source that failed to scan is exactly the one a retry must
8
+ * come back to.
9
+ */
10
+ export function retrySourcesForFailedSync(options, facts) {
11
+ const sources = new Set();
12
+ if ((options.codexSessionFiles?.length ?? 0) > 0 ||
13
+ (options.codexAttributionScan?.directory_read_failed_count ?? 0) > 0 ||
14
+ (options.codexAttributionScan?.stat_failed_count ?? 0) > 0 ||
15
+ facts?.evidence_completeness.source_counts.some((count) => count.source.startsWith("codex_") &&
16
+ count.scanned_count + count.included_count + count.reused_count > 0)) {
17
+ sources.add("codex");
18
+ }
19
+ if ((options.claudeSessionFiles?.length ?? 0) > 0 ||
20
+ (options.claudeAttributionScan?.project_dir_read_failed_count ?? 0) > 0 ||
21
+ (options.claudeAttributionScan?.session_stat_failed_count ?? 0) > 0 ||
22
+ (options.claudeAttributionScan?.sidecar_dir_read_failed_count ?? 0) > 0 ||
23
+ (options.claudeAttributionScan?.sidecar_stat_failed_count ?? 0) > 0 ||
24
+ facts?.evidence_completeness.source_counts.some((count) => count.source.startsWith("claude_") &&
25
+ count.scanned_count + count.included_count + count.reused_count > 0)) {
26
+ sources.add("claude_code");
27
+ }
28
+ return [...sources];
29
+ }
30
+ /**
31
+ * The failed uploads a later attempt could still rescue.
32
+ *
33
+ * An object storage has already refused on its own terms is not one of them,
34
+ * and counting it as one is what kept Edward's Mac in `retry_pending` through
35
+ * 13 consecutive syncs that were never going to end differently (BLI-2528).
36
+ */
37
+ function retryableFailedOutcomes(outcomes) {
38
+ return outcomes.filter((outcome) => outcome.upload_state === "upload_failed" &&
39
+ !isPermanentUploadFailure(outcome.reason));
40
+ }
41
+ /** Failed uploads that no retry can rescue, kept so they can still be named. */
42
+ function permanentFailedOutcomes(outcomes) {
43
+ return outcomes.filter((outcome) => outcome.upload_state === "upload_failed" &&
44
+ isPermanentUploadFailure(outcome.reason));
45
+ }
46
+ /**
47
+ * The reason to show for objects that failed for good.
48
+ *
49
+ * Returns null when there are none. These never queue a retry, but they must
50
+ * never disappear either: a machine with a permanently rejected object has
51
+ * missing collection, and a status that reads clean would hide it.
52
+ */
53
+ export function permanentEvidenceFailureReason(outcomes) {
54
+ const reasons = new Set(permanentFailedOutcomes(outcomes).map((outcome) => outcome.reason ?? "unknown"));
55
+ if (reasons.size === 0)
56
+ return null;
57
+ return `raw_evidence_permanently_rejected:${[...reasons].sort().join(",")}`;
58
+ }
59
+ /**
60
+ * Leave a dated record of the objects this machine gave up on.
61
+ *
62
+ * stderr, which launchd captures to `sync.err.log`, so an unattended machine
63
+ * still says what it lost. Reason labels and counts only — never a path or a
64
+ * byte of content.
65
+ */
66
+ export function logPermanentlyRejectedEvidence(options) {
67
+ console.error("[cockpit-sync] raw evidence permanently rejected", JSON.stringify({
68
+ attempted_at: options.attemptedAt,
69
+ reason: options.reason,
70
+ object_count: permanentFailedOutcomes(options.outcomes).length,
71
+ }));
72
+ }
73
+ /**
74
+ * Is there anything a later sync could still turn into evidence?
75
+ *
76
+ * Any one of these is enough: an upload that failed for a rescuable reason,
77
+ * evidence deferred by this sync's byte or object budget, a completeness
78
+ * payload that reports failure, or a skip a retry could undo.
79
+ */
80
+ export function hasRetryableEvidenceGap(facts, outcomes) {
81
+ if (retryableFailedOutcomes(outcomes).length > 0) {
82
+ return true;
83
+ }
84
+ if (!facts)
85
+ return false;
86
+ if (facts.deferred_byte_budget_count > 0 ||
87
+ facts.deferred_object_budget_count > 0) {
88
+ return true;
89
+ }
90
+ if (facts.evidence_completeness.status === "failed" ||
91
+ facts.evidence_completeness.totals.failed_count > 0 ||
92
+ facts.evidence_completeness.failure_reasons.length > 0) {
93
+ return true;
94
+ }
95
+ return facts.evidence_completeness.skip_reasons.some(({ reason }) => isRetryableEvidenceSkipReason(reason));
96
+ }
97
+ /**
98
+ * Skip reasons a later sync can still turn into evidence.
99
+ *
100
+ * `delivery_backoff_holding` is one of them, and it has to be: a source held by
101
+ * backoff is missing collection right now. If it did not land here the sync
102
+ * would read clean while a transcript sat undelivered for nine days, which is
103
+ * the exact shape of BLI-3066.
104
+ */
105
+ function isRetryableEvidenceSkipReason(reason) {
106
+ if (reason === DELIVERY_BACKOFF_HOLDING_REASON)
107
+ return true;
108
+ return /(?:read|stat|directory)_failed|session_limit_overflow/iu.test(reason);
109
+ }
110
+ /** Every distinct reason behind the gap, sorted, as one spool-ready label. */
111
+ export function retryableEvidenceGapReason(facts, outcomes) {
112
+ const reasons = new Set();
113
+ for (const outcome of retryableFailedOutcomes(outcomes)) {
114
+ reasons.add(outcome.reason ?? "upload_failed");
115
+ }
116
+ if (facts) {
117
+ if (facts.deferred_byte_budget_count > 0)
118
+ reasons.add("deferred_byte_budget");
119
+ if (facts.deferred_object_budget_count > 0) {
120
+ reasons.add("deferred_object_budget");
121
+ }
122
+ for (const { reason } of facts.evidence_completeness.failure_reasons) {
123
+ reasons.add(reason);
124
+ }
125
+ if (facts.evidence_completeness.status === "failed" &&
126
+ facts.evidence_completeness.failure_reasons.length === 0) {
127
+ reasons.add("evidence_completeness_failed");
128
+ }
129
+ for (const { reason } of facts.evidence_completeness.skip_reasons) {
130
+ if (isRetryableEvidenceSkipReason(reason)) {
131
+ reasons.add(reason);
132
+ }
133
+ }
134
+ }
135
+ return `partial_raw_evidence_retry_required:${[
136
+ ...reasons,
137
+ ].sort().join(",") || "unknown"}`;
138
+ }
139
+ /**
140
+ * The counts `cockpit sync --json` and `cockpit status` read.
141
+ *
142
+ * Two kinds of reuse are added together on purpose: files this machine skipped
143
+ * because its own cursor already had the bytes, and files the server recognised
144
+ * from their content hash.
145
+ */
146
+ export function summarizeRawEvidenceDelivery(built, outcomes, uploadedChunkCount, cursor, staging, now) {
147
+ const stuck = summarizeStuckEvidence(staging, now);
148
+ const cursorReused = built.raw_evidence_facts?.reused ?? [];
149
+ const serverReusedCount = outcomes.filter((outcome) => outcome.upload_state === "reused_existing").length;
150
+ const failed = outcomes.filter((outcome) => outcome.upload_state === "upload_failed");
151
+ const retryRequired = hasRetryableEvidenceGap(built.raw_evidence_facts, outcomes);
152
+ const retryReason = retryRequired
153
+ ? retryableEvidenceGapReason(built.raw_evidence_facts, outcomes)
154
+ : null;
155
+ const cursorReusedOutcomes = cursorReused.map((entry) => ({
156
+ object_key: cursor.objects[entry.content_hash_sha256]?.object_key ?? "",
157
+ raw_evidence_pointer_id: cursor.objects[entry.content_hash_sha256]?.object_key ?? "",
158
+ kind: entry.kind,
159
+ codex_session_id: entry.codex_session_id,
160
+ ...(entry.artifact_metadata
161
+ ? { artifact_metadata: entry.artifact_metadata }
162
+ : {}),
163
+ upload_state: "reused_existing",
164
+ reason: "cursor_content_match",
165
+ }));
166
+ return {
167
+ raw_evidence_file_count: built.raw_evidence_upload_files.length,
168
+ raw_evidence_uploaded_object_count: outcomes.filter((outcome) => outcome.upload_state === "uploaded").length,
169
+ raw_evidence_uploaded_chunk_count: uploadedChunkCount,
170
+ raw_evidence_reused_count: cursorReused.length + serverReusedCount,
171
+ raw_evidence_failed_count: failed.length,
172
+ raw_evidence_sanitized_count: built.raw_evidence_facts?.sanitized_count ?? 0,
173
+ raw_evidence_failure_reasons: [
174
+ ...new Set(failed.map((outcome) => outcome.reason ?? "unknown")),
175
+ ],
176
+ raw_evidence_retry_required: retryRequired,
177
+ raw_evidence_retry_reasons: retryReason
178
+ ? retryReason
179
+ .replace(/^partial_raw_evidence_retry_required:/u, "")
180
+ .split(",")
181
+ .filter(Boolean)
182
+ : [],
183
+ raw_evidence_outcomes: [
184
+ ...outcomes.map((outcome) => ({
185
+ object_key: outcome.object_key,
186
+ raw_evidence_pointer_id: outcome.pointer.raw_evidence_pointer_id,
187
+ kind: outcome.kind,
188
+ codex_session_id: outcome.codex_session_id,
189
+ ...(outcome.artifact_metadata
190
+ ? { artifact_metadata: outcome.artifact_metadata }
191
+ : {}),
192
+ upload_state: outcome.upload_state,
193
+ reason: outcome.reason,
194
+ })),
195
+ ...cursorReusedOutcomes,
196
+ ],
197
+ raw_evidence_deferred_byte_budget: built.raw_evidence_facts?.deferred_byte_budget_count ?? 0,
198
+ raw_evidence_deferred_object_budget: built.raw_evidence_facts?.deferred_object_budget_count ?? 0,
199
+ cursor_tracked_object_count: Object.keys(cursor.objects).length,
200
+ raw_evidence_delivery_held_count: outcomes.filter((outcome) => outcome.reason === DELIVERY_BACKOFF_HOLDING_REASON).length + (built.raw_evidence_facts?.delivery_held_count ?? 0),
201
+ raw_evidence_stuck_object_count: stuck.stuck_object_count,
202
+ raw_evidence_max_delivery_attempts: stuck.max_attempts,
203
+ raw_evidence_oldest_delivery_failure_at: stuck.oldest_first_failed_at,
204
+ };
205
+ }