@miraland-labs/conduit-bridge 0.11.7 → 0.11.10
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 +33 -5
- package/dist/ensure-pull-request.js +42 -7
- package/dist/ensure-test-evidence.js +20 -8
- package/dist/execution.js +1 -0
- package/dist/on-shift-apply.js +52 -0
- package/dist/ops.js +92 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -15,9 +15,10 @@ import { DRIVERS } from "./driver.js";
|
|
|
15
15
|
import { driversHeartbeatReport, driverLabel, heartbeatStatusForDrivers, isSupportedDriverId, listDriverLanes, onlineDriverIds, seedDriversFromDetection, seedDriverLanes, setDriverFuel, setDriversOnline, } from "./drivers.js";
|
|
16
16
|
import { buildWorkspaceBrief } from "./brief.js";
|
|
17
17
|
import { ensureCheckout } from "./checkout.js";
|
|
18
|
+
import { maybeApplyOnShiftIntent } from "./on-shift-apply.js";
|
|
19
|
+
import { loadOpsEnv, OPS_VERBS, runOps } from "./ops.js";
|
|
18
20
|
import { pumpExecutionSlots, renewLeases } from "./execution.js";
|
|
19
21
|
import { installRunnerService, uninstallRunnerService } from "./service.js";
|
|
20
|
-
import { OPS_VERBS, runOps } from "./ops.js";
|
|
21
22
|
import { BRIDGE_PROTOCOL_VERSION, cachedBridgePreflight, unavailableWorkspacePreflight } from "./preflight.js";
|
|
22
23
|
const [command] = process.argv.slice(2);
|
|
23
24
|
/** Read our own package version so every runner start logs exactly which build is live. */
|
|
@@ -334,7 +335,7 @@ async function initOps() {
|
|
|
334
335
|
console.log(`Config file (separate): ${envPath}`);
|
|
335
336
|
console.log("");
|
|
336
337
|
console.log("Works the same on macOS, Linux, and Windows via:");
|
|
337
|
-
console.log(` ${bridgeUsage("ops", "<connect|install|online|offline|status|disconnect|uninstall>")}`);
|
|
338
|
+
console.log(` ${bridgeUsage("ops", "<connect|install|switch|online|offline|status|disconnect|uninstall>")}`);
|
|
338
339
|
console.log("");
|
|
339
340
|
console.log("1. Edit env once:");
|
|
340
341
|
console.log(` Create folder: ${configDir}`);
|
|
@@ -358,7 +359,7 @@ async function initOps() {
|
|
|
358
359
|
async function opsCommand() {
|
|
359
360
|
const verb = process.argv[3];
|
|
360
361
|
if (!verb || !OPS_VERBS.includes(verb)) {
|
|
361
|
-
throw new Error(`Usage: ${bridgeUsage("ops", "<connect|install|online|offline|status|doctor|disconnect|uninstall>", "[driver…]")}`);
|
|
362
|
+
throw new Error(`Usage: ${bridgeUsage("ops", "<connect|install|switch|online|offline|status|doctor|disconnect|uninstall>", "[driver…]")}`);
|
|
362
363
|
}
|
|
363
364
|
await runOps(verb, process.argv.slice(4));
|
|
364
365
|
}
|
|
@@ -473,7 +474,30 @@ async function runner() {
|
|
|
473
474
|
const preflight = workspace
|
|
474
475
|
? await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined })
|
|
475
476
|
: unavailableWorkspacePreflight({ config });
|
|
476
|
-
await heartbeat(client, config, currentBrief, preflight);
|
|
477
|
+
const hb = await heartbeat(client, config, currentBrief, preflight);
|
|
478
|
+
const applied = await maybeApplyOnShiftIntent({
|
|
479
|
+
client,
|
|
480
|
+
currentWorkspace: workspace,
|
|
481
|
+
onShift: hb.on_shift,
|
|
482
|
+
runInstall: async (nextWorkspace, repo) => {
|
|
483
|
+
let env;
|
|
484
|
+
try {
|
|
485
|
+
env = loadOpsEnv();
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
env = undefined;
|
|
489
|
+
}
|
|
490
|
+
await runOps("install", [], {
|
|
491
|
+
env: env
|
|
492
|
+
? { ...env, CONDUIT_WORKSPACE: nextWorkspace, CONDUIT_REPO: repo || env.CONDUIT_REPO }
|
|
493
|
+
: undefined,
|
|
494
|
+
});
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
if (applied) {
|
|
498
|
+
console.log("On-shift workspace applied; exiting so the background service restarts on the new checkout.");
|
|
499
|
+
process.exit(0);
|
|
500
|
+
}
|
|
477
501
|
await renewLeases(client, config);
|
|
478
502
|
if (workspace && onlineDriverIds(config).length) {
|
|
479
503
|
progressed = await pumpExecutionSlots(client, config, workspace, brief, timeoutMs, {
|
|
@@ -500,7 +524,7 @@ async function runner() {
|
|
|
500
524
|
async function heartbeat(client, config, brief, preflight) {
|
|
501
525
|
const online = onlineDriverIds(config);
|
|
502
526
|
const status = heartbeatStatusForDrivers(online);
|
|
503
|
-
await client.request("/runner/v1/heartbeat", { method: "POST", body: JSON.stringify({
|
|
527
|
+
const response = await client.request("/runner/v1/heartbeat", { method: "POST", body: JSON.stringify({
|
|
504
528
|
status,
|
|
505
529
|
capabilities: config.capabilities,
|
|
506
530
|
lease_capacity: config.leaseCapacity,
|
|
@@ -511,6 +535,10 @@ async function heartbeat(client, config, brief, preflight) {
|
|
|
511
535
|
preflight,
|
|
512
536
|
...(brief ? { workspace_brief: brief } : {}),
|
|
513
537
|
}) });
|
|
538
|
+
const onShift = response.on_shift && typeof response.on_shift === "object"
|
|
539
|
+
? response.on_shift
|
|
540
|
+
: null;
|
|
541
|
+
return { on_shift: onShift };
|
|
514
542
|
}
|
|
515
543
|
async function disconnect() {
|
|
516
544
|
const { values } = parseArgs({ args: process.argv.slice(3), options: { yes: { type: "boolean", short: "y" } } });
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { execFile } from "node:child_process";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
|
+
import { normalizeRepositoryUrl } from "./brief.js";
|
|
8
9
|
const execFileAsync = promisify(execFile);
|
|
9
10
|
/** Mirror execution.ts FORGE_TRANSPORT_PATTERN — keep local to avoid import cycles. */
|
|
10
11
|
const FORGE_TRANSPORT_PATTERN = /unable to access '?https?:\/\/|error in the http2 framing layer|could not resolve host|connection (?:reset|timed out|refused)|\bcurl\b.*\b(?:52|55|56|92)\b|remote end hung up unexpectedly|\brpc failed\b|tls handshake|network is unreachable|operation timed out/i;
|
|
@@ -30,20 +31,54 @@ export function commitsMatch(reported, actual) {
|
|
|
30
31
|
return false;
|
|
31
32
|
return left === right || left.startsWith(right) || right.startsWith(left);
|
|
32
33
|
}
|
|
34
|
+
/** Forge owner/repo identity from a PR / merge-request URL (host/path form used as fingerprint). */
|
|
35
|
+
export function fingerprintFromChangeRequestUrl(url) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = new URL(url);
|
|
38
|
+
const github = parsed.pathname.match(/^\/([^/]+)\/([^/]+)\/pull\/\d+\/?$/i);
|
|
39
|
+
if (github)
|
|
40
|
+
return normalizeRepositoryUrl(`${parsed.host}/${github[1]}/${github[2]}`);
|
|
41
|
+
const gitlab = parsed.pathname.match(/^\/(.+)\/-\/merge_requests\/\d+\/?$/i);
|
|
42
|
+
if (gitlab)
|
|
43
|
+
return normalizeRepositoryUrl(`${parsed.host}/${gitlab[1]}`);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function pullRequestMatchesRepositoryFingerprint(url, repositoryFingerprint) {
|
|
51
|
+
if (!repositoryFingerprint)
|
|
52
|
+
return false;
|
|
53
|
+
const fromUrl = fingerprintFromChangeRequestUrl(url);
|
|
54
|
+
if (!fromUrl)
|
|
55
|
+
return false;
|
|
56
|
+
return fromUrl === normalizeRepositoryUrl(repositoryFingerprint);
|
|
57
|
+
}
|
|
33
58
|
/**
|
|
34
59
|
* Agents often put the PR URL only on evidence.uri and omit pull_request_url.
|
|
35
|
-
* Adopt it so finalize does not re-push and die on a forge blip after the PR already exists
|
|
60
|
+
* Adopt it so finalize does not re-push and die on a forge blip after the PR already exists —
|
|
61
|
+
* but only when the URI is this work package's repository (Invariant 11).
|
|
36
62
|
*/
|
|
37
|
-
export function adoptPullRequestUrlFromEvidence(report) {
|
|
38
|
-
if (report.pull_request_url && isAbsoluteHttpsUrl(report.pull_request_url))
|
|
63
|
+
export function adoptPullRequestUrlFromEvidence(report, repositoryFingerprint) {
|
|
64
|
+
if (report.pull_request_url && isAbsoluteHttpsUrl(report.pull_request_url)) {
|
|
65
|
+
if (repositoryFingerprint
|
|
66
|
+
&& !pullRequestMatchesRepositoryFingerprint(report.pull_request_url, repositoryFingerprint)) {
|
|
67
|
+
throw new Error("pull_request_url does not match the work package repository");
|
|
68
|
+
}
|
|
69
|
+
return report;
|
|
70
|
+
}
|
|
71
|
+
if (!repositoryFingerprint)
|
|
39
72
|
return report;
|
|
40
73
|
for (const item of report.evidence) {
|
|
41
74
|
const uri = item.uri?.trim();
|
|
42
75
|
if (!uri || !isAbsoluteHttpsUrl(uri))
|
|
43
76
|
continue;
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
if (!/\/(?:pull|merge_requests)\/\d+/i.test(uri))
|
|
78
|
+
continue;
|
|
79
|
+
if (!pullRequestMatchesRepositoryFingerprint(uri, repositoryFingerprint))
|
|
80
|
+
continue;
|
|
81
|
+
return { ...report, pull_request_url: uri };
|
|
47
82
|
}
|
|
48
83
|
return report;
|
|
49
84
|
}
|
|
@@ -122,7 +157,7 @@ export async function pushOriginHead(workspace, options = {}) {
|
|
|
122
157
|
export async function ensureDeliveryPullRequest(input) {
|
|
123
158
|
const readHead = input.readHeadCommit ?? workspaceHeadCommit;
|
|
124
159
|
const push = input.pushOriginHead ?? pushOriginHead;
|
|
125
|
-
let report = adoptPullRequestUrlFromEvidence(input.report);
|
|
160
|
+
let report = adoptPullRequestUrlFromEvidence(input.report, input.repositoryFingerprint);
|
|
126
161
|
if (report.head_commit && (input.spec.change_scope?.length ?? 0) > 0) {
|
|
127
162
|
const head = await readHead(input.workspace);
|
|
128
163
|
if (!commitsMatch(report.head_commit, head)) {
|
|
@@ -52,11 +52,22 @@ async function defaultRunCommand(command, workspace) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Bridge may attach genuine command output; it must not invent which criteria that output proves.
|
|
57
|
+
* Reuse only criteria the agent already mapped onto a test-kind evidence entry (including thin ones
|
|
58
|
+
* we are replacing). Empty mapping lets validateDeliveryReport reject met-without-evidence.
|
|
59
|
+
*/
|
|
60
|
+
export function criteriaForTestEvidence(report) {
|
|
61
|
+
const mapped = new Set();
|
|
62
|
+
for (const item of report.evidence) {
|
|
63
|
+
if (item.kind !== "test")
|
|
64
|
+
continue;
|
|
65
|
+
for (const criterion of item.acceptance_criteria ?? []) {
|
|
66
|
+
if (criterion)
|
|
67
|
+
mapped.add(criterion);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return [...mapped];
|
|
60
71
|
}
|
|
61
72
|
/**
|
|
62
73
|
* If test evidence is required and missing/thin, run one bounded verification command
|
|
@@ -85,10 +96,11 @@ export async function ensureTestEvidence(input) {
|
|
|
85
96
|
`exit ${result.code}`,
|
|
86
97
|
].map((line) => line.slice(0, 4_000)).slice(0, 100);
|
|
87
98
|
if (result.code !== 0) {
|
|
88
|
-
|
|
99
|
+
// Prefix must match FINALIZE_CONTRACT_PATTERN ("Agent report") — not Bridge-fault laundering.
|
|
100
|
+
throw new Error(`Agent report: Verification failed (${command}): ${details.join("\n").slice(0, 2_000)}`);
|
|
89
101
|
}
|
|
90
102
|
if (details.join("\n").trim().length < TEST_EVIDENCE_DETAILS_MIN) {
|
|
91
|
-
throw new Error(`Verification produced insufficient output for test evidence (${command})`);
|
|
103
|
+
throw new Error(`Agent report: Verification produced insufficient output for test evidence (${command})`);
|
|
92
104
|
}
|
|
93
105
|
const withoutThinTest = input.report.evidence.filter((item) => {
|
|
94
106
|
if (item.kind !== "test")
|
|
@@ -106,7 +118,7 @@ export async function ensureTestEvidence(input) {
|
|
|
106
118
|
kind: "test",
|
|
107
119
|
name: command,
|
|
108
120
|
details,
|
|
109
|
-
acceptance_criteria: criteriaForTestEvidence(input.report
|
|
121
|
+
acceptance_criteria: criteriaForTestEvidence(input.report),
|
|
110
122
|
},
|
|
111
123
|
],
|
|
112
124
|
};
|
package/dist/execution.js
CHANGED
|
@@ -544,6 +544,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
544
544
|
spec,
|
|
545
545
|
grants,
|
|
546
546
|
title: task.objective,
|
|
547
|
+
repositoryFingerprint: executionContract.repository_fingerprint,
|
|
547
548
|
});
|
|
548
549
|
// Mechanical test path: agent often forgets verbatim make test / npm test output.
|
|
549
550
|
report = await ensureTestEvidence({
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 2: when Connect queued a workspace intent, Bridge applies it once
|
|
3
|
+
* (ops.env + install/restart) then acks so the next LaunchAgent start uses the new path.
|
|
4
|
+
*/
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { defaultOpsEnvPath, expandOpsValue, loadOpsEnv, writeOpsEnvFile } from "./ops.js";
|
|
8
|
+
function pathsEqual(left, right) {
|
|
9
|
+
return resolve(expandOpsValue(left)) === resolve(expandOpsValue(right));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* If control-plane intent names a different workspace than this runner, apply and return true
|
|
13
|
+
* (caller should exit so install-service / LaunchAgent restarts).
|
|
14
|
+
*/
|
|
15
|
+
export async function maybeApplyOnShiftIntent(input) {
|
|
16
|
+
const intentPath = input.onShift?.on_shift_intent?.workspace_path?.trim();
|
|
17
|
+
if (!intentPath)
|
|
18
|
+
return false;
|
|
19
|
+
const target = resolve(expandOpsValue(intentPath));
|
|
20
|
+
const current = input.currentWorkspace ? resolve(expandOpsValue(input.currentWorkspace)) : null;
|
|
21
|
+
if (current && pathsEqual(current, target)) {
|
|
22
|
+
// Already on the declared path — leave intent for Bound clear on heartbeat.
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const repo = (input.onShift?.on_shift_intent?.repository_url
|
|
26
|
+
?? input.onShift?.expected_repository
|
|
27
|
+
?? "").trim();
|
|
28
|
+
if (input.writeEnv) {
|
|
29
|
+
input.writeEnv(target, repo);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
let envPath = defaultOpsEnvPath(homedir());
|
|
33
|
+
try {
|
|
34
|
+
envPath = loadOpsEnv().loadedFrom ?? envPath;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// First apply may create ops.env at the default path.
|
|
38
|
+
}
|
|
39
|
+
writeOpsEnvFile(envPath, {
|
|
40
|
+
CONDUIT_WORKSPACE: target,
|
|
41
|
+
...(repo ? { CONDUIT_REPO: repo } : {}),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Ack before restart so a rebound service does not re-apply forever.
|
|
45
|
+
await input.client.request("/runner/v1/machine/ack-on-shift-intent", {
|
|
46
|
+
method: "POST",
|
|
47
|
+
body: "{}",
|
|
48
|
+
});
|
|
49
|
+
console.log(`Applying on-shift workspace intent: ${target}${repo ? ` (${repo})` : ""}`);
|
|
50
|
+
await input.runInstall(target, repo);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
package/dist/ops.js
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
* Invoked as: npx @miraland-labs/conduit-bridge ops <connect|install|…>
|
|
4
4
|
*/
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { homedir, platform } from "node:os";
|
|
8
|
-
import { join, resolve } from "node:path";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { parseArgs } from "node:util";
|
|
10
|
+
import { ConduitClient } from "./client.js";
|
|
9
11
|
import { loadConfig } from "./config.js";
|
|
10
12
|
import { describePreflightIssue, runBridgePreflight } from "./preflight.js";
|
|
11
13
|
export const OPS_VERBS = [
|
|
12
|
-
"connect", "install", "online", "offline", "status", "doctor", "disconnect", "uninstall",
|
|
14
|
+
"connect", "install", "switch", "online", "offline", "status", "doctor", "disconnect", "uninstall",
|
|
13
15
|
];
|
|
14
16
|
const LOCAL_FUEL_DRIVERS = new Set(["cursor", "pi", "kiro", "antigravity"]);
|
|
15
17
|
export function defaultOpsEnvPath(home = homedir()) {
|
|
@@ -88,6 +90,36 @@ export function requireOpsEnv(env) {
|
|
|
88
90
|
export function splitOpsList(value) {
|
|
89
91
|
return value.split(/[,\s]+/).map((item) => item.trim()).filter(Boolean);
|
|
90
92
|
}
|
|
93
|
+
/** Update keys in an existing ops.env (or create from current values). Preserves comments/unknown keys when possible. */
|
|
94
|
+
export function writeOpsEnvFile(path, patch, existingText) {
|
|
95
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
96
|
+
const keys = ["CONDUIT_URL", "CONDUIT_ORG", "CONDUIT_WORKSPACE", "CONDUIT_REPO", "CONDUIT_DRIVERS", "CONDUIT_ROLES"];
|
|
97
|
+
const current = existingText ?? (existsSync(path) ? readFileSync(path, "utf8") : "");
|
|
98
|
+
const lines = current ? current.split(/\r?\n/) : [];
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
const next = lines.map((raw) => {
|
|
101
|
+
const trimmed = raw.trim();
|
|
102
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
103
|
+
return raw;
|
|
104
|
+
const eq = trimmed.indexOf("=");
|
|
105
|
+
if (eq <= 0)
|
|
106
|
+
return raw;
|
|
107
|
+
const key = trimmed.slice(0, eq).trim();
|
|
108
|
+
if (!keys.includes(key))
|
|
109
|
+
return raw;
|
|
110
|
+
seen.add(key);
|
|
111
|
+
const value = patch[key];
|
|
112
|
+
if (value === undefined)
|
|
113
|
+
return raw;
|
|
114
|
+
return `${key}=${value}`;
|
|
115
|
+
});
|
|
116
|
+
for (const key of keys) {
|
|
117
|
+
if (seen.has(key) || patch[key] === undefined)
|
|
118
|
+
continue;
|
|
119
|
+
next.push(`${key}=${patch[key]}`);
|
|
120
|
+
}
|
|
121
|
+
writeFileSync(path, `${next.filter((line, index) => !(index === next.length - 1 && line === "")).join("\n").replace(/\n*$/, "\n")}`, { mode: 0o600 });
|
|
122
|
+
}
|
|
91
123
|
export function resolveDrivers(env, argv) {
|
|
92
124
|
const fromArgs = argv.map((item) => item.trim()).filter(Boolean);
|
|
93
125
|
const drivers = fromArgs.length ? fromArgs : splitOpsList(env.CONDUIT_DRIVERS);
|
|
@@ -175,6 +207,63 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
175
207
|
return;
|
|
176
208
|
}
|
|
177
209
|
requireOpsEnv(env);
|
|
210
|
+
if (verb === "switch") {
|
|
211
|
+
const { values } = parseArgs({
|
|
212
|
+
args: argv,
|
|
213
|
+
options: {
|
|
214
|
+
project: { type: "string" },
|
|
215
|
+
workspace: { type: "string" },
|
|
216
|
+
repo: { type: "string" },
|
|
217
|
+
},
|
|
218
|
+
allowPositionals: true,
|
|
219
|
+
});
|
|
220
|
+
const projectId = values.project?.trim();
|
|
221
|
+
if (!projectId) {
|
|
222
|
+
throw new Error("Usage: ops switch --project <project-id> [--workspace <path>] [--repo <url>]");
|
|
223
|
+
}
|
|
224
|
+
const workspaceRaw = values.workspace?.trim() || env.CONDUIT_WORKSPACE;
|
|
225
|
+
if (!workspaceRaw)
|
|
226
|
+
throw new Error(`Set CONDUIT_WORKSPACE in ${defaultOpsEnvPath()} or pass --workspace`);
|
|
227
|
+
const workspace = resolve(expandOpsValue(workspaceRaw));
|
|
228
|
+
const repo = (values.repo?.trim() || env.CONDUIT_REPO || "").trim();
|
|
229
|
+
const envPath = env.loadedFrom ?? defaultOpsEnvPath();
|
|
230
|
+
// Control plane first — do not rewrite ops.env if affinity replace fails.
|
|
231
|
+
const config = await (deps.loadBridgeConfig ?? loadConfig)();
|
|
232
|
+
const client = new ConduitClient(config);
|
|
233
|
+
const result = await client.request("/runner/v1/machine/switch-project", {
|
|
234
|
+
method: "POST",
|
|
235
|
+
body: JSON.stringify({
|
|
236
|
+
project_id: projectId,
|
|
237
|
+
workspace_path: workspace,
|
|
238
|
+
...(repo ? { repository_url: repo } : {}),
|
|
239
|
+
}),
|
|
240
|
+
});
|
|
241
|
+
console.log(`Control plane on-shift: ${String(result.project_id ?? projectId)} (${String(result.binding_status ?? "pending")})`);
|
|
242
|
+
writeOpsEnvFile(envPath, {
|
|
243
|
+
CONDUIT_WORKSPACE: workspace,
|
|
244
|
+
...(repo ? { CONDUIT_REPO: repo } : {}),
|
|
245
|
+
});
|
|
246
|
+
// Local apply owns the checkout; clear queued intent so heartbeat does not re-apply.
|
|
247
|
+
try {
|
|
248
|
+
await client.request("/runner/v1/machine/ack-on-shift-intent", { method: "POST", body: "{}" });
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// Older control planes omit ack — local ops.env still wins.
|
|
252
|
+
}
|
|
253
|
+
console.log(`Updated ${envPath}`);
|
|
254
|
+
console.log(`On-shift workspace: ${workspace}${repo ? ` (${repo})` : ""}`);
|
|
255
|
+
// Same install path as ops install — restart runner against the new checkout.
|
|
256
|
+
const switched = loadOpsEnv();
|
|
257
|
+
await runOps("install", [], {
|
|
258
|
+
...deps,
|
|
259
|
+
env: {
|
|
260
|
+
...switched,
|
|
261
|
+
CONDUIT_WORKSPACE: workspace,
|
|
262
|
+
CONDUIT_REPO: repo || switched.CONDUIT_REPO,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
178
267
|
if (verb === "connect") {
|
|
179
268
|
if (!env.CONDUIT_URL)
|
|
180
269
|
throw new Error(`Set CONDUIT_URL in ${defaultOpsEnvPath()}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.10",
|
|
4
4
|
"description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity agents for a Conduit organization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|