@bli-cockpit/cli 0.2.237 → 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 +73 -30
- package/dist/commands/cal.js +36 -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/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 +52 -50
- package/dist/commands/local-args-tower-careers.js +73 -54
- package/dist/commands/local-args-tower-docs-msg.js +55 -45
- 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-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 +6 -2
- 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/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/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
|
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
24
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
24
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.238");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -165,6 +165,45 @@ export async function admitOutsideRootSessions(options) {
|
|
|
165
165
|
if (sightings.length === 0) {
|
|
166
166
|
return { ...scan, outsideRoots: null };
|
|
167
167
|
}
|
|
168
|
+
const { admitted, holds, counts, budget } = selectOutsideRootSessions(sightings, plan);
|
|
169
|
+
if (admitted.length === 0) {
|
|
170
|
+
console.error("[session-sync] outside-root raw upload", JSON.stringify({ reason: "nothing_to_upload_outside_roots", ...counts, ...budget }));
|
|
171
|
+
return { ...scan, outsideRoots: { workspace: null, holds, counts } };
|
|
172
|
+
}
|
|
173
|
+
const workspace = outsideRootsWorkspace(options.homeDir);
|
|
174
|
+
try {
|
|
175
|
+
await ensureOutsideRootsWorkspace(workspace);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
// Not silent, and not a crash: every outside-root session keeps waiting and
|
|
179
|
+
// says why, and the in-root sessions of this tick are unaffected.
|
|
180
|
+
for (const sighting of admitted) {
|
|
181
|
+
holds.set(outsideRootKey(sighting.source, sighting.sessionId), "outside_root_workspace_unavailable");
|
|
182
|
+
}
|
|
183
|
+
console.error("[session-sync] outside-root raw upload", JSON.stringify({ reason: "workspace_unavailable", ...counts, ...budget, ...describeError(error) }));
|
|
184
|
+
return { ...scan, outsideRoots: { workspace: null, holds, counts } };
|
|
185
|
+
}
|
|
186
|
+
const admittedKeys = new Set(admitted.map((sighting) => outsideRootKey(sighting.source, sighting.sessionId)));
|
|
187
|
+
const labelled = {
|
|
188
|
+
...scan,
|
|
189
|
+
codexAttribution: {
|
|
190
|
+
...scan.codexAttribution,
|
|
191
|
+
results: scan.codexAttribution.results.map((result) => admittedKeys.has(outsideRootKey("codex", result.codex_session_id)) && isUnclaimedOutsideRootResult(result)
|
|
192
|
+
? { ...result, worktree: workspace }
|
|
193
|
+
: result),
|
|
194
|
+
},
|
|
195
|
+
claudeAttribution: {
|
|
196
|
+
...scan.claudeAttribution,
|
|
197
|
+
results: scan.claudeAttribution.results.map((result) => admittedKeys.has(outsideRootKey("claude_code", result.claude_session_id)) && isUnclaimedOutsideRootResult(result)
|
|
198
|
+
? { ...result, worktree: workspace }
|
|
199
|
+
: result),
|
|
200
|
+
},
|
|
201
|
+
outsideRoots: { workspace, holds, counts },
|
|
202
|
+
};
|
|
203
|
+
console.error("[session-sync] outside-root raw upload", JSON.stringify({ reason: "outside_root_sessions_admitted", ...counts, ...budget }));
|
|
204
|
+
return labelled;
|
|
205
|
+
}
|
|
206
|
+
function selectOutsideRootSessions(sightings, plan) {
|
|
168
207
|
const cursorFor = (source) => source === "codex" ? plan.codexCursorBefore : plan.claudeCursorBefore;
|
|
169
208
|
const free = [];
|
|
170
209
|
const owed = [];
|
|
@@ -207,42 +246,7 @@ export async function admitOutsideRootSessions(options) {
|
|
|
207
246
|
bytes_per_tick: OUTSIDE_ROOT_BYTES_PER_TICK,
|
|
208
247
|
objects_per_tick: OUTSIDE_ROOT_OBJECTS_PER_TICK,
|
|
209
248
|
};
|
|
210
|
-
|
|
211
|
-
console.error("[session-sync] outside-root raw upload", JSON.stringify({ reason: "nothing_to_upload_outside_roots", ...counts, ...budget }));
|
|
212
|
-
return { ...scan, outsideRoots: { workspace: null, holds, counts } };
|
|
213
|
-
}
|
|
214
|
-
const workspace = outsideRootsWorkspace(options.homeDir);
|
|
215
|
-
try {
|
|
216
|
-
await ensureOutsideRootsWorkspace(workspace);
|
|
217
|
-
}
|
|
218
|
-
catch (error) {
|
|
219
|
-
// Not silent, and not a crash: every outside-root session keeps waiting and
|
|
220
|
-
// says why, and the in-root sessions of this tick are unaffected.
|
|
221
|
-
for (const sighting of admitted) {
|
|
222
|
-
holds.set(outsideRootKey(sighting.source, sighting.sessionId), "outside_root_workspace_unavailable");
|
|
223
|
-
}
|
|
224
|
-
console.error("[session-sync] outside-root raw upload", JSON.stringify({ reason: "workspace_unavailable", ...counts, ...budget, ...describeError(error) }));
|
|
225
|
-
return { ...scan, outsideRoots: { workspace: null, holds, counts } };
|
|
226
|
-
}
|
|
227
|
-
const admittedKeys = new Set(admitted.map((sighting) => outsideRootKey(sighting.source, sighting.sessionId)));
|
|
228
|
-
const labelled = {
|
|
229
|
-
...scan,
|
|
230
|
-
codexAttribution: {
|
|
231
|
-
...scan.codexAttribution,
|
|
232
|
-
results: scan.codexAttribution.results.map((result) => admittedKeys.has(outsideRootKey("codex", result.codex_session_id)) && isUnclaimedOutsideRootResult(result)
|
|
233
|
-
? { ...result, worktree: workspace }
|
|
234
|
-
: result),
|
|
235
|
-
},
|
|
236
|
-
claudeAttribution: {
|
|
237
|
-
...scan.claudeAttribution,
|
|
238
|
-
results: scan.claudeAttribution.results.map((result) => admittedKeys.has(outsideRootKey("claude_code", result.claude_session_id)) && isUnclaimedOutsideRootResult(result)
|
|
239
|
-
? { ...result, worktree: workspace }
|
|
240
|
-
: result),
|
|
241
|
-
},
|
|
242
|
-
outsideRoots: { workspace, holds, counts },
|
|
243
|
-
};
|
|
244
|
-
console.error("[session-sync] outside-root raw upload", JSON.stringify({ reason: "outside_root_sessions_admitted", ...counts, ...budget }));
|
|
245
|
-
return labelled;
|
|
249
|
+
return { admitted, holds, counts, budget };
|
|
246
250
|
}
|
|
247
251
|
/**
|
|
248
252
|
* Says why an outside-root session did not ride this tick, on its report row.
|
|
@@ -47,16 +47,7 @@ export async function planSyncFromLocalState(options) {
|
|
|
47
47
|
const config = await readLocalCollectorConfig(options.paths).catch(() => null);
|
|
48
48
|
const claudeEnabled = config?.collect_claude_jsonl !== false;
|
|
49
49
|
const collectionRoots = normalizeCollectionRoots(options.approvedCollectionRoots ?? config?.default_repo_paths ?? []);
|
|
50
|
-
const [codexCursorBefore, claudeCursorBefore, uploadSpool, outsideRootCatchUpPending] = await
|
|
51
|
-
readRawEvidenceCursor(options.paths).catch(() => emptyRawEvidenceCursorState()),
|
|
52
|
-
claudeEnabled
|
|
53
|
-
? readRawEvidenceCursor(options.paths, {
|
|
54
|
-
filename: CLAUDE_CURSOR_FILENAME,
|
|
55
|
-
}).catch(() => emptyRawEvidenceCursorState())
|
|
56
|
-
: Promise.resolve(emptyRawEvidenceCursorState()),
|
|
57
|
-
readLocalUploadSpoolState(options.paths).catch(() => null),
|
|
58
|
-
outsideRootCatchUpIsPending(options.paths),
|
|
59
|
-
]);
|
|
50
|
+
const [codexCursorBefore, claudeCursorBefore, uploadSpool, outsideRootCatchUpPending] = await readSyncRetryState(options.paths, claudeEnabled);
|
|
60
51
|
// A spooled upload from a CLI old enough not to have recorded its source
|
|
61
52
|
// could have come from either one, so both sources own it until it clears.
|
|
62
53
|
const legacyRetryPending = Boolean(uploadSpool?.pending_uploads.some((entry) => entry.raw_evidence_file_count > 0 && entry.retry_sources.length === 0));
|
|
@@ -78,6 +69,19 @@ export async function planSyncFromLocalState(options) {
|
|
|
78
69
|
outsideRootCatchUpPending,
|
|
79
70
|
};
|
|
80
71
|
}
|
|
72
|
+
async function readSyncRetryState(paths, claudeEnabled) {
|
|
73
|
+
const [codexCursorBefore, claudeCursorBefore, uploadSpool, outsideRootCatchUpPending] = await Promise.all([
|
|
74
|
+
readRawEvidenceCursor(paths).catch(() => emptyRawEvidenceCursorState()),
|
|
75
|
+
claudeEnabled
|
|
76
|
+
? readRawEvidenceCursor(paths, {
|
|
77
|
+
filename: CLAUDE_CURSOR_FILENAME,
|
|
78
|
+
}).catch(() => emptyRawEvidenceCursorState())
|
|
79
|
+
: Promise.resolve(emptyRawEvidenceCursorState()),
|
|
80
|
+
readLocalUploadSpoolState(paths).catch(() => null),
|
|
81
|
+
outsideRootCatchUpIsPending(paths),
|
|
82
|
+
]);
|
|
83
|
+
return [codexCursorBefore, claudeCursorBefore, uploadSpool, outsideRootCatchUpPending];
|
|
84
|
+
}
|
|
81
85
|
/**
|
|
82
86
|
* How far back a retry-widened scan should reopen: the age of the oldest
|
|
83
87
|
* undurable file this cursor is still carrying, plus a day of buffer — or
|
|
@@ -44,28 +44,7 @@ export async function syncEveryTargetWorktree(options) {
|
|
|
44
44
|
items: syncWorktrees,
|
|
45
45
|
shouldYield: run.yieldCheck,
|
|
46
46
|
describe: (outcome) => ({ status: outcome.sync.status }),
|
|
47
|
-
runOne: (worktree) =>
|
|
48
|
-
// The outside-root pass has its own per-tick allowance (bytes and files),
|
|
49
|
-
// so a busy in-root tick cannot starve it and it cannot take in-root room.
|
|
50
|
-
if (isOutsideRootsWorkspace(worktree)) {
|
|
51
|
-
giveOutsideRootsTheirOwnAllowance(rawEvidenceBudget);
|
|
52
|
-
}
|
|
53
|
-
return syncOneWorktree({
|
|
54
|
-
run,
|
|
55
|
-
now: options.now,
|
|
56
|
-
plan: options.plan,
|
|
57
|
-
scan,
|
|
58
|
-
worktree,
|
|
59
|
-
syncWorktrees,
|
|
60
|
-
rawEvidenceBudget,
|
|
61
|
-
// A target git discovery never produced is one the fallback attribution
|
|
62
|
-
// synthesized, and it syncs through its own attributed work context.
|
|
63
|
-
isDiscoveredWorktree: discoveredWorktreeKeys.has(liveSyncTargetKey(worktree)),
|
|
64
|
-
recordDampedClaudeSession: (sessionId, priorPointer) => {
|
|
65
|
-
dampedClaudePointers.set(sessionId, priorPointer);
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
},
|
|
47
|
+
runOne: (worktree) => syncTargetWithAllowance(options, worktree, syncWorktrees, rawEvidenceBudget, discoveredWorktreeKeys, dampedClaudePointers),
|
|
69
48
|
});
|
|
70
49
|
const outcomes = loop.results;
|
|
71
50
|
const everyWorktreeUploaded = outcomes.every((outcome) => outcome.sync.status === "uploaded");
|
|
@@ -77,6 +56,29 @@ export async function syncEveryTargetWorktree(options) {
|
|
|
77
56
|
yieldedWorktreesLeft: loop.left,
|
|
78
57
|
};
|
|
79
58
|
}
|
|
59
|
+
function syncTargetWithAllowance(options, worktree, syncWorktrees, rawEvidenceBudget, discoveredWorktreeKeys, dampedClaudePointers) {
|
|
60
|
+
const { run, scan } = options;
|
|
61
|
+
// The outside-root pass has its own per-tick allowance (bytes and files),
|
|
62
|
+
// so a busy in-root tick cannot starve it and it cannot take in-root room.
|
|
63
|
+
if (isOutsideRootsWorkspace(worktree)) {
|
|
64
|
+
giveOutsideRootsTheirOwnAllowance(rawEvidenceBudget);
|
|
65
|
+
}
|
|
66
|
+
return syncOneWorktree({
|
|
67
|
+
run,
|
|
68
|
+
now: options.now,
|
|
69
|
+
plan: options.plan,
|
|
70
|
+
scan,
|
|
71
|
+
worktree,
|
|
72
|
+
syncWorktrees,
|
|
73
|
+
rawEvidenceBudget,
|
|
74
|
+
// A target git discovery never produced is one the fallback attribution
|
|
75
|
+
// synthesized, and it syncs through its own attributed work context.
|
|
76
|
+
isDiscoveredWorktree: discoveredWorktreeKeys.has(liveSyncTargetKey(worktree)),
|
|
77
|
+
recordDampedClaudeSession: (sessionId, priorPointer) => {
|
|
78
|
+
dampedClaudePointers.set(sessionId, priorPointer);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
80
82
|
/** Every Claude session the cursor already holds a durable pointer for. */
|
|
81
83
|
function claudeDurablePointersFromCursor(claudeCursorBefore) {
|
|
82
84
|
const pointersBySessionId = new Map();
|
|
@@ -108,14 +108,7 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
108
108
|
plan,
|
|
109
109
|
scan,
|
|
110
110
|
});
|
|
111
|
-
const ticketByWorktree =
|
|
112
|
-
for (const worktree of options.worktrees) {
|
|
113
|
-
const context = await readLocalWorkContextForRepo(paths, worktree.repo_root).catch(() => null);
|
|
114
|
-
const ticket = context?.linear_ticket_id ?? context?.active_ticket_id;
|
|
115
|
-
if (ticket)
|
|
116
|
-
ticketByWorktree.set(worktree.worktree_fingerprint, ticket);
|
|
117
|
-
}
|
|
118
|
-
console.error("[session-sync] bindings read", JSON.stringify({ bound_worktrees: ticketByWorktree.size, reason: "active_work_context" }));
|
|
111
|
+
const ticketByWorktree = await readWorktreeTickets(paths, options.worktrees);
|
|
119
112
|
// BLI-13028: which commits each session's working tree created while it ran.
|
|
120
113
|
// Read from the scan as it was BEFORE the outside-root step: the synthetic
|
|
121
114
|
// workspace is not a git tree, and no commit belongs to it.
|
|
@@ -131,6 +124,21 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
131
124
|
now,
|
|
132
125
|
claudePriorDurablePointers: worktreePass.claudePriorDurablePointers,
|
|
133
126
|
}), commitsBySession), scan.outsideRoots?.holds);
|
|
127
|
+
reportCountedSessionFacts(sessions);
|
|
128
|
+
return finishAttributedSync(options, { paths, now, plan, scan, sessions, worktreePass });
|
|
129
|
+
}
|
|
130
|
+
async function readWorktreeTickets(paths, worktrees) {
|
|
131
|
+
const ticketByWorktree = new Map();
|
|
132
|
+
for (const worktree of worktrees) {
|
|
133
|
+
const context = await readLocalWorkContextForRepo(paths, worktree.repo_root).catch(() => null);
|
|
134
|
+
const ticket = context?.linear_ticket_id ?? context?.active_ticket_id;
|
|
135
|
+
if (ticket)
|
|
136
|
+
ticketByWorktree.set(worktree.worktree_fingerprint, ticket);
|
|
137
|
+
}
|
|
138
|
+
console.error("[session-sync] bindings read", JSON.stringify({ bound_worktrees: ticketByWorktree.size, reason: "active_work_context" }));
|
|
139
|
+
return ticketByWorktree;
|
|
140
|
+
}
|
|
141
|
+
function reportCountedSessionFacts(sessions) {
|
|
134
142
|
// BLI-4341: every session should arrive counted. Say how many did, and say
|
|
135
143
|
// it on the success path too: a silent drop from "all" to "some" is exactly
|
|
136
144
|
// the kind of decay nobody notices.
|
|
@@ -141,6 +149,9 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
141
149
|
counted: sessionsCounted,
|
|
142
150
|
tokens_total: sessions.reduce((total, session) => total + (session.session_facts?.total_tokens ?? 0), 0),
|
|
143
151
|
}));
|
|
152
|
+
}
|
|
153
|
+
async function finishAttributedSync(options, context) {
|
|
154
|
+
const { paths, now, plan, scan, sessions, worktreePass } = context;
|
|
144
155
|
const delivery = await reportSessionsAndAdvanceCursors({
|
|
145
156
|
run: options,
|
|
146
157
|
paths,
|
package/dist/commands/slack.js
CHANGED
|
@@ -31,21 +31,27 @@ export async function runSlack(command, io) {
|
|
|
31
31
|
return writeCommandFailure(io, command.json, result);
|
|
32
32
|
writeLine(io.stderr, `[slack cli] connection answered ${JSON.stringify({ action: command.action, workspace: command.workspace ?? "all", reason: "ok" })}`);
|
|
33
33
|
const body = asRecord(result.body);
|
|
34
|
-
|
|
35
|
-
writeLine(io.stdout, JSON.stringify(body));
|
|
36
|
-
else if (command.action === "disconnect")
|
|
37
|
-
writeLine(io.stdout, `Disconnected Slack for ${command.workspace}.`);
|
|
38
|
-
else
|
|
39
|
-
for (const connection of (body["connections"] ?? [])) {
|
|
40
|
-
if (!command.workspace || command.workspace === connection["workspace"])
|
|
41
|
-
writeLine(io.stdout, `${connection["workspace"]}: ${connection["connected"] ? "connected" : "Connect Slack"} ${new URL(String(connection["connectPath"]), tower.dashboardUrl).toString()}`);
|
|
42
|
-
}
|
|
34
|
+
writeConnectionReply(command, io, body, tower.dashboardUrl);
|
|
43
35
|
return 0;
|
|
44
36
|
}
|
|
45
37
|
return command.action === "coverage"
|
|
46
38
|
? runCoverageAction(command, io, tower)
|
|
47
39
|
: runReadAction(command, io, tower);
|
|
48
40
|
}
|
|
41
|
+
function writeConnectionReply(command, io, body, dashboardUrl) {
|
|
42
|
+
if (command.json) {
|
|
43
|
+
writeLine(io.stdout, JSON.stringify(body));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (command.action === "disconnect") {
|
|
47
|
+
writeLine(io.stdout, `Disconnected Slack for ${command.workspace}.`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
for (const connection of (body["connections"] ?? [])) {
|
|
51
|
+
if (!command.workspace || command.workspace === connection["workspace"])
|
|
52
|
+
writeLine(io.stdout, `${connection["workspace"]}: ${connection["connected"] ? "connected" : "Connect Slack"} ${new URL(String(connection["connectPath"]), dashboardUrl).toString()}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
49
55
|
async function runCoverageAction(command, io, tower) {
|
|
50
56
|
const params = new URLSearchParams();
|
|
51
57
|
if (command.workspace)
|
|
@@ -266,13 +272,7 @@ function writeReadReply(command, io, payload, read, messages) {
|
|
|
266
272
|
else {
|
|
267
273
|
if (messages.length > 0)
|
|
268
274
|
writeLine(io.stdout, "");
|
|
269
|
-
|
|
270
|
-
writeLine(io.stdout, `#${String(message["channel"] ?? "?")} ${String(message["author"] ?? "?")} ${String(message["messageTs"] ?? "?")}`);
|
|
271
|
-
writeLine(io.stdout, ` ${String(message["text"] ?? "")}`);
|
|
272
|
-
if (typeof message["permalink"] === "string") {
|
|
273
|
-
writeLine(io.stdout, dim(` ${message["permalink"]}`, styled));
|
|
274
|
-
}
|
|
275
|
-
}
|
|
275
|
+
writeExpandedMessages(io, messages, styled);
|
|
276
276
|
}
|
|
277
277
|
// A refusal's summary IS its note (tools-slack.ts `boundary`); print it once.
|
|
278
278
|
if (typeof read["note"] === "string" && read["note"] && read["note"] !== header) {
|
|
@@ -280,6 +280,15 @@ function writeReadReply(command, io, payload, read, messages) {
|
|
|
280
280
|
writeLine(io.stdout, dim(read["note"], styled));
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
|
+
function writeExpandedMessages(io, messages, styled) {
|
|
284
|
+
for (const message of messages) {
|
|
285
|
+
writeLine(io.stdout, `#${String(message["channel"] ?? "?")} ${String(message["author"] ?? "?")} ${String(message["messageTs"] ?? "?")}`);
|
|
286
|
+
writeLine(io.stdout, ` ${String(message["text"] ?? "")}`);
|
|
287
|
+
if (typeof message["permalink"] === "string") {
|
|
288
|
+
writeLine(io.stdout, dim(` ${message["permalink"]}`, styled));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
283
292
|
function compactMessageLine(message) {
|
|
284
293
|
return `- #${String(message["channel"] ?? "?")} — ${String(message["author"] ?? "?")} — `
|
|
285
294
|
+ `${String(message["messageTs"] ?? "?")} — "${String(message["text"] ?? "")}" — ${String(message["permalink"] ?? "")}`;
|
|
@@ -37,7 +37,17 @@ export async function runSourcesCheck(command, io, now = new Date()) {
|
|
|
37
37
|
const imageMode = await resolveImageHandlingMode(paths.state_dir);
|
|
38
38
|
console.error("[collector sources] judging against this machine's image mode", JSON.stringify({ reason: `image_mode_from_${imageMode.source}`, mode: imageMode.mode, slim_aware: imageMode.slimAware }));
|
|
39
39
|
const report = emptyReport(command, remote, local.newer_count, local.file_read_failed + local.directory_read_failed);
|
|
40
|
-
|
|
40
|
+
await judgeLocalTranscripts(local.files, remote, cursors, imageMode, describedKeys, report);
|
|
41
|
+
reportSourcesCheck(report, remote, startedAt);
|
|
42
|
+
if (command.json)
|
|
43
|
+
writeLine(io.stdout, JSON.stringify(report, null, 2));
|
|
44
|
+
else
|
|
45
|
+
for (const line of sourcesCheckLines(report))
|
|
46
|
+
writeLine(io.stdout, line);
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
async function judgeLocalTranscripts(files, remote, cursors, imageMode, describedKeys, report) {
|
|
50
|
+
for (const file of files) {
|
|
41
51
|
const judged = await judgeSource({
|
|
42
52
|
file,
|
|
43
53
|
remotes: remote.files.get(file.key) ?? [],
|
|
@@ -67,6 +77,8 @@ export async function runSourcesCheck(command, io, now = new Date()) {
|
|
|
67
77
|
});
|
|
68
78
|
}
|
|
69
79
|
report.files.sort((a, b) => VERDICTS.indexOf(a.verdict) - VERDICTS.indexOf(b.verdict) || a.path.localeCompare(b.path));
|
|
80
|
+
}
|
|
81
|
+
function reportSourcesCheck(report, remote, startedAt) {
|
|
70
82
|
// Counts and bytes only: never a path, a session id or a byte of content.
|
|
71
83
|
console.error("[collector sources] check", JSON.stringify({
|
|
72
84
|
reason: `sources_check_${report.status}`,
|
|
@@ -83,12 +95,6 @@ export async function runSourcesCheck(command, io, now = new Date()) {
|
|
|
83
95
|
reasons: report.reasons,
|
|
84
96
|
elapsed_ms: Date.now() - startedAt,
|
|
85
97
|
}));
|
|
86
|
-
if (command.json)
|
|
87
|
-
writeLine(io.stdout, JSON.stringify(report, null, 2));
|
|
88
|
-
else
|
|
89
|
-
for (const line of sourcesCheckLines(report))
|
|
90
|
-
writeLine(io.stdout, line);
|
|
91
|
-
return 0;
|
|
92
98
|
}
|
|
93
99
|
function emptyReport(command, remote, newerSkipped, unreadable) {
|
|
94
100
|
const { files, ...remoteCounts } = remote;
|
|
@@ -133,8 +133,26 @@ function trimmed(value) {
|
|
|
133
133
|
export async function discoverLocalTranscripts(options) {
|
|
134
134
|
const roots = sourcesRoots(options.homeDir);
|
|
135
135
|
const result = { files: [], newer_count: 0, directory_read_failed: 0, file_read_failed: 0 };
|
|
136
|
-
const found = [];
|
|
137
136
|
let firstFailure = null;
|
|
137
|
+
const recordFailure = (error) => { firstFailure ??= describeError(error); };
|
|
138
|
+
const found = await findTranscriptFiles(options, roots, result, recordFailure);
|
|
139
|
+
await nameTranscriptFiles(found, result, recordFailure);
|
|
140
|
+
if (result.directory_read_failed > 0 || result.file_read_failed > 0) {
|
|
141
|
+
console.error("[collector sources] some transcripts could not be read; they are left out of the answer", JSON.stringify({
|
|
142
|
+
reason: "sources_discovery_incomplete",
|
|
143
|
+
directory_read_failed: result.directory_read_failed,
|
|
144
|
+
file_read_failed: result.file_read_failed,
|
|
145
|
+
found_count: result.files.length,
|
|
146
|
+
...(firstFailure ? { first_failure: firstFailure } : {}),
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
function shapeOrder(shape) {
|
|
152
|
+
return shape.kind === "claude_main" ? 0 : shape.kind === "codex_rollout" ? 1 : 2;
|
|
153
|
+
}
|
|
154
|
+
async function findTranscriptFiles(options, roots, result, recordFailure) {
|
|
155
|
+
const found = [];
|
|
138
156
|
const stack = [...roots.codex, roots.claudeProjects];
|
|
139
157
|
const seen = new Set();
|
|
140
158
|
while (stack.length > 0) {
|
|
@@ -146,7 +164,7 @@ export async function discoverLocalTranscripts(options) {
|
|
|
146
164
|
catch (error) {
|
|
147
165
|
if (error?.code !== "ENOENT") {
|
|
148
166
|
result.directory_read_failed += 1;
|
|
149
|
-
|
|
167
|
+
recordFailure(error);
|
|
150
168
|
}
|
|
151
169
|
continue;
|
|
152
170
|
}
|
|
@@ -164,7 +182,7 @@ export async function discoverLocalTranscripts(options) {
|
|
|
164
182
|
seen.add(full);
|
|
165
183
|
const stat = await fs.stat(full).catch((error) => {
|
|
166
184
|
result.file_read_failed += 1;
|
|
167
|
-
|
|
185
|
+
recordFailure(error);
|
|
168
186
|
return null;
|
|
169
187
|
});
|
|
170
188
|
if (!stat)
|
|
@@ -176,6 +194,9 @@ export async function discoverLocalTranscripts(options) {
|
|
|
176
194
|
found.push({ file: full, size: stat.size, mtimeMs: stat.mtimeMs, shape });
|
|
177
195
|
}
|
|
178
196
|
}
|
|
197
|
+
return found;
|
|
198
|
+
}
|
|
199
|
+
async function nameTranscriptFiles(found, result, recordFailure) {
|
|
179
200
|
// Claude mains first, so a subagent file can take its main's content id.
|
|
180
201
|
const claudeMainIds = new Map();
|
|
181
202
|
found.sort((a, b) => shapeOrder(a.shape) - shapeOrder(b.shape) || a.file.localeCompare(b.file));
|
|
@@ -185,7 +206,7 @@ export async function discoverLocalTranscripts(options) {
|
|
|
185
206
|
if (item.shape.kind !== "claude_subagent") {
|
|
186
207
|
const read = await readContentSessionId(item.file, source).then((id) => ({ ok: true, id }), (error) => {
|
|
187
208
|
result.file_read_failed += 1;
|
|
188
|
-
|
|
209
|
+
recordFailure(error);
|
|
189
210
|
return { ok: false, id: null };
|
|
190
211
|
});
|
|
191
212
|
if (!read.ok)
|
|
@@ -216,17 +237,4 @@ export async function discoverLocalTranscripts(options) {
|
|
|
216
237
|
key: transcriptKeyFor(source, named.session_id, item.shape.kind === "claude_subagent" ? item.shape.sidecar_stem : null),
|
|
217
238
|
});
|
|
218
239
|
}
|
|
219
|
-
if (result.directory_read_failed > 0 || result.file_read_failed > 0) {
|
|
220
|
-
console.error("[collector sources] some transcripts could not be read; they are left out of the answer", JSON.stringify({
|
|
221
|
-
reason: "sources_discovery_incomplete",
|
|
222
|
-
directory_read_failed: result.directory_read_failed,
|
|
223
|
-
file_read_failed: result.file_read_failed,
|
|
224
|
-
found_count: result.files.length,
|
|
225
|
-
...(firstFailure ? { first_failure: firstFailure } : {}),
|
|
226
|
-
}));
|
|
227
|
-
}
|
|
228
|
-
return result;
|
|
229
|
-
}
|
|
230
|
-
function shapeOrder(shape) {
|
|
231
|
-
return shape.kind === "claude_main" ? 0 : shape.kind === "codex_rollout" ? 1 : 2;
|
|
232
240
|
}
|
|
@@ -162,24 +162,7 @@ upload = {}) {
|
|
|
162
162
|
const mode = upload.mode ?? IMAGE_HANDLING_MODE;
|
|
163
163
|
const describedKeys = upload.describedKeys ?? new Set();
|
|
164
164
|
const slimAware = upload.slimAware ?? mode === "describe_and_drop";
|
|
165
|
-
const redactForUpload = (text, original) =>
|
|
166
|
-
const result = sanitizeTextEvidenceForUpload({
|
|
167
|
-
text,
|
|
168
|
-
originalBytes: text === original.toString("utf8") ? original : Buffer.from(text, "utf8"),
|
|
169
|
-
redactedFields: [`${file.kind === "codex_rollout" ? "codex_jsonl" : file.kind === "claude_main" ? "claude_jsonl" : "claude_jsonl_sidecar"}.body`],
|
|
170
|
-
secretLikeFileName: isSecretLikePath(path.basename(file.file_path)),
|
|
171
|
-
});
|
|
172
|
-
const hash = sha256(result.bytes);
|
|
173
|
-
if (hash === REDACTION_STUB_SHA256)
|
|
174
|
-
return null;
|
|
175
|
-
// A whole-file mask is one 40-byte line, not the content: every copy Tower holds would look
|
|
176
|
-
// "at least as large" and read stored_full. No redacted form, so the raw size answers instead.
|
|
177
|
-
if (isWholeFileMask(result.bytes)) {
|
|
178
|
-
console.error("[sources-verdict] redacted form is the whole-file mask: judged by raw size", JSON.stringify({ reason: "whole_file_mask", source: file.source, file_bytes: file.byte_size }));
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
return { sha256: hash, byte_size: result.bytes.byteLength };
|
|
182
|
-
};
|
|
165
|
+
const redactForUpload = (text, original) => redactTranscriptForm(file, text, original);
|
|
183
166
|
let split = null;
|
|
184
167
|
let whole = null;
|
|
185
168
|
let bytes = null;
|
|
@@ -230,6 +213,24 @@ upload = {}) {
|
|
|
230
213
|
},
|
|
231
214
|
};
|
|
232
215
|
}
|
|
216
|
+
function redactTranscriptForm(file, text, original) {
|
|
217
|
+
const result = sanitizeTextEvidenceForUpload({
|
|
218
|
+
text,
|
|
219
|
+
originalBytes: text === original.toString("utf8") ? original : Buffer.from(text, "utf8"),
|
|
220
|
+
redactedFields: [`${file.kind === "codex_rollout" ? "codex_jsonl" : file.kind === "claude_main" ? "claude_jsonl" : "claude_jsonl_sidecar"}.body`],
|
|
221
|
+
secretLikeFileName: isSecretLikePath(path.basename(file.file_path)),
|
|
222
|
+
});
|
|
223
|
+
const hash = sha256(result.bytes);
|
|
224
|
+
if (hash === REDACTION_STUB_SHA256)
|
|
225
|
+
return null;
|
|
226
|
+
// A whole-file mask is one 40-byte line, not the content: every copy Tower holds would look
|
|
227
|
+
// "at least as large" and read stored_full. No redacted form, so the raw size answers instead.
|
|
228
|
+
if (isWholeFileMask(result.bytes)) {
|
|
229
|
+
console.error("[sources-verdict] redacted form is the whole-file mask: judged by raw size", JSON.stringify({ reason: "whole_file_mask", source: file.source, file_bytes: file.byte_size }));
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
return { sha256: hash, byte_size: result.bytes.byteLength };
|
|
233
|
+
}
|
|
233
234
|
function sha256(buffer) {
|
|
234
235
|
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
235
236
|
}
|
package/dist/commands/speed.js
CHANGED
|
@@ -41,25 +41,7 @@ export async function runSpeed(command, io, deps = {}) {
|
|
|
41
41
|
: everyPage;
|
|
42
42
|
const results = [];
|
|
43
43
|
for (const page of chosen) {
|
|
44
|
-
|
|
45
|
-
for (let run = 0; run < runs; run += 1) {
|
|
46
|
-
const sample = await timeOneRead(page, { dashboardUrl, deviceToken: session.device_token, io, now, log });
|
|
47
|
-
samples.push(sample);
|
|
48
|
-
// A page this account may not open answers the same way every time.
|
|
49
|
-
if (classifyStatus(sample.status) === "not_permitted")
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
const summary = summarizeSamples(page, samples);
|
|
53
|
-
results.push(summary);
|
|
54
|
-
log(`[speed cli] page measured ${JSON.stringify({
|
|
55
|
-
page: summary.page,
|
|
56
|
-
outcome: summary.outcome,
|
|
57
|
-
http_status: summary.http_status,
|
|
58
|
-
runs: samples.length,
|
|
59
|
-
failed_runs: summary.failed_runs,
|
|
60
|
-
p50_ms: summary.p50_ms,
|
|
61
|
-
...(summary.reason ? { reason: summary.reason } : {}),
|
|
62
|
-
})}`);
|
|
44
|
+
results.push(await measurePage(page, runs, { dashboardUrl, deviceToken: session.device_token, io, now, log }));
|
|
63
45
|
}
|
|
64
46
|
const ranked = rankPages(results);
|
|
65
47
|
const failed = ranked.filter((page) => page.outcome === "failed").length;
|
|
@@ -86,6 +68,28 @@ export async function runSpeed(command, io, deps = {}) {
|
|
|
86
68
|
}
|
|
87
69
|
return failed === 0 ? 0 : 1;
|
|
88
70
|
}
|
|
71
|
+
async function measurePage(page, runs, context) {
|
|
72
|
+
const { dashboardUrl, deviceToken, io, now, log } = context;
|
|
73
|
+
const samples = [];
|
|
74
|
+
for (let run = 0; run < runs; run += 1) {
|
|
75
|
+
const sample = await timeOneRead(page, { dashboardUrl, deviceToken, io, now, log });
|
|
76
|
+
samples.push(sample);
|
|
77
|
+
// A page this account may not open answers the same way every time.
|
|
78
|
+
if (classifyStatus(sample.status) === "not_permitted")
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
const summary = summarizeSamples(page, samples);
|
|
82
|
+
log(`[speed cli] page measured ${JSON.stringify({
|
|
83
|
+
page: summary.page,
|
|
84
|
+
outcome: summary.outcome,
|
|
85
|
+
http_status: summary.http_status,
|
|
86
|
+
runs: samples.length,
|
|
87
|
+
failed_runs: summary.failed_runs,
|
|
88
|
+
p50_ms: summary.p50_ms,
|
|
89
|
+
...(summary.reason ? { reason: summary.reason } : {}),
|
|
90
|
+
})}`);
|
|
91
|
+
return summary;
|
|
92
|
+
}
|
|
89
93
|
async function timeOneRead(page, context) {
|
|
90
94
|
const startedAt = context.now();
|
|
91
95
|
const answer = await towerRequest({
|
package/dist/commands/start.js
CHANGED
|
@@ -13,22 +13,7 @@ export async function runStart(command, io) {
|
|
|
13
13
|
};
|
|
14
14
|
const worktrees = await discoverCommandWorktrees(command.repoRoot, { maxDepth: command.maxDepth, maxRepos: command.maxRepos, homeDir: command.homeDir }, io);
|
|
15
15
|
if (worktrees.length > 1) {
|
|
16
|
-
const contexts = await
|
|
17
|
-
homeDir: command.homeDir,
|
|
18
|
-
repoRoot: worktree.repo_root,
|
|
19
|
-
branch: command.branch,
|
|
20
|
-
activeTicketId: command.activeTicketId,
|
|
21
|
-
...ticketIds,
|
|
22
|
-
clearTicket: command.clearTicket,
|
|
23
|
-
topicLabel: command.topicLabel,
|
|
24
|
-
topicSummaryRedacted: command.topicSummaryRedacted,
|
|
25
|
-
workIntent: command.workIntent,
|
|
26
|
-
workPhase: command.workPhase,
|
|
27
|
-
intentSource: command.intentSource,
|
|
28
|
-
intentConfidence: command.intentConfidence,
|
|
29
|
-
operatorId: command.operatorId,
|
|
30
|
-
sessionId: command.sessionId,
|
|
31
|
-
})));
|
|
16
|
+
const contexts = await startDiscoveredWorktrees(command, ticketIds, worktrees);
|
|
32
17
|
if (command.json) {
|
|
33
18
|
writeLine(io.stdout, JSON.stringify({ mode: "multi_repo", contexts, ...(binding ? { ticket_binding: binding } : {}) }, null, 2));
|
|
34
19
|
return 0;
|
|
@@ -56,6 +41,25 @@ export async function runStart(command, io) {
|
|
|
56
41
|
writeLine(io.stdout, `Context: ${context.work_context_id}`);
|
|
57
42
|
return 0;
|
|
58
43
|
}
|
|
44
|
+
async function startDiscoveredWorktrees(command, ticketIds, worktrees) {
|
|
45
|
+
const contexts = await Promise.all(worktrees.map((worktree) => startLocalWorkContext({
|
|
46
|
+
homeDir: command.homeDir,
|
|
47
|
+
repoRoot: worktree.repo_root,
|
|
48
|
+
branch: command.branch,
|
|
49
|
+
activeTicketId: command.activeTicketId,
|
|
50
|
+
...ticketIds,
|
|
51
|
+
clearTicket: command.clearTicket,
|
|
52
|
+
topicLabel: command.topicLabel,
|
|
53
|
+
topicSummaryRedacted: command.topicSummaryRedacted,
|
|
54
|
+
workIntent: command.workIntent,
|
|
55
|
+
workPhase: command.workPhase,
|
|
56
|
+
intentSource: command.intentSource,
|
|
57
|
+
intentConfidence: command.intentConfidence,
|
|
58
|
+
operatorId: command.operatorId,
|
|
59
|
+
sessionId: command.sessionId,
|
|
60
|
+
})));
|
|
61
|
+
return contexts;
|
|
62
|
+
}
|
|
59
63
|
/**
|
|
60
64
|
* Both trackers' ids when they are known, and the reason when one is not. A
|
|
61
65
|
* lookup that resolved nothing still prints its sentence: a person who typed
|