@miraland-labs/conduit-bridge 0.16.112 → 0.16.114

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/cli.js CHANGED
@@ -655,13 +655,6 @@ async function runner() {
655
655
  await heartbeat(client, config, workspace, currentBrief, preflight, intentProof, bootstrapResult);
656
656
  }
657
657
  await renewLeases(client, config);
658
- if (workspace && onlineDriverIds(config).length) {
659
- // Observation runs before authoring: an owner waiting on an answer should not queue behind
660
- // a long delivery, and the control plane already counted this slot as busy.
661
- progressed = await executeNextInvestigation(client, config, workspace, brief, timeoutMs, {
662
- managedRoot: hb.on_shift?.managed_workspace_root ?? null,
663
- }) || progressed;
664
- }
665
658
  if (workspace && onlineDriverIds(config).length) {
666
659
  // Read the workspace again, then publish it. The last brief stands if the read fails.
667
660
  const publishWorkspaceState = async (options = {}) => {
@@ -671,6 +664,15 @@ async function runner() {
671
664
  const hb = await heartbeat(client, config, workspace, currentBrief, preflight, undefined, bootstrapResult);
672
665
  return { preflight, hb };
673
666
  };
667
+ // Observation runs before authoring: an owner waiting on an answer should not queue behind
668
+ // a long delivery, and the control plane already counted this slot as busy.
669
+ progressed = await executeNextInvestigation(client, config, workspace, brief, timeoutMs, {
670
+ managedRoot: hb.on_shift?.managed_workspace_root ?? null,
671
+ heartbeat: async () => {
672
+ await publishWorkspaceState();
673
+ },
674
+ heartbeatIntervalMs: intervalMs,
675
+ }) || progressed;
674
676
  progressed = await pumpExecutionSlots(client, config, workspace, brief, timeoutMs, {
675
677
  heartbeat: async () => {
676
678
  await publishWorkspaceState();
@@ -16,7 +16,7 @@ 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
21
  import { createRequire } from "node:module";
22
22
  import { tmpdir } from "node:os";
@@ -100,6 +100,8 @@ export const DELIVERY_VERIFICATION_HEADER = "DELIVERY VERIFICATION";
100
100
  export const DELIVERY_VERIFICATION_PROBE_INSTRUCTION = 'For every BRIEF KEY that states behaviour, add one entry to probes: either {"key":"[sN]","test":"<registered command that asserts it>","checks":"<one line: what it proves>"} or {"key":"[sN]","lang":"ts|py|sh","source":"<a short script that exits 0 only when the key holds at this commit and non-zero otherwise; it may import repository modules by absolute path from the working directory>","checks":"<one line>"}. You do not run probes; Bridge runs them after you finish. Keys that state no behaviour (wording, scope, docs) get no probe.';
101
101
  const BRIEF_JSON_EXAMPLE = '{"summary":"one paragraph: what the change does and whether it meets the criteria","findings":["grounded fact with file/symbol"],"verification":["command — result"],"limitations":["what you could not settle"],"criteria":[{"criterion":"c1","assessment":"supported|unsupported|unclear","reason":"what you saw"}],"command_failures":["command that exited non-zero — the decisive output line"],"files_read":0,"bytes_read":0,"commands_run":["command you actually ran"]}';
102
102
  const BRIEF_JSON_EXAMPLE_WITH_PROBES = '{"summary":"one paragraph: what the change does and whether it meets the criteria","findings":["grounded fact with file/symbol"],"verification":["command — result"],"limitations":["what you could not settle"],"criteria":[{"criterion":"c1","assessment":"supported|unsupported|unclear","reason":"what you saw"}],"command_failures":["command that exited non-zero — the decisive output line"],"probes":[],"files_read":0,"bytes_read":0,"commands_run":["command you actually ran"]}';
103
+ /** Named in both prompts so the lane knows each list's maximum before it writes. */
104
+ export const BRIEF_LIMITS_SENTENCE = "Limits: findings at most 12, verification at most 8, limitations at most 8, criteria at most 40, command_failures at most 10, commands_run at most 10, probes at most 20. Keep the most decisive entries.";
103
105
  export function isDeliveryVerification(assignment) {
104
106
  return assignment.question.trimStart().startsWith(DELIVERY_VERIFICATION_HEADER);
105
107
  }
@@ -121,6 +123,7 @@ export function buildInvestigationPrompt(assignment, commit, context) {
121
123
  "",
122
124
  "Return only one fenced ```json object with exactly this shape:",
123
125
  '{"summary":"direct answer to the question","findings":["grounded fact with file/symbol"],"verification":["command you actually ran"],"limitations":["what you could not settle"],"files_read":0,"bytes_read":0,"commands_run":["command you actually ran"]}',
126
+ BRIEF_LIMITS_SENTENCE,
124
127
  "summary is a string; the other list fields are arrays of strings. Close the fence and add no prose after it.",
125
128
  ].join("\n");
126
129
  }
@@ -164,6 +167,7 @@ export function buildDeliveryVerificationPrompt(assignment, commit, context) {
164
167
  "",
165
168
  "Return only one fenced ```json object with exactly this shape:",
166
169
  keys.length ? BRIEF_JSON_EXAMPLE_WITH_PROBES : BRIEF_JSON_EXAMPLE,
170
+ BRIEF_LIMITS_SENTENCE,
167
171
  keys.length
168
172
  ? "criteria has one entry per ACCEPTANCE key. probes has at most 20 entries, one per BRIEF KEY that states behaviour. Close the fence and add no prose after it."
169
173
  : "criteria has one entry per ACCEPTANCE key. Close the fence and add no prose after it.",
@@ -208,6 +212,83 @@ function settleCommandsRun(gates, reported) {
208
212
  export const UNUSABLE_BRIEF = "Investigation returned no usable brief";
209
213
  /** T130: how much of an unusable reply travels with the failure, so the next run can be diagnosed. */
210
214
  const UNUSABLE_BRIEF_TAIL_CHARS = 800;
215
+ const BRIEF_SUMMARY_MAX = 4_000;
216
+ const BRIEF_ITEM_MAX = 1_000;
217
+ const BRIEF_FILES_READ_MAX = 24;
218
+ const BRIEF_BYTES_READ_MAX = 512 * 1024;
219
+ const BRIEF_COMMAND_RUN_MAX = 200;
220
+ const BRIEF_LIST_MAX = {
221
+ findings: 12,
222
+ verification: 8,
223
+ limitations: 8,
224
+ criteria: 40,
225
+ command_failures: 10,
226
+ commands_run: 10,
227
+ probes: 20,
228
+ };
229
+ function clipBriefText(value, max) {
230
+ return value.length > max ? value.slice(0, max) : value;
231
+ }
232
+ function clipBriefStringField(record, key, max) {
233
+ const value = record[key];
234
+ if (typeof value === "string")
235
+ record[key] = clipBriefText(value, max);
236
+ }
237
+ function fitBriefStrings(items, max) {
238
+ return items.map((item) => typeof item === "string" ? clipBriefText(item, max) : item);
239
+ }
240
+ function fitCriteriaItems(items) {
241
+ return items.map((item) => {
242
+ if (!item || typeof item !== "object" || Array.isArray(item))
243
+ return item;
244
+ const row = { ...item };
245
+ clipBriefStringField(row, "criterion", BRIEF_ITEM_MAX);
246
+ clipBriefStringField(row, "reason", BRIEF_ITEM_MAX);
247
+ return row;
248
+ });
249
+ }
250
+ /**
251
+ * Cut an over-full brief to the contract before schema parse. Only size is fitted; a wrong type,
252
+ * a missing summary, or an unknown assessment still fails as today. Probe entries are kept as
253
+ * written — clipping them would change what Bridge later executes.
254
+ */
255
+ export function fitBriefToContract(candidate) {
256
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
257
+ return { fitted: candidate, cuts: [] };
258
+ }
259
+ const brief = { ...candidate };
260
+ const cuts = [];
261
+ const fitNamedList = (key, fitItems) => {
262
+ const value = brief[key];
263
+ if (!Array.isArray(value))
264
+ return;
265
+ const max = BRIEF_LIST_MAX[key];
266
+ if (value.length > max)
267
+ cuts.push({ list: key, kept: max, total: value.length });
268
+ brief[key] = fitItems(value.slice(0, max));
269
+ };
270
+ fitNamedList("findings", (items) => fitBriefStrings(items, BRIEF_ITEM_MAX));
271
+ fitNamedList("verification", (items) => fitBriefStrings(items, BRIEF_ITEM_MAX));
272
+ fitNamedList("limitations", (items) => fitBriefStrings(items, BRIEF_ITEM_MAX));
273
+ fitNamedList("criteria", fitCriteriaItems);
274
+ fitNamedList("command_failures", (items) => fitBriefStrings(items, BRIEF_ITEM_MAX));
275
+ fitNamedList("commands_run", (items) => fitBriefStrings(items, BRIEF_COMMAND_RUN_MAX));
276
+ fitNamedList("probes", (items) => items);
277
+ clipBriefStringField(brief, "summary", BRIEF_SUMMARY_MAX);
278
+ if (typeof brief.files_read === "number" && brief.files_read > BRIEF_FILES_READ_MAX) {
279
+ brief.files_read = BRIEF_FILES_READ_MAX;
280
+ }
281
+ if (typeof brief.bytes_read === "number" && brief.bytes_read > BRIEF_BYTES_READ_MAX) {
282
+ brief.bytes_read = BRIEF_BYTES_READ_MAX;
283
+ }
284
+ if (cuts.length) {
285
+ const notes = cuts.map((cut) => clipBriefText(`Bridge kept the first ${cut.kept} of ${cut.total} ${cut.list}`, BRIEF_ITEM_MAX));
286
+ const existing = Array.isArray(brief.limitations) ? [...brief.limitations] : [];
287
+ const room = Math.max(0, BRIEF_LIST_MAX.limitations - notes.length);
288
+ brief.limitations = [...existing.slice(0, room), ...notes];
289
+ }
290
+ return { fitted: brief, cuts };
291
+ }
211
292
  /**
212
293
  * T130: an unusable brief used to fail with five words. The first production verification retry
213
294
  * (2026-09-09, acme-sim / forge R20) returned "no usable brief" twice and nobody could say whether the
@@ -222,7 +303,7 @@ export function parseInvestigationBrief(text) {
222
303
  catch {
223
304
  throw new Error(`${UNUSABLE_BRIEF}: no JSON object in the reply. Reply tail: ${redactSecrets(boundedTail(text, UNUSABLE_BRIEF_TAIL_CHARS))}`);
224
305
  }
225
- const parsed = investigationBriefSchema.safeParse(candidate);
306
+ const parsed = investigationBriefSchema.safeParse(fitBriefToContract(candidate).fitted);
226
307
  if (!parsed.success) {
227
308
  const issues = parsed.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
228
309
  throw new Error(`${UNUSABLE_BRIEF}: the JSON does not match the brief contract (${issues}). Reply tail: ${redactSecrets(boundedTail(text, UNUSABLE_BRIEF_TAIL_CHARS))}`);
@@ -274,6 +355,47 @@ const PROBE_MAX = 20;
274
355
  const PROBE_COMMAND_MAX = 300;
275
356
  const PROBE_CHECKS_MAX = 500;
276
357
  const PROBE_KEY_MAX = 40;
358
+ export const BRIDGE_PACKAGE_NAME = "@miraland-labs/conduit-bridge";
359
+ export const BRIDGE_SRC_PREFIX = "packages/conduit-bridge/src/";
360
+ export const PROBE_RUN_ENTRY = "packages/conduit-bridge/src/probe-run.ts";
361
+ export const DELIVERED_PROBE_SELF_CHECK_TAIL = "delivered probe instrument failed its self-check";
362
+ export const KNOWN_ANSWER_PROBES = [
363
+ { key: "[known-holds]", lang: "sh", source: "echo 'PROBE HOLDS'\nexit 0", checks: "known answer: holds" },
364
+ { key: "[known-fails]", lang: "sh", source: "echo 'PROBE FAILS'\nexit 1", checks: "known answer: fails" },
365
+ ];
366
+ /**
367
+ * True when this is Conduit's own repository and a changed path between base and the delivered
368
+ * commit is under the Bridge source. Git errors are not a change: the running instrument stays.
369
+ */
370
+ export async function probeInstrumentChanged(workspace, baseCommit, commit) {
371
+ if (baseCommit === null)
372
+ return false;
373
+ try {
374
+ const { stdout: raw } = await execFileAsync("git", ["show", `${commit}:packages/conduit-bridge/package.json`], {
375
+ cwd: workspace,
376
+ timeout: 30_000,
377
+ maxBuffer: 1_000_000,
378
+ });
379
+ const name = JSON.parse(raw).name;
380
+ if (name !== BRIDGE_PACKAGE_NAME)
381
+ return false;
382
+ const { stdout: diff } = await execFileAsync("git", ["diff", "--name-only", `${baseCommit}..${commit}`], {
383
+ cwd: workspace,
384
+ timeout: 30_000,
385
+ maxBuffer: 8_000_000,
386
+ });
387
+ return diff.split("\n").some((path) => {
388
+ const trimmed = path.trim();
389
+ return trimmed === "packages/conduit-bridge/src" || trimmed.startsWith(BRIDGE_SRC_PREFIX);
390
+ });
391
+ }
392
+ catch {
393
+ return false;
394
+ }
395
+ }
396
+ export function knownAnswerProbesPassed(witnesses) {
397
+ return witnesses.length === 2 && witnesses[0]?.verdict === "holds" && witnesses[1]?.verdict === "fails";
398
+ }
277
399
  const PROBE_HOLDS_LINE = "PROBE HOLDS";
278
400
  const PROBE_FAILS_LINE = "PROBE FAILS";
279
401
  function lastNonEmptyLine(text) {
@@ -364,6 +486,7 @@ function witnessRecord(input) {
364
486
  tail: probeTail(input.stderr, input.stdout),
365
487
  checks: clipWitnessField(input.checks, PROBE_CHECKS_MAX),
366
488
  verdict: input.verdict,
489
+ instrument: "running",
367
490
  };
368
491
  }
369
492
  /**
@@ -438,6 +561,109 @@ export async function runDeliveryVerificationProbes(input) {
438
561
  }
439
562
  return witnesses;
440
563
  }
564
+ const deliveredWitnessSchema = z.object({
565
+ witnesses: z.array(z.object({
566
+ key: z.string(),
567
+ kind: z.enum(["test", "probe"]),
568
+ command: z.string(),
569
+ exit_code: z.number(),
570
+ tail: z.string(),
571
+ checks: z.string(),
572
+ verdict: z.enum(["holds", "fails", "broken"]),
573
+ instrument: z.enum(["running", "delivered"]).optional(),
574
+ }).passthrough()),
575
+ });
576
+ function stampInstrument(witnesses, instrument) {
577
+ return witnesses.map((witness) => ({ ...witness, instrument }));
578
+ }
579
+ function markSelfCheckBroken(witnesses, reason) {
580
+ const tail = boundedTail(`${DELIVERED_PROBE_SELF_CHECK_TAIL}: ${reason}`, PROBE_TAIL_CHARS);
581
+ return stampInstrument(witnesses.map((witness) => ({
582
+ ...witness,
583
+ verdict: "broken",
584
+ tail,
585
+ })), "running");
586
+ }
587
+ function toStepWitness(witness) {
588
+ return {
589
+ key: witness.key,
590
+ kind: witness.kind,
591
+ command: witness.command,
592
+ exit_code: witness.exit_code,
593
+ tail: witness.tail,
594
+ checks: witness.checks,
595
+ verdict: witness.verdict,
596
+ instrument: witness.instrument === "delivered" ? "delivered" : "running",
597
+ };
598
+ }
599
+ async function execDeliveredProbeRun(argv, stdin, workspace, timeoutMs) {
600
+ const [file, ...args] = argv;
601
+ if (!file)
602
+ throw new Error("delivered probe command is empty");
603
+ return await new Promise((resolve, reject) => {
604
+ const child = spawn(file, args, {
605
+ cwd: workspace,
606
+ env: probeEnv(),
607
+ stdio: ["pipe", "pipe", "pipe"],
608
+ });
609
+ let stdout = "";
610
+ let stderr = "";
611
+ const timer = setTimeout(() => {
612
+ child.kill("SIGKILL");
613
+ }, timeoutMs);
614
+ child.stdout?.on("data", (chunk) => { stdout += String(chunk); });
615
+ child.stderr?.on("data", (chunk) => { stderr += String(chunk); });
616
+ child.on("error", (error) => {
617
+ clearTimeout(timer);
618
+ reject(error);
619
+ });
620
+ child.on("close", (code) => {
621
+ clearTimeout(timer);
622
+ resolve({ code: code ?? -1, stdout, stderr });
623
+ });
624
+ child.stdin?.on("error", () => undefined);
625
+ child.stdin?.end(stdin);
626
+ });
627
+ }
628
+ export async function runDeliveredProbeInstrument(input) {
629
+ const entry = join(input.workspace, PROBE_RUN_ENTRY);
630
+ const timeoutMs = PROBE_TIMEOUT_MS * (input.probes.length + 1);
631
+ const stdin = JSON.stringify({ probes: input.probes });
632
+ const argv = ["node", "--import", tsxLoader, entry];
633
+ const result = await (input.exec ?? execDeliveredProbeRun)(argv, stdin, input.workspace, timeoutMs);
634
+ if (result.code !== 0) {
635
+ throw new Error(result.stderr.trim() || `delivered probe-run exited ${result.code}`);
636
+ }
637
+ try {
638
+ return stampInstrument(deliveredWitnessSchema.parse(JSON.parse(result.stdout)).witnesses.map(toStepWitness), "delivered");
639
+ }
640
+ catch (error) {
641
+ throw error instanceof Error ? error : new Error("delivered probe instrument returned no witness list");
642
+ }
643
+ }
644
+ export async function runDeliveryVerificationProbesForAttempt(input) {
645
+ const runRunning = () => runDeliveryVerificationProbes({
646
+ workspace: input.workspace,
647
+ probes: input.probes,
648
+ runCommand: input.runCommand,
649
+ });
650
+ if (!input.instrumentChanged) {
651
+ return stampInstrument(await runRunning(), "running");
652
+ }
653
+ const runDelivered = input.runDeliveredProbes ?? ((args) => runDeliveredProbeInstrument(args));
654
+ let selfCheckDetail = null;
655
+ try {
656
+ const check = await runDelivered({ workspace: input.workspace, probes: KNOWN_ANSWER_PROBES });
657
+ if (knownAnswerProbesPassed(check)) {
658
+ return stampInstrument(await runDelivered({ workspace: input.workspace, probes: input.probes }), "delivered");
659
+ }
660
+ selfCheckDetail = `gave ${check.map((witness) => witness.verdict).join(", ")}`;
661
+ }
662
+ catch (error) {
663
+ selfCheckDetail = redactSecrets(error instanceof Error ? error.message : String(error));
664
+ }
665
+ return markSelfCheckBroken(await runRunning(), selfCheckDetail ?? "");
666
+ }
441
667
  async function settle(client, investigationId, body) {
442
668
  await client.request(`/runner/v1/investigations/${investigationId}/settle`, {
443
669
  method: "POST",
@@ -533,6 +759,19 @@ export async function executeNextInvestigation(client, config, workspace, brief,
533
759
  }).catch((error) => console.error(`Investigation ${assignment.id} lease renewal failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`));
534
760
  }, options.renewIntervalMs ?? INVESTIGATION_RENEW_INTERVAL_MS);
535
761
  renewTimer.unref?.();
762
+ let heartbeatRunning = false;
763
+ const heartbeat = options.heartbeat;
764
+ const heartbeatTimer = heartbeat
765
+ ? setInterval(() => {
766
+ if (heartbeatRunning)
767
+ return;
768
+ heartbeatRunning = true;
769
+ void heartbeat()
770
+ .catch((error) => console.error(`Heartbeat failed during investigation: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`))
771
+ .finally(() => { heartbeatRunning = false; });
772
+ }, options.heartbeatIntervalMs ?? 15_000)
773
+ : null;
774
+ heartbeatTimer?.unref?.();
536
775
  try {
537
776
  attemptWorkspace = await createAttemptWorktree({
538
777
  sourceWorkspace: workspace,
@@ -602,10 +841,12 @@ export async function executeNextInvestigation(client, config, workspace, brief,
602
841
  return true;
603
842
  }
604
843
  const stepWitness = deliveryVerification
605
- ? await runDeliveryVerificationProbes({
844
+ ? await runDeliveryVerificationProbesForAttempt({
606
845
  workspace: attemptWorkspace,
607
846
  probes: observed.probes ?? [],
847
+ instrumentChanged: await probeInstrumentChanged(attemptWorkspace, assignment.base_commit, commit),
608
848
  runCommand: options.runProbeCommand,
849
+ runDeliveredProbes: options.runDeliveredProbes,
609
850
  })
610
851
  : [];
611
852
  if (deliveryVerification)
@@ -651,6 +892,8 @@ export async function executeNextInvestigation(client, config, workspace, brief,
651
892
  }
652
893
  finally {
653
894
  clearInterval(renewTimer);
895
+ if (heartbeatTimer)
896
+ clearInterval(heartbeatTimer);
654
897
  if (attemptWorkspace) {
655
898
  await removeAttemptWorktree(workspace, attemptWorkspace)
656
899
  .catch((error) => console.error(`Investigation worktree release failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`));
@@ -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.112",
3
+ "version": "0.16.114",
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": {