@miraland-labs/conduit-bridge 0.16.113 → 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();
@@ -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))}`);
@@ -495,11 +576,12 @@ const deliveredWitnessSchema = z.object({
495
576
  function stampInstrument(witnesses, instrument) {
496
577
  return witnesses.map((witness) => ({ ...witness, instrument }));
497
578
  }
498
- function markSelfCheckBroken(witnesses) {
579
+ function markSelfCheckBroken(witnesses, reason) {
580
+ const tail = boundedTail(`${DELIVERED_PROBE_SELF_CHECK_TAIL}: ${reason}`, PROBE_TAIL_CHARS);
499
581
  return stampInstrument(witnesses.map((witness) => ({
500
582
  ...witness,
501
583
  verdict: "broken",
502
- tail: DELIVERED_PROBE_SELF_CHECK_TAIL,
584
+ tail,
503
585
  })), "running");
504
586
  }
505
587
  function toStepWitness(witness) {
@@ -569,16 +651,18 @@ export async function runDeliveryVerificationProbesForAttempt(input) {
569
651
  return stampInstrument(await runRunning(), "running");
570
652
  }
571
653
  const runDelivered = input.runDeliveredProbes ?? ((args) => runDeliveredProbeInstrument(args));
654
+ let selfCheckDetail = null;
572
655
  try {
573
656
  const check = await runDelivered({ workspace: input.workspace, probes: KNOWN_ANSWER_PROBES });
574
657
  if (knownAnswerProbesPassed(check)) {
575
658
  return stampInstrument(await runDelivered({ workspace: input.workspace, probes: input.probes }), "delivered");
576
659
  }
660
+ selfCheckDetail = `gave ${check.map((witness) => witness.verdict).join(", ")}`;
577
661
  }
578
- catch {
579
- // Known-answer failure and a delivered-instrument throw take the same path: running probes, then broken.
662
+ catch (error) {
663
+ selfCheckDetail = redactSecrets(error instanceof Error ? error.message : String(error));
580
664
  }
581
- return markSelfCheckBroken(await runRunning());
665
+ return markSelfCheckBroken(await runRunning(), selfCheckDetail ?? "");
582
666
  }
583
667
  async function settle(client, investigationId, body) {
584
668
  await client.request(`/runner/v1/investigations/${investigationId}/settle`, {
@@ -675,6 +759,19 @@ export async function executeNextInvestigation(client, config, workspace, brief,
675
759
  }).catch((error) => console.error(`Investigation ${assignment.id} lease renewal failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`));
676
760
  }, options.renewIntervalMs ?? INVESTIGATION_RENEW_INTERVAL_MS);
677
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?.();
678
775
  try {
679
776
  attemptWorkspace = await createAttemptWorktree({
680
777
  sourceWorkspace: workspace,
@@ -795,6 +892,8 @@ export async function executeNextInvestigation(client, config, workspace, brief,
795
892
  }
796
893
  finally {
797
894
  clearInterval(renewTimer);
895
+ if (heartbeatTimer)
896
+ clearInterval(heartbeatTimer);
798
897
  if (attemptWorkspace) {
799
898
  await removeAttemptWorktree(workspace, attemptWorkspace)
800
899
  .catch((error) => console.error(`Investigation worktree release failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.16.113",
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": {