@bli-cockpit/cli 0.2.49 → 0.2.51

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 (57) hide show
  1. package/dist/adapters/raw-evidence-claude-reader.js +108 -0
  2. package/dist/adapters/raw-evidence-codex-reader.js +147 -0
  3. package/dist/adapters/raw-evidence-collection-state.js +199 -0
  4. package/dist/adapters/raw-evidence-facts.js +338 -0
  5. package/dist/adapters/raw-evidence-git-diff-reader.js +187 -0
  6. package/dist/adapters/raw-evidence-image-reader.js +107 -0
  7. package/dist/adapters/raw-evidence-sanitize.js +56 -0
  8. package/dist/adapters/raw-evidence-transcript-file.js +182 -0
  9. package/dist/adapters/raw-evidence.js +63 -1183
  10. package/dist/commands/backfill-batches.js +34 -0
  11. package/dist/commands/backfill-candidates.js +54 -0
  12. package/dist/commands/backfill-checkpoint.js +101 -0
  13. package/dist/commands/backfill-command-line.js +70 -0
  14. package/dist/commands/backfill-evidence-outcomes.js +104 -0
  15. package/dist/commands/backfill-issues.js +265 -0
  16. package/dist/commands/backfill-output.js +75 -0
  17. package/dist/commands/backfill-plan.js +71 -0
  18. package/dist/commands/backfill-reasons.js +107 -0
  19. package/dist/commands/backfill-report.js +298 -0
  20. package/dist/commands/backfill-result.js +150 -0
  21. package/dist/commands/backfill-scan.js +274 -0
  22. package/dist/commands/backfill-scope.js +114 -0
  23. package/dist/commands/backfill-session-report.js +145 -0
  24. package/dist/commands/backfill-types.js +1 -0
  25. package/dist/commands/backfill-upload.js +212 -0
  26. package/dist/commands/backfill.js +41 -1961
  27. package/dist/commands/doctor.js +57 -0
  28. package/dist/commands/jarvis-trace.js +184 -0
  29. package/dist/commands/jarvis.js +144 -4
  30. package/dist/commands/local-args-collector.js +26 -0
  31. package/dist/commands/local-args-tower.js +21 -0
  32. package/dist/commands/local-args.js +3 -1
  33. package/dist/commands/local-help.js +19 -2
  34. package/dist/commands/local.js +3 -0
  35. package/dist/commands/memory-install-claude.js +294 -0
  36. package/dist/commands/memory-install-codex.js +205 -0
  37. package/dist/commands/memory-install-contract.js +286 -0
  38. package/dist/commands/memory-install-files.js +63 -0
  39. package/dist/commands/memory-install-skills.js +121 -0
  40. package/dist/commands/memory-install-toml.js +265 -0
  41. package/dist/commands/memory-install.js +465 -0
  42. package/dist/commands/public-root.js +1 -1
  43. package/dist/commands/sync-followups.js +105 -0
  44. package/dist/commands/sync.js +7 -1
  45. package/dist/local-state-attributed-target.js +75 -0
  46. package/dist/local-state-config.js +147 -0
  47. package/dist/local-state-files.js +59 -0
  48. package/dist/local-state-identity.js +73 -0
  49. package/dist/local-state-pairing.js +263 -0
  50. package/dist/local-state-paths.js +61 -0
  51. package/dist/local-state-session.js +68 -0
  52. package/dist/local-state-status.js +163 -0
  53. package/dist/local-state-work-context.js +190 -0
  54. package/dist/local-state.js +34 -848
  55. package/dist/tower-client.js +3 -2
  56. package/dist/tower-stream.js +57 -3
  57. package/package.json +2 -1
