@miraland-labs/conduit-bridge 0.16.42 → 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 +26 -3
- package/dist/cli.js +56 -6
- package/dist/client.js +6 -2
- package/dist/config.js +53 -2
- package/dist/driver.js +248 -57
- package/dist/drivers.js +59 -7
- package/dist/ensure-pull-request.js +10 -0
- package/dist/ensure-test-evidence.js +199 -31
- package/dist/execution-class.js +4 -1
- package/dist/execution-facts.js +52 -0
- package/dist/execution.js +611 -74
- package/dist/failure-signal.js +220 -8
- package/dist/git-witness.js +33 -0
- package/dist/ops.js +4 -1
- package/dist/preflight.js +73 -38
- package/dist/quota-reset.js +69 -4
- package/dist/version.js +13 -2
- package/dist/workspace-refresh.js +110 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|