@miraland-labs/conduit-bridge 0.16.111 → 0.16.113
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/investigation.js +191 -7
- package/dist/probe-run.js +28 -0
- package/package.json +1 -1
package/dist/investigation.js
CHANGED
|
@@ -16,13 +16,22 @@ import { DRIVERS, extractAgentReportJsonText, parseJsonObjectCandidate } from ".
|
|
|
16
16
|
import { pickDriverForClaim, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
|
|
17
17
|
import { diffStatSince } from "./git-witness.js";
|
|
18
18
|
import { switchManagedWorkspace } from "./on-shift-apply.js";
|
|
19
|
-
import { execFile } from "node:child_process";
|
|
19
|
+
import { execFile, spawn } from "node:child_process";
|
|
20
20
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
21
|
+
import { createRequire } from "node:module";
|
|
21
22
|
import { tmpdir } from "node:os";
|
|
22
23
|
import { join } from "node:path";
|
|
23
24
|
import { promisify } from "node:util";
|
|
24
25
|
import { isRunnableVerificationCommand } from "./execution-class.js";
|
|
25
26
|
const execFileAsync = promisify(execFile);
|
|
27
|
+
const tsxLoader = (() => {
|
|
28
|
+
try {
|
|
29
|
+
return createRequire(import.meta.url).resolve("tsx");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return "tsx";
|
|
33
|
+
}
|
|
34
|
+
})();
|
|
26
35
|
const budgetSchema = z.object({
|
|
27
36
|
max_files: z.number().int().min(1).max(24).optional().default(8),
|
|
28
37
|
max_duration_ms: z.number().int().min(10_000).max(15 * 60_000).optional().default(5 * 60_000),
|
|
@@ -265,6 +274,70 @@ const PROBE_MAX = 20;
|
|
|
265
274
|
const PROBE_COMMAND_MAX = 300;
|
|
266
275
|
const PROBE_CHECKS_MAX = 500;
|
|
267
276
|
const PROBE_KEY_MAX = 40;
|
|
277
|
+
export const BRIDGE_PACKAGE_NAME = "@miraland-labs/conduit-bridge";
|
|
278
|
+
export const BRIDGE_SRC_PREFIX = "packages/conduit-bridge/src/";
|
|
279
|
+
export const PROBE_RUN_ENTRY = "packages/conduit-bridge/src/probe-run.ts";
|
|
280
|
+
export const DELIVERED_PROBE_SELF_CHECK_TAIL = "delivered probe instrument failed its self-check";
|
|
281
|
+
export const KNOWN_ANSWER_PROBES = [
|
|
282
|
+
{ key: "[known-holds]", lang: "sh", source: "echo 'PROBE HOLDS'\nexit 0", checks: "known answer: holds" },
|
|
283
|
+
{ key: "[known-fails]", lang: "sh", source: "echo 'PROBE FAILS'\nexit 1", checks: "known answer: fails" },
|
|
284
|
+
];
|
|
285
|
+
/**
|
|
286
|
+
* True when this is Conduit's own repository and a changed path between base and the delivered
|
|
287
|
+
* commit is under the Bridge source. Git errors are not a change: the running instrument stays.
|
|
288
|
+
*/
|
|
289
|
+
export async function probeInstrumentChanged(workspace, baseCommit, commit) {
|
|
290
|
+
if (baseCommit === null)
|
|
291
|
+
return false;
|
|
292
|
+
try {
|
|
293
|
+
const { stdout: raw } = await execFileAsync("git", ["show", `${commit}:packages/conduit-bridge/package.json`], {
|
|
294
|
+
cwd: workspace,
|
|
295
|
+
timeout: 30_000,
|
|
296
|
+
maxBuffer: 1_000_000,
|
|
297
|
+
});
|
|
298
|
+
const name = JSON.parse(raw).name;
|
|
299
|
+
if (name !== BRIDGE_PACKAGE_NAME)
|
|
300
|
+
return false;
|
|
301
|
+
const { stdout: diff } = await execFileAsync("git", ["diff", "--name-only", `${baseCommit}..${commit}`], {
|
|
302
|
+
cwd: workspace,
|
|
303
|
+
timeout: 30_000,
|
|
304
|
+
maxBuffer: 8_000_000,
|
|
305
|
+
});
|
|
306
|
+
return diff.split("\n").some((path) => {
|
|
307
|
+
const trimmed = path.trim();
|
|
308
|
+
return trimmed === "packages/conduit-bridge/src" || trimmed.startsWith(BRIDGE_SRC_PREFIX);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export function knownAnswerProbesPassed(witnesses) {
|
|
316
|
+
return witnesses.length === 2 && witnesses[0]?.verdict === "holds" && witnesses[1]?.verdict === "fails";
|
|
317
|
+
}
|
|
318
|
+
const PROBE_HOLDS_LINE = "PROBE HOLDS";
|
|
319
|
+
const PROBE_FAILS_LINE = "PROBE FAILS";
|
|
320
|
+
function lastNonEmptyLine(text) {
|
|
321
|
+
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
322
|
+
return lines.at(-1) ?? "";
|
|
323
|
+
}
|
|
324
|
+
function sourceProbeVerdict(exitCode, stdout) {
|
|
325
|
+
const last = lastNonEmptyLine(stdout);
|
|
326
|
+
if (exitCode === 0 && last === PROBE_HOLDS_LINE)
|
|
327
|
+
return "holds";
|
|
328
|
+
if (exitCode === 1 && last === PROBE_FAILS_LINE)
|
|
329
|
+
return "fails";
|
|
330
|
+
return "broken";
|
|
331
|
+
}
|
|
332
|
+
function testProbeVerdict(ran, exitCode) {
|
|
333
|
+
if (!ran)
|
|
334
|
+
return "broken";
|
|
335
|
+
return exitCode === 0 ? "holds" : "fails";
|
|
336
|
+
}
|
|
337
|
+
function probeTail(stderr, stdout) {
|
|
338
|
+
const stderrTail = stderr.length > PROBE_TAIL_CHARS ? stderr.slice(stderr.length - PROBE_TAIL_CHARS) : stderr;
|
|
339
|
+
return boundedTail(`${stderrTail}\n${stdout}`, PROBE_TAIL_CHARS);
|
|
340
|
+
}
|
|
268
341
|
function probeEnv() {
|
|
269
342
|
const env = {};
|
|
270
343
|
if (process.env.PATH !== undefined)
|
|
@@ -282,14 +355,14 @@ function probeInterpreter(language) {
|
|
|
282
355
|
}
|
|
283
356
|
function probeFilename(language) {
|
|
284
357
|
if (language === "ts")
|
|
285
|
-
return "probe.
|
|
358
|
+
return "probe.mts";
|
|
286
359
|
if (language === "py")
|
|
287
360
|
return "probe.py";
|
|
288
361
|
return "probe.sh";
|
|
289
362
|
}
|
|
290
363
|
function probeArgv(language, file) {
|
|
291
364
|
if (language === "ts")
|
|
292
|
-
return ["node", "--import",
|
|
365
|
+
return ["node", "--import", tsxLoader, file];
|
|
293
366
|
if (language === "py")
|
|
294
367
|
return ["python3", file];
|
|
295
368
|
return ["bash", file];
|
|
@@ -314,9 +387,11 @@ async function execProbe(argv, workspace) {
|
|
|
314
387
|
const err = error;
|
|
315
388
|
if (typeof err.code === "string")
|
|
316
389
|
throw error;
|
|
390
|
+
const stdout = String(err.stdout ?? "");
|
|
391
|
+
const stderr = String(err.stderr ?? "");
|
|
317
392
|
return {
|
|
318
|
-
stdout
|
|
319
|
-
stderr:
|
|
393
|
+
stdout,
|
|
394
|
+
stderr: stdout === "" && stderr === "" ? String(err.message || "probe failed") : stderr,
|
|
320
395
|
code: err.killed === true || typeof err.code !== "number" ? -1 : err.code,
|
|
321
396
|
};
|
|
322
397
|
}
|
|
@@ -327,8 +402,10 @@ function witnessRecord(input) {
|
|
|
327
402
|
kind: input.kind,
|
|
328
403
|
command: clipWitnessField(input.command, PROBE_COMMAND_MAX),
|
|
329
404
|
exit_code: input.exitCode,
|
|
330
|
-
tail:
|
|
405
|
+
tail: probeTail(input.stderr, input.stdout),
|
|
331
406
|
checks: clipWitnessField(input.checks, PROBE_CHECKS_MAX),
|
|
407
|
+
verdict: input.verdict,
|
|
408
|
+
instrument: "running",
|
|
332
409
|
};
|
|
333
410
|
}
|
|
334
411
|
/**
|
|
@@ -346,17 +423,20 @@ export async function runDeliveryVerificationProbes(input) {
|
|
|
346
423
|
if (!isRunnableVerificationCommand(named)) {
|
|
347
424
|
witnesses.push(witnessRecord({
|
|
348
425
|
key: probe.key, kind: "test", command: named, exitCode: -1, stdout: "", stderr: "not a registered command", checks,
|
|
426
|
+
verdict: "broken",
|
|
349
427
|
}));
|
|
350
428
|
continue;
|
|
351
429
|
}
|
|
352
430
|
let exitCode = -1;
|
|
353
431
|
let stdout = "";
|
|
354
432
|
let stderr = "";
|
|
433
|
+
let ran = false;
|
|
355
434
|
try {
|
|
356
435
|
const result = await (input.runCommand ?? runBoundedVerificationCommand)(named, input.workspace);
|
|
357
436
|
exitCode = result.code;
|
|
358
437
|
stdout = result.stdout;
|
|
359
438
|
stderr = result.stderr;
|
|
439
|
+
ran = true;
|
|
360
440
|
}
|
|
361
441
|
catch (error) {
|
|
362
442
|
stderr = error instanceof Error ? error.message : String(error);
|
|
@@ -364,6 +444,7 @@ export async function runDeliveryVerificationProbes(input) {
|
|
|
364
444
|
}
|
|
365
445
|
witnesses.push(witnessRecord({
|
|
366
446
|
key: probe.key, kind: "test", command: named, exitCode, stdout, stderr, checks,
|
|
447
|
+
verdict: testProbeVerdict(ran, exitCode),
|
|
367
448
|
}));
|
|
368
449
|
continue;
|
|
369
450
|
}
|
|
@@ -394,10 +475,111 @@ export async function runDeliveryVerificationProbes(input) {
|
|
|
394
475
|
}
|
|
395
476
|
witnesses.push(witnessRecord({
|
|
396
477
|
key: probe.key, kind: "probe", command, exitCode, stdout, stderr, checks,
|
|
478
|
+
verdict: sourceProbeVerdict(exitCode, stdout),
|
|
397
479
|
}));
|
|
398
480
|
}
|
|
399
481
|
return witnesses;
|
|
400
482
|
}
|
|
483
|
+
const deliveredWitnessSchema = z.object({
|
|
484
|
+
witnesses: z.array(z.object({
|
|
485
|
+
key: z.string(),
|
|
486
|
+
kind: z.enum(["test", "probe"]),
|
|
487
|
+
command: z.string(),
|
|
488
|
+
exit_code: z.number(),
|
|
489
|
+
tail: z.string(),
|
|
490
|
+
checks: z.string(),
|
|
491
|
+
verdict: z.enum(["holds", "fails", "broken"]),
|
|
492
|
+
instrument: z.enum(["running", "delivered"]).optional(),
|
|
493
|
+
}).passthrough()),
|
|
494
|
+
});
|
|
495
|
+
function stampInstrument(witnesses, instrument) {
|
|
496
|
+
return witnesses.map((witness) => ({ ...witness, instrument }));
|
|
497
|
+
}
|
|
498
|
+
function markSelfCheckBroken(witnesses) {
|
|
499
|
+
return stampInstrument(witnesses.map((witness) => ({
|
|
500
|
+
...witness,
|
|
501
|
+
verdict: "broken",
|
|
502
|
+
tail: DELIVERED_PROBE_SELF_CHECK_TAIL,
|
|
503
|
+
})), "running");
|
|
504
|
+
}
|
|
505
|
+
function toStepWitness(witness) {
|
|
506
|
+
return {
|
|
507
|
+
key: witness.key,
|
|
508
|
+
kind: witness.kind,
|
|
509
|
+
command: witness.command,
|
|
510
|
+
exit_code: witness.exit_code,
|
|
511
|
+
tail: witness.tail,
|
|
512
|
+
checks: witness.checks,
|
|
513
|
+
verdict: witness.verdict,
|
|
514
|
+
instrument: witness.instrument === "delivered" ? "delivered" : "running",
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
async function execDeliveredProbeRun(argv, stdin, workspace, timeoutMs) {
|
|
518
|
+
const [file, ...args] = argv;
|
|
519
|
+
if (!file)
|
|
520
|
+
throw new Error("delivered probe command is empty");
|
|
521
|
+
return await new Promise((resolve, reject) => {
|
|
522
|
+
const child = spawn(file, args, {
|
|
523
|
+
cwd: workspace,
|
|
524
|
+
env: probeEnv(),
|
|
525
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
526
|
+
});
|
|
527
|
+
let stdout = "";
|
|
528
|
+
let stderr = "";
|
|
529
|
+
const timer = setTimeout(() => {
|
|
530
|
+
child.kill("SIGKILL");
|
|
531
|
+
}, timeoutMs);
|
|
532
|
+
child.stdout?.on("data", (chunk) => { stdout += String(chunk); });
|
|
533
|
+
child.stderr?.on("data", (chunk) => { stderr += String(chunk); });
|
|
534
|
+
child.on("error", (error) => {
|
|
535
|
+
clearTimeout(timer);
|
|
536
|
+
reject(error);
|
|
537
|
+
});
|
|
538
|
+
child.on("close", (code) => {
|
|
539
|
+
clearTimeout(timer);
|
|
540
|
+
resolve({ code: code ?? -1, stdout, stderr });
|
|
541
|
+
});
|
|
542
|
+
child.stdin?.on("error", () => undefined);
|
|
543
|
+
child.stdin?.end(stdin);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
export async function runDeliveredProbeInstrument(input) {
|
|
547
|
+
const entry = join(input.workspace, PROBE_RUN_ENTRY);
|
|
548
|
+
const timeoutMs = PROBE_TIMEOUT_MS * (input.probes.length + 1);
|
|
549
|
+
const stdin = JSON.stringify({ probes: input.probes });
|
|
550
|
+
const argv = ["node", "--import", tsxLoader, entry];
|
|
551
|
+
const result = await (input.exec ?? execDeliveredProbeRun)(argv, stdin, input.workspace, timeoutMs);
|
|
552
|
+
if (result.code !== 0) {
|
|
553
|
+
throw new Error(result.stderr.trim() || `delivered probe-run exited ${result.code}`);
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
return stampInstrument(deliveredWitnessSchema.parse(JSON.parse(result.stdout)).witnesses.map(toStepWitness), "delivered");
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
throw error instanceof Error ? error : new Error("delivered probe instrument returned no witness list");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
export async function runDeliveryVerificationProbesForAttempt(input) {
|
|
563
|
+
const runRunning = () => runDeliveryVerificationProbes({
|
|
564
|
+
workspace: input.workspace,
|
|
565
|
+
probes: input.probes,
|
|
566
|
+
runCommand: input.runCommand,
|
|
567
|
+
});
|
|
568
|
+
if (!input.instrumentChanged) {
|
|
569
|
+
return stampInstrument(await runRunning(), "running");
|
|
570
|
+
}
|
|
571
|
+
const runDelivered = input.runDeliveredProbes ?? ((args) => runDeliveredProbeInstrument(args));
|
|
572
|
+
try {
|
|
573
|
+
const check = await runDelivered({ workspace: input.workspace, probes: KNOWN_ANSWER_PROBES });
|
|
574
|
+
if (knownAnswerProbesPassed(check)) {
|
|
575
|
+
return stampInstrument(await runDelivered({ workspace: input.workspace, probes: input.probes }), "delivered");
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
// Known-answer failure and a delivered-instrument throw take the same path: running probes, then broken.
|
|
580
|
+
}
|
|
581
|
+
return markSelfCheckBroken(await runRunning());
|
|
582
|
+
}
|
|
401
583
|
async function settle(client, investigationId, body) {
|
|
402
584
|
await client.request(`/runner/v1/investigations/${investigationId}/settle`, {
|
|
403
585
|
method: "POST",
|
|
@@ -562,10 +744,12 @@ export async function executeNextInvestigation(client, config, workspace, brief,
|
|
|
562
744
|
return true;
|
|
563
745
|
}
|
|
564
746
|
const stepWitness = deliveryVerification
|
|
565
|
-
? await
|
|
747
|
+
? await runDeliveryVerificationProbesForAttempt({
|
|
566
748
|
workspace: attemptWorkspace,
|
|
567
749
|
probes: observed.probes ?? [],
|
|
750
|
+
instrumentChanged: await probeInstrumentChanged(attemptWorkspace, assignment.base_commit, commit),
|
|
568
751
|
runCommand: options.runProbeCommand,
|
|
752
|
+
runDeliveredProbes: options.runDeliveredProbes,
|
|
569
753
|
})
|
|
570
754
|
: [];
|
|
571
755
|
if (deliveryVerification)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Delivered probe instrument entry. `{ "probes": [...] }` on stdin, `{ witnesses }` on stdout. */
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { runDeliveryVerificationProbes } from "./investigation.js";
|
|
4
|
+
const probeRunInputSchema = z.object({
|
|
5
|
+
probes: z.array(z.object({
|
|
6
|
+
key: z.string().trim().min(1).max(40),
|
|
7
|
+
test: z.string().max(300).optional(),
|
|
8
|
+
lang: z.enum(["ts", "py", "sh"]).optional(),
|
|
9
|
+
source: z.string().max(8_000).optional(),
|
|
10
|
+
checks: z.string().max(500).optional().default(""),
|
|
11
|
+
})).max(20),
|
|
12
|
+
});
|
|
13
|
+
async function readStdin() {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
for await (const chunk of process.stdin) {
|
|
16
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
17
|
+
}
|
|
18
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const parsed = probeRunInputSchema.parse(JSON.parse(await readStdin()));
|
|
22
|
+
const witnesses = await runDeliveryVerificationProbes({ workspace: process.cwd(), probes: parsed.probes });
|
|
23
|
+
process.stdout.write(`${JSON.stringify({ witnesses })}\n`);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.113",
|
|
4
4
|
"description": "Conduit Bridge CLI \u2014 join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity / Grok Build agents for a Conduit organization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|