@miraland-labs/conduit-bridge 0.16.41 → 0.16.70

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/brief.js CHANGED
@@ -12,7 +12,7 @@ const MANIFESTS = [
12
12
  ];
13
13
  /** Prefer test before typecheck/lint so discovery order matches what pickVerificationCommand wants. */
14
14
  const VERIFICATION_SCRIPTS = ["test", "verify", "typecheck", "lint", "build"];
15
- const MAKE_VERIFICATION_TARGETS = ["test", "check", "verify", "replay", "typecheck", "lint", "build"];
15
+ export const MAKE_VERIFICATION_TARGETS = ["test", "check", "verify", "replay", "typecheck", "lint", "build"];
16
16
  const EXCLUDED_DIRECTORIES = new Set(["node_modules", "dist", "build", "target", "coverage", ".venv", "venv"]);
17
17
  export async function buildWorkspaceBrief(workspace) {
18
18
  const entries = await readdir(workspace, { withFileTypes: true });
@@ -168,6 +168,8 @@ export async function resolveAttemptStartCommit(workspace, requestedBase, claime
168
168
  * recipe body was always repository-authored.
169
169
  */
170
170
  const MAX_DECLARED_VERIFICATION = 8;
171
+ /** Keep in sync with `workspaceBriefSchema.verification` in `src/runner/routes.ts`. */
172
+ const MAX_VERIFICATION_COMMANDS = 10;
171
173
  export function parseDeclaredVerification(text) {
172
174
  const declared = [];
173
175
  for (const raw of text.split("\n")) {
@@ -182,12 +184,30 @@ export function parseDeclaredVerification(text) {
182
184
  return declared;
183
185
  }
184
186
  export async function readDeclaredVerification(workspace) {
187
+ let text;
185
188
  try {
186
- return parseDeclaredVerification(await readFile(join(workspace, ".conduit", "verification"), "utf8"));
189
+ text = await readFile(join(workspace, ".conduit", "verification"), "utf8");
187
190
  }
188
191
  catch {
189
192
  return []; // no declaration, or unreadable: discovery still applies
190
193
  }
194
+ const declared = parseDeclaredVerification(text);
195
+ // A line the shape check refuses, or a line after the eighth, was dropped with no reason given:
196
+ // the gate was then absent from every list and the operator had nothing to read. One line per
197
+ // dropped declaration says which line went and why it can go.
198
+ for (const raw of text.split("\n")) {
199
+ const line = raw.split("#")[0].trim();
200
+ if (!line || declared.includes(line))
201
+ continue;
202
+ console.warn(JSON.stringify({
203
+ event: "declared_verification_dropped",
204
+ line: line.slice(0, 200),
205
+ reason: isBoundedVerificationCommand(line)
206
+ ? `more than ${MAX_DECLARED_VERIFICATION} declared commands`
207
+ : "not a bounded verification command",
208
+ }));
209
+ }
210
+ return declared;
191
211
  }
192
212
  export async function discoverVerificationCommands(workspace, files) {
193
213
  // Declared first: the order is a preference order for every reader that takes one command.
@@ -240,5 +260,8 @@ export async function discoverVerificationCommands(workspace, files) {
240
260
  if (files.has("gradlew"))
241
261
  commands.push("./gradlew test");
242
262
  }
243
- return [...new Set(commands)];
263
+ // The heartbeat schema caps `verification` at 10. A gate-rich checkout discovers more than that,
264
+ // and the whole heartbeat was then refused: the computer went dark with no reason on any card.
265
+ // Declared commands come first, so the cap removes only the least preferred discovered names.
266
+ return [...new Set(commands)].slice(0, MAX_VERIFICATION_COMMANDS);
244
267
  }
package/dist/cli.js CHANGED
@@ -11,8 +11,8 @@ import { ConduitClient, ConduitRequestError } from "./client.js";
11
11
  import { BRIDGE_LEASE_CAPACITY, BRIDGE_MAX_LEASE_CAPACITY, clampLeaseCapacity, clearLocalConnection, clearPendingConnection, loadConfig, loadConfigIfPresent, loadOrCreateInstallationId, loadPendingConnection, redactSecrets, saveConfig, saveConfigPrefs, saveDriverQuota, savePendingConnection, suggestMachineName, } from "./config.js";
12
12
  import { runMcp } from "./mcp.js";
13
13
  import { detectInstalledClients, localFuelOnlyClients, suggestFuelSource } from "./detect.js";
14
- import { DRIVERS } from "./driver.js";
15
- import { driversHeartbeatReport, driverLabel, heartbeatStatusForDrivers, isSupportedDriverId, listDriverLanes, onlineDriverIds, seedDriversFromDetection, seedDriverLanes, setDriverFuel, setDriversOnline, laneDispatchBlock, laneStatuses, } from "./drivers.js";
14
+ import { DRIVERS, MODEL_TIERS } from "./driver.js";
15
+ import { driversHeartbeatReport, driverLabel, heartbeatStatusForDrivers, isSupportedDriverId, listDriverLanes, onlineDriverIds, seedDriversFromDetection, seedDriverLanes, setDriverFuel, setDriverModels, setDriversOnline, formatLaneTiers, laneDispatchBlock, laneStatuses, } from "./drivers.js";
16
16
  import { buildWorkspaceBrief } from "./brief.js";
17
17
  import { parseAgentTimeoutMinutes } from "./execution-budget.js";
18
18
  import { BOOTSTRAP_RESULTS, buildSovereignExecutionFacts } from "./execution-facts.js";
@@ -21,7 +21,8 @@ import { buildOnShiftIntentProof, maybeApplyOnShiftIntent } from "./on-shift-app
21
21
  import { loadOpsEnv, OPS_VERBS, runOps } from "./ops.js";
22
22
  import { pumpExecutionSlots, renewLeases } from "./execution.js";
23
23
  import { applyRunnerToolPath, installRunnerService, uninstallRunnerService } from "./service.js";
24
- import { BRIDGE_PROTOCOL_VERSION, cachedBridgePreflight, unavailableWorkspacePreflight } from "./preflight.js";
24
+ import { BRIDGE_PROTOCOL_VERSION, cachedBridgePreflight, PREFLIGHT_CACHE_TTL_MS, unavailableWorkspacePreflight } from "./preflight.js";
25
+ import { refreshSourceWorkspace } from "./workspace-refresh.js";
25
26
  import { executeNextInvestigation } from "./investigation.js";
26
27
  import { bridgeVersion } from "./version.js";
27
28
  const [command] = process.argv.slice(2);
@@ -444,12 +445,16 @@ async function driversCommand() {
444
445
  // Printing the count is the only way an operator sees why dispatch keeps avoiding this lane.
445
446
  const slow = lane.timeouts ? ` timeouts=${lane.timeouts} (demoted)` : "";
446
447
  console.log(` ${lane.id.padEnd(14)} ${lane.state.padEnd(8)} fuel=${lane.fuel} allowance=${lane.allowance}${when}${reset}${slow} (${lane.label})`);
448
+ // An unbound tier runs the CLI default and Holds on the first vendor abort, so the map is
449
+ // printed here — the operator learned a typo such as `fast` only from that Hold before.
450
+ console.log(` tiers ${formatLaneTiers(lane.tiers)}`);
447
451
  }
448
452
  const blocked = laneDispatchBlock(laneStatuses(config));
449
453
  console.log(blocked
450
454
  ? `Dispatch: BLOCKED — ${blocked}.`
451
455
  : `Dispatch: ready — eligible ${laneStatuses(config).filter((lane) => lane.eligible).map((lane) => lane.id).join(", ")}.`);
452
456
  const online = onlineDriverIds(config);
457
+ console.log(`Bind a tier: ${bridgeUsage("drivers", "models", "<driver-id>", MODEL_TIERS.join("|"), "<model…>")}`);
453
458
  console.log(online.length
454
459
  ? `Toggle a lane: ${bridgeUsage("drivers", "online|offline", "<id…>")}`
455
460
  : `All offline — no new claims. Bring one online: ${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}`);
@@ -500,7 +505,20 @@ async function driversCommand() {
500
505
  console.log(`${driverLabel(id)} lane fuel set to ${mode}`);
501
506
  return;
502
507
  }
503
- throw new Error(`Usage: ${bridgeUsage("drivers", "[list|online|offline|fuel|quota]", "…")}`);
508
+ if (sub === "models") {
509
+ const id = process.argv[4]?.trim();
510
+ const tier = process.argv[5]?.trim();
511
+ const names = process.argv.slice(6).map((name) => name.trim()).filter(Boolean);
512
+ if (!id || !tier || !MODEL_TIERS.includes(tier) || !names.length) {
513
+ throw new Error(`Usage: ${bridgeUsage("drivers", "models", "<driver-id>", MODEL_TIERS.join("|"), "<model…>")}`);
514
+ }
515
+ config = seedDriverLanes(config, [id]).config;
516
+ config = setDriverModels(config, id, tier, names);
517
+ await saveConfigPrefs(config);
518
+ console.log(`${driverLabel(id)} lane ${tier} tier: ${names.join(", ")}`);
519
+ return;
520
+ }
521
+ throw new Error(`Usage: ${bridgeUsage("drivers", "[list|online|offline|fuel|models|quota]", "…")}`);
504
522
  }
505
523
  async function runner() {
506
524
  applyRunnerToolPath();
@@ -562,9 +580,12 @@ async function runner() {
562
580
  config.drivers = latest.drivers;
563
581
  config.fuelSource = latest.fuelSource;
564
582
  config.leaseCapacity = latest.leaseCapacity;
583
+ // Refresh before discovery: the brief and the readiness report must describe origin's code,
584
+ // not whatever this checkout was left on. Cadence is the readiness report's own, not the beat.
585
+ const refresh = workspace ? await reportedRefresh(workspace) : null;
565
586
  const currentBrief = workspace ? await buildWorkspaceBrief(workspace).catch(() => brief) : null;
566
587
  const preflight = workspace
567
- ? await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined })
588
+ ? await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined, refresh })
568
589
  : unavailableWorkspacePreflight({ config });
569
590
  const hb = await heartbeat(client, config, workspace, currentBrief, preflight, undefined, bootstrapResult);
570
591
  if (hb.staleBridge) {
@@ -614,8 +635,9 @@ async function runner() {
614
635
  if (workspace && onlineDriverIds(config).length) {
615
636
  // Read the workspace again, then publish it. The last brief stands if the read fails.
616
637
  const publishWorkspaceState = async (options = {}) => {
638
+ const refresh = await reportedRefresh(workspace, options);
617
639
  const currentBrief = await buildWorkspaceBrief(workspace).catch(() => brief);
618
- const preflight = await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined }, options);
640
+ const preflight = await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined, refresh }, options);
619
641
  const hb = await heartbeat(client, config, workspace, currentBrief, preflight, undefined, bootstrapResult);
620
642
  return { preflight, hb };
621
643
  };
