@miraland-labs/conduit-bridge 0.16.14 → 0.16.16
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/checkout.js +15 -2
- package/dist/config.js +81 -15
- package/dist/drivers.js +64 -1
- package/dist/execution.js +140 -3
- package/dist/investigation.js +13 -1
- package/dist/ops.js +24 -7
- package/dist/preflight.js +20 -2
- package/dist/workspace-bootstrap.js +26 -0
- package/package.json +1 -1
package/dist/checkout.js
CHANGED
|
@@ -34,6 +34,18 @@ async function gitOriginUrl(workspace, exec) {
|
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
/** A control-plane repository fingerprint needs a transport before Git can clone it. */
|
|
38
|
+
function cloneRepositoryUrl(repositoryUrl) {
|
|
39
|
+
const value = repositoryUrl.trim();
|
|
40
|
+
const slash = value.indexOf("/");
|
|
41
|
+
if (slash <= 0 || value.startsWith(".") || value.includes("\\") || /^[^/@\s]+@[^:]+:/.test(value))
|
|
42
|
+
return value;
|
|
43
|
+
const host = value.slice(0, slash);
|
|
44
|
+
const path = value.slice(slash + 1);
|
|
45
|
+
if (!host.includes(".") || /\s/.test(value) || path.split("/").filter(Boolean).length < 2)
|
|
46
|
+
return value;
|
|
47
|
+
return `https://${value}`;
|
|
48
|
+
}
|
|
37
49
|
/**
|
|
38
50
|
* Ensure `workspace` is a checkout of `repositoryUrl`.
|
|
39
51
|
*
|
|
@@ -49,11 +61,12 @@ export async function ensureCheckout(workspace, repositoryUrl, deps = {}) {
|
|
|
49
61
|
const wanted = normalizeRepositoryUrl(repositoryUrl);
|
|
50
62
|
if (!wanted)
|
|
51
63
|
throw new Error("ensure_checkout_invalid_url");
|
|
64
|
+
const cloneUrl = cloneRepositoryUrl(repositoryUrl);
|
|
52
65
|
const exists = await pathExists(workspace, accessFn);
|
|
53
66
|
if (!exists) {
|
|
54
67
|
await mkdirFn(dirname(workspace), { recursive: true });
|
|
55
68
|
try {
|
|
56
|
-
await exec("git", ["clone", "--",
|
|
69
|
+
await exec("git", ["clone", "--", cloneUrl, workspace], {
|
|
57
70
|
timeout: 300_000,
|
|
58
71
|
maxBuffer: 2_000_000,
|
|
59
72
|
});
|
|
@@ -73,7 +86,7 @@ export async function ensureCheckout(workspace, repositoryUrl, deps = {}) {
|
|
|
73
86
|
throw new Error("ensure_checkout_path_unreadable");
|
|
74
87
|
if (entries.length === 0) {
|
|
75
88
|
try {
|
|
76
|
-
await exec("git", ["clone", "--",
|
|
89
|
+
await exec("git", ["clone", "--", cloneUrl, workspace], {
|
|
77
90
|
timeout: 300_000,
|
|
78
91
|
maxBuffer: 2_000_000,
|
|
79
92
|
});
|
package/dist/config.js
CHANGED
|
@@ -85,12 +85,27 @@ export async function loadConfigIfPresent() {
|
|
|
85
85
|
continue;
|
|
86
86
|
const state = lane.state === "online" ? "online" : "offline";
|
|
87
87
|
const fuel = lane.fuel === "local" || lane.fuel === "conduit" ? lane.fuel : undefined;
|
|
88
|
-
|
|
88
|
+
// Every field a lane carries must be listed here. This sanitizer duplicates the shape check in
|
|
89
|
+
// `normalizeDrivers` (drivers.ts) — importing it would close the loop config → drivers → driver
|
|
90
|
+
// → config — so a new lane field has to be added in both, and forgetting here is silent: the
|
|
91
|
+
// value survives in memory, then vanishes on the next load. `quota` was lost exactly that way.
|
|
92
|
+
const quota = plainQuota(lane.quota);
|
|
93
|
+
cleaned[id] = { state, ...(fuel ? { fuel } : {}), ...(quota ? { quota } : {}) };
|
|
89
94
|
}
|
|
90
95
|
config.drivers = cleaned;
|
|
91
96
|
}
|
|
92
97
|
return config;
|
|
93
98
|
}
|
|
99
|
+
/** A stored quota record, or nothing. Malformed is dropped rather than thrown — see normalizeQuota. */
|
|
100
|
+
function plainQuota(raw) {
|
|
101
|
+
if (!raw || typeof raw !== "object")
|
|
102
|
+
return undefined;
|
|
103
|
+
if (typeof raw.exhausted !== "boolean" || typeof raw.observed_at !== "string")
|
|
104
|
+
return undefined;
|
|
105
|
+
if (raw.resets_at !== null && typeof raw.resets_at !== "string")
|
|
106
|
+
return undefined;
|
|
107
|
+
return { exhausted: raw.exhausted, resets_at: raw.resets_at, observed_at: raw.observed_at };
|
|
108
|
+
}
|
|
94
109
|
async function loadRuntime() {
|
|
95
110
|
try {
|
|
96
111
|
const raw = JSON.parse(await readFile(runtimePath, "utf8"));
|
|
@@ -169,6 +184,22 @@ export function mergeRuntimeState(disk, snapshot) {
|
|
|
169
184
|
tombstones: Object.keys(tombstones).length ? tombstones : undefined,
|
|
170
185
|
};
|
|
171
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Quota is runner-learned state, not an operator preference. A prefs writer may have loaded its
|
|
189
|
+
* snapshot before another process recorded or cleared quota, so disk is authoritative for that one
|
|
190
|
+
* field. Lane presence and every operator-owned lane setting still come from the writer's snapshot.
|
|
191
|
+
*/
|
|
192
|
+
function mergeCurrentDriverQuota(disk, snapshot) {
|
|
193
|
+
if (!snapshot || !disk)
|
|
194
|
+
return snapshot;
|
|
195
|
+
const drivers = {};
|
|
196
|
+
for (const [id, lane] of Object.entries(snapshot)) {
|
|
197
|
+
const { quota: _staleQuota, ...prefs } = lane;
|
|
198
|
+
const quota = plainQuota(disk.drivers?.[id]?.quota);
|
|
199
|
+
drivers[id] = { ...prefs, ...(quota ? { quota } : {}) };
|
|
200
|
+
}
|
|
201
|
+
return drivers;
|
|
202
|
+
}
|
|
172
203
|
/**
|
|
173
204
|
* Persist Bridge config. Operator prefs and runtime attempt state are written as separate files under
|
|
174
205
|
* a cross-process lock so `drivers online|offline` cannot erase an in-flight claim (and vice versa).
|
|
@@ -178,26 +209,25 @@ export async function saveConfig(config, options = {}) {
|
|
|
178
209
|
const run = async () => {
|
|
179
210
|
await withConfigLock(async () => {
|
|
180
211
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
212
|
+
let diskPrefs = null;
|
|
213
|
+
try {
|
|
214
|
+
diskPrefs = JSON.parse(await readFile(path, "utf8"));
|
|
215
|
+
}
|
|
216
|
+
catch { /* no prior config */ }
|
|
181
217
|
// One-time migrate: if runtime.json is missing, preserve legacy activeAttempts from config.json.
|
|
182
218
|
const existingRuntime = await loadRuntime();
|
|
183
|
-
if (!existingRuntime) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
if (legacy.sessions && typeof legacy.sessions === "object") {
|
|
192
|
-
legacySessions = legacy.sessions;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
catch { /* no prior config */ }
|
|
219
|
+
if (!existingRuntime && diskPrefs) {
|
|
220
|
+
const legacyAttempts = diskPrefs.activeAttempts && typeof diskPrefs.activeAttempts === "object"
|
|
221
|
+
? diskPrefs.activeAttempts
|
|
222
|
+
: {};
|
|
223
|
+
const legacySessions = diskPrefs.sessions && typeof diskPrefs.sessions === "object"
|
|
224
|
+
? diskPrefs.sessions
|
|
225
|
+
: undefined;
|
|
196
226
|
if (Object.keys(legacyAttempts).length || legacySessions) {
|
|
197
227
|
await writeJsonAtomic(runtimePath, { activeAttempts: legacyAttempts, sessions: legacySessions });
|
|
198
228
|
}
|
|
199
229
|
}
|
|
200
|
-
const prefs = { ...snapshot };
|
|
230
|
+
const prefs = { ...snapshot, drivers: mergeCurrentDriverQuota(diskPrefs, snapshot.drivers) };
|
|
201
231
|
delete prefs.activeAttempts;
|
|
202
232
|
delete prefs.sessions;
|
|
203
233
|
const prefsForDisk = { ...prefs, activeAttempts: {}, sessions: undefined };
|
|
@@ -219,6 +249,42 @@ export async function saveConfig(config, options = {}) {
|
|
|
219
249
|
export async function saveConfigPrefs(config) {
|
|
220
250
|
await saveConfig(config, { prefsOnly: true });
|
|
221
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Write one lane's observed quota without touching anything else on disk.
|
|
254
|
+
*
|
|
255
|
+
* A run can outlive the config it started with: the operator may take a lane offline, or change its
|
|
256
|
+
* fuel, while an agent is still working. Persisting the runner's whole in-memory snapshot at the end
|
|
257
|
+
* of that run would put the operator's change back the way it was — a `drivers offline` silently
|
|
258
|
+
* undone minutes later by an unrelated write. So this reads the current prefs under the same lock
|
|
259
|
+
* every other writer uses, edits only `drivers[id].quota`, and writes back.
|
|
260
|
+
*
|
|
261
|
+
* A lane the operator has since removed is left removed: quota describes a lane, and is worth
|
|
262
|
+
* nothing without one.
|
|
263
|
+
*/
|
|
264
|
+
export async function saveDriverQuota(driverId, quota) {
|
|
265
|
+
const run = async () => {
|
|
266
|
+
await withConfigLock(async () => {
|
|
267
|
+
let prefs;
|
|
268
|
+
try {
|
|
269
|
+
prefs = JSON.parse(await readFile(path, "utf8"));
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return; // No prefs on disk yet: nothing to attach a quota to.
|
|
273
|
+
}
|
|
274
|
+
const lane = prefs.drivers?.[driverId];
|
|
275
|
+
if (!lane || typeof lane !== "object")
|
|
276
|
+
return;
|
|
277
|
+
if (quota)
|
|
278
|
+
lane.quota = quota;
|
|
279
|
+
else
|
|
280
|
+
delete lane.quota;
|
|
281
|
+
await writeJsonAtomic(path, { ...prefs, activeAttempts: {}, sessions: undefined });
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
const next = saveChain.then(run, run);
|
|
285
|
+
saveChain = next.catch(() => undefined);
|
|
286
|
+
await next;
|
|
287
|
+
}
|
|
222
288
|
/**
|
|
223
289
|
* Claim/session write that leaves prefs untouched and merges with on-disk attempts from other
|
|
224
290
|
* Bridge processes (supervisor vs MCP) instead of replacing the whole runtime snapshot.
|
package/dist/drivers.js
CHANGED
|
@@ -54,10 +54,67 @@ export function normalizeDrivers(raw) {
|
|
|
54
54
|
continue;
|
|
55
55
|
const state = lane.state === "online" ? "online" : "offline";
|
|
56
56
|
const fuel = lane.fuel === "local" || lane.fuel === "conduit" ? lane.fuel : undefined;
|
|
57
|
-
|
|
57
|
+
const quota = normalizeQuota(lane.quota);
|
|
58
|
+
out[id] = { state, ...(fuel ? { fuel } : {}), ...(quota ? { quota } : {}) };
|
|
58
59
|
}
|
|
59
60
|
return out;
|
|
60
61
|
}
|
|
62
|
+
/** Drop a malformed quota rather than fail the whole config: a bad record must not dark a lane. */
|
|
63
|
+
function normalizeQuota(raw) {
|
|
64
|
+
if (!raw || typeof raw !== "object")
|
|
65
|
+
return undefined;
|
|
66
|
+
const value = raw;
|
|
67
|
+
if (typeof value.exhausted !== "boolean" || typeof value.observed_at !== "string")
|
|
68
|
+
return undefined;
|
|
69
|
+
const resetsAt = typeof value.resets_at === "string" ? value.resets_at : null;
|
|
70
|
+
return { exhausted: value.exhausted, resets_at: resetsAt, observed_at: value.observed_at };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* This lane's quota as of now, with a passed reset retired.
|
|
74
|
+
*
|
|
75
|
+
* The stored record says only what the vendor refused and when it said the window would refill.
|
|
76
|
+
* Deciding `exhausted` here rather than at write time is what keeps a refilled lane from staying
|
|
77
|
+
* dark until something happens to run on it — and it self-corrects a stale record, which matters
|
|
78
|
+
* because the preflight the control plane reads may be minutes old.
|
|
79
|
+
*/
|
|
80
|
+
export function laneQuota(config, driverId, now = Date.now()) {
|
|
81
|
+
const quota = normalizeDrivers(config.drivers)[driverId]?.quota;
|
|
82
|
+
if (!quota)
|
|
83
|
+
return null;
|
|
84
|
+
if (!quota.exhausted)
|
|
85
|
+
return quota;
|
|
86
|
+
const resets = quota.resets_at ? Date.parse(quota.resets_at) : Number.NaN;
|
|
87
|
+
if (Number.isFinite(resets) && resets <= now)
|
|
88
|
+
return { ...quota, exhausted: false };
|
|
89
|
+
// A refusal that named no reset time cannot expire on its own, and a lane held out of
|
|
90
|
+
// pickDriverForClaim can never run the work that would prove it well again. That is a deadlock
|
|
91
|
+
// one misread error message away, so an unreset claim is only believed for so long: after the
|
|
92
|
+
// longest window a vendor plausibly enforces, let the lane try and be refused again if it truly
|
|
93
|
+
// is spent. Costs one attempt to recover; costs the whole machine not to.
|
|
94
|
+
const observed = Date.parse(quota.observed_at);
|
|
95
|
+
if (!quota.resets_at && Number.isFinite(observed) && now - observed > UNRESET_QUOTA_TRUST_MS) {
|
|
96
|
+
return { ...quota, exhausted: false };
|
|
97
|
+
}
|
|
98
|
+
return quota;
|
|
99
|
+
}
|
|
100
|
+
/** How long a refusal with no stated reset is trusted. A week covers the longest plan windows. */
|
|
101
|
+
const UNRESET_QUOTA_TRUST_MS = 7 * 24 * 60 * 60_000;
|
|
102
|
+
/** True when this lane is not currently refused by its vendor. Lanes without a record are free. */
|
|
103
|
+
export function driverFuelAvailable(config, driverId, now = Date.now()) {
|
|
104
|
+
if (resolveDriverFuel(config, driverId) !== "local")
|
|
105
|
+
return true;
|
|
106
|
+
return laneQuota(config, driverId, now)?.exhausted !== true;
|
|
107
|
+
}
|
|
108
|
+
/** Record (or clear) what the vendor said about this lane's window. */
|
|
109
|
+
export function recordDriverQuota(config, driverId, quota) {
|
|
110
|
+
if (!isSupportedDriverId(driverId))
|
|
111
|
+
return config;
|
|
112
|
+
const drivers = normalizeDrivers(config.drivers);
|
|
113
|
+
const lane = drivers[driverId] ?? (localFuelOnlyDriver(driverId) ? { state: "offline", fuel: "local" } : { state: "offline" });
|
|
114
|
+
const { quota: _dropped, ...rest } = lane;
|
|
115
|
+
drivers[driverId] = quota ? { ...rest, quota } : rest;
|
|
116
|
+
return { ...config, drivers };
|
|
117
|
+
}
|
|
61
118
|
/**
|
|
62
119
|
* Ensure detected (or listed) drivers exist in config. New lanes default offline (fail-closed).
|
|
63
120
|
* Does not flip existing online/offline state.
|
|
@@ -160,12 +217,18 @@ export function driversHeartbeatReport(config, activeAttempts, processOnlineIds)
|
|
|
160
217
|
/**
|
|
161
218
|
* Pick an online driver for a new claim: least loaded among online (shared machine pool).
|
|
162
219
|
* `processOnlineIds` overrides config online lanes (tests / harness).
|
|
220
|
+
*
|
|
221
|
+
* A lane whose subscription window is spent is excluded here rather than at each call site: there
|
|
222
|
+
* are five, and one forgotten would hand work to a lane whose vendor is refusing it — spending an
|
|
223
|
+
* attempt to relearn what the last refusal already told us. Every lane dry returns null, the same
|
|
224
|
+
* answer as no lane online, which the callers already handle.
|
|
163
225
|
*/
|
|
164
226
|
export function pickDriverForClaim(config, processOnlineIds, eligible = () => true) {
|
|
165
227
|
const candidates = processOnlineIds === undefined || processOnlineIds === null
|
|
166
228
|
? onlineDriverIds(config)
|
|
167
229
|
: processOnlineIds;
|
|
168
230
|
const online = candidates
|
|
231
|
+
.filter((id) => driverFuelAvailable(config, id))
|
|
169
232
|
.filter((id) => isSupportedDriverId(id))
|
|
170
233
|
.filter(eligible);
|
|
171
234
|
if (!online.length)
|
package/dist/execution.js
CHANGED
|
@@ -2,10 +2,10 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { ConduitRequestError } from "./client.js";
|
|
5
|
-
import { redactSecrets } from "./config.js";
|
|
5
|
+
import { redactSecrets, saveDriverQuota } from "./config.js";
|
|
6
6
|
import { DRIVERS, agentReportTemplate, buildAssignmentPrompt, evidenceKinds, extractAgentReportJsonText, fuelEndpoint, normalizeEvidenceDigest, parseAgentReport, parseJsonObjectCandidate, pickModelCandidate, requireStampedExecutionClass, tierForRisk } from "./driver.js";
|
|
7
7
|
import { assertClassFloor } from "./execution-class.js";
|
|
8
|
-
import { pickDriverForClaim, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
|
|
8
|
+
import { pickDriverForClaim, recordDriverQuota, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
|
|
9
9
|
import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, quarantineAttemptWorktree, removeAttemptWorktree, } from "./attempt-worktree.js";
|
|
10
10
|
import { execFile } from "node:child_process";
|
|
11
11
|
import { promisify } from "node:util";
|
|
@@ -68,6 +68,91 @@ export const FORGE_TRANSPORT_PATTERN = /unable to access '?https?:\/\/|error in
|
|
|
68
68
|
export function forgeTransportFailure(message) {
|
|
69
69
|
return FORGE_TRANSPORT_PATTERN.test(message);
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* A vendor refusing a local-fuel run because the operator's subscription window is spent.
|
|
73
|
+
*
|
|
74
|
+
* Distinct from `driver_not_authenticated`: the login is fine, the allowance is not. Conflating the
|
|
75
|
+
* two sends the owner to re-authenticate a CLI that is already signed in, which fixes nothing and
|
|
76
|
+
* costs an evening. Deliberately narrow — an unmatched refusal degrades to an ordinary agent
|
|
77
|
+
* failure, which is exactly today's behaviour.
|
|
78
|
+
*/
|
|
79
|
+
export const SUBSCRIPTION_EXHAUSTED_PATTERN = /\b(?:usage|rate)[ _-]?limit(?:ed|s)?\b|\bquota (?:exceeded|exhausted|reached)\b|\b(?:http|status|code)\W{0,3}429\b|\b429\b(?=\W{0,3}too many)|too many requests|out of (?:credits|usage)/i;
|
|
80
|
+
/**
|
|
81
|
+
* Does this failure mean the plan is spent, rather than the prompt being wrong?
|
|
82
|
+
*
|
|
83
|
+
* The distinction that matters is a context or output cap: the model refusing *this prompt* is not
|
|
84
|
+
* the plan refusing *this month*, and darking a lane for it is the worst outcome available — a
|
|
85
|
+
* refusal that carried no reset keeps the lane out of `pickDriverForClaim`, so nothing runs on it,
|
|
86
|
+
* so nothing can prove it well again until the trust window ages out.
|
|
87
|
+
*
|
|
88
|
+
* That is handled by requiring an explicit refusal phrase above rather than by vetoing "limit"
|
|
89
|
+
* separately. An earlier veto pass did both and made a concatenated error ("context limit exceeded;
|
|
90
|
+
* fallback failed: HTTP 429") read as healthy — hiding a genuine refusal behind an unrelated
|
|
91
|
+
* sentence in the same string.
|
|
92
|
+
*/
|
|
93
|
+
export function subscriptionExhausted(message) {
|
|
94
|
+
return SUBSCRIPTION_EXHAUSTED_PATTERN.test(message);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* When the window refills, if the vendor said so.
|
|
98
|
+
*
|
|
99
|
+
* Vendors phrase this as an absolute clock time ("resets at 3pm"), a duration ("try again in 2h
|
|
100
|
+
* 14m"), or a Unix epoch. Returning null when none matches is fine — `laneQuota` treats a record
|
|
101
|
+
* without a reset as exhausted until something proves otherwise, and the next successful run clears
|
|
102
|
+
* it. Guessing a reset time would be worse: an invented one un-darks the lane early and the owner
|
|
103
|
+
* watches the same refusal twice.
|
|
104
|
+
*/
|
|
105
|
+
export function parseQuotaResetAt(message, now = Date.now()) {
|
|
106
|
+
const epoch = /\bresets?[ _-]?(?:at|in)?\b\D{0,12}(\d{10})\b/i.exec(message);
|
|
107
|
+
if (epoch)
|
|
108
|
+
return plausibleReset(Number(epoch[1]) * 1000, now);
|
|
109
|
+
// Units must end where they claim to. Unanchored, `m` swallowed the "m" of "500ms" and of
|
|
110
|
+
// "2 months" and read both as minutes — turning a 500-millisecond backoff into an eight-hour
|
|
111
|
+
// blackout of the only local lane.
|
|
112
|
+
const duration = /\b(?:try again|retry|resets?|available again)\b[^.\n]{0,24}?\bin\b\s*(?:(\d+)\s*(?:h|hrs?|hours?)\b(?!\w))?\s*(?:(\d+)\s*(?:m|mins?|minutes?)\b(?!\w))?\s*(?:(\d+)\s*(?:s|secs?|seconds?)\b(?!\w))?\s*(?:(\d+)\s*(?:ms|msecs?|milliseconds?)\b(?!\w))?/i.exec(message);
|
|
113
|
+
if (duration && (duration[1] || duration[2] || duration[3] || duration[4])) {
|
|
114
|
+
const ms = (Number(duration[1] ?? 0) * 3_600 + Number(duration[2] ?? 0) * 60 + Number(duration[3] ?? 0)) * 1_000
|
|
115
|
+
+ Number(duration[4] ?? 0);
|
|
116
|
+
if (ms > 0)
|
|
117
|
+
return plausibleReset(now + ms, now);
|
|
118
|
+
}
|
|
119
|
+
const iso = /\bresets?\b[^.\n]{0,24}?(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)/i.exec(message);
|
|
120
|
+
if (iso)
|
|
121
|
+
return plausibleReset(Date.parse(iso[1]), now);
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* A reset time worth believing, or none at all.
|
|
126
|
+
*
|
|
127
|
+
* Two failure modes this closes. A number lifted from prose is not always a clock — a request id
|
|
128
|
+
* that happens to be ten digits reads as a date in 2286 and would dark the lane for centuries. And
|
|
129
|
+
* `new Date(x).toISOString()` *throws* on a non-finite or out-of-range value, which here would
|
|
130
|
+
* escape between the driver returning and `queueTerminal`, stranding the attempt in `agent_running`.
|
|
131
|
+
* Anything outside a plausible billing window is treated as "no reset stated", which is the honest
|
|
132
|
+
* answer and already a supported state.
|
|
133
|
+
*/
|
|
134
|
+
function plausibleReset(ms, now) {
|
|
135
|
+
if (!Number.isFinite(ms))
|
|
136
|
+
return null;
|
|
137
|
+
if (ms > now + MAX_PLAUSIBLE_RESET_MS)
|
|
138
|
+
return null;
|
|
139
|
+
// A reset that has just passed is kept, not discarded. Rejecting it returned null, which does not
|
|
140
|
+
// mean "already refilled" — it means "no reset stated", the one case trusted for a whole week. A
|
|
141
|
+
// second of clock skew or a slow stderr flush would then dark the lane for seven days instead of
|
|
142
|
+
// clearing it at once. Keeping the stale timestamp lets `laneQuota` retire it on the next read.
|
|
143
|
+
if (ms < now - MAX_STALE_RESET_MS)
|
|
144
|
+
return null;
|
|
145
|
+
try {
|
|
146
|
+
return new Date(ms).toISOString();
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** No vendor window runs longer than a month; past that the parse was a coincidence, not a clock. */
|
|
153
|
+
const MAX_PLAUSIBLE_RESET_MS = 31 * 24 * 3_600_000;
|
|
154
|
+
/** How far back a reset may sit and still be a real one the run simply outlived. */
|
|
155
|
+
const MAX_STALE_RESET_MS = 24 * 3_600_000;
|
|
71
156
|
/** Environment faults that must Hold — mirror CP ENVIRONMENT_FAILURES message patterns. */
|
|
72
157
|
const FINALIZE_ENVIRONMENT_PATTERN = /base[_ ]not[_ ]ancestor|required base commit is not available|source[_ ]workspace[_ ]dirty|uncommitted changes|dirty workspace|workspace[_ ]head[_ ]changed|workspace[_ ]repository|workspace[_ ]unavailable|driver[_ ]not[_ ]authenticated|not logged in|no login|not authenticated|login required|no[_ ]online[_ ]driver|bridge[_ ]preflight|stale bridge|bridge version/i;
|
|
73
158
|
/** Delivery-report / grant / evidence contract defects (non-retryable rework). */
|
|
@@ -87,6 +172,49 @@ export function classifyFinalizeFailure(message) {
|
|
|
87
172
|
// Distinct from execution_contract_failed so owners see a Bridge fault, not a contract lie.
|
|
88
173
|
return { retryable: false, error: `Bridge finalize interrupted: ${message}` };
|
|
89
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Learn this lane's fuel state from what the run just did.
|
|
177
|
+
*
|
|
178
|
+
* A refusal records the window; any completed run clears it, because finishing work is proof the
|
|
179
|
+
* allowance is back — stronger evidence than a reset time the vendor may never have given. Only
|
|
180
|
+
* local fuel is tracked: a `conduit`-fuelled lane spends the organization's gateway credential,
|
|
181
|
+
* whose ceiling is the project budget, not a personal window.
|
|
182
|
+
*/
|
|
183
|
+
/**
|
|
184
|
+
* Observe this run's fuel signal and write it down.
|
|
185
|
+
*
|
|
186
|
+
* Persisting is not optional. The runner reloads `config.drivers` from disk every cycle so lane
|
|
187
|
+
* toggles apply without a restart, so a record kept only in memory is erased within seconds and the
|
|
188
|
+
* lane takes work again as if nothing happened — the feature looks like it works and does not.
|
|
189
|
+
*
|
|
190
|
+
* Every agent run goes through here, not just the first. A repair turn or a land continuation is
|
|
191
|
+
* often the request that actually crosses the limit, and an attempt that fails there would otherwise
|
|
192
|
+
* leave the lane eligible.
|
|
193
|
+
*/
|
|
194
|
+
export async function learnDriverFuel(config, driverId, fuelSource, result) {
|
|
195
|
+
const drivers = observeDriverFuel(config, driverId, fuelSource, result).drivers;
|
|
196
|
+
if (JSON.stringify(drivers ?? {}) === JSON.stringify(config.drivers ?? {}))
|
|
197
|
+
return;
|
|
198
|
+
config.drivers = drivers;
|
|
199
|
+
// Only this lane's quota reaches disk. Writing the whole snapshot would carry the rest of a config
|
|
200
|
+
// that may be minutes stale back over the operator's newer choices — see saveDriverQuota.
|
|
201
|
+
// A fuel record is worth less than the run it came from: never let a disk fault fail the attempt.
|
|
202
|
+
await saveDriverQuota(driverId, drivers?.[driverId]?.quota).catch(() => undefined);
|
|
203
|
+
}
|
|
204
|
+
export function observeDriverFuel(config, driverId, fuelSource, result, now = Date.now()) {
|
|
205
|
+
if (fuelSource !== "local")
|
|
206
|
+
return config;
|
|
207
|
+
if (result.status !== "failed")
|
|
208
|
+
return recordDriverQuota(config, driverId, null);
|
|
209
|
+
const message = result.error ?? "";
|
|
210
|
+
if (!subscriptionExhausted(message))
|
|
211
|
+
return config;
|
|
212
|
+
return recordDriverQuota(config, driverId, {
|
|
213
|
+
exhausted: true,
|
|
214
|
+
resets_at: parseQuotaResetAt(message, now),
|
|
215
|
+
observed_at: new Date(now).toISOString(),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
90
218
|
/** Failures that require an operator/configuration change must never burn the remaining attempts. */
|
|
91
219
|
export function retryableAgentFailure(message) {
|
|
92
220
|
// `cannot enforce` is a driver projection refusing a class it structurally cannot bound (Pi and
|
|
@@ -115,6 +243,7 @@ const taskDetailSchema = z.object({
|
|
|
115
243
|
execution_mode: z.enum(["agent", "human"]).optional().default("agent"),
|
|
116
244
|
execution_kind: z.enum(["authoring", "diagnosis"]).optional().default("authoring"),
|
|
117
245
|
source_attempt_id: z.string().uuid().nullable().optional().default(null),
|
|
246
|
+
repair_mode: z.enum(["none", "briefed", "blind"]).optional().default("none"),
|
|
118
247
|
});
|
|
119
248
|
const taskSpecSchema = z.object({
|
|
120
249
|
goal: z.string().optional(), scope: z.array(z.string()).optional(), boundaries: z.array(z.string()).optional(),
|
|
@@ -824,7 +953,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
824
953
|
// A fresh attempt after a failed delivery must start clean: resuming a session whose last
|
|
825
954
|
// turn already "finished" makes the agent reply conversationally without the report block.
|
|
826
955
|
const resumeSessionId = options.forceResumeSessionId
|
|
827
|
-
?? (reworkFeedback ? config.sessions?.[taskId] : undefined);
|
|
956
|
+
?? (reworkFeedback && task.repair_mode !== "briefed" ? config.sessions?.[taskId] : undefined);
|
|
828
957
|
const result = await driver.run({
|
|
829
958
|
prompt,
|
|
830
959
|
workspace: attemptWorkspace,
|
|
@@ -842,6 +971,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
842
971
|
});
|
|
843
972
|
if (result.sessionId)
|
|
844
973
|
config.sessions = { ...config.sessions, [taskId]: result.sessionId };
|
|
974
|
+
await learnDriverFuel(config, driver.name, fuelSource, result);
|
|
845
975
|
if (diagnosis) {
|
|
846
976
|
if (result.status === "failed") {
|
|
847
977
|
const message = result.error ?? "Read-only diagnosis failed";
|
|
@@ -938,6 +1068,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
938
1068
|
agentReply: reportText,
|
|
939
1069
|
})) {
|
|
940
1070
|
const continuation = await runLandContinuationTurn({
|
|
1071
|
+
config,
|
|
941
1072
|
client,
|
|
942
1073
|
taskId,
|
|
943
1074
|
attemptId: active.attemptId,
|
|
@@ -973,6 +1104,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
973
1104
|
if (requiresLandCommit({ grants, spec }) && gitEmpty && claimsWork) {
|
|
974
1105
|
if (!landContinuationUsed) {
|
|
975
1106
|
const continuation = await runLandContinuationTurn({
|
|
1107
|
+
config,
|
|
976
1108
|
client,
|
|
977
1109
|
taskId,
|
|
978
1110
|
attemptId: active.attemptId,
|
|
@@ -1072,6 +1204,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
1072
1204
|
console.error(`Assignment ${taskId} report-only repair failed: ${redactSecrets(message)}`);
|
|
1073
1205
|
return;
|
|
1074
1206
|
}
|
|
1207
|
+
await learnDriverFuel(config, driver.name, fuelSource, repaired);
|
|
1075
1208
|
if (repaired.sessionId) {
|
|
1076
1209
|
config.sessions = { ...config.sessions, [taskId]: repaired.sessionId };
|
|
1077
1210
|
resumeSessionId = repaired.sessionId;
|
|
@@ -1133,6 +1266,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
1133
1266
|
&& reportClaimsRepositoryWork(report)
|
|
1134
1267
|
&& await isLandTreeEmpty({ workspace: attemptWorkspace, spec, grants })) {
|
|
1135
1268
|
const continuation = await runLandContinuationTurn({
|
|
1269
|
+
config,
|
|
1136
1270
|
client,
|
|
1137
1271
|
taskId,
|
|
1138
1272
|
attemptId: active.attemptId,
|
|
@@ -1177,6 +1311,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
1177
1311
|
}
|
|
1178
1312
|
try {
|
|
1179
1313
|
report = await finalizeRepositoryLand({
|
|
1314
|
+
config,
|
|
1180
1315
|
client,
|
|
1181
1316
|
taskId,
|
|
1182
1317
|
attemptId: active.attemptId,
|
|
@@ -1346,6 +1481,7 @@ async function runLandContinuationTurn(input) {
|
|
|
1346
1481
|
fuel: input.fuel,
|
|
1347
1482
|
fuelSource: input.fuelSource,
|
|
1348
1483
|
});
|
|
1484
|
+
await learnDriverFuel(input.config, input.driver.name, input.fuelSource ?? "conduit", continuation);
|
|
1349
1485
|
if (continuation.status === "failed") {
|
|
1350
1486
|
throw new AgentNoLandCommitError(input.spec.repository?.base_commit ?? "unknown");
|
|
1351
1487
|
}
|
|
@@ -1386,6 +1522,7 @@ async function finalizeRepositoryLand(input) {
|
|
|
1386
1522
|
throw new AgentNoLandCommitError(input.spec.repository?.base_commit ?? "unknown");
|
|
1387
1523
|
}
|
|
1388
1524
|
const continuation = await runLandContinuationTurn({
|
|
1525
|
+
config: input.config,
|
|
1389
1526
|
client: input.client,
|
|
1390
1527
|
taskId: input.taskId,
|
|
1391
1528
|
attemptId: input.attemptId,
|
package/dist/investigation.js
CHANGED
|
@@ -9,8 +9,9 @@ import { z } from "zod";
|
|
|
9
9
|
import { createAttemptWorktree, removeAttemptWorktree } from "./attempt-worktree.js";
|
|
10
10
|
import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl } from "./brief.js";
|
|
11
11
|
import { redactSecrets } from "./config.js";
|
|
12
|
+
import { learnDriverFuel } from "./execution.js";
|
|
12
13
|
import { DRIVERS, extractAgentReportJsonText, parseJsonObjectCandidate } from "./driver.js";
|
|
13
|
-
import { pickDriverForClaim, supportsReadOnlyDiagnosis } from "./drivers.js";
|
|
14
|
+
import { pickDriverForClaim, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
|
|
14
15
|
import { changedPathsSince } from "./execution.js";
|
|
15
16
|
import { execFile } from "node:child_process";
|
|
16
17
|
import { promisify } from "node:util";
|
|
@@ -21,6 +22,7 @@ const budgetSchema = z.object({
|
|
|
21
22
|
}).passthrough();
|
|
22
23
|
export const investigationAssignmentSchema = z.object({
|
|
23
24
|
id: z.string().uuid(),
|
|
25
|
+
project_id: z.string().uuid(),
|
|
24
26
|
kind: z.literal("repository"),
|
|
25
27
|
question: z.string().min(1).max(4_000),
|
|
26
28
|
grants: z.array(z.string()).default([]),
|
|
@@ -181,6 +183,13 @@ export async function executeNextInvestigation(client, config, workspace, brief,
|
|
|
181
183
|
attemptId: assignment.id,
|
|
182
184
|
startCommit: commit,
|
|
183
185
|
});
|
|
186
|
+
// One value, used to run and to record. Read separately they drifted: nothing was passed to the
|
|
187
|
+
// driver, so it defaulted to Conduit fuel, while the result was filed against the lane's
|
|
188
|
+
// configured source — letting a gateway refusal mark the operator's personal subscription spent.
|
|
189
|
+
const investigationFuel = resolveDriverFuel(config, driver.name);
|
|
190
|
+
const fuel = investigationFuel === "conduit"
|
|
191
|
+
? { baseUrl: config.baseUrl, gatewayKey: await client.ensureFuel(assignment.project_id) }
|
|
192
|
+
: undefined;
|
|
184
193
|
const result = await driver.run({
|
|
185
194
|
prompt: buildInvestigationPrompt(assignment, commit),
|
|
186
195
|
workspace: attemptWorkspace,
|
|
@@ -190,8 +199,11 @@ export async function executeNextInvestigation(client, config, workspace, brief,
|
|
|
190
199
|
// The same signal the read-only diagnosis lane uses; it is what denies edits in the driver.
|
|
191
200
|
workRole: "diagnose",
|
|
192
201
|
executionClass: "verify",
|
|
202
|
+
fuelSource: investigationFuel,
|
|
203
|
+
fuel,
|
|
193
204
|
timeoutMs: Math.min(timeoutMs ?? assignment.budget.max_duration_ms, assignment.budget.max_duration_ms),
|
|
194
205
|
});
|
|
206
|
+
await learnDriverFuel(config, driver.name, investigationFuel, result);
|
|
195
207
|
if (result.status === "failed") {
|
|
196
208
|
const message = result.error ?? "Investigation run failed";
|
|
197
209
|
await settle(client, assignment.id, { lease_token: leaseToken, status: "failed", error: redactSecrets(message).slice(0, 2_000), retryable: true });
|
package/dist/ops.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
|
-
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
9
9
|
import { parseArgs } from "node:util";
|
|
10
10
|
import { ConduitClient } from "./client.js";
|
|
11
11
|
import { loadConfig } from "./config.js";
|
|
@@ -15,6 +15,7 @@ import { driverIdsFromDetectedLabels } from "./drivers.js";
|
|
|
15
15
|
import { BRIDGE_PROTOCOL_VERSION, describePreflightIssue, runBridgePreflight } from "./preflight.js";
|
|
16
16
|
import { applyRunnerToolPath, runnerServiceWorkspaceWarnings } from "./service.js";
|
|
17
17
|
import { bridgeVersion } from "./version.js";
|
|
18
|
+
import { bootstrapManagedWorkspace } from "./workspace-bootstrap.js";
|
|
18
19
|
export const OPS_VERBS = [
|
|
19
20
|
"connect", "enroll", "install", "switch", "online", "offline", "status", "doctor", "disconnect", "uninstall",
|
|
20
21
|
];
|
|
@@ -529,19 +530,35 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
529
530
|
}
|
|
530
531
|
const workspace = resolve(expandOpsValue(installEnv.CONDUIT_WORKSPACE));
|
|
531
532
|
const drivers = await (deps.resolveInstallDrivers ?? resolveInstallDrivers)(installEnv, installArgv);
|
|
532
|
-
for (const id of drivers) {
|
|
533
|
-
if (LOCAL_FUEL_DRIVERS.has(id))
|
|
534
|
-
runBridge(["drivers", "fuel", id, "local"]);
|
|
535
|
-
}
|
|
536
|
-
runBridge(["drivers", "online", ...drivers]);
|
|
537
|
-
applyRunnerToolPath();
|
|
538
533
|
if (installEnv.CONDUIT_REPO) {
|
|
539
534
|
const checkout = deps.ensureCheckout ?? ensureCheckout;
|
|
540
535
|
const result = await checkout(workspace, installEnv.CONDUIT_REPO);
|
|
541
536
|
console.log(result === "cloned"
|
|
542
537
|
? `Cloned ${installEnv.CONDUIT_REPO} into ${workspace}`
|
|
543
538
|
: `Workspace already matches ${installEnv.CONDUIT_REPO}`);
|
|
539
|
+
const managedRoot = installEnv.CONDUIT_MANAGED_ROOT
|
|
540
|
+
? resolve(expandOpsValue(installEnv.CONDUIT_MANAGED_ROOT))
|
|
541
|
+
: null;
|
|
542
|
+
const managedRelative = managedRoot ? relative(managedRoot, workspace) : "";
|
|
543
|
+
const managedCheckout = Boolean(managedRoot
|
|
544
|
+
&& managedRelative
|
|
545
|
+
&& managedRelative !== ".."
|
|
546
|
+
&& !managedRelative.startsWith(`..${sep}`)
|
|
547
|
+
&& !isAbsolute(managedRelative));
|
|
548
|
+
if (managedCheckout) {
|
|
549
|
+
const bootstrap = deps.bootstrapWorkspace ?? bootstrapManagedWorkspace;
|
|
550
|
+
const bootstrapResult = bootstrap(workspace);
|
|
551
|
+
if (bootstrapResult === "installed")
|
|
552
|
+
console.log(`Installed locked project dependencies in ${workspace}`);
|
|
553
|
+
}
|
|
544
554
|
}
|
|
555
|
+
// Do not advertise an executable lane until a managed checkout has completed bootstrap.
|
|
556
|
+
for (const id of drivers) {
|
|
557
|
+
if (LOCAL_FUEL_DRIVERS.has(id))
|
|
558
|
+
runBridge(["drivers", "fuel", id, "local"]);
|
|
559
|
+
}
|
|
560
|
+
runBridge(["drivers", "online", ...drivers]);
|
|
561
|
+
applyRunnerToolPath();
|
|
545
562
|
// A managed root is intentionally empty until the console assigns its first repository. It
|
|
546
563
|
// still needs a resident runner, and `ops install` is also the recovery path after enrollment.
|
|
547
564
|
const waitingOnManagedAssignment = Boolean(installEnv.CONDUIT_MANAGED_ROOT
|
package/dist/preflight.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import { buildWorkspaceBrief, normalizeRepositoryUrl } from "./brief.js";
|
|
5
5
|
import { hasAntigravityLogin, hasClaudeLogin, hasGrokLogin, hasOpenAiLogin, hasOpenCodeLogin, resolveCodexExecutable, } from "./driver.js";
|
|
6
|
-
import { localFuelOnlyDriver, onlineDriverIds, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
|
|
6
|
+
import { laneQuota, localFuelOnlyDriver, onlineDriverIds, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
|
|
7
7
|
/** Protocol 3: read-only diagnosis can reuse a retained failed-attempt worktree. */
|
|
8
8
|
// 4: each driver snapshot reports diagnosis_read_only, so the control plane can require a
|
|
9
9
|
// capable diagnostic lane *before* dispatching instead of learning it from a failed attempt.
|
|
@@ -116,9 +116,22 @@ export async function runBridgePreflight(input, deps = {}) {
|
|
|
116
116
|
if (fuel === "local" && !await localAuthenticationReady(driver, input.workspace, run)) {
|
|
117
117
|
return { issue: { code: "driver_not_authenticated", driver }, snapshot: { id: driver, version: versionText, ready: false, diagnosis_read_only: diagnosisReadOnly } };
|
|
118
118
|
}
|
|
119
|
-
|
|
119
|
+
// Reported, not probed: the window state was learned from the vendor's last refusal and read
|
|
120
|
+
// back through laneQuota, which retires a passed reset. Nothing is executed here, so a lane
|
|
121
|
+
// running dry costs the heartbeat nothing.
|
|
122
|
+
const quota = fuel === "local" ? laneQuota(input.config, driver) ?? undefined : undefined;
|
|
123
|
+
return {
|
|
124
|
+
issue: null,
|
|
125
|
+
snapshot: { id: driver, version: versionText, ready: quota?.exhausted !== true, diagnosis_read_only: diagnosisReadOnly, ...(quota ? { quota } : {}) },
|
|
126
|
+
};
|
|
120
127
|
}));
|
|
121
128
|
issues.push(...driverChecks.map((check) => check.issue).filter((issue) => issue !== null));
|
|
129
|
+
// One dry lane is not a broken machine — the others still take work, and marking the machine
|
|
130
|
+
// unready over it would strand a fuelled lane. Only a fleet with nothing left to spend is an
|
|
131
|
+
// issue, which is also what keeps `ready === (issues.length === 0)` true either way.
|
|
132
|
+
if (driverChecks.length > 0 && driverChecks.every((check) => check.snapshot.quota?.exhausted === true)) {
|
|
133
|
+
issues.push({ code: "driver_quota_exhausted" });
|
|
134
|
+
}
|
|
122
135
|
return { ready: issues.length === 0, checked_at: new Date().toISOString(), workspace_clean: workspaceClean,
|
|
123
136
|
drivers: driverChecks.map((check) => check.snapshot), models_fingerprint: modelsFingerprint(input.config), issues };
|
|
124
137
|
}
|
|
@@ -165,5 +178,10 @@ export function describePreflightIssue(issue) {
|
|
|
165
178
|
return `Agent CLI is missing from PATH${lane}`;
|
|
166
179
|
if (issue.code === "driver_not_authenticated")
|
|
167
180
|
return `Agent CLI is not logged in${lane}`;
|
|
181
|
+
// Named before the fallback: an exhausted lane fell through to "requires local fuel", which sends
|
|
182
|
+
// the operator to change a fuel setting when the lane is configured correctly and simply spent.
|
|
183
|
+
if (issue.code === "driver_quota_exhausted") {
|
|
184
|
+
return "Every online agent lane has spent its subscription allowance; work resumes when a window refills, or bring another lane online";
|
|
185
|
+
}
|
|
168
186
|
return `Agent lane requires local fuel${lane}`;
|
|
169
187
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
function defaultRunner(command, args, workspace) {
|
|
5
|
+
const result = spawnSync(command, args, {
|
|
6
|
+
cwd: workspace,
|
|
7
|
+
env: process.env,
|
|
8
|
+
stdio: "inherit",
|
|
9
|
+
timeout: 10 * 60_000,
|
|
10
|
+
});
|
|
11
|
+
return { status: result.status, ...(result.error ? { error: result.error } : {}) };
|
|
12
|
+
}
|
|
13
|
+
/** Install locked JavaScript dependencies before a Conduit-managed checkout advertises readiness. */
|
|
14
|
+
export function bootstrapManagedWorkspace(workspace, run = defaultRunner) {
|
|
15
|
+
if (!existsSync(join(workspace, "package.json")))
|
|
16
|
+
return "not_applicable";
|
|
17
|
+
if (!existsSync(join(workspace, "package-lock.json"))) {
|
|
18
|
+
throw new Error("managed_workspace_bootstrap_package_lock_required");
|
|
19
|
+
}
|
|
20
|
+
const result = run("npm", ["ci", "--include=optional", "--no-audit", "--no-fund"], workspace);
|
|
21
|
+
if (result.error)
|
|
22
|
+
throw new Error(`managed_workspace_bootstrap_failed:${result.error.message}`);
|
|
23
|
+
if (result.status !== 0)
|
|
24
|
+
throw new Error(`managed_workspace_bootstrap_failed:exit_${result.status ?? "unknown"}`);
|
|
25
|
+
return "installed";
|
|
26
|
+
}
|
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.16",
|
|
4
4
|
"description": "Conduit Bridge CLI — 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": {
|