@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
|
@@ -46,6 +46,47 @@ export function claudeDefaultEffort(model) {
|
|
|
46
46
|
const claudeFamily = (model) => /opus|sonnet|haiku/i.exec(model ?? "")?.[0].toLowerCase() ?? null;
|
|
47
47
|
/** Bounded, explicit locations only. Never recurse through a home or read env files. */
|
|
48
48
|
export async function collectAgentSetup(home, platform = process.platform, io = files, now = new Date()) {
|
|
49
|
+
const reader = createSetupReader(home, platform, io);
|
|
50
|
+
const { paths, join, read, list, json } = reader;
|
|
51
|
+
const resolveVersion = createVersionResolver(reader, platform, io);
|
|
52
|
+
// Everything slow starts together: both `--version` checks (up to 5 s each) and
|
|
53
|
+
// both transcript samples run side by side, so the whole step costs one wait.
|
|
54
|
+
const observe = async (tool) => {
|
|
55
|
+
const samples = await io.transcripts?.(tool, home).catch(() => []) ?? [];
|
|
56
|
+
return observeTranscripts(tool, samples);
|
|
57
|
+
};
|
|
58
|
+
const claudeObserving = observe("claude"), codexObserving = observe("codex");
|
|
59
|
+
const [claudeVersion, codexVersion, claudeObserved, codexObserved] = await Promise.all([resolveVersion("claude", claudeObserving), resolveVersion("codex", codexObserving), claudeObserving, codexObserving]);
|
|
60
|
+
const codexHooks = await json(".codex", "hooks.json");
|
|
61
|
+
const claudePlugins = await json(".claude", "plugins", "installed_plugins.json");
|
|
62
|
+
const { pluginSkills, pluginAgents, pluginInventoryUnknown } = await readPluginInventory(claudePlugins, paths, io);
|
|
63
|
+
const claudeSkills = await list(".claude", "skills");
|
|
64
|
+
const claudeAgents = await list(".claude", "agents");
|
|
65
|
+
const sharedSkills = await list(".agents", "skills");
|
|
66
|
+
const codexSkills = await list(".codex", "skills");
|
|
67
|
+
const systemSkills = await list(".codex", "skills", ".system");
|
|
68
|
+
const claudeSettings = await json(".claude", "settings.json");
|
|
69
|
+
const claudeConfig = await json(".claude.json");
|
|
70
|
+
const toml = await read(join(".codex", "config.toml"));
|
|
71
|
+
// Read only simple quoted scalar values. Complex TOML is unknown, not guessed.
|
|
72
|
+
const rootToml = toml?.split(/^\s*\[/m)[0] ?? "";
|
|
73
|
+
const scalar = (key) => safeName(new RegExp(`^${key}\\s*=\\s*"([^"\\n]+)"\\s*(?:#.*)?$`, "m").exec(rootToml)?.[1]);
|
|
74
|
+
const codexMcp = readCodexMcp(toml);
|
|
75
|
+
const claudeMcp = readClaudeMcp(claudeConfig);
|
|
76
|
+
const instructions = await readInstructionFiles(join, io);
|
|
77
|
+
const result = buildSetupInventory({
|
|
78
|
+
now, platform, claudeSettings, claudeObserved, codexObserved,
|
|
79
|
+
claudeVersion, codexVersion, claudeSkills, claudeAgents, sharedSkills, codexSkills, systemSkills,
|
|
80
|
+
claudePlugins, pluginInventoryUnknown, pluginSkills, pluginAgents, claudeMcp, codexMcp,
|
|
81
|
+
toml, rootToml, scalar, codexHooks, instructions
|
|
82
|
+
});
|
|
83
|
+
const evidence = (tool) => ({ version_source: result[tool].version_source, model_basis: result[tool].model_basis, effort_basis: result[tool].effort_basis });
|
|
84
|
+
console.error("[setup] versions read", JSON.stringify({ reason: "version_resolved", claude: evidence("claude"), codex: evidence("codex"), transcript_files: { claude: claudeObserved.files, codex: codexObserved.files } }));
|
|
85
|
+
const unknownFields = [result.claude, result.codex].reduce((count, tool) => count + ["skills", "plugins", "agents", "hooks", "mcp"].filter(key => tool[key] === null).length, 0);
|
|
86
|
+
console.error("[setup] inventory read", JSON.stringify({ reason: unknownFields ? "inventory_partial" : "inventory_observed", unknown_fields: unknownFields, os: result.os, mcp_count: (result.claude.mcp?.length ?? 0) + (result.codex.mcp?.length ?? 0) }));
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
function createSetupReader(home, platform, io) {
|
|
49
90
|
const paths = platform === "win32" ? path.win32 : path.posix;
|
|
50
91
|
const join = (...parts) => paths.join(home, ...parts);
|
|
51
92
|
const read = async (file) => {
|
|
@@ -76,8 +117,12 @@ export async function collectAgentSetup(home, platform = process.platform, io =
|
|
|
76
117
|
return null;
|
|
77
118
|
}
|
|
78
119
|
};
|
|
120
|
+
return { paths, join, read, list, json };
|
|
121
|
+
}
|
|
122
|
+
function createVersionResolver(reader, platform, io) {
|
|
123
|
+
const { paths, join, read } = reader;
|
|
79
124
|
const packageName = (tool) => tool === "claude" ? "@anthropic-ai/claude-code" : "@openai/codex";
|
|
80
|
-
|
|
125
|
+
async function versionFromPackage(tool) {
|
|
81
126
|
const parts = packageName(tool).split("/");
|
|
82
127
|
// Where npm puts a global package: Windows keeps it under AppData, macOS
|
|
83
128
|
// under the prefix of the node that installed it (Homebrew, /usr/local,
|
|
@@ -107,7 +152,8 @@ export async function collectAgentSetup(home, platform = process.platform, io =
|
|
|
107
152
|
}
|
|
108
153
|
}
|
|
109
154
|
return null;
|
|
110
|
-
}
|
|
155
|
+
}
|
|
156
|
+
;
|
|
111
157
|
const resolveVersion = async (tool, observing) => {
|
|
112
158
|
const viaCli = await io.version?.(tool, platform).catch(() => null);
|
|
113
159
|
if (safeName(viaCli))
|
|
@@ -120,16 +166,9 @@ export async function collectAgentSetup(home, platform = process.platform, io =
|
|
|
120
166
|
const viaTranscript = safeName((await observing).version);
|
|
121
167
|
return viaTranscript ? { value: viaTranscript, source: "transcript" } : { value: null, source: "none" };
|
|
122
168
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const samples = await io.transcripts?.(tool, home).catch(() => []) ?? [];
|
|
127
|
-
return observeTranscripts(tool, samples);
|
|
128
|
-
};
|
|
129
|
-
const claudeObserving = observe("claude"), codexObserving = observe("codex");
|
|
130
|
-
const [claudeVersion, codexVersion, claudeObserved, codexObserved] = await Promise.all([resolveVersion("claude", claudeObserving), resolveVersion("codex", codexObserving), claudeObserving, codexObserving]);
|
|
131
|
-
const codexHooks = await json(".codex", "hooks.json");
|
|
132
|
-
const claudePlugins = await json(".claude", "plugins", "installed_plugins.json");
|
|
169
|
+
return resolveVersion;
|
|
170
|
+
}
|
|
171
|
+
async function readPluginInventory(claudePlugins, paths, io) {
|
|
133
172
|
const pluginSkills = [], pluginAgents = [];
|
|
134
173
|
let pluginInventoryUnknown = false;
|
|
135
174
|
for (const [pluginName, installations] of Object.entries(object(claudePlugins?.plugins))) {
|
|
@@ -142,45 +181,55 @@ export async function collectAgentSetup(home, platform = process.platform, io =
|
|
|
142
181
|
continue;
|
|
143
182
|
}
|
|
144
183
|
for (const category of ["skills", "agents"]) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
if (error.code !== "ENOENT")
|
|
151
|
-
pluginInventoryUnknown = true;
|
|
152
|
-
}
|
|
184
|
+
const target = category === "skills" ? pluginSkills : pluginAgents;
|
|
185
|
+
if (await readPluginCategory(pluginName, directory, category, paths, io, target))
|
|
186
|
+
pluginInventoryUnknown = true;
|
|
153
187
|
}
|
|
154
188
|
}
|
|
155
189
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
190
|
+
return { pluginSkills, pluginAgents, pluginInventoryUnknown };
|
|
191
|
+
}
|
|
192
|
+
async function readPluginCategory(pluginName, directory, category, paths, io, target) {
|
|
193
|
+
try {
|
|
194
|
+
const entries = names(await io.list(paths.join(directory, category)));
|
|
195
|
+
target.push(...entries.map(name => `${pluginName}.${name}`));
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
return error.code !== "ENOENT";
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function readCodexMcp(toml) {
|
|
203
|
+
return toml === null ? null : [...toml.matchAll(/^\s*\[mcp_servers\.(?:"([a-z0-9_.@+-]+)"|([a-z0-9_-]+))\]\s*$/gim)].flatMap(match => {
|
|
168
204
|
const name = safeName(match[1] ?? match[2]);
|
|
169
205
|
if (!name)
|
|
170
206
|
return [];
|
|
171
207
|
const block = toml.slice(match.index + match[0].length).split(/^\s*\[/m)[0];
|
|
172
208
|
return [{ name, transport: /^\s*command\s*=/m.test(block) ? "stdio" : /^\s*url\s*=/m.test(block) ? "http" : "unknown" }];
|
|
173
209
|
}).slice(0, 500);
|
|
210
|
+
}
|
|
211
|
+
function readClaudeMcp(claudeConfig) {
|
|
174
212
|
const claudeServers = [object(claudeConfig?.mcpServers), ...Object.values(object(claudeConfig?.projects)).map(project => object(object(project).mcpServers))];
|
|
175
|
-
|
|
213
|
+
return claudeConfig === null ? null : claudeServers.flatMap(servers => Object.entries(servers)).flatMap(([key, value]) => {
|
|
176
214
|
const name = safeName(key);
|
|
177
215
|
if (!name)
|
|
178
216
|
return [];
|
|
179
217
|
const config = object(value);
|
|
180
|
-
const transport = config
|
|
218
|
+
const transport = readClaudeMcpTransport(config);
|
|
181
219
|
return [{ name, transport }];
|
|
182
220
|
}).slice(0, 500);
|
|
183
|
-
|
|
221
|
+
}
|
|
222
|
+
function readClaudeMcpTransport(config) {
|
|
223
|
+
if (config.type === "sse")
|
|
224
|
+
return "sse";
|
|
225
|
+
if (config.type === "http" || config.url)
|
|
226
|
+
return "http";
|
|
227
|
+
if (config.command)
|
|
228
|
+
return "stdio";
|
|
229
|
+
return "unknown";
|
|
230
|
+
}
|
|
231
|
+
async function readInstructionFiles(join, io) {
|
|
232
|
+
return await Promise.all(["CLAUDE.md", "AGENTS.md"].map(async (name) => {
|
|
184
233
|
const file = join(name === "CLAUDE.md" ? ".claude" : ".codex", name);
|
|
185
234
|
try {
|
|
186
235
|
const content = await io.read(file);
|
|
@@ -190,6 +239,9 @@ export async function collectAgentSetup(home, platform = process.platform, io =
|
|
|
190
239
|
return { name, exists: error.code === "ENOENT" ? false : null, sha256: null, bytes: null };
|
|
191
240
|
}
|
|
192
241
|
}));
|
|
242
|
+
}
|
|
243
|
+
function buildSetupInventory(input) {
|
|
244
|
+
const { now, platform, claudeSettings, claudeObserved, codexObserved, claudeVersion, codexVersion, claudeSkills, claudeAgents, sharedSkills, codexSkills, systemSkills, claudePlugins, pluginInventoryUnknown, pluginSkills, pluginAgents, claudeMcp, codexMcp, toml, rootToml, scalar, codexHooks, instructions } = input;
|
|
193
245
|
// Model: what the person set, else what their sessions actually ran most.
|
|
194
246
|
const claudeSetModel = safeName(claudeSettings?.model), codexSetModel = scalar("model");
|
|
195
247
|
const claudeModel = claudeSetModel ?? claudeObserved.model;
|
|
@@ -198,25 +250,24 @@ export async function collectAgentSetup(home, platform = process.platform, io =
|
|
|
198
250
|
const claudeDefaultFor = claudeSetModel && claudeFamily(claudeSetModel) !== claudeFamily(claudeObserved.model) ? claudeSetModel : claudeObserved.model ?? claudeSetModel;
|
|
199
251
|
const claudeSetEffort = safeName(claudeSettings?.effortLevel), codexSetEffort = scalar("model_reasoning_effort");
|
|
200
252
|
const claudeDefault = claudeSetEffort ? null : claudeDefaultEffort(claudeDefaultFor);
|
|
201
|
-
|
|
253
|
+
return AgentSetupSchema.parse({
|
|
202
254
|
checked_at: now.toISOString(), os: platform === "darwin" || platform === "win32" ? platform : "other", cli_version: LOCAL_COLLECTOR_VERSION,
|
|
203
|
-
claude: {
|
|
255
|
+
claude: {
|
|
256
|
+
version: claudeVersion.value, version_source: claudeVersion.source,
|
|
204
257
|
model: claudeModel, model_basis: claudeSetModel ? "set" : claudeModel ? "used" : "none",
|
|
205
258
|
effort: claudeSetEffort ?? claudeDefault, effort_basis: claudeSetEffort ? "set" : "default",
|
|
206
259
|
skills: claudeSkills === null || pluginInventoryUnknown ? null : names([...claudeSkills, ...pluginSkills]), agents: claudeAgents === null || pluginInventoryUnknown ? null : names([...claudeAgents, ...pluginAgents]),
|
|
207
260
|
plugins: claudePlugins === null ? null : names(Object.keys(object(claudePlugins.plugins))),
|
|
208
|
-
hooks: claudeSettings === null ? null : names(Object.keys(object(claudeSettings.hooks))), mcp: claudeMcp
|
|
209
|
-
|
|
261
|
+
hooks: claudeSettings === null ? null : names(Object.keys(object(claudeSettings.hooks))), mcp: claudeMcp
|
|
262
|
+
},
|
|
263
|
+
codex: {
|
|
264
|
+
version: codexVersion.value, version_source: codexVersion.source,
|
|
210
265
|
model: codexSetModel ?? codexObserved.model, model_basis: codexSetModel ? "set" : codexObserved.model ? "used" : "none",
|
|
211
266
|
effort: codexSetEffort ?? codexObserved.effort, effort_basis: codexSetEffort ? "set" : codexObserved.effort ? "observed" : "default",
|
|
212
267
|
skills: codexSkills === null || sharedSkills === null || systemSkills === null ? null : names([...(codexSkills ?? []), ...(sharedSkills ?? []), ...(systemSkills ?? [])]),
|
|
213
268
|
agents: toml === null ? null : names([...toml.matchAll(/^\s*\[agents\.([a-z0-9_-]+)\]\s*$/gim)].map(match => match[1])),
|
|
214
269
|
plugins: toml === null ? null : names([...toml.matchAll(/^\s*\[plugins\."([a-z0-9_@.+-]+)"\]\s*$/gim)].map(match => match[1])),
|
|
215
|
-
hooks: codexHooks === null && !/^notify\s*=/m.test(rootToml) ? null : names([...(codexHooks === null ? [] : Object.keys(object(codexHooks.hooks))), ...(/^notify\s*=/m.test(rootToml) ? ["notify"] : [])]), mcp: codexMcp
|
|
270
|
+
hooks: codexHooks === null && !/^notify\s*=/m.test(rootToml) ? null : names([...(codexHooks === null ? [] : Object.keys(object(codexHooks.hooks))), ...(/^notify\s*=/m.test(rootToml) ? ["notify"] : [])]), mcp: codexMcp
|
|
271
|
+
}, instructions,
|
|
216
272
|
});
|
|
217
|
-
const evidence = (tool) => ({ version_source: result[tool].version_source, model_basis: result[tool].model_basis, effort_basis: result[tool].effort_basis });
|
|
218
|
-
console.error("[setup] versions read", JSON.stringify({ reason: "version_resolved", claude: evidence("claude"), codex: evidence("codex"), transcript_files: { claude: claudeObserved.files, codex: codexObserved.files } }));
|
|
219
|
-
const unknownFields = [result.claude, result.codex].reduce((count, tool) => count + ["skills", "plugins", "agents", "hooks", "mcp"].filter(key => tool[key] === null).length, 0);
|
|
220
|
-
console.error("[setup] inventory read", JSON.stringify({ reason: unknownFields ? "inventory_partial" : "inventory_observed", unknown_fields: unknownFields, os: result.os, mcp_count: (result.claude.mcp?.length ?? 0) + (result.codex.mcp?.length ?? 0) }));
|
|
221
|
-
return result;
|
|
222
273
|
}
|
|
@@ -78,6 +78,9 @@ async function healDetached(command, io, deps) {
|
|
|
78
78
|
// BLI-12828: absent is created now; only a deliberate uninstall stops it.
|
|
79
79
|
return { status: "skipped", reason: "autostart_uninstalled_by_operator" };
|
|
80
80
|
}
|
|
81
|
+
return writeAndVerifyRegistration(command, exec, platform, roots, before.status);
|
|
82
|
+
}
|
|
83
|
+
async function writeAndVerifyRegistration(command, exec, platform, roots, beforeStatus) {
|
|
81
84
|
let written;
|
|
82
85
|
try {
|
|
83
86
|
written = await writeDarwinAutostartPlist({
|
|
@@ -107,6 +110,9 @@ async function healDetached(command, io, deps) {
|
|
|
107
110
|
detail: redactedHealthDetail(reregistered.detail ?? "").slice(0, DETAIL_MAX_CHARS),
|
|
108
111
|
};
|
|
109
112
|
}
|
|
113
|
+
return verifyRegistration(command, exec, platform, roots, beforeStatus, written.node_path_reason);
|
|
114
|
+
}
|
|
115
|
+
async function verifyRegistration(command, exec, platform, roots, beforeStatus, nodePathReason) {
|
|
110
116
|
// Verify against what launchd returns, not against what we sent it
|
|
111
117
|
// (BLI-2541). A bootstrap that exits 0 and a registration that reads back
|
|
112
118
|
// healthy are two different claims.
|
|
@@ -127,8 +133,8 @@ async function healDetached(command, io, deps) {
|
|
|
127
133
|
}
|
|
128
134
|
return {
|
|
129
135
|
status: "ok",
|
|
130
|
-
reason:
|
|
131
|
-
detail: `node_path_reason=${
|
|
136
|
+
reason: beforeStatus === "absent" ? "autostart_created" : "autostart_healed",
|
|
137
|
+
detail: `node_path_reason=${nodePathReason}`,
|
|
132
138
|
};
|
|
133
139
|
}
|
|
134
140
|
async function waitForParentExit(parentPid, deps) {
|
|
@@ -117,6 +117,9 @@ async function stateFor(result, roots, options) {
|
|
|
117
117
|
...detail,
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
+
return readRegisteredRuntime(roots, options);
|
|
121
|
+
}
|
|
122
|
+
async function readRegisteredRuntime(roots, options) {
|
|
120
123
|
// BLI-3553: "loaded" only means the scheduler accepted the registration. It
|
|
121
124
|
// says nothing about whether the binary that registration names still exists
|
|
122
125
|
// — and `brew upgrade node` deletes exactly that. Ask the filesystem about
|
|
@@ -50,17 +50,20 @@ function summarizeEvidenceOutcomesBySession(sync) {
|
|
|
50
50
|
else if (outcome.raw_evidence_pointer_id &&
|
|
51
51
|
(outcome.upload_state === "uploaded" ||
|
|
52
52
|
outcome.upload_state === "reused_existing")) {
|
|
53
|
-
|
|
54
|
-
summary.durableMainCount += 1;
|
|
55
|
-
}
|
|
56
|
-
else if (outcome.kind === "claude_jsonl_sidecar") {
|
|
57
|
-
summary.durableSidecarCount += 1;
|
|
58
|
-
}
|
|
53
|
+
countDurableEvidence(summary, outcome);
|
|
59
54
|
}
|
|
60
55
|
outcomesBySession.set(key, summary);
|
|
61
56
|
}
|
|
62
57
|
return outcomesBySession;
|
|
63
58
|
}
|
|
59
|
+
function countDurableEvidence(summary, outcome) {
|
|
60
|
+
if (outcome.kind === "codex_jsonl" || outcome.kind === "claude_jsonl") {
|
|
61
|
+
summary.durableMainCount += 1;
|
|
62
|
+
}
|
|
63
|
+
else if (outcome.kind === "claude_jsonl_sidecar") {
|
|
64
|
+
summary.durableSidecarCount += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
64
67
|
/** Sidecars this session owes a pointer for; one already skipped at scan time is not owed. */
|
|
65
68
|
function expectedSidecarCount(candidate) {
|
|
66
69
|
if (candidate.source !== "claude_code")
|
|
@@ -71,6 +71,9 @@ function addCandidateScanIssues(issues, candidates, omittedCandidateCount) {
|
|
|
71
71
|
countScanIssue(issues, "candidate_worktree_unavailable", candidates.filter((candidate) => isRawEvidenceUploadableAttributionState(candidate.state, candidate.worktree !== null) && candidate.worktree === null).length, "candidate");
|
|
72
72
|
countScanIssue(issues, "claude_main_file_too_large", candidates.filter((candidate) => candidate.source === "claude_code" &&
|
|
73
73
|
candidate.claude?.main_file_oversized).length, "candidate");
|
|
74
|
+
addClaudeSidecarIssues(issues, candidates);
|
|
75
|
+
}
|
|
76
|
+
function addClaudeSidecarIssues(issues, candidates) {
|
|
74
77
|
countScanIssue(issues, "claude_sidecar_limit_applied", candidates.reduce((total, candidate) => total + (candidate.claude?.sidecars_capped ?? 0), 0), "candidate");
|
|
75
78
|
countScanIssue(issues, "claude_sidecar_file_unreadable", candidates.reduce((total, candidate) => total +
|
|
76
79
|
(candidate.claude?.sidecar_files.filter((sidecar) => sidecar.skipped_reason === "file_read_failed" ||
|
|
@@ -44,28 +44,7 @@ export async function scanBackfillRun(command, io, now) {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
const { collectionRoots, sources, worktreeDiscovery } = scope;
|
|
47
|
-
const
|
|
48
|
-
? emptyBackfillCursorState()
|
|
49
|
-
: await readBackfillCursor(paths);
|
|
50
|
-
const scopedCursor = prepareBackfillCursorForScope(storedCursor, collectionRoots, sources);
|
|
51
|
-
const cursor = scopedCursor.cursor;
|
|
52
|
-
const pointerlessTerminalSessions = await loadPointerlessTerminalSessions(paths, sources);
|
|
53
|
-
// BLI-11102: a redaction-crash stub is not a delivery. The server names
|
|
54
|
-
// them; those sessions reopen exactly like pointer-less ones. A dry run
|
|
55
|
-
// keeps its one-request contract (the reachability probe) and does not ask.
|
|
56
|
-
// BLI-11112: the sync's stub pass brings the door's answer with it.
|
|
57
|
-
const stubReoffer = command.stubPass
|
|
58
|
-
? command.stubPass.plan
|
|
59
|
-
: command.dryRun
|
|
60
|
-
? emptyStubReofferPlan("dry_run")
|
|
61
|
-
: await loadStubReofferPlan({
|
|
62
|
-
paths,
|
|
63
|
-
dashboardUrl: scope.dashboardUrl,
|
|
64
|
-
sources,
|
|
65
|
-
fetchImpl: io.fetch,
|
|
66
|
-
});
|
|
67
|
-
reopenStubbedSessions(pointerlessTerminalSessions, stubReoffer);
|
|
68
|
-
const stubCursorHashes = await loadStubCursorHashes(paths, stubReoffer);
|
|
47
|
+
const { scopedCursor, cursor, pointerlessTerminalSessions, stubReoffer, stubCursorHashes } = await reopenUndeliveredHistory(command, io, paths, scope);
|
|
69
48
|
const scan = await scanBackfillSessions({
|
|
70
49
|
command,
|
|
71
50
|
homeDir: command.homeDir ?? os.homedir(),
|
|
@@ -99,6 +78,32 @@ export async function scanBackfillRun(command, io, now) {
|
|
|
99
78
|
deterministicSkipCandidateKeys: deterministicSkipCandidateKeys(scan.candidates),
|
|
100
79
|
};
|
|
101
80
|
}
|
|
81
|
+
async function reopenUndeliveredHistory(command, io, paths, scope) {
|
|
82
|
+
const { collectionRoots, sources } = scope;
|
|
83
|
+
const storedCursor = command.dryRun
|
|
84
|
+
? emptyBackfillCursorState()
|
|
85
|
+
: await readBackfillCursor(paths);
|
|
86
|
+
const scopedCursor = prepareBackfillCursorForScope(storedCursor, collectionRoots, sources);
|
|
87
|
+
const cursor = scopedCursor.cursor;
|
|
88
|
+
const pointerlessTerminalSessions = await loadPointerlessTerminalSessions(paths, sources);
|
|
89
|
+
// BLI-11102: a redaction-crash stub is not a delivery. The server names
|
|
90
|
+
// them; those sessions reopen exactly like pointer-less ones. A dry run
|
|
91
|
+
// keeps its one-request contract (the reachability probe) and does not ask.
|
|
92
|
+
// BLI-11112: the sync's stub pass brings the door's answer with it.
|
|
93
|
+
const stubReoffer = command.stubPass
|
|
94
|
+
? command.stubPass.plan
|
|
95
|
+
: command.dryRun
|
|
96
|
+
? emptyStubReofferPlan("dry_run")
|
|
97
|
+
: await loadStubReofferPlan({
|
|
98
|
+
paths,
|
|
99
|
+
dashboardUrl: scope.dashboardUrl,
|
|
100
|
+
sources,
|
|
101
|
+
fetchImpl: io.fetch,
|
|
102
|
+
});
|
|
103
|
+
reopenStubbedSessions(pointerlessTerminalSessions, stubReoffer);
|
|
104
|
+
const stubCursorHashes = await loadStubCursorHashes(paths, stubReoffer);
|
|
105
|
+
return { scopedCursor, cursor, pointerlessTerminalSessions, stubReoffer, stubCursorHashes };
|
|
106
|
+
}
|
|
102
107
|
/**
|
|
103
108
|
* Finishes the scan's ledger before anything acts on it: adds what repo
|
|
104
109
|
* discovery could not enumerate, reads every candidate file once to find the
|
|
@@ -21,15 +21,7 @@ const BACKFILL_MAX_CONSECUTIVE_FAILURES = 3;
|
|
|
21
21
|
*/
|
|
22
22
|
export async function uploadBackfillBatches(command, io, lock, ctx) {
|
|
23
23
|
const uploadable = uploadableCandidates(ctx.scan.candidates);
|
|
24
|
-
|
|
25
|
-
// sync's stub pass (BLI-11112), which hands the lock to a waiting backfill
|
|
26
|
-
// at the next one. Ordinary backfill retains its existing batching.
|
|
27
|
-
const plannedBatches = buildBackfillBatches(uploadable);
|
|
28
|
-
const batches = command.deadlineMs !== undefined || command.onProgress || command.stubPass
|
|
29
|
-
? plannedBatches.flatMap((batch) => batch.candidates.map((candidate) => ({
|
|
30
|
-
worktree: batch.worktree, candidates: [candidate],
|
|
31
|
-
})))
|
|
32
|
-
: plannedBatches;
|
|
24
|
+
const batches = planUploadBatches(command, uploadable);
|
|
33
25
|
const rawEvidenceBudget = {
|
|
34
26
|
remainingBytes: RAW_EVIDENCE_DEFAULT_BYTE_BUDGET,
|
|
35
27
|
remainingObjects: RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET,
|
|
@@ -38,29 +30,8 @@ export async function uploadBackfillBatches(command, io, lock, ctx) {
|
|
|
38
30
|
let consecutiveFailures = 0;
|
|
39
31
|
let blockedAt;
|
|
40
32
|
let failureReason = blockingScanIssues(ctx.scan.issues)[0]?.reason;
|
|
41
|
-
const stoppedAt = (what, index) => (
|
|
42
|
-
|
|
43
|
-
batch_index: index + 1,
|
|
44
|
-
batch_total: batches.length,
|
|
45
|
-
done: tally.done,
|
|
46
|
-
total: uploadable.length,
|
|
47
|
-
});
|
|
48
|
-
const reportProgress = () => {
|
|
49
|
-
if (!command.onProgress)
|
|
50
|
-
return;
|
|
51
|
-
const unresolved = new Set([
|
|
52
|
-
...uploadable.map(candidateCursorKey).filter((key) => !tally.durableCandidateKeys.has(key)),
|
|
53
|
-
...ctx.scan.retryable_candidate_keys,
|
|
54
|
-
]);
|
|
55
|
-
command.onProgress({
|
|
56
|
-
processed: tally.done,
|
|
57
|
-
uploaded: tally.backfilledSessions,
|
|
58
|
-
remaining: unresolved.size + ctx.scan.omitted_candidate_count + ctx.scan.issues
|
|
59
|
-
.filter((issue) => issue.scope === "global")
|
|
60
|
-
.reduce((total, issue) => total + issue.count, 0),
|
|
61
|
-
elapsedMs: Date.now() - ctx.now.getTime(),
|
|
62
|
-
});
|
|
63
|
-
};
|
|
33
|
+
const stoppedAt = (what, index) => describeBatchStop(what, index, batches.length, tally.done, uploadable.length);
|
|
34
|
+
const reportProgress = () => reportBackfillProgress(command, ctx, uploadable, tally);
|
|
64
35
|
reportProgress();
|
|
65
36
|
// BLI-11091: outside-root sessions upload under a synthetic workspace that
|
|
66
37
|
// only exists once something needs it (a dry run never gets here).
|
|
@@ -69,15 +40,10 @@ export async function uploadBackfillBatches(command, io, lock, ctx) {
|
|
|
69
40
|
await ensureOutsideRootsWorkspace(outsideWorkspace);
|
|
70
41
|
for (const [index, batch] of batches.entries()) {
|
|
71
42
|
await lock.handle.heartbeat();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
if (command.stubPass?.shouldYield && (await command.stubPass.shouldYield())) {
|
|
78
|
-
blockedAt = stoppedAt("yielded to a waiting backfill", index);
|
|
79
|
-
failureReason = "yielded_to_backfill";
|
|
80
|
-
console.error("[collector stubs] stub pass yielded the collection lock to a waiting backfill", JSON.stringify({ reason: "backfill_waiting", sessions_done: index, sessions_left: batches.length - index }));
|
|
43
|
+
const before = await stopBeforeBatch(command, index, batches.length);
|
|
44
|
+
if (before) {
|
|
45
|
+
blockedAt = stoppedAt(before.what, index);
|
|
46
|
+
failureReason = before.reason;
|
|
81
47
|
break;
|
|
82
48
|
}
|
|
83
49
|
const outcome = await uploadOneBackfillBatch(command, io, ctx, batch, rawEvidenceBudget);
|
|
@@ -88,30 +54,73 @@ export async function uploadBackfillBatches(command, io, lock, ctx) {
|
|
|
88
54
|
writeLine(io.stdout, `Uploaded ${tally.done}/${uploadable.length} (batch ${index + 1}/${batches.length})`);
|
|
89
55
|
}
|
|
90
56
|
await yieldToEventLoop();
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
// BLI-12832: the disk gate refused a copy. Every later batch would be
|
|
97
|
-
// refused the same way, so stop here; the cursor only advanced through
|
|
98
|
-
// sessions that earned a durable pointer, and the next run starts from
|
|
99
|
-
// the same place once room returns.
|
|
100
|
-
const stagingDeferred = stagingDeferredFiles(rawEvidenceBudget.staging);
|
|
101
|
-
if (stagingDeferred > 0) {
|
|
102
|
-
blockedAt = stoppedAt("staging paused: not enough room to copy more evidence", index);
|
|
103
|
-
failureReason = "deferred_budget_exhausted";
|
|
104
|
-
console.error("[backfill] stopped, the staging gate is holding new copies", JSON.stringify({ reason: "staging_deferred", files_deferred: stagingDeferred, batch_index: index + 1 }));
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
if (consecutiveFailures >= BACKFILL_MAX_CONSECUTIVE_FAILURES) {
|
|
108
|
-
blockedAt = stoppedAt("consecutive upload failures", index);
|
|
109
|
-
failureReason = failedBatchReason(outcome.sync, outcome.missingDurableMain);
|
|
57
|
+
const after = stopAfterBatch(outcome, rawEvidenceBudget, consecutiveFailures, index);
|
|
58
|
+
if (after) {
|
|
59
|
+
blockedAt = stoppedAt(after.what, index);
|
|
60
|
+
failureReason = after.reason;
|
|
110
61
|
break;
|
|
111
62
|
}
|
|
112
63
|
}
|
|
113
64
|
return { ...tally, uploadable, batches, blockedAt, failureReason };
|
|
114
65
|
}
|
|
66
|
+
function planUploadBatches(command, uploadable) {
|
|
67
|
+
// Doctor needs a boundary after each session, and so does the scheduled
|
|
68
|
+
// sync's stub pass (BLI-11112), which hands the lock to a waiting backfill
|
|
69
|
+
// at the next one. Ordinary backfill retains its existing batching.
|
|
70
|
+
const plannedBatches = buildBackfillBatches(uploadable);
|
|
71
|
+
return command.deadlineMs !== undefined || command.onProgress || command.stubPass
|
|
72
|
+
? plannedBatches.flatMap((batch) => batch.candidates.map((candidate) => ({
|
|
73
|
+
worktree: batch.worktree, candidates: [candidate],
|
|
74
|
+
})))
|
|
75
|
+
: plannedBatches;
|
|
76
|
+
}
|
|
77
|
+
function describeBatchStop(what, index, batchTotal, done, total) {
|
|
78
|
+
return { what, batch_index: index + 1, batch_total: batchTotal, done, total };
|
|
79
|
+
}
|
|
80
|
+
function reportBackfillProgress(command, ctx, uploadable, tally) {
|
|
81
|
+
if (!command.onProgress)
|
|
82
|
+
return;
|
|
83
|
+
const unresolved = new Set([
|
|
84
|
+
...uploadable.map(candidateCursorKey).filter((key) => !tally.durableCandidateKeys.has(key)),
|
|
85
|
+
...ctx.scan.retryable_candidate_keys,
|
|
86
|
+
]);
|
|
87
|
+
command.onProgress({
|
|
88
|
+
processed: tally.done,
|
|
89
|
+
uploaded: tally.backfilledSessions,
|
|
90
|
+
remaining: unresolved.size + ctx.scan.omitted_candidate_count + ctx.scan.issues
|
|
91
|
+
.filter((issue) => issue.scope === "global")
|
|
92
|
+
.reduce((total, issue) => total + issue.count, 0),
|
|
93
|
+
elapsedMs: Date.now() - ctx.now.getTime(),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async function stopBeforeBatch(command, index, batchCount) {
|
|
97
|
+
if (command.deadlineMs !== undefined && Date.now() >= command.deadlineMs) {
|
|
98
|
+
return { what: "backfill time budget reached", reason: "deferred_budget_exhausted" };
|
|
99
|
+
}
|
|
100
|
+
if (command.stubPass?.shouldYield && (await command.stubPass.shouldYield())) {
|
|
101
|
+
console.error("[collector stubs] stub pass yielded the collection lock to a waiting backfill", JSON.stringify({ reason: "backfill_waiting", sessions_done: index, sessions_left: batchCount - index }));
|
|
102
|
+
return { what: "yielded to a waiting backfill", reason: "yielded_to_backfill" };
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function stopAfterBatch(outcome, rawEvidenceBudget, consecutiveFailures, index) {
|
|
107
|
+
if (outcome.deferred) {
|
|
108
|
+
return { what: "raw evidence budget exhausted", reason: "deferred_budget_exhausted" };
|
|
109
|
+
}
|
|
110
|
+
// BLI-12832: the disk gate refused a copy. Every later batch would be
|
|
111
|
+
// refused the same way, so stop here; the cursor only advanced through
|
|
112
|
+
// sessions that earned a durable pointer, and the next run starts from
|
|
113
|
+
// the same place once room returns.
|
|
114
|
+
const stagingDeferred = stagingDeferredFiles(rawEvidenceBudget.staging);
|
|
115
|
+
if (stagingDeferred > 0) {
|
|
116
|
+
console.error("[backfill] stopped, the staging gate is holding new copies", JSON.stringify({ reason: "staging_deferred", files_deferred: stagingDeferred, batch_index: index + 1 }));
|
|
117
|
+
return { what: "staging paused: not enough room to copy more evidence", reason: "deferred_budget_exhausted" };
|
|
118
|
+
}
|
|
119
|
+
if (consecutiveFailures >= BACKFILL_MAX_CONSECUTIVE_FAILURES) {
|
|
120
|
+
return { what: "consecutive upload failures", reason: failedBatchReason(outcome.sync, outcome.missingDurableMain) };
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
115
124
|
/** Uploads one batch and judges it; every judgement here is a named predicate. */
|
|
116
125
|
async function uploadOneBackfillBatch(command, io, ctx, batch, rawEvidenceBudget) {
|
|
117
126
|
const sync = await syncBackfillBatch({
|
|
@@ -121,27 +121,12 @@ export async function runBackfill(command, io) {
|
|
|
121
121
|
// is working, so wait for it (bounded) instead of exiting at batch 0. Giving
|
|
122
122
|
// up is `sync_lock_held`, a blocked result and so a non-zero exit. The
|
|
123
123
|
// sync's own stub pass (BLI-11112) runs inside the sync's lock and skips it.
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
waited = await acquireSyncLockWithWait(paths, command.syncLockWaitMs ?? BACKFILL_SYNC_LOCK_WAIT_MS);
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
await lock.handle.release();
|
|
135
|
-
throw error;
|
|
136
|
-
}
|
|
137
|
-
if (!waited.acquired) {
|
|
138
|
-
await lock.handle.release();
|
|
139
|
-
return lockHeldResult(command, scanned, {
|
|
140
|
-
failureReason: "sync_lock_held",
|
|
141
|
-
what: "sync collection lock held",
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
collectionLock = waited.lock;
|
|
124
|
+
const collectionLock = await waitForCollectionLock(command, paths, lock);
|
|
125
|
+
if (!collectionLock) {
|
|
126
|
+
return lockHeldResult(command, scanned, {
|
|
127
|
+
failureReason: "sync_lock_held",
|
|
128
|
+
what: "sync collection lock held",
|
|
129
|
+
});
|
|
145
130
|
}
|
|
146
131
|
try {
|
|
147
132
|
// Backfill and scheduled/manual sync share upload spool and raw-evidence
|
|
@@ -161,4 +146,24 @@ export async function runBackfill(command, io) {
|
|
|
161
146
|
await collectionLock.handle.release();
|
|
162
147
|
await lock.handle.release();
|
|
163
148
|
}
|
|
149
|
+
}
|
|
150
|
+
async function waitForCollectionLock(command, paths, lock) {
|
|
151
|
+
if (command.stubPass) {
|
|
152
|
+
return { handle: { release: async () => undefined } };
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
let waited;
|
|
156
|
+
try {
|
|
157
|
+
waited = await acquireSyncLockWithWait(paths, command.syncLockWaitMs ?? BACKFILL_SYNC_LOCK_WAIT_MS);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
await lock.handle.release();
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
if (!waited.acquired) {
|
|
164
|
+
await lock.handle.release();
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
return waited.lock;
|
|
168
|
+
}
|
|
164
169
|
}
|
|
@@ -63,6 +63,10 @@ export async function runBriefDone(command, io) {
|
|
|
63
63
|
writeFailure(command, io, body.error ?? "not_saved", body.reply ?? body.error ?? "It did not save.");
|
|
64
64
|
return 1;
|
|
65
65
|
}
|
|
66
|
+
writeTickSuccess(command, io, body, done, found);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
function writeTickSuccess(command, io, body, done, found) {
|
|
66
70
|
if (command.json) {
|
|
67
71
|
writeLine(io.stdout, JSON.stringify(body));
|
|
68
72
|
}
|
|
@@ -76,7 +80,6 @@ export async function runBriefDone(command, io) {
|
|
|
76
80
|
item_key: found.itemKey,
|
|
77
81
|
has_ticket: found.ticketIdentifier != null,
|
|
78
82
|
})}`);
|
|
79
|
-
return 0;
|
|
80
83
|
}
|
|
81
84
|
/** The page, with its claim ids, so an item can be checked before anything is sent. */
|
|
82
85
|
async function readThePage(dashboardUrl, deviceToken, io, log) {
|