@botbuddy/cli 1.37.0 → 1.38.0
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/package.json +1 -1
- package/src/lane-status.mjs +22 -0
- package/src/stack.mjs +6 -0
- package/src/telemetry-outbox.mjs +30 -5
package/package.json
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// BOT-1319 — report a phase to the BotBuddy lefthook lane wrapper that may be
|
|
2
|
+
// running this command (botbuddy-web `scripts/log-test-run.mjs`, the same file
|
|
3
|
+
// protocol as `scripts/lib/lane-status.mjs`). The wrapper exports
|
|
4
|
+
// $BOTBUDDY_LANE_STATUS_FILE and turns each atomic rewrite into one lifecycle
|
|
5
|
+
// progress event, so a lane parked here for Docker capacity shows as
|
|
6
|
+
// `waiting · queued_for_capacity` instead of as silent test time. Unset outside a
|
|
7
|
+
// wrapped lane → no-op. Never throws.
|
|
8
|
+
|
|
9
|
+
import { renameSync, writeFileSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
export function writeLaneStatus(status, env = process.env) {
|
|
12
|
+
const path = env.BOTBUDDY_LANE_STATUS_FILE;
|
|
13
|
+
if (!path) return false;
|
|
14
|
+
try {
|
|
15
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
16
|
+
writeFileSync(tmp, JSON.stringify({ ...status, at: new Date().toISOString() }));
|
|
17
|
+
renameSync(tmp, path);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/stack.mjs
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// labelled fallback for when no Helper is available: it runs `supabase start`/
|
|
22
22
|
// `stop` itself and self-activates/finalizes via the fallback MCP tools.
|
|
23
23
|
|
|
24
|
+
import { writeLaneStatus } from "./lane-status.mjs";
|
|
24
25
|
import { spawn, spawnSync } from "child_process";
|
|
25
26
|
import { readFileSync, realpathSync } from "fs";
|
|
26
27
|
import { open, unlink, readFile } from "fs/promises";
|
|
@@ -1497,6 +1498,7 @@ export async function cmdUp(opts, {
|
|
|
1497
1498
|
machineUuidFn = machineUuid,
|
|
1498
1499
|
assertIsolated = validateLocalExecTarget,
|
|
1499
1500
|
refreshAuth = refreshRejectedCacheAuth,
|
|
1501
|
+
reportLaneStatus = writeLaneStatus,
|
|
1500
1502
|
waitCapacity = defaultWaitCapacity,
|
|
1501
1503
|
} = {}) {
|
|
1502
1504
|
let slot;
|
|
@@ -1641,6 +1643,9 @@ export async function cmdUp(opts, {
|
|
|
1641
1643
|
const reusedExisting = state === "active";
|
|
1642
1644
|
|
|
1643
1645
|
if (state === "queued") {
|
|
1646
|
+
// BOT-1319: a wrapped lefthook lane records this park as waiting for capacity.
|
|
1647
|
+
const where = [d.queue_position != null ? `queue position ${d.queue_position}` : null, d.host_key ? `on ${d.host_key}` : null].filter(Boolean).join(" ");
|
|
1648
|
+
reportLaneStatus({ phase: "waiting", reason: "queued_for_capacity", detail: where || "host at capacity" });
|
|
1644
1649
|
if (opts.noWait) {
|
|
1645
1650
|
return emitResult(buildReceipt({ command: "up", outcome: "queued", lease_id: leaseId, state, host_key: d.host_key, slot, queued: true, queue_position: d.queue_position, holders: d.holders }), opts, EXIT.QUEUED);
|
|
1646
1651
|
}
|
|
@@ -1651,6 +1656,7 @@ export async function cmdUp(opts, {
|
|
|
1651
1656
|
if (parked.error) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: parked.error }), opts, EXIT.BACKEND);
|
|
1652
1657
|
state = parked.state || "provisioning";
|
|
1653
1658
|
}
|
|
1659
|
+
reportLaneStatus({ phase: "testing", reason: "provisioning" });
|
|
1654
1660
|
|
|
1655
1661
|
// Reach `active`: local-exec provisions itself; otherwise wait for the Helper.
|
|
1656
1662
|
if (state !== "active") {
|
package/src/telemetry-outbox.mjs
CHANGED
|
@@ -323,11 +323,19 @@ export async function createTelemetryOutbox({
|
|
|
323
323
|
try { imported = JSON.parse(await readFile(legacyImportPath, "utf8")); } catch { /* first migration */ }
|
|
324
324
|
let names = [];
|
|
325
325
|
try { names = (await readdir(legacyDirectory)).filter((name) => name.endsWith(".json")); } catch (error) { if (error?.code === "ENOENT") return { imported: 0, skipped: 0 }; throw error; }
|
|
326
|
+
// BOT-1319: the test-ingest reporter archives what it posted under reported/
|
|
327
|
+
// before the next hook can recover it; a summary whose lifecycle was degraded
|
|
328
|
+
// is still eligible there (others under reported/ were already counted).
|
|
329
|
+
let archived = [];
|
|
330
|
+
try { archived = (await readdir(join(legacyDirectory, "reported"))).filter((name) => name.endsWith(".json")); } catch { /* none archived */ }
|
|
331
|
+
const entries = [
|
|
332
|
+
...names.sort().map((name) => ({ name, file: join(legacyDirectory, name), archived: false })),
|
|
333
|
+
...archived.sort().map((name) => ({ name, file: join(legacyDirectory, "reported", name), archived: true })),
|
|
334
|
+
];
|
|
326
335
|
let added = 0;
|
|
327
336
|
let skipped = 0;
|
|
328
|
-
for (const name of
|
|
337
|
+
for (const { name, file, archived: fromArchive } of entries) {
|
|
329
338
|
if (added >= maxReceipts || Date.now() >= deadline) break;
|
|
330
|
-
const file = join(legacyDirectory, name);
|
|
331
339
|
let raw;
|
|
332
340
|
try { raw = await readFile(file, "utf8"); } catch { skipped += 1; continue; }
|
|
333
341
|
const fingerprint = hash(`${name}\0${raw}`);
|
|
@@ -335,13 +343,30 @@ export async function createTelemetryOutbox({
|
|
|
335
343
|
let summary;
|
|
336
344
|
try { summary = JSON.parse(raw); } catch { skipped += 1; continue; }
|
|
337
345
|
if (!summary || typeof summary !== "object" || typeof summary.runner !== "string") { skipped += 1; continue; }
|
|
346
|
+
if (fromArchive && summary.lifecycle?.telemetry !== "degraded") continue;
|
|
347
|
+
// BOT-1319: the lane wrapper (and anything nested in a wrapped lane) stamps
|
|
348
|
+
// `lifecycle` when a canonical execution already carries this run. Importing
|
|
349
|
+
// it too would count the same run twice; record it as seen and move on. A
|
|
350
|
+
// wrapper summary stamped `telemetry: "degraded"` never reached the outbox
|
|
351
|
+
// durably, so it stays eligible and imports like a historical summary.
|
|
352
|
+
const lifecycle = summary.lifecycle;
|
|
353
|
+
if (lifecycle && typeof lifecycle === "object" && (lifecycle.owned_by || lifecycle.telemetry === "durable")) {
|
|
354
|
+
imported[fingerprint] = { name, imported_at: now().toISOString(), lifecycle: true };
|
|
355
|
+
skipped += 1;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
338
358
|
const sourceRunId = `legacy-${fingerprint.slice(0, 32)}`;
|
|
339
359
|
const started = typeof summary.started_at === "string" ? summary.started_at : now().toISOString();
|
|
340
360
|
const finished = typeof summary.finished_at === "string" ? summary.finished_at : now().toISOString();
|
|
341
|
-
|
|
342
|
-
const
|
|
361
|
+
// An explicit skip (BOT-1319 wrapper, verdict "skip", exit 0) is never green.
|
|
362
|
+
const isSkip = summary.verdict === "skip";
|
|
363
|
+
const passed = !isSkip && (summary.verdict === "pass" || summary.exit_code === 0);
|
|
364
|
+
const context = { repo: repository, environment: "local", runner: summary.runner, phase: passed || isSkip ? "complete" : "failed", health: "uninstrumented", ...(isSkip ? { skipped: 1 } : {}) };
|
|
365
|
+
const terminal = isSkip
|
|
366
|
+
? { outcome: "canceled", diagnostic: { code: "skipped", category: "skip", message: String(summary.skipped_reason ?? "skipped").slice(0, 500) } }
|
|
367
|
+
: { outcome: passed ? "passed" : "failed" };
|
|
343
368
|
await append({ schema_version: 1, action: "start", producer_kind: "hook", source_run_id: sourceRunId, attempt: 1, producer_event_id: `${sourceRunId}:start`, producer_sequence: 0, occurred_at: started, context });
|
|
344
|
-
await append({ schema_version: 1, action: "terminal", producer_kind: "hook", source_run_id: sourceRunId, attempt: 1, producer_event_id: `${sourceRunId}:terminal`, producer_sequence: 1, occurred_at: finished, context,
|
|
369
|
+
await append({ schema_version: 1, action: "terminal", producer_kind: "hook", source_run_id: sourceRunId, attempt: 1, producer_event_id: `${sourceRunId}:terminal`, producer_sequence: 1, occurred_at: finished, context, ...terminal });
|
|
345
370
|
imported[fingerprint] = { name, imported_at: now().toISOString() };
|
|
346
371
|
added += 1;
|
|
347
372
|
}
|