@@ -0,0 +1,212 @@
1
+ import { RAW_EVIDENCE_DEFAULT_BYTE_BUDGET, RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET, } from "../adapters/raw-evidence.js";
2
+ import { startLocalWorkContext } from "../local-state.js";
3
+ import { LocalUploadBlockedError, syncLocalAmbientEnvelope, } from "../upload.js";
4
+ import { buildBackfillBatches, uploadableCandidates, } from "./backfill-batches.js";
5
+ import { candidateCursorKey } from "./backfill-candidates.js";
6
+ import { countSessionUploadFailures, deferredEvidenceCount, durableBackfillCandidateKeys, } from "./backfill-evidence-outcomes.js";
7
+ import { blockingScanIssues } from "./backfill-issues.js";
8
+ import { writeLine } from "./backfill-output.js";
9
+ const BACKFILL_MAX_CONSECUTIVE_FAILURES = 3;
10
+ /**
11
+ * UPLOAD: sync every uploadable candidate in fixed-size batches, heartbeating
12
+ * the backfill lock between batches, stopping early on an exhausted budget or
13
+ * three consecutive failed batches. A candidate whose main is durably
14
+ * oversized-skipped (BLI-2727) is accounted for, not missing — it will never
15
+ * earn a durable pointer under the current cap, and treating it as "still
16
+ * missing" would fail every batch it happens to share with genuinely uploaded
17
+ * siblings, so it is excluded via `oversizedCandidateKeys` throughout.
18
+ */
19
+ export async function uploadBackfillBatches(command, io, lock, ctx) {
20
+ const uploadable = uploadableCandidates(ctx.scan.candidates);
21
+ const batches = buildBackfillBatches(uploadable);
22
+ const rawEvidenceBudget = {
23
+ remainingBytes: RAW_EVIDENCE_DEFAULT_BYTE_BUDGET,
24
+ remainingObjects: RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET,
25
+ };
26
+ const tally = emptyUploadTally();
27
+ let consecutiveFailures = 0;
28
+ let blockedAt;
29
+ let failureReason = blockingScanIssues(ctx.scan.issues)[0]?.reason;
30
+ const stoppedAt = (what, index) => ({
31
+ what,
32
+ batch_index: index + 1,
33
+ batch_total: batches.length,
34
+ done: tally.done,
35
+ total: uploadable.length,
36
+ });
37
+ for (const [index, batch] of batches.entries()) {
38
+ await lock.handle.heartbeat();
39
+ const outcome = await uploadOneBackfillBatch(command, io, ctx, batch, rawEvidenceBudget);
40
+ foldBatchIntoTally(tally, batch, outcome);
41
+ consecutiveFailures = outcome.failed ? consecutiveFailures + 1 : 0;
42
+ if (!command.json) {
43
+ writeLine(io.stdout, `Uploaded ${tally.done}/${uploadable.length} (batch ${index + 1}/${batches.length})`);
44
+ }
45
+ await yieldToEventLoop();
46
+ if (outcome.deferred) {
47
+ blockedAt = stoppedAt("raw evidence budget exhausted", index);
48
+ failureReason = "deferred_budget_exhausted";
49
+ break;
50
+ }
51
+ if (consecutiveFailures >= BACKFILL_MAX_CONSECUTIVE_FAILURES) {
52
+ blockedAt = stoppedAt("consecutive upload failures", index);
53
+ failureReason = failedBatchReason(outcome.sync, outcome.missingDurableMain);
54
+ break;
55
+ }
56
+ }
57
+ return { ...tally, uploadable, batches, blockedAt, failureReason };
58
+ }
59
+ /** Uploads one batch and judges it; every judgement here is a named predicate. */
60
+ async function uploadOneBackfillBatch(command, io, ctx, batch, rawEvidenceBudget) {
61
+ const sync = await syncBackfillBatch({
62
+ command,
63
+ batch,
64
+ worktrees: ctx.worktrees,
65
+ codexAttribution: ctx.scan.codexAttribution,
66
+ claudeAttribution: ctx.scan.claudeAttribution,
67
+ rawEvidenceBudget,
68
+ fetchImpl: io.fetch,
69
+ });
70
+ const durableInBatch = durableBackfillCandidateKeys(batch, sync);
71
+ const missingDurableMain = countMissingDurableMains(batch, durableInBatch, ctx.oversizedCandidateKeys);
72
+ return {
73
+ sync,
74
+ durableInBatch,
75
+ missingDurableMain,
76
+ failed: didBatchFail(sync, missingDurableMain),
77
+ deferred: deferredEvidenceCount(sync) > 0,
78
+ };
79
+ }
80
+ function emptyUploadTally() {
81
+ return {
82
+ syncResults: [],
83
+ completedBatches: 0,
84
+ failedBatches: 0,
85
+ done: 0,
86
+ failed: 0,
87
+ deferred: 0,
88
+ uploadedObjects: 0,
89
+ uploadedChunks: 0,
90
+ backfilledSessions: 0,
91
+ durableCandidateKeys: new Set(),
92
+ };
93
+ }
94
+ function foldBatchIntoTally(tally, batch, outcome) {
95
+ tally.syncResults.push(outcome.sync);
96
+ tally.done += batch.candidates.length;
97
+ tally.uploadedObjects += outcome.sync.raw_evidence_uploaded_object_count;
98
+ tally.uploadedChunks += outcome.sync.raw_evidence_uploaded_chunk_count;
99
+ tally.failed += countSessionUploadFailures(outcome.sync);
100
+ tally.deferred += deferredEvidenceCount(outcome.sync);
101
+ for (const key of outcome.durableInBatch)
102
+ tally.durableCandidateKeys.add(key);
103
+ tally.backfilledSessions = tally.durableCandidateKeys.size;
104
+ if (outcome.failed)
105
+ tally.failedBatches += 1;
106
+ if (!outcome.failed && !outcome.deferred)
107
+ tally.completedBatches += 1;
108
+ }
109
+ /**
110
+ * Candidates in this batch that still owe a durable main transcript. An
111
+ * oversized skip is excluded (BLI-2727): it can never earn a pointer under the
112
+ * current cap, and counting it here would fail every batch it happens to share
113
+ * with genuinely uploaded siblings.
114
+ */
115
+ function countMissingDurableMains(batch, durableInBatch, oversizedCandidateKeys) {
116
+ return batch.candidates.filter((candidate) => !durableInBatch.has(candidateCursorKey(candidate)) &&
117
+ !oversizedCandidateKeys.has(candidateCursorKey(candidate))).length;
118
+ }
119
+ /** A batch counts as failed if it did not upload, lost a session, or left one pointerless. */
120
+ function didBatchFail(sync, missingDurableMain) {
121
+ return (sync.status !== "uploaded" ||
122
+ countSessionUploadFailures(sync) > 0 ||
123
+ missingDurableMain > 0);
124
+ }
125
+ /** Names which of the three failure shapes ended the run, most specific first. */
126
+ function failedBatchReason(sync, missingDurableMain) {
127
+ if (sync.status === "spooled")
128
+ return sync.failure_reason;
129
+ if (missingDurableMain > 0)
130
+ return "durable_session_pointer_missing";
131
+ return "upload_failed";
132
+ }
133
+ /**
134
+ * Hands one batch to the ordinary sync path. A batch whose repo has no local
135
+ * work context yet is not a failure — backfill routinely visits repos this
136
+ * machine has never synced — so that one blocker is answered by creating the
137
+ * context and retrying; every other error propagates.
138
+ */
139
+ async function syncBackfillBatch(options) {
140
+ const syncOptions = backfillBatchSyncOptions(options);
141
+ try {
142
+ return await syncLocalAmbientEnvelope(syncOptions);
143
+ }
144
+ catch (error) {
145
+ if (error instanceof LocalUploadBlockedError &&
146
+ error.blocker === "missing_context") {
147
+ await startLocalWorkContext({
148
+ homeDir: options.command.homeDir,
149
+ repoRoot: options.batch.worktree.repo_root,
150
+ branch: options.batch.worktree.branch,
151
+ });
152
+ return await syncLocalAmbientEnvelope(syncOptions);
153
+ }
154
+ throw error;
155
+ }
156
+ }
157
+ /** The batch translated into the live sync path's envelope, session files and all. */
158
+ function backfillBatchSyncOptions(options) {
159
+ return {
160
+ homeDir: options.command.homeDir,
161
+ repoRoot: options.batch.worktree.repo_root,
162
+ worktreeInventory: worktreeInventoryForRepo(options.batch.worktree, options.worktrees),
163
+ codexSessionFiles: options.batch.candidates
164
+ .filter((candidate) => candidate.source === "codex")
165
+ .map((candidate) => ({
166
+ local_path: candidate.file_path,
167
+ codex_session_id: candidate.session_id,
168
+ })),
169
+ codexAttributionScan: options.codexAttribution ?? undefined,
170
+ claudeSessionFiles: options.batch.candidates
171
+ .filter((candidate) => candidate.source === "claude_code")
172
+ .map((candidate) => ({
173
+ local_path: candidate.file_path,
174
+ claude_session_id: candidate.session_id,
175
+ main_file_oversized: Boolean(candidate.claude?.main_file_oversized),
176
+ skip_main: false,
177
+ sidecar_files: candidate.claude?.sidecar_files
178
+ .filter((sidecar) => !sidecar.skipped_reason)
179
+ .map((sidecar) => ({ local_path: sidecar.local_path })) ?? [],
180
+ })),
181
+ claudeAttributionScan: options.claudeAttribution ?? undefined,
182
+ rawEvidenceBudget: options.rawEvidenceBudget,
183
+ // `cockpit backfill` is only ever an operator asking — by hand, through
184
+ // onboarding, through doctor, or by running the retry command Cockpit
185
+ // printed. The delivery-backoff window is the scheduler's cadence, so it
186
+ // does not gate this pass: honouring it here made the retry that follows a
187
+ // failed commit a 15-minute no-op that reported
188
+ // `durable_session_pointer_missing` and never named the hold (BLI-3118).
189
+ evidenceDeliveryMode: "operator_retry",
190
+ fetch: options.fetchImpl,
191
+ };
192
+ }
193
+ function worktreeInventoryForRepo(current, worktrees) {
194
+ return worktrees
195
+ .filter((worktree) => worktree.repo_fingerprint
196
+ ? worktree.repo_fingerprint === current.repo_fingerprint
197
+ : worktree.repo_label === current.repo_label)
198
+ .map((worktree) => ({
199
+ repo: worktree.repo_root,
200
+ repo_label: worktree.repo_label,
201
+ repo_fingerprint: worktree.repo_fingerprint,
202
+ repo_origin_url: worktree.repo_origin_url ?? undefined,
203
+ head_sha: worktree.head_sha ?? undefined,
204
+ worktree_label: worktree.worktree_label,
205
+ worktree_fingerprint: worktree.worktree_fingerprint,
206
+ worktree_is_primary: worktree.worktree_is_primary,
207
+ branch: worktree.branch,
208
+ }));
209
+ }
210
+ function yieldToEventLoop() {
211
+ return new Promise((resolve) => setTimeout(resolve, 0));
212
+ }