@@ -648,6 +670,34 @@ async function runner() {
648
670
  await new Promise((resolveSleep) => setTimeout(resolveSleep, intervalMs));
649
671
  }
650
672
  }
673
+ /** Last state announced, so a workspace that stays diverged does not print on every cycle. */
674
+ let lastRefreshState = null;
675
+ /** Last refresh and when it ran, so the fetch keeps the readiness report's cadence. */
676
+ let lastRefresh = null;
677
+ /**
678
+ * Fast-forward the source workspace and announce only what changed.
679
+ *
680
+ * The fetch runs on the readiness report's cadence, not the heartbeat's: forced before every claim,
681
+ * and at most once every five minutes otherwise. A fetch on every fifteen-second beat is traffic
682
+ * nothing can use, because the readiness report it feeds only recomputes on that same cadence.
683
+ */
684
+ async function reportedRefresh(workspace, options = {}) {
685
+ if (!options.force && lastRefresh && Date.now() - lastRefresh.at < PREFLIGHT_CACHE_TTL_MS)
686
+ return lastRefresh.result;
687
+ const refresh = await refreshSourceWorkspace(workspace).catch(() => null);
688
+ lastRefresh = { at: Date.now(), result: refresh };
689
+ const state = refresh ? `${refresh.state}:${refresh.local_commits}` : null;
690
+ if (state !== lastRefreshState) {
691
+ lastRefreshState = state;
692
+ if (refresh?.state === "fast_forwarded") {
693
+ console.log(`Workspace fast-forwarded to origin/${refresh.branch}.`);
694
+ }
695
+ else if (refresh?.state === "diverged") {
696
+ console.error(`Workspace cannot fast-forward: ${refresh.local_commits} local-only commit(s) on ${refresh.branch}. Park or push them; this computer takes no work until it can follow origin.`);
697
+ }
698
+ }
699
+ return refresh;
700
+ }
651
701
  async function heartbeat(client, config, workspacePath, brief, preflight, intentProof, bootstrapResult) {
652
702
  const online = onlineDriverIds(config);
653
703
  const status = heartbeatStatusForDrivers(online);
package/dist/client.js CHANGED
@@ -2,10 +2,14 @@ import { removeRuntimeAttempt, saveConfigPrefs, saveRuntime } from "./config.js"
2
2
  export class ConduitRequestError extends Error {
3
3
  status;
4
4
  code;
5
- constructor(message, status, code) {
5
+ details;
6
+ constructor(message, status, code,
7
+ /** The control plane's own structured reason, e.g. which execution fact changed. */
8
+ details) {
6
9
  super(message);
7
10
  this.status = status;
8
11
  this.code = code;
12
+ this.details = details;
9
13
  }
10
14
  }
11
15
  export class ConduitClient {
@@ -36,7 +40,7 @@ export class ConduitClient {
36
40
  });
37
41
  const data = await response.json();
38
42
  if (!response.ok)
39
- throw new ConduitRequestError(data.error?.message ?? `Conduit request failed (${response.status})`, response.status, data.error?.code);
43
+ throw new ConduitRequestError(data.error?.message ?? `Conduit request failed (${response.status})`, response.status, data.error?.code, data.error?.details);
40
44
  return data;
41
45
  }
42
46
  async claim(taskId, attemptId, extras) {
package/dist/config.js CHANGED
@@ -2,6 +2,7 @@ import { chmod, mkdir, open, readFile, rename, unlink, writeFile, stat } from "n
2
2
  import { randomBytes } from "node:crypto";
3
3
  import { homedir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
+ import { MODEL_TIERS } from "./driver.js";
5
6
  /** Serialize config writes in-process so concurrent claim/phase updates cannot clobber each other. */
6
7
  let saveChain = Promise.resolve();
7
8
  /** Default when join omits --capacity. */
@@ -84,18 +85,59 @@ export async function loadConfigIfPresent() {
84
85
  if (!lane || typeof lane !== "object")
85
86
  continue;
86
87
  const state = lane.state === "online" ? "online" : "offline";
88
+ const modelsPolicy = lane.models_policy === "declared" ? "declared" : undefined;
87
89
  const fuel = lane.fuel === "local" || lane.fuel === "conduit" ? lane.fuel : undefined;
88
90
  // Every field a lane carries must be listed here. This sanitizer duplicates the shape check in
89
91
  // `normalizeDrivers` (drivers.ts) — importing it would close the loop config → drivers → driver
90
92
  // → config — so a new lane field has to be added in both, and forgetting here is silent: the
91
93
  // value survives in memory, then vanishes on the next load. `quota` was lost exactly that way.
92
94
  const quota = plainQuota(lane.quota);
93
- cleaned[id] = { state, ...(fuel ? { fuel } : {}), ...(quota ? { quota } : {}) };
95
+ const models = plainTierModels(lane.models, `drivers.${id}.models`);
96
+ cleaned[id] = { state, ...(modelsPolicy ? { models_policy: modelsPolicy } : {}), ...(fuel ? { fuel } : {}), ...(quota ? { quota } : {}),
97
+ ...(models && Object.keys(models).length ? { models } : {}) };
94
98
  }
95
99
  config.drivers = cleaned;
96
100
  }
101
+ if (config.models !== undefined)
102
+ config.models = plainTierModels(config.models, "models");
97
103
  return config;
98
104
  }
105
+ /** Tier keys already reported. The runner reloads the config every 15 seconds; one warning is enough. */
106
+ const warnedTierKeys = new Set();
107
+ /**
108
+ * Keep only the three intelligence tiers, and name each other key one time.
109
+ *
110
+ * A key such as `fast` is not a tier, so it binds nothing: every package of that risk runs the
111
+ * CLI default and the first vendor abort Holds. Dropping the key silently is what made that typo
112
+ * findable only from the Hold, hours later.
113
+ */
114
+ function plainTierModels(raw, where) {
115
+ if (!raw || typeof raw !== "object")
116
+ return undefined;
117
+ const kept = {};
118
+ for (const [key, value] of Object.entries(raw)) {
119
+ if (MODEL_TIERS.includes(key)) {
120
+ if (typeof value === "string" || Array.isArray(value))
121
+ kept[key] = value;
122
+ continue;
123
+ }
124
+ const named = `${where}.${key}`;
125
+ if (warnedTierKeys.has(named))
126
+ continue;
127
+ warnedTierKeys.add(named);
128
+ console.warn(`Bridge configuration: \`${named}\` is not an intelligence tier and is ignored. The tiers are ${MODEL_TIERS.join(", ")}.`);
129
+ }
130
+ return kept;
131
+ }
132
+ /**
133
+ * Model candidates for one tier on one lane, best first: the lane's own map, then the machine-wide
134
+ * map. One reader for every surface — resolution, the preflight report and the operator printers.
135
+ */
136
+ export function tierCandidates(config, driverId, tier) {
137
+ const configured = config.drivers?.[driverId]?.models?.[tier] ?? config.models?.[tier];
138
+ const listed = !configured ? [] : (Array.isArray(configured) ? configured : [configured]);
139
+ return listed.filter((candidate) => typeof candidate === "string" && candidate.trim().length > 0);
140
+ }
99
141
  /** A stored quota record, or nothing. Malformed is dropped rather than thrown — see normalizeQuota. */
100
142
  function plainQuota(raw) {
101
143
  if (!raw || typeof raw !== "object")
@@ -394,5 +436,14 @@ export function suggestMachineName(host, installationId) {
394
436
  return `${base.slice(0, 199 - suffix.length)}-${suffix}`;
395
437
  }
396
438
  export function redactSecrets(value) {
397
- return value.replace(/(runner_sk_|gateway_sk_|lease_|connect_)[A-Za-z0-9-]+/g, "$1[redacted]");
439
+ // Conduit's own key classes, then the provider and forge shapes an agent's own environment
440
+ // carries (ANTHROPIC_API_KEY, XAI_API_KEY, GEMINI_API_KEY, a GitHub token, a bearer header) —
441
+ // an agent that prints its environment on failure must not leak them through this terminal.
442
+ return value
443
+ .replace(/(runner_sk_|gateway_sk_|lease_|connect_)[A-Za-z0-9-]+/g, "$1[redacted]")
444
+ .replace(/sk-[A-Za-z0-9_-]{8,}/g, "sk-[redacted]")
445
+ .replace(/xai-[A-Za-z0-9]+/g, "xai-[redacted]")
446
+ .replace(/AIza[0-9A-Za-z_-]{35}/g, "AIza[redacted]")
447
+ .replace(/(ghp_|gho_|github_pat_|glpat-)[A-Za-z0-9_-]+/g, "$1[redacted]")
448
+ .replace(/Bearer\s+\S+/g, "Bearer [redacted]");
398
449
  }