@miraland-labs/conduit-bridge 0.9.0 → 0.9.2
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/README.md +47 -42
- package/dist/cli.js +120 -10
- package/dist/config.js +11 -0
- package/dist/driver.js +11 -1
- package/dist/execution.js +40 -9
- package/dist/ops.js +199 -0
- package/dist/service.js +25 -2
- package/ops/conduit-ops +10 -0
- package/ops/conduit-ops.cmd +8 -0
- package/ops/connect.cmd +4 -0
- package/ops/connect.sh +4 -0
- package/ops/disconnect.cmd +4 -0
- package/ops/disconnect.sh +4 -0
- package/ops/env.example +13 -0
- package/ops/install.cmd +4 -0
- package/ops/install.sh +4 -0
- package/ops/offline.cmd +4 -0
- package/ops/offline.sh +4 -0
- package/ops/online.cmd +4 -0
- package/ops/online.sh +4 -0
- package/ops/status.cmd +4 -0
- package/ops/status.sh +4 -0
- package/ops/uninstall.cmd +4 -0
- package/ops/uninstall.sh +4 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,71 +2,76 @@
|
|
|
2
2
|
|
|
3
3
|
Local Bridge CLI for [Conduit](https://github.com/miralandlabs/conduit). Connects a computer to one organization, claims work, and drives a local agent.
|
|
4
4
|
|
|
5
|
-
**Current npm:** `0.
|
|
5
|
+
**Current npm:** `0.9.2` — **ops** helpers for macOS / Linux / Windows, LaunchAgent/systemd **PATH** for `~/.local/bin`, disconnect/disengage, multi-driver lanes, shared slots **1–4**, worktrees, optional `--ensure-checkout`.
|
|
6
6
|
|
|
7
7
|
## Prerequisites
|
|
8
8
|
|
|
9
9
|
- Node.js 20+
|
|
10
|
-
- One or more supported agent CLIs
|
|
11
|
-
- **Claude Code** — `claude`
|
|
12
|
-
- **Codex** — `codex`
|
|
13
|
-
- **OpenCode** — `opencode`
|
|
14
|
-
- **Cursor Agent** — `agent` (local fuel
|
|
10
|
+
- One or more supported agent CLIs:
|
|
11
|
+
- **Claude Code** — `claude`
|
|
12
|
+
- **Codex** — `codex`
|
|
13
|
+
- **OpenCode** — `opencode`
|
|
14
|
+
- **Cursor Agent** — `agent` (local fuel)
|
|
15
15
|
- **Kiro CLI** — `kiro-cli` (local fuel)
|
|
16
16
|
- **Antigravity** — `agy` (local fuel)
|
|
17
|
-
- macOS or Linux for `install-service` (Windows: keep a terminal runner open)
|
|
18
17
|
|
|
19
|
-
##
|
|
18
|
+
## Easy path (all platforms)
|
|
19
|
+
|
|
20
|
+
One config file + the same `ops` commands on macOS, Linux, and Windows:
|
|
20
21
|
|
|
21
22
|
```bash
|
|
22
|
-
|
|
23
|
+
# 1) Config once
|
|
24
|
+
mkdir -p ~/.config/conduit
|
|
25
|
+
# Windows: mkdir %USERPROFILE%\.config\conduit
|
|
26
|
+
npx @miraland-labs/conduit-bridge@latest init-ops
|
|
27
|
+
cp ~/conduit/ops/env.example ~/.config/conduit/ops.env
|
|
28
|
+
# edit ops.env (URL, WORKSPACE, optional REPO, DRIVERS, ROLES)
|
|
29
|
+
|
|
30
|
+
# 2) Connect → approve in browser → install
|
|
31
|
+
npx @miraland-labs/conduit-bridge@latest ops connect
|
|
32
|
+
npx @miraland-labs/conduit-bridge@latest ops install
|
|
33
|
+
npx @miraland-labs/conduit-bridge@latest ops status
|
|
23
34
|
```
|
|
24
35
|
|
|
25
|
-
|
|
36
|
+
Later:
|
|
26
37
|
|
|
27
38
|
```bash
|
|
28
|
-
npx @miraland-labs/conduit-bridge
|
|
29
|
-
npx @miraland-labs/conduit-bridge
|
|
30
|
-
npx @miraland-labs/conduit-bridge
|
|
31
|
-
npx @miraland-labs/conduit-bridge drivers offline claude-code # finish in-flight; no new claims
|
|
32
|
-
npx @miraland-labs/conduit-bridge runner --workspace /path/to/repo
|
|
39
|
+
npx @miraland-labs/conduit-bridge@latest ops online cursor
|
|
40
|
+
npx @miraland-labs/conduit-bridge@latest ops offline cursor
|
|
41
|
+
npx @miraland-labs/conduit-bridge@latest ops disconnect
|
|
33
42
|
```
|
|
34
43
|
|
|
35
|
-
|
|
44
|
+
Optional local shortcuts after `init-ops` (same verbs):
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
| Platform | Examples |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| macOS / Linux | `~/conduit/ops/connect.sh`, `install.sh`, … |
|
|
49
|
+
| Windows | `%USERPROFILE%\conduit\ops\connect.cmd`, `install.cmd`, … |
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
**Windows note:** `ops install` brings lanes online and prints a `runner --workspace …` command to keep open (no LaunchAgent). macOS/Linux install a background service.
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
npx @miraland-labs/conduit-bridge install-service --workspace /path/to/repo
|
|
45
|
-
# optional: bring a lane online at install time
|
|
46
|
-
npx @miraland-labs/conduit-bridge install-service --agent claude-code --workspace /path/to/repo
|
|
47
|
-
```
|
|
53
|
+
`ops.env` keys: `CONDUIT_URL`, `CONDUIT_ORG` (optional), `CONDUIT_WORKSPACE`, `CONDUIT_REPO` (optional), `CONDUIT_DRIVERS` (default `cursor`), `CONDUIT_ROLES` (default `implement research`). Values expand `$HOME`, `%USERPROFILE%`, and `~`.
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
## Advanced CLI
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
```bash
|
|
58
|
+
npx @miraland-labs/conduit-bridge join --url <https://your-conduit> --organization <slug>
|
|
59
|
+
npx @miraland-labs/conduit-bridge drivers online cursor
|
|
60
|
+
npx @miraland-labs/conduit-bridge install-service --workspace /path/to/repo --ensure-checkout https://github.com/org/repo.git
|
|
61
|
+
npx @miraland-labs/conduit-bridge disconnect --yes
|
|
62
|
+
```
|
|
52
63
|
|
|
53
|
-
**Capacity:** Connect-approved `lease_capacity` (1–4) is a **shared pool** for all online lanes on that computer.
|
|
64
|
+
**Capacity:** Connect-approved `lease_capacity` (1–4) is a **shared pool** for all online lanes on that computer.
|
|
54
65
|
|
|
55
66
|
## Grant enforcement
|
|
56
67
|
|
|
57
|
-
Every driver enforces the assignment's granted actions with its CLI's native mechanism — never just the prompt:
|
|
58
|
-
|
|
59
68
|
| Driver | Fuel | Mechanism |
|
|
60
69
|
| --- | --- | --- |
|
|
61
|
-
| `claude-code` | pump or local | `--allowedTools` / `--disallowedTools` per grant
|
|
62
|
-
| `codex` | pump or local | sandbox `read-only` / `workspace-write
|
|
63
|
-
| `opencode` | pump or local |
|
|
64
|
-
| `cursor` | local only | per-run `.cursor/cli.json`
|
|
65
|
-
| `kiro` | local only | `--trust-tools
|
|
66
|
-
| `antigravity` | local only | `-p --mode plan`
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
**Verification status:**
|
|
71
|
-
- `claude-code`, `codex`, `cursor`, `opencode`, `antigravity` — verified end-to-end against their real CLIs (agy 1.1.3: `-p --mode` args, Google login, plain-text output confirmed through the driver).
|
|
72
|
-
- `kiro` — flag surface and login preflight verified against kiro-cli 2.13.0 (`chat --no-interactive --trust-tools --resume-id`; tool names; `whoami`-based login check that avoids the browser-login hang). A full run is **pending a `KIRO_API_KEY`** — kiro-cli requires auth to execute and cannot be exercised offline.
|
|
70
|
+
| `claude-code` | pump or local | `--allowedTools` / `--disallowedTools` per grant |
|
|
71
|
+
| `codex` | pump or local | sandbox `read-only` / `workspace-write` |
|
|
72
|
+
| `opencode` | pump or local | per-run `opencode.json` bash deny list |
|
|
73
|
+
| `cursor` | local only | per-run `.cursor/cli.json` permissions |
|
|
74
|
+
| `kiro` | local only | `--trust-tools` allowlist |
|
|
75
|
+
| `antigravity` | local only | `-p --mode plan` / `accept-edits` |
|
|
76
|
+
|
|
77
|
+
Cursor/Kiro/Antigravity require local fuel (`ops install` sets this for you).
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import { resolve, dirname, join as pathJoin } from "node:path";
|
|
4
|
-
import { hostname, userInfo } from "node:os";
|
|
4
|
+
import { hostname, homedir, platform, userInfo } from "node:os";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import {
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
8
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
7
9
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { ConduitClient } from "./client.js";
|
|
9
|
-
import { BRIDGE_LEASE_CAPACITY, BRIDGE_MAX_LEASE_CAPACITY, clampLeaseCapacity, clearPendingConnection, loadConfig, loadConfigIfPresent, loadOrCreateInstallationId, loadPendingConnection, redactSecrets, saveConfig, saveConfigPrefs, savePendingConnection, suggestMachineName, } from "./config.js";
|
|
10
|
+
import { ConduitClient, ConduitRequestError } from "./client.js";
|
|
11
|
+
import { BRIDGE_LEASE_CAPACITY, BRIDGE_MAX_LEASE_CAPACITY, clampLeaseCapacity, clearLocalConnection, clearPendingConnection, loadConfig, loadConfigIfPresent, loadOrCreateInstallationId, loadPendingConnection, redactSecrets, saveConfig, saveConfigPrefs, savePendingConnection, suggestMachineName, } from "./config.js";
|
|
10
12
|
import { runMcp } from "./mcp.js";
|
|
11
13
|
import { detectInstalledClients, localFuelOnlyClients, suggestFuelSource } from "./detect.js";
|
|
12
14
|
import { DRIVERS } from "./driver.js";
|
|
@@ -15,6 +17,7 @@ import { buildWorkspaceBrief } from "./brief.js";
|
|
|
15
17
|
import { ensureCheckout } from "./checkout.js";
|
|
16
18
|
import { pumpExecutionSlots, renewLeases } from "./execution.js";
|
|
17
19
|
import { installRunnerService, uninstallRunnerService } from "./service.js";
|
|
20
|
+
import { OPS_VERBS, runOps } from "./ops.js";
|
|
18
21
|
const [command] = process.argv.slice(2);
|
|
19
22
|
/** Read our own package version so every runner start logs exactly which build is live. */
|
|
20
23
|
function bridgeVersion() {
|
|
@@ -54,6 +57,27 @@ async function connect() {
|
|
|
54
57
|
throw new Error(data.error?.message ?? `Connect failed (${response.status})`);
|
|
55
58
|
await finishConnection(baseUrl, data, parseFuelSource(values.fuel));
|
|
56
59
|
}
|
|
60
|
+
async function promptLine(question) {
|
|
61
|
+
if (!input.isTTY || !output.isTTY) {
|
|
62
|
+
throw new Error("Interactive input is unavailable; pass the value on the command line");
|
|
63
|
+
}
|
|
64
|
+
const rl = createInterface({ input, output });
|
|
65
|
+
try {
|
|
66
|
+
return (await rl.question(question)).trim();
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
rl.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function resolveJoinOrganization(explicit) {
|
|
73
|
+
const raw = explicit?.trim() || await promptLine("Join which org? ");
|
|
74
|
+
const organization = raw.toLowerCase();
|
|
75
|
+
if (!organization)
|
|
76
|
+
throw new Error("Organization slug is required");
|
|
77
|
+
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(organization))
|
|
78
|
+
throw new Error("Organization must be its lowercase workspace slug");
|
|
79
|
+
return organization;
|
|
80
|
+
}
|
|
57
81
|
async function join() {
|
|
58
82
|
const { values } = parseArgs({ args: process.argv.slice(3), options: {
|
|
59
83
|
url: { type: "string" }, resume: { type: "boolean" }, machine: { type: "string" }, operator: { type: "string" },
|
|
@@ -75,13 +99,11 @@ async function join() {
|
|
|
75
99
|
if (pending && !values.resume)
|
|
76
100
|
console.log(`Resuming the pending connection request for ${pending.baseUrl}.`);
|
|
77
101
|
if (!pending) {
|
|
78
|
-
if (!values.url
|
|
79
|
-
throw new Error(`Usage: ${bridgeUsage("join", "--url", "<worker-url>", "--organization
|
|
102
|
+
if (!values.url) {
|
|
103
|
+
throw new Error(`Usage: ${bridgeUsage("join", "--url", "<worker-url>", "[--organization <slug>]", "[--machine <name>]", "[--capability <role>]", "[--fuel conduit|local]")}`);
|
|
80
104
|
}
|
|
81
105
|
const baseUrl = normalizeBaseUrl(values.url);
|
|
82
|
-
const organization = values.organization
|
|
83
|
-
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(organization))
|
|
84
|
-
throw new Error("Organization must be its lowercase workspace slug");
|
|
106
|
+
const organization = await resolveJoinOrganization(values.organization);
|
|
85
107
|
// A config that fails validation (for example pre-organization) must not
|
|
86
108
|
// block rejoining — join replaces it with a complete identity.
|
|
87
109
|
const connected = await loadConfigIfPresent().catch(() => null);
|
|
@@ -296,6 +318,56 @@ async function uninstallService() {
|
|
|
296
318
|
const result = await uninstallRunnerService();
|
|
297
319
|
console.log(`Removed Conduit runner service (${result.platform}): ${result.path}`);
|
|
298
320
|
}
|
|
321
|
+
/** Copy cross-platform ops launchers to ~/conduit/ops (default). Env stays in ~/.config. */
|
|
322
|
+
async function initOps() {
|
|
323
|
+
const targetArg = process.argv[3];
|
|
324
|
+
const target = resolve(targetArg ?? pathJoin(homedir(), "conduit", "ops"));
|
|
325
|
+
const packageRoot = pathJoin(dirname(fileURLToPath(import.meta.url)), "..");
|
|
326
|
+
const source = pathJoin(packageRoot, "ops");
|
|
327
|
+
if (!existsSync(source)) {
|
|
328
|
+
throw new Error(`Ops kit missing from package at ${source}. Reinstall @miraland-labs/conduit-bridge.`);
|
|
329
|
+
}
|
|
330
|
+
mkdirSync(target, { recursive: true });
|
|
331
|
+
for (const name of readdirSync(source)) {
|
|
332
|
+
cpSync(pathJoin(source, name), pathJoin(target, name));
|
|
333
|
+
if (name.endsWith(".sh") || name === "conduit-ops")
|
|
334
|
+
chmodSync(pathJoin(target, name), 0o755);
|
|
335
|
+
}
|
|
336
|
+
const configDir = pathJoin(homedir(), ".config", "conduit");
|
|
337
|
+
const envPath = pathJoin(configDir, "ops.env");
|
|
338
|
+
const host = platform();
|
|
339
|
+
console.log(`Ops launchers installed at ${target}`);
|
|
340
|
+
console.log(`Config file (separate): ${envPath}`);
|
|
341
|
+
console.log("");
|
|
342
|
+
console.log("Works the same on macOS, Linux, and Windows via:");
|
|
343
|
+
console.log(` ${bridgeUsage("ops", "<connect|install|online|offline|status|disconnect|uninstall>")}`);
|
|
344
|
+
console.log("");
|
|
345
|
+
console.log("1. Edit env once:");
|
|
346
|
+
console.log(` Create folder: ${configDir}`);
|
|
347
|
+
console.log(` Copy ${pathJoin(target, "env.example")} → ${envPath}`);
|
|
348
|
+
console.log(` # edit ${envPath}`);
|
|
349
|
+
console.log("2. Connect (approve in browser):");
|
|
350
|
+
if (host === "win32") {
|
|
351
|
+
console.log(` ${pathJoin(target, "connect.cmd")}`);
|
|
352
|
+
console.log(" # or: npx @miraland-labs/conduit-bridge@latest ops connect");
|
|
353
|
+
console.log("3. Install / start runner:");
|
|
354
|
+
console.log(` ${pathJoin(target, "install.cmd")}`);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
console.log(` ${pathJoin(target, "connect.sh")}`);
|
|
358
|
+
console.log(" # or: npx @miraland-labs/conduit-bridge@latest ops connect");
|
|
359
|
+
console.log("3. Install runner:");
|
|
360
|
+
console.log(` ${pathJoin(target, "install.sh")}`);
|
|
361
|
+
}
|
|
362
|
+
console.log("Later: status · online · offline · disconnect (same names .sh / .cmd)");
|
|
363
|
+
}
|
|
364
|
+
async function opsCommand() {
|
|
365
|
+
const verb = process.argv[3];
|
|
366
|
+
if (!verb || !OPS_VERBS.includes(verb)) {
|
|
367
|
+
throw new Error(`Usage: ${bridgeUsage("ops", "<connect|install|online|offline|status|disconnect|uninstall>", "[driver…]")}`);
|
|
368
|
+
}
|
|
369
|
+
await runOps(verb, process.argv.slice(4));
|
|
370
|
+
}
|
|
299
371
|
async function fuelCommand() {
|
|
300
372
|
const mode = process.argv[3];
|
|
301
373
|
if (mode !== "local" && mode !== "conduit") {
|
|
@@ -448,11 +520,45 @@ async function heartbeat(client, config, brief, processOnlineIds) {
|
|
|
448
520
|
...(brief ? { workspace_brief: brief } : {}),
|
|
449
521
|
}) });
|
|
450
522
|
}
|
|
523
|
+
async function disconnect() {
|
|
524
|
+
const { values } = parseArgs({ args: process.argv.slice(3), options: { yes: { type: "boolean", short: "y" } } });
|
|
525
|
+
const config = await loadConfigIfPresent().catch(() => null);
|
|
526
|
+
if (!config) {
|
|
527
|
+
await clearLocalConnection();
|
|
528
|
+
console.log("This Bridge is not connected to an organization.");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (!values.yes) {
|
|
532
|
+
const answer = (await promptLine(`Disengage this computer from the organization and clear local credentials? [y/N] `)).toLowerCase();
|
|
533
|
+
if (answer !== "y" && answer !== "yes") {
|
|
534
|
+
console.log("Cancelled.");
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
await new ConduitClient(config).request("/runner/v1/disengage", { method: "POST", body: "{}" });
|
|
540
|
+
console.log("Conduit disengaged this computer from the organization.");
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
// Local clear still proceeds when the server already revoked the key (admin disengage).
|
|
544
|
+
if (error instanceof ConduitRequestError && (error.status === 401 || error.status === 404)) {
|
|
545
|
+
console.warn(`Server disengage skipped (${error.status}): ${redactSecrets(error.message)}`);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
await clearLocalConnection();
|
|
552
|
+
console.log("Local Bridge credentials cleared. Run join to connect again (same or another org).");
|
|
553
|
+
console.log(`Tip: stop a background runner with \`${bridgeUsage("uninstall-service")}\` if one is installed.`);
|
|
554
|
+
}
|
|
451
555
|
try {
|
|
452
556
|
if (command === "connect")
|
|
453
557
|
await connect();
|
|
454
558
|
else if (command === "join")
|
|
455
559
|
await join();
|
|
560
|
+
else if (command === "disconnect")
|
|
561
|
+
await disconnect();
|
|
456
562
|
else if (command === "fuel")
|
|
457
563
|
await fuelCommand();
|
|
458
564
|
else if (command === "drivers")
|
|
@@ -465,8 +571,12 @@ try {
|
|
|
465
571
|
await installService();
|
|
466
572
|
else if (command === "uninstall-service")
|
|
467
573
|
await uninstallService();
|
|
574
|
+
else if (command === "init-ops")
|
|
575
|
+
await initOps();
|
|
576
|
+
else if (command === "ops")
|
|
577
|
+
await opsCommand();
|
|
468
578
|
else
|
|
469
|
-
throw new Error(`Usage: ${BRIDGE_NPX} <join|connect|fuel|drivers|mcp|runner|install-service|uninstall-service>`);
|
|
579
|
+
throw new Error(`Usage: ${BRIDGE_NPX} <join|disconnect|connect|fuel|drivers|mcp|runner|install-service|uninstall-service|init-ops|ops>`);
|
|
470
580
|
}
|
|
471
581
|
catch (error) {
|
|
472
582
|
console.error(error instanceof Error ? redactSecrets(error.message) : "Conduit command failed");
|
package/dist/config.js
CHANGED
|
@@ -260,6 +260,17 @@ export async function clearPendingConnection() {
|
|
|
260
260
|
throw error;
|
|
261
261
|
});
|
|
262
262
|
}
|
|
263
|
+
/** Drop local org credentials after a server-side disengage. Keeps installation.json. */
|
|
264
|
+
export async function clearLocalConnection() {
|
|
265
|
+
await withConfigLock(async () => {
|
|
266
|
+
for (const target of [path, runtimePath, pendingPath]) {
|
|
267
|
+
await unlink(target).catch((error) => {
|
|
268
|
+
if (error.code !== "ENOENT")
|
|
269
|
+
throw error;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
263
274
|
export async function loadOrCreateInstallationId() {
|
|
264
275
|
try {
|
|
265
276
|
const identity = JSON.parse(await readFile(installationPath, "utf8"));
|
package/dist/driver.js
CHANGED
|
@@ -3,6 +3,15 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
function deliveryLanguageRule(language) {
|
|
7
|
+
if (language === "zh") {
|
|
8
|
+
return "- Write every human-facing delivery packet string (outcome, changes, verification, assumptions, risks, limitations, criterion text) in Chinese (Simplified). Keep JSON keys and enum token values in English.";
|
|
9
|
+
}
|
|
10
|
+
if (language === "en") {
|
|
11
|
+
return "- Write every human-facing delivery packet string (outcome, changes, verification, assumptions, risks, limitations, criterion text) in English. Keep JSON keys and enum token values in English.";
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
6
15
|
export const evidenceKinds = ["change", "test", "preview", "research", "documentation"];
|
|
7
16
|
export const agentReportTemplate = '{"outcome": "one-paragraph summary", "changes": ["path — what changed"], "verification": ["command — result"], "acceptance_results": [{"criterion": "exact criterion text", "status": "met|not_met|unknown"}], "evidence": [{"kind": "change|test|preview|research|documentation", "name": "concise evidence name", "uri": "external URL if one exists", "digest": "optional digest", "details": ["observable result"], "acceptance_criteria": ["exact criterion text supported by this evidence"]}], "assumptions": [], "risks": [], "limitations": [], "head_commit": "full sha of your final commit, omit if none"}';
|
|
8
17
|
/**
|
|
@@ -131,7 +140,8 @@ export function buildAssignmentPrompt(context) {
|
|
|
131
140
|
...(context.grants.includes("branch_create") ? branchCreateCommands : []),
|
|
132
141
|
...(context.grants.includes("pr_create") ? prCreateCommands : []),
|
|
133
142
|
];
|
|
134
|
-
|
|
143
|
+
const languageRule = deliveryLanguageRule(packageContext?.working_language ?? spec.working_language);
|
|
144
|
+
lines.push("", "RULES", "- Stay within the change scope and boundaries.", ...(languageRule ? [languageRule] : []),
|
|
135
145
|
// Name the executable commands. The shell allow-list is exact, so an agent that guesses at
|
|
136
146
|
// `bash scripts/validate-pack.sh`, `python3 ...` or `echo` gets every call rejected and concludes
|
|
137
147
|
// it has no shell at all — observed twice on live Daily Signal runs, both times while the
|
package/dist/execution.js
CHANGED
|
@@ -44,6 +44,7 @@ const taskSpecSchema = z.object({
|
|
|
44
44
|
change_scope: z.array(z.string()).optional(), work_role: z.string().optional(),
|
|
45
45
|
repository: z.object({ url: z.string().optional(), base_commit: z.string().optional() }).nullable().optional(),
|
|
46
46
|
risk_level: z.string().optional(),
|
|
47
|
+
deliverable: z.enum(["repository", "artifact"]).optional().default("repository"),
|
|
47
48
|
});
|
|
48
49
|
const executionContractSchema = z.object({
|
|
49
50
|
repository_fingerprint: z.string().nullable().optional().default(null),
|
|
@@ -58,6 +59,7 @@ const workPackageSchema = z.object({
|
|
|
58
59
|
acceptance: z.array(z.string()).optional(),
|
|
59
60
|
change_scope: z.array(z.string()).optional(),
|
|
60
61
|
required_evidence: z.array(z.string()).optional(),
|
|
62
|
+
deliverable: z.enum(["repository", "artifact"]).optional().default("repository"),
|
|
61
63
|
initiative: z.object({
|
|
62
64
|
title: z.string().nullable().optional(),
|
|
63
65
|
desired_outcome: z.string().nullable().optional(),
|
|
@@ -65,6 +67,7 @@ const workPackageSchema = z.object({
|
|
|
65
67
|
}).optional(),
|
|
66
68
|
decisions: z.array(z.object({ question: z.string(), decision: z.string() })).optional(),
|
|
67
69
|
rework_feedback: z.string().nullable().optional(),
|
|
70
|
+
working_language: z.enum(["en", "zh"]).optional(),
|
|
68
71
|
}).nullable().optional();
|
|
69
72
|
export async function renewLeases(client, config) {
|
|
70
73
|
for (const active of Object.values(config.activeAttempts)) {
|
|
@@ -272,7 +275,10 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
272
275
|
const task = taskDetailSchema.parse(detail.task);
|
|
273
276
|
const executionContract = executionContractSchema.parse(detail.execution_contract ?? {});
|
|
274
277
|
const workPackage = workPackageSchema.parse(detail.work_package) ?? null;
|
|
275
|
-
const
|
|
278
|
+
const parsedSpec = parseTaskSpec(task.spec_json);
|
|
279
|
+
const deliverable = workPackage?.deliverable ?? parsedSpec.deliverable;
|
|
280
|
+
const spec = { ...parsedSpec, deliverable };
|
|
281
|
+
const artifactDelivery = deliverable === "artifact";
|
|
276
282
|
const grants = z.array(z.string()).parse(task.grants_json ? JSON.parse(task.grants_json) : []);
|
|
277
283
|
const reworkFeedback = task.delivery_state === "changes_requested" ? changesRequestedFeedback(task.delivery_summary) : null;
|
|
278
284
|
// Recompile current state at claim time — earlier packages may have moved the repo.
|
|
@@ -309,6 +315,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
309
315
|
}
|
|
310
316
|
}
|
|
311
317
|
let attemptWorkspace;
|
|
318
|
+
let deliverySubmitted = false;
|
|
312
319
|
if (options.existingWorktree) {
|
|
313
320
|
attemptWorkspace = options.existingWorktree;
|
|
314
321
|
}
|
|
@@ -486,20 +493,26 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
486
493
|
}
|
|
487
494
|
await client.updateAttempt(taskId, { phase: "agent_finished", delivery: { spec, report } });
|
|
488
495
|
await submitFinishedDelivery(client, taskId);
|
|
496
|
+
deliverySubmitted = true;
|
|
489
497
|
console.log(`Assignment ${taskId} delivered for review and acceptance.`);
|
|
490
498
|
}
|
|
491
499
|
finally {
|
|
492
500
|
clearInterval(renewTimer);
|
|
493
501
|
if (heartbeatTimer)
|
|
494
502
|
clearInterval(heartbeatTimer);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
await
|
|
503
|
+
if (artifactDelivery && !deliverySubmitted) {
|
|
504
|
+
console.error(`Artifact delivery was not submitted; retained generated files at ${attemptWorkspace}`);
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
// F-07: dispose the attempt worktree so the next claim cannot inherit edits.
|
|
508
|
+
// Terminal submit may already have cleared activeAttempts — never throw from cleanup.
|
|
509
|
+
const path = config.activeAttempts[taskId]?.worktreePath ?? attemptWorkspace;
|
|
510
|
+
await removeAttemptWorktree(workspace, path).catch((error) => {
|
|
511
|
+
console.error(`Attempt worktree cleanup failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`);
|
|
512
|
+
});
|
|
513
|
+
if (config.activeAttempts[taskId]) {
|
|
514
|
+
await client.updateAttempt(taskId, { worktreePath: undefined }).catch(() => undefined);
|
|
515
|
+
}
|
|
503
516
|
}
|
|
504
517
|
}
|
|
505
518
|
}
|
|
@@ -658,6 +671,24 @@ async function prepareDelivery(client, attemptId, taskId, report) {
|
|
|
658
671
|
}
|
|
659
672
|
export function validateDeliveryReport(report, spec, grants = []) {
|
|
660
673
|
validateChangeScope(report, spec.change_scope ?? []);
|
|
674
|
+
if (spec.deliverable === "artifact") {
|
|
675
|
+
const published = report.evidence.some((item) => {
|
|
676
|
+
if (!["preview", "research", "documentation"].includes(item.kind))
|
|
677
|
+
return false;
|
|
678
|
+
if (!item.uri || !item.digest || !/^sha256:[0-9a-f]{64}$/i.test(item.digest))
|
|
679
|
+
return false;
|
|
680
|
+
try {
|
|
681
|
+
const protocol = new URL(item.uri).protocol;
|
|
682
|
+
return protocol === "https:" || protocol === "http:";
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
if (!published) {
|
|
689
|
+
throw new Error("Artifact delivery requires a published HTTP(S) URL and sha256 digest");
|
|
690
|
+
}
|
|
691
|
+
}
|
|
661
692
|
// Invariant 9: grants bound what an attempt may do. Cursor/plan-mode are soft hints the headless
|
|
662
693
|
// agent can leave, so repository-write authority is enforced here on the reported delivery: a task
|
|
663
694
|
// without repo_write must not report repository changes.
|
package/dist/ops.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform operator helpers (macOS / Linux / Windows).
|
|
3
|
+
* Invoked as: npx @miraland-labs/conduit-bridge ops <connect|install|…>
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { homedir, platform } from "node:os";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
export const OPS_VERBS = [
|
|
10
|
+
"connect", "install", "online", "offline", "status", "disconnect", "uninstall",
|
|
11
|
+
];
|
|
12
|
+
const LOCAL_FUEL_DRIVERS = new Set(["cursor", "kiro", "antigravity"]);
|
|
13
|
+
export function defaultOpsEnvPath(home = homedir()) {
|
|
14
|
+
return join(home, ".config", "conduit", "ops.env");
|
|
15
|
+
}
|
|
16
|
+
/** Expand $HOME, %USERPROFILE%, and leading ~ for cross-platform env values. */
|
|
17
|
+
export function expandOpsValue(value, home = homedir()) {
|
|
18
|
+
let next = value.trim();
|
|
19
|
+
if ((next.startsWith('"') && next.endsWith('"')) || (next.startsWith("'") && next.endsWith("'"))) {
|
|
20
|
+
next = next.slice(1, -1);
|
|
21
|
+
}
|
|
22
|
+
next = next.replace(/%USERPROFILE%/gi, home).replace(/%HOME%/gi, home);
|
|
23
|
+
next = next.replace(/\$HOME\b/g, home).replace(/\$\{HOME\}/g, home);
|
|
24
|
+
if (next.startsWith("~/") || next === "~")
|
|
25
|
+
next = join(home, next.slice(2));
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
export function parseOpsEnvFile(text, home = homedir()) {
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
31
|
+
const line = raw.trim();
|
|
32
|
+
if (!line || line.startsWith("#"))
|
|
33
|
+
continue;
|
|
34
|
+
const eq = line.indexOf("=");
|
|
35
|
+
if (eq <= 0)
|
|
36
|
+
continue;
|
|
37
|
+
const key = line.slice(0, eq).trim();
|
|
38
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
39
|
+
continue;
|
|
40
|
+
out[key] = expandOpsValue(line.slice(eq + 1), home);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
export function loadOpsEnv(home = homedir(), cwd = process.cwd()) {
|
|
45
|
+
const candidates = [
|
|
46
|
+
process.env.CONDUIT_OPS_ENV,
|
|
47
|
+
join(cwd, ".conduit-ops.env"),
|
|
48
|
+
defaultOpsEnvPath(home),
|
|
49
|
+
].filter((path) => Boolean(path));
|
|
50
|
+
let loadedFrom = null;
|
|
51
|
+
let fileValues = {};
|
|
52
|
+
for (const path of candidates) {
|
|
53
|
+
if (!existsSync(path))
|
|
54
|
+
continue;
|
|
55
|
+
fileValues = parseOpsEnvFile(readFileSync(path, "utf8"), home);
|
|
56
|
+
loadedFrom = path;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
const pick = (key, fallback) => {
|
|
60
|
+
const fromEnv = process.env[key];
|
|
61
|
+
if (fromEnv !== undefined && fromEnv !== "")
|
|
62
|
+
return expandOpsValue(fromEnv, home);
|
|
63
|
+
if (fileValues[key])
|
|
64
|
+
return fileValues[key];
|
|
65
|
+
return fallback;
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
CONDUIT_URL: pick("CONDUIT_URL", ""),
|
|
69
|
+
CONDUIT_ORG: pick("CONDUIT_ORG", ""),
|
|
70
|
+
CONDUIT_WORKSPACE: pick("CONDUIT_WORKSPACE", ""),
|
|
71
|
+
CONDUIT_REPO: pick("CONDUIT_REPO", ""),
|
|
72
|
+
CONDUIT_DRIVERS: pick("CONDUIT_DRIVERS", "cursor"),
|
|
73
|
+
CONDUIT_ROLES: pick("CONDUIT_ROLES", "implement research"),
|
|
74
|
+
loadedFrom,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function requireOpsEnv(env) {
|
|
78
|
+
if (env.loadedFrom || env.CONDUIT_URL || env.CONDUIT_WORKSPACE)
|
|
79
|
+
return;
|
|
80
|
+
const path = defaultOpsEnvPath();
|
|
81
|
+
throw new Error(`Missing ops env. Create it once:\n` +
|
|
82
|
+
` Create folder: ${join(homedir(), ".config", "conduit")}\n` +
|
|
83
|
+
` Copy env.example to: ${path}\n` +
|
|
84
|
+
` Edit that file, then retry.`);
|
|
85
|
+
}
|
|
86
|
+
export function splitOpsList(value) {
|
|
87
|
+
return value.split(/[,\s]+/).map((item) => item.trim()).filter(Boolean);
|
|
88
|
+
}
|
|
89
|
+
export function resolveDrivers(env, argv) {
|
|
90
|
+
const fromArgs = argv.map((item) => item.trim()).filter(Boolean);
|
|
91
|
+
const drivers = fromArgs.length ? fromArgs : splitOpsList(env.CONDUIT_DRIVERS);
|
|
92
|
+
if (!drivers.length)
|
|
93
|
+
throw new Error("No drivers. Set CONDUIT_DRIVERS or pass ids.");
|
|
94
|
+
return drivers;
|
|
95
|
+
}
|
|
96
|
+
/** Quote args for a copy-pasteable shell/cmd line (paths with spaces). */
|
|
97
|
+
export function shellQuoteArgs(args) {
|
|
98
|
+
return args.map((arg) => {
|
|
99
|
+
if (!/[^\w@%+=:,./\\-]/i.test(arg))
|
|
100
|
+
return arg;
|
|
101
|
+
return `"${arg.replaceAll('"', '\\"')}"`;
|
|
102
|
+
}).join(" ");
|
|
103
|
+
}
|
|
104
|
+
function defaultRunBridge(args, options = {}) {
|
|
105
|
+
// Re-enter this package's CLI in a child so ops stays a thin orchestrator.
|
|
106
|
+
const cli = process.argv[1];
|
|
107
|
+
const node = process.execPath;
|
|
108
|
+
const result = spawnSync(node, [cli, ...args], { stdio: "inherit", env: process.env });
|
|
109
|
+
if (result.error) {
|
|
110
|
+
if (options.allowFail)
|
|
111
|
+
return;
|
|
112
|
+
throw result.error;
|
|
113
|
+
}
|
|
114
|
+
if (result.status !== 0) {
|
|
115
|
+
if (options.allowFail)
|
|
116
|
+
return;
|
|
117
|
+
process.exit(result.status ?? 1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export async function runOps(verb, argv = [], deps = {}) {
|
|
121
|
+
const runBridge = deps.runBridge ?? defaultRunBridge;
|
|
122
|
+
const host = deps.host ?? platform();
|
|
123
|
+
const env = deps.env ?? loadOpsEnv();
|
|
124
|
+
if (verb === "disconnect") {
|
|
125
|
+
runBridge(["uninstall-service"], { allowFail: true });
|
|
126
|
+
runBridge(["disconnect", "--yes"]);
|
|
127
|
+
console.log("Disconnected. Run: npx @miraland-labs/conduit-bridge@latest ops connect");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (verb === "uninstall") {
|
|
131
|
+
runBridge(["uninstall-service"]);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (verb === "status") {
|
|
135
|
+
if (env.loadedFrom) {
|
|
136
|
+
console.log(`Env: ${env.loadedFrom}`);
|
|
137
|
+
console.log(`URL: ${env.CONDUIT_URL || "(unset)"}`);
|
|
138
|
+
console.log(`Workspace: ${env.CONDUIT_WORKSPACE || "(unset)"}`);
|
|
139
|
+
console.log(`Repo: ${env.CONDUIT_REPO || "(none)"}`);
|
|
140
|
+
console.log(`Drivers: ${env.CONDUIT_DRIVERS}`);
|
|
141
|
+
console.log(`Roles: ${env.CONDUIT_ROLES}`);
|
|
142
|
+
console.log("");
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
console.log("No ops.env loaded (showing drivers only).\n");
|
|
146
|
+
}
|
|
147
|
+
runBridge(["drivers"]);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Lane toggles only need Bridge config + optional driver ids — not a full ops.env.
|
|
151
|
+
if (verb === "online" || verb === "offline") {
|
|
152
|
+
const drivers = resolveDrivers(env, argv);
|
|
153
|
+
runBridge(["drivers", verb, ...drivers]);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
requireOpsEnv(env);
|
|
157
|
+
if (verb === "connect") {
|
|
158
|
+
if (!env.CONDUIT_URL)
|
|
159
|
+
throw new Error(`Set CONDUIT_URL in ${defaultOpsEnvPath()}`);
|
|
160
|
+
const args = ["join", "--url", env.CONDUIT_URL];
|
|
161
|
+
if (env.CONDUIT_ORG)
|
|
162
|
+
args.push("--organization", env.CONDUIT_ORG);
|
|
163
|
+
for (const role of splitOpsList(env.CONDUIT_ROLES))
|
|
164
|
+
args.push("--capability", role);
|
|
165
|
+
console.log(`Connecting to ${env.CONDUIT_URL} (roles: ${env.CONDUIT_ROLES})`);
|
|
166
|
+
console.log("Approve this computer in Connect when the browser opens.");
|
|
167
|
+
runBridge(args);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (verb === "install") {
|
|
171
|
+
if (!env.CONDUIT_WORKSPACE)
|
|
172
|
+
throw new Error(`Set CONDUIT_WORKSPACE in ${defaultOpsEnvPath()}`);
|
|
173
|
+
const workspace = resolve(expandOpsValue(env.CONDUIT_WORKSPACE));
|
|
174
|
+
const drivers = resolveDrivers(env, argv);
|
|
175
|
+
for (const id of drivers) {
|
|
176
|
+
if (LOCAL_FUEL_DRIVERS.has(id))
|
|
177
|
+
runBridge(["drivers", "fuel", id, "local"]);
|
|
178
|
+
}
|
|
179
|
+
runBridge(["drivers", "online", ...drivers]);
|
|
180
|
+
if (host === "win32") {
|
|
181
|
+
const runnerArgs = ["runner", "--workspace", workspace];
|
|
182
|
+
if (env.CONDUIT_REPO)
|
|
183
|
+
runnerArgs.push("--ensure-checkout", env.CONDUIT_REPO);
|
|
184
|
+
console.log("Windows: background install-service is not available.");
|
|
185
|
+
console.log("Keep a terminal open and run:");
|
|
186
|
+
console.log(` npx @miraland-labs/conduit-bridge@latest ${shellQuoteArgs(runnerArgs)}`);
|
|
187
|
+
console.log(`Drivers online: ${drivers.join(", ")}`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const installArgs = ["install-service", "--workspace", workspace];
|
|
191
|
+
if (env.CONDUIT_REPO)
|
|
192
|
+
installArgs.push("--ensure-checkout", env.CONDUIT_REPO);
|
|
193
|
+
console.log(`Installing runner for ${workspace} (drivers: ${drivers.join(", ")})`);
|
|
194
|
+
runBridge(installArgs);
|
|
195
|
+
console.log("Done. Check with: npx @miraland-labs/conduit-bridge@latest ops status");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
throw new Error(`Unknown ops verb: ${verb}`);
|
|
199
|
+
}
|
package/dist/service.js
CHANGED
|
@@ -34,7 +34,24 @@ export function runnerProgramArguments(options = {}) {
|
|
|
34
34
|
args.push("--agent-timeout-minutes", options.agentTimeoutMinutes);
|
|
35
35
|
return args;
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* PATH for LaunchAgent/systemd so agent CLIs in ~/.local/bin and Homebrew resolve
|
|
39
|
+
* without the operator hand-editing the unit.
|
|
40
|
+
*/
|
|
41
|
+
export function runnerServicePath(home = homedir(), nodeBin = dirname(process.execPath)) {
|
|
42
|
+
const parts = [
|
|
43
|
+
nodeBin,
|
|
44
|
+
join(home, ".local", "bin"),
|
|
45
|
+
"/opt/homebrew/bin",
|
|
46
|
+
"/usr/local/bin",
|
|
47
|
+
"/usr/bin",
|
|
48
|
+
"/bin",
|
|
49
|
+
"/usr/sbin",
|
|
50
|
+
"/sbin",
|
|
51
|
+
];
|
|
52
|
+
return [...new Set(parts.filter(Boolean))].join(":");
|
|
53
|
+
}
|
|
54
|
+
export function launchdPlist(programArguments, stdoutPath, pathEnv = runnerServicePath()) {
|
|
38
55
|
const argsXml = programArguments.map((arg) => ` <string>${escapeXml(arg)}</string>`).join("\n");
|
|
39
56
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
40
57
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -46,6 +63,11 @@ export function launchdPlist(programArguments, stdoutPath) {
|
|
|
46
63
|
<array>
|
|
47
64
|
${argsXml}
|
|
48
65
|
</array>
|
|
66
|
+
<key>EnvironmentVariables</key>
|
|
67
|
+
<dict>
|
|
68
|
+
<key>PATH</key>
|
|
69
|
+
<string>${escapeXml(pathEnv)}</string>
|
|
70
|
+
</dict>
|
|
49
71
|
<key>RunAtLoad</key>
|
|
50
72
|
<true/>
|
|
51
73
|
<key>KeepAlive</key>
|
|
@@ -58,7 +80,7 @@ ${argsXml}
|
|
|
58
80
|
</plist>
|
|
59
81
|
`;
|
|
60
82
|
}
|
|
61
|
-
export function systemdUserUnit(programArguments, workingDirectory) {
|
|
83
|
+
export function systemdUserUnit(programArguments, workingDirectory, pathEnv = runnerServicePath()) {
|
|
62
84
|
const execStart = programArguments.map(escapeSystemd).join(" ");
|
|
63
85
|
return `[Unit]
|
|
64
86
|
Description=Conduit Bridge runner
|
|
@@ -68,6 +90,7 @@ Wants=network-online.target
|
|
|
68
90
|
[Service]
|
|
69
91
|
Type=simple
|
|
70
92
|
WorkingDirectory=${workingDirectory}
|
|
93
|
+
Environment=PATH=${escapeSystemd(pathEnv)}
|
|
71
94
|
ExecStart=${execStart}
|
|
72
95
|
Restart=always
|
|
73
96
|
RestartSec=5
|
package/ops/conduit-ops
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Usage: ./conduit-ops <connect|install|online|offline|status|disconnect|uninstall> [args…]
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
if [[ $# -lt 1 ]]; then
|
|
5
|
+
echo "Usage: $0 <connect|install|online|offline|status|disconnect|uninstall> [args…]" >&2
|
|
6
|
+
exit 1
|
|
7
|
+
fi
|
|
8
|
+
verb=$1
|
|
9
|
+
shift
|
|
10
|
+
exec npx --yes @miraland-labs/conduit-bridge@latest ops "$verb" "$@"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM Usage: conduit-ops.cmd <connect|install|online|offline|status|disconnect|uninstall> [args…]
|
|
3
|
+
if "%~1"=="" (
|
|
4
|
+
echo Usage: %~nx0 ^<connect^|install^|online^|offline^|status^|disconnect^|uninstall^> [args…]
|
|
5
|
+
exit /b 1
|
|
6
|
+
)
|
|
7
|
+
npx --yes @miraland-labs/conduit-bridge@latest ops %*
|
|
8
|
+
if errorlevel 1 exit /b %errorlevel%
|
package/ops/connect.cmd
ADDED
package/ops/connect.sh
ADDED
package/ops/env.example
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Configuration (not scripts). Copy to ~/.config/conduit/ops.env and edit once.
|
|
2
|
+
# Same file works on macOS, Linux, and Windows ($HOME / %USERPROFILE% / ~ are expanded).
|
|
3
|
+
#
|
|
4
|
+
# Cross-platform commands (any OS):
|
|
5
|
+
# npx @miraland-labs/conduit-bridge@latest ops connect
|
|
6
|
+
# npx @miraland-labs/conduit-bridge@latest ops install
|
|
7
|
+
# Local shortcuts after init-ops: ~/conduit/ops/*.sh (Mac/Linux) or *.cmd (Windows)
|
|
8
|
+
CONDUIT_URL=https://api.conduit.miraland.io
|
|
9
|
+
# CONDUIT_ORG=your-org-slug
|
|
10
|
+
CONDUIT_WORKSPACE=$HOME/agentic/promote-pr402
|
|
11
|
+
# CONDUIT_REPO=https://github.com/miralandlabs/promote-pr402.git
|
|
12
|
+
CONDUIT_DRIVERS=cursor
|
|
13
|
+
CONDUIT_ROLES=implement research
|
package/ops/install.cmd
ADDED
package/ops/install.sh
ADDED
package/ops/offline.cmd
ADDED
package/ops/offline.sh
ADDED
package/ops/online.cmd
ADDED
package/ops/online.sh
ADDED
package/ops/status.cmd
ADDED
package/ops/status.sh
ADDED
package/ops/uninstall.sh
ADDED
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "Conduit Bridge CLI — join, connect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Kiro / Antigravity agents for a Conduit organization",
|
|
3
|
+
"version": "0.9.2",
|
|
4
|
+
"description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Kiro / Antigravity agents for a Conduit organization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"conduit": "dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
+
"ops",
|
|
11
12
|
"README.md"
|
|
12
13
|
],
|
|
13
14
|
"engines": {
|