@miraland-labs/conduit-bridge 0.16.113 → 0.16.115
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 +11 -10
- package/dist/investigation.js +111 -19
- package/package.json +1 -1
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,10 +664,18 @@ async function runner() {
|
|
|
671
664
|
const hb = await heartbeat(client, config, workspace, currentBrief, preflight, undefined, bootstrapResult);
|
|
672
665
|
return { preflight, hb };
|
|
673
666
|
};
|
|
667
|
+
const publishHeartbeat = async () => {
|
|
668
|
+
await publishWorkspaceState();
|
|
669
|
+
};
|
|
670
|
+
// Observation runs before authoring: an owner waiting on an answer should not queue behind
|
|
671
|
+
// a long delivery, and the control plane already counted this slot as busy.
|
|
672
|
+
progressed = await executeNextInvestigation(client, config, workspace, brief, timeoutMs, {
|
|
673
|
+
managedRoot: hb.on_shift?.managed_workspace_root ?? null,
|
|
674
|
+
heartbeat: publishHeartbeat,
|
|
675
|
+
heartbeatIntervalMs: intervalMs,
|
|
676
|
+
}) || progressed;
|
|
674
677
|
progressed = await pumpExecutionSlots(client, config, workspace, brief, timeoutMs, {
|
|
675
|
-
heartbeat:
|
|
676
|
-
await publishWorkspaceState();
|
|
677
|
-
},
|
|
678
|
+
heartbeat: publishHeartbeat,
|
|
678
679
|
beforeClaim: async (driverId) => {
|
|
679
680
|
// Never let the five-minute heartbeat cache span a CLI upgrade into a certified claim.
|
|
680
681
|
// Publish the fresh probe first; the Control Plane then remains the authority for the
|
package/dist/investigation.js
CHANGED
|
@@ -17,9 +17,8 @@ import { pickDriverForClaim, resolveDriverFuel, supportsReadOnlyDiagnosis } from
|
|
|
17
17
|
import { diffStatSince } from "./git-witness.js";
|
|
18
18
|
import { switchManagedWorkspace } from "./on-shift-apply.js";
|
|
19
19
|
import { execFile, spawn } from "node:child_process";
|
|
20
|
-
import {
|
|
20
|
+
import { rm, writeFile } from "node:fs/promises";
|
|
21
21
|
import { createRequire } from "node:module";
|
|
22
|
-
import { tmpdir } from "node:os";
|
|
23
22
|
import { join } from "node:path";
|
|
24
23
|
import { promisify } from "node:util";
|
|
25
24
|
import { isRunnableVerificationCommand } from "./execution-class.js";
|
|
@@ -97,9 +96,11 @@ const investigationBriefSchema = z.object({
|
|
|
97
96
|
/** T122: a delivery verification is an investigation whose question carries this header. */
|
|
98
97
|
export const DELIVERY_VERIFICATION_HEADER = "DELIVERY VERIFICATION";
|
|
99
98
|
/** Owner-verbatim: the lane writes probe source; Bridge executes. Shown only when BRIEF KEYS are present. */
|
|
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
|
|
99
|
+
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 run from the repository root that prints PROBE HOLDS as its last line and exits 0 when the key holds at this commit and prints PROBE FAILS as its last line and exits 1 when it does not; any other ending is recorded as broken; it may import repository modules by paths relative to the repository root>","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
100
|
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
101
|
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"]}';
|
|
102
|
+
/** Named in both prompts so the lane knows each list's maximum before it writes. */
|
|
103
|
+
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; each text entry at most 1000 characters, commands_run entries at most 200 characters. Keep the most decisive entries.";
|
|
103
104
|
export function isDeliveryVerification(assignment) {
|
|
104
105
|
return assignment.question.trimStart().startsWith(DELIVERY_VERIFICATION_HEADER);
|
|
105
106
|
}
|
|
@@ -121,6 +122,7 @@ export function buildInvestigationPrompt(assignment, commit, context) {
|
|
|
121
122
|
"",
|
|
122
123
|
"Return only one fenced ```json object with exactly this shape:",
|
|
123
124
|
'{"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"]}',
|
|
125
|
+
BRIEF_LIMITS_SENTENCE,
|
|
124
126
|
"summary is a string; the other list fields are arrays of strings. Close the fence and add no prose after it.",
|
|
125
127
|
].join("\n");
|
|
126
128
|
}
|
|
@@ -164,6 +166,7 @@ export function buildDeliveryVerificationPrompt(assignment, commit, context) {
|
|
|
164
166
|
"",
|
|
165
167
|
"Return only one fenced ```json object with exactly this shape:",
|
|
166
168
|
keys.length ? BRIEF_JSON_EXAMPLE_WITH_PROBES : BRIEF_JSON_EXAMPLE,
|
|
169
|
+
BRIEF_LIMITS_SENTENCE,
|
|
167
170
|
keys.length
|
|
168
171
|
? "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
172
|
: "criteria has one entry per ACCEPTANCE key. Close the fence and add no prose after it.",
|
|
@@ -208,6 +211,83 @@ function settleCommandsRun(gates, reported) {
|
|
|
208
211
|
export const UNUSABLE_BRIEF = "Investigation returned no usable brief";
|
|
209
212
|
/** T130: how much of an unusable reply travels with the failure, so the next run can be diagnosed. */
|
|
210
213
|
const UNUSABLE_BRIEF_TAIL_CHARS = 800;
|
|
214
|
+
const BRIEF_SUMMARY_MAX = 4_000;
|
|
215
|
+
const BRIEF_ITEM_MAX = 1_000;
|
|
216
|
+
const BRIEF_FILES_READ_MAX = 24;
|
|
217
|
+
const BRIEF_BYTES_READ_MAX = 512 * 1024;
|
|
218
|
+
const BRIEF_COMMAND_RUN_MAX = 200;
|
|
219
|
+
const BRIEF_LIST_MAX = {
|
|
220
|
+
findings: 12,
|
|
221
|
+
verification: 8,
|
|
222
|
+
limitations: 8,
|
|
223
|
+
criteria: 40,
|
|
224
|
+
command_failures: 10,
|
|
225
|
+
commands_run: 10,
|
|
226
|
+
probes: 20,
|
|
227
|
+
};
|
|
228
|
+
function clipBriefText(value, max) {
|
|
229
|
+
return value.length > max ? value.slice(0, max) : value;
|
|
230
|
+
}
|
|
231
|
+
function clipBriefStringField(record, key, max) {
|
|
232
|
+
const value = record[key];
|
|
233
|
+
if (typeof value === "string")
|
|
234
|
+
record[key] = clipBriefText(value, max);
|
|
235
|
+
}
|
|
236
|
+
function fitBriefStrings(items, max) {
|
|
237
|
+
return items.map((item) => typeof item === "string" ? clipBriefText(item, max) : item);
|
|
238
|
+
}
|
|
239
|
+
function fitCriteriaItems(items) {
|
|
240
|
+
return items.map((item) => {
|
|
241
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
242
|
+
return item;
|
|
243
|
+
const row = { ...item };
|
|
244
|
+
clipBriefStringField(row, "criterion", BRIEF_ITEM_MAX);
|
|
245
|
+
clipBriefStringField(row, "reason", BRIEF_ITEM_MAX);
|
|
246
|
+
return row;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Cut an over-full brief to the contract before schema parse. Only size is fitted; a wrong type,
|
|
251
|
+
* a missing summary, or an unknown assessment still fails as today. Probe entries are kept as
|
|
252
|
+
* written — clipping them would change what Bridge later executes.
|
|
253
|
+
*/
|
|
254
|
+
export function fitBriefToContract(candidate) {
|
|
255
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
256
|
+
return { fitted: candidate, cuts: [] };
|
|
257
|
+
}
|
|
258
|
+
const brief = { ...candidate };
|
|
259
|
+
const cuts = [];
|
|
260
|
+
const fitNamedList = (key, fitItems) => {
|
|
261
|
+
const value = brief[key];
|
|
262
|
+
if (!Array.isArray(value))
|
|
263
|
+
return;
|
|
264
|
+
const max = BRIEF_LIST_MAX[key];
|
|
265
|
+
if (value.length > max)
|
|
266
|
+
cuts.push({ list: key, kept: max, total: value.length });
|
|
267
|
+
brief[key] = fitItems(value.slice(0, max));
|
|
268
|
+
};
|
|
269
|
+
fitNamedList("findings", (items) => fitBriefStrings(items, BRIEF_ITEM_MAX));
|
|
270
|
+
fitNamedList("verification", (items) => fitBriefStrings(items, BRIEF_ITEM_MAX));
|
|
271
|
+
fitNamedList("limitations", (items) => fitBriefStrings(items, BRIEF_ITEM_MAX));
|
|
272
|
+
fitNamedList("criteria", fitCriteriaItems);
|
|
273
|
+
fitNamedList("command_failures", (items) => fitBriefStrings(items, BRIEF_ITEM_MAX));
|
|
274
|
+
fitNamedList("commands_run", (items) => fitBriefStrings(items, BRIEF_COMMAND_RUN_MAX));
|
|
275
|
+
fitNamedList("probes", (items) => items);
|
|
276
|
+
clipBriefStringField(brief, "summary", BRIEF_SUMMARY_MAX);
|
|
277
|
+
if (typeof brief.files_read === "number" && brief.files_read > BRIEF_FILES_READ_MAX) {
|
|
278
|
+
brief.files_read = BRIEF_FILES_READ_MAX;
|
|
279
|
+
}
|
|
280
|
+
if (typeof brief.bytes_read === "number" && brief.bytes_read > BRIEF_BYTES_READ_MAX) {
|
|
281
|
+
brief.bytes_read = BRIEF_BYTES_READ_MAX;
|
|
282
|
+
}
|
|
283
|
+
if (cuts.length) {
|
|
284
|
+
const notes = cuts.map((cut) => clipBriefText(`Bridge kept the first ${cut.kept} of ${cut.total} ${cut.list}`, BRIEF_ITEM_MAX));
|
|
285
|
+
const existing = Array.isArray(brief.limitations) ? [...brief.limitations] : [];
|
|
286
|
+
const room = Math.max(0, BRIEF_LIST_MAX.limitations - notes.length);
|
|
287
|
+
brief.limitations = [...existing.slice(0, room), ...notes];
|
|
288
|
+
}
|
|
289
|
+
return { fitted: brief, cuts };
|
|
290
|
+
}
|
|
211
291
|
/**
|
|
212
292
|
* T130: an unusable brief used to fail with five words. The first production verification retry
|
|
213
293
|
* (2026-09-09, acme-sim / forge R20) returned "no usable brief" twice and nobody could say whether the
|
|
@@ -222,7 +302,7 @@ export function parseInvestigationBrief(text) {
|
|
|
222
302
|
catch {
|
|
223
303
|
throw new Error(`${UNUSABLE_BRIEF}: no JSON object in the reply. Reply tail: ${redactSecrets(boundedTail(text, UNUSABLE_BRIEF_TAIL_CHARS))}`);
|
|
224
304
|
}
|
|
225
|
-
const parsed = investigationBriefSchema.safeParse(candidate);
|
|
305
|
+
const parsed = investigationBriefSchema.safeParse(fitBriefToContract(candidate).fitted);
|
|
226
306
|
if (!parsed.success) {
|
|
227
307
|
const issues = parsed.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
228
308
|
throw new Error(`${UNUSABLE_BRIEF}: the JSON does not match the brief contract (${issues}). Reply tail: ${redactSecrets(boundedTail(text, UNUSABLE_BRIEF_TAIL_CHARS))}`);
|
|
@@ -354,11 +434,8 @@ function probeInterpreter(language) {
|
|
|
354
434
|
return "bash";
|
|
355
435
|
}
|
|
356
436
|
function probeFilename(language) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (language === "py")
|
|
360
|
-
return "probe.py";
|
|
361
|
-
return "probe.sh";
|
|
437
|
+
const extension = language === "ts" ? "mts" : language === "py" ? "py" : "sh";
|
|
438
|
+
return `.conduit-probe-${crypto.randomUUID()}.${extension}`;
|
|
362
439
|
}
|
|
363
440
|
function probeArgv(language, file) {
|
|
364
441
|
if (language === "ts")
|
|
@@ -452,13 +529,11 @@ export async function runDeliveryVerificationProbes(input) {
|
|
|
452
529
|
continue;
|
|
453
530
|
const language = probe.lang;
|
|
454
531
|
const command = `${probeInterpreter(language)} ${probe.key}`;
|
|
455
|
-
|
|
532
|
+
const file = join(input.workspace, probeFilename(language));
|
|
456
533
|
let exitCode = -1;
|
|
457
534
|
let stdout = "";
|
|
458
535
|
let stderr = "";
|
|
459
536
|
try {
|
|
460
|
-
dir = await mkdtemp(join(tmpdir(), "conduit-probe-"));
|
|
461
|
-
const file = join(dir, probeFilename(language));
|
|
462
537
|
await writeFile(file, source, { encoding: "utf8" });
|
|
463
538
|
const result = await execProbe(probeArgv(language, file), input.workspace);
|
|
464
539
|
exitCode = result.code;
|
|
@@ -470,8 +545,7 @@ export async function runDeliveryVerificationProbes(input) {
|
|
|
470
545
|
exitCode = -1;
|
|
471
546
|
}
|
|
472
547
|
finally {
|
|
473
|
-
|
|
474
|
-
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
548
|
+
await rm(file, { force: true }).catch(() => undefined);
|
|
475
549
|
}
|
|
476
550
|
witnesses.push(witnessRecord({
|
|
477
551
|
key: probe.key, kind: "probe", command, exitCode, stdout, stderr, checks,
|
|
@@ -495,11 +569,12 @@ const deliveredWitnessSchema = z.object({
|
|
|
495
569
|
function stampInstrument(witnesses, instrument) {
|
|
496
570
|
return witnesses.map((witness) => ({ ...witness, instrument }));
|
|
497
571
|
}
|
|
498
|
-
function markSelfCheckBroken(witnesses) {
|
|
572
|
+
function markSelfCheckBroken(witnesses, reason) {
|
|
573
|
+
const tail = boundedTail(`${DELIVERED_PROBE_SELF_CHECK_TAIL}: ${reason}`, PROBE_TAIL_CHARS);
|
|
499
574
|
return stampInstrument(witnesses.map((witness) => ({
|
|
500
575
|
...witness,
|
|
501
576
|
verdict: "broken",
|
|
502
|
-
tail
|
|
577
|
+
tail,
|
|
503
578
|
})), "running");
|
|
504
579
|
}
|
|
505
580
|
function toStepWitness(witness) {
|
|
@@ -569,16 +644,18 @@ export async function runDeliveryVerificationProbesForAttempt(input) {
|
|
|
569
644
|
return stampInstrument(await runRunning(), "running");
|
|
570
645
|
}
|
|
571
646
|
const runDelivered = input.runDeliveredProbes ?? ((args) => runDeliveredProbeInstrument(args));
|
|
647
|
+
let selfCheckDetail = null;
|
|
572
648
|
try {
|
|
573
649
|
const check = await runDelivered({ workspace: input.workspace, probes: KNOWN_ANSWER_PROBES });
|
|
574
650
|
if (knownAnswerProbesPassed(check)) {
|
|
575
651
|
return stampInstrument(await runDelivered({ workspace: input.workspace, probes: input.probes }), "delivered");
|
|
576
652
|
}
|
|
653
|
+
selfCheckDetail = `gave ${check.map((witness) => witness.verdict).join(", ")}`;
|
|
577
654
|
}
|
|
578
|
-
catch {
|
|
579
|
-
|
|
655
|
+
catch (error) {
|
|
656
|
+
selfCheckDetail = redactSecrets(error instanceof Error ? error.message : String(error));
|
|
580
657
|
}
|
|
581
|
-
return markSelfCheckBroken(await runRunning());
|
|
658
|
+
return markSelfCheckBroken(await runRunning(), selfCheckDetail ?? "");
|
|
582
659
|
}
|
|
583
660
|
async function settle(client, investigationId, body) {
|
|
584
661
|
await client.request(`/runner/v1/investigations/${investigationId}/settle`, {
|
|
@@ -675,6 +752,19 @@ export async function executeNextInvestigation(client, config, workspace, brief,
|
|
|
675
752
|
}).catch((error) => console.error(`Investigation ${assignment.id} lease renewal failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`));
|
|
676
753
|
}, options.renewIntervalMs ?? INVESTIGATION_RENEW_INTERVAL_MS);
|
|
677
754
|
renewTimer.unref?.();
|
|
755
|
+
let heartbeatRunning = false;
|
|
756
|
+
const heartbeat = options.heartbeat;
|
|
757
|
+
const heartbeatTimer = heartbeat
|
|
758
|
+
? setInterval(() => {
|
|
759
|
+
if (heartbeatRunning)
|
|
760
|
+
return;
|
|
761
|
+
heartbeatRunning = true;
|
|
762
|
+
void heartbeat()
|
|
763
|
+
.catch((error) => console.error(`Heartbeat failed during investigation: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`))
|
|
764
|
+
.finally(() => { heartbeatRunning = false; });
|
|
765
|
+
}, options.heartbeatIntervalMs ?? 15_000)
|
|
766
|
+
: null;
|
|
767
|
+
heartbeatTimer?.unref?.();
|
|
678
768
|
try {
|
|
679
769
|
attemptWorkspace = await createAttemptWorktree({
|
|
680
770
|
sourceWorkspace: workspace,
|
|
@@ -795,6 +885,8 @@ export async function executeNextInvestigation(client, config, workspace, brief,
|
|
|
795
885
|
}
|
|
796
886
|
finally {
|
|
797
887
|
clearInterval(renewTimer);
|
|
888
|
+
if (heartbeatTimer)
|
|
889
|
+
clearInterval(heartbeatTimer);
|
|
798
890
|
if (attemptWorkspace) {
|
|
799
891
|
await removeAttemptWorktree(workspace, attemptWorkspace)
|
|
800
892
|
.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.
|
|
3
|
+
"version": "0.16.115",
|
|
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": {
|