@miraland-labs/conduit-bridge 0.11.9 → 0.11.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brief.js +3 -2
- package/dist/cli.js +31 -3
- package/dist/driver.js +3 -0
- package/dist/ensure-test-evidence.js +35 -27
- package/dist/execution.js +7 -3
- package/dist/mcp.js +7 -1
- package/dist/on-shift-apply.js +55 -0
- package/dist/ops.js +12 -1
- package/package.json +1 -1
package/dist/brief.js
CHANGED
|
@@ -8,7 +8,8 @@ const MANIFESTS = [
|
|
|
8
8
|
"Cargo.toml", "pyproject.toml", "pytest.ini", "setup.cfg", "go.mod",
|
|
9
9
|
"Makefile", "build.gradle", "build.gradle.kts", "gradlew",
|
|
10
10
|
];
|
|
11
|
-
|
|
11
|
+
/** Prefer test before typecheck/lint so discovery order matches what pickVerificationCommand wants. */
|
|
12
|
+
const VERIFICATION_SCRIPTS = ["test", "verify", "typecheck", "lint", "build"];
|
|
12
13
|
const EXCLUDED_DIRECTORIES = new Set(["node_modules", "dist", "build", "target", "coverage", ".venv", "venv"]);
|
|
13
14
|
export async function buildWorkspaceBrief(workspace) {
|
|
14
15
|
const entries = await readdir(workspace, { withFileTypes: true });
|
|
@@ -208,7 +209,7 @@ export async function discoverVerificationCommands(workspace, files) {
|
|
|
208
209
|
if (files.has("Makefile")) {
|
|
209
210
|
try {
|
|
210
211
|
const makefile = await readFile(join(workspace, "Makefile"), "utf8");
|
|
211
|
-
for (const target of ["
|
|
212
|
+
for (const target of ["test", "check"])
|
|
212
213
|
if (new RegExp(`^${target}\\s*:`, "m").test(makefile))
|
|
213
214
|
commands.push(`make ${target}`);
|
|
214
215
|
}
|
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. */
|
|
@@ -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" } } });
|
package/dist/driver.js
CHANGED
|
@@ -225,6 +225,9 @@ function sanitizeReportShape(raw) {
|
|
|
225
225
|
// Drop non-URL / empty uris so an optional bad uri doesn't fail the whole report.
|
|
226
226
|
if ("uri" in evidence && (typeof evidence.uri !== "string" || !z.string().url().max(4_000).safeParse(evidence.uri).success))
|
|
227
227
|
delete evidence.uri;
|
|
228
|
+
// Witness is Bridge-authored provenance. An agent claiming it must not survive parse.
|
|
229
|
+
if ("witness" in evidence)
|
|
230
|
+
delete evidence.witness;
|
|
228
231
|
}
|
|
229
232
|
}
|
|
230
233
|
// Same rule for optional top-level scalars: null/"" means absent, not invalid.
|
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* When the contract requires test evidence
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* When the contract requires test evidence, Bridge runs one bounded verification
|
|
3
|
+
* command and attaches the verbatim result. Agent prose may supplement that run;
|
|
4
|
+
* it never substitutes for it.
|
|
5
5
|
*/
|
|
6
6
|
import { execFile } from "node:child_process";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { isBoundedVerificationCommand } from "./execution-class.js";
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
const TEST_EVIDENCE_DETAILS_MIN = 32;
|
|
11
|
-
|
|
11
|
+
/** Contract requires test + test_run — Bridge always runs; agent paste is not a substitute. */
|
|
12
|
+
export function needsTestEvidence(_report, spec, grants) {
|
|
12
13
|
if (!grants.includes("test_run"))
|
|
13
14
|
return false;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
return (spec.required_evidence ?? []).includes("test");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Prefer real test runners over typecheck/lint/build/verify so `required_evidence: ["test"]`
|
|
19
|
+
* is not satisfied by a silent `npm run typecheck`.
|
|
20
|
+
*/
|
|
21
|
+
export function isPreferentialTestCommand(command) {
|
|
22
|
+
return /^(npm run test|pnpm test|yarn test|cargo test|go test(?: \.\/\.\.\.)?|make test|python -m pytest|pytest(?: [\w./=-]+)?|\.\/gradlew test)$/.test(command.trim());
|
|
20
23
|
}
|
|
21
24
|
export function pickVerificationCommand(commands) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
25
|
+
const bounded = commands
|
|
26
|
+
.map((command) => command.trim())
|
|
27
|
+
.filter((command) => command && isBoundedVerificationCommand(command));
|
|
28
|
+
return bounded.find(isPreferentialTestCommand) ?? bounded[0] ?? null;
|
|
28
29
|
}
|
|
29
30
|
function argvForBoundedCommand(command) {
|
|
30
31
|
return command.trim().split(/\s+/);
|
|
@@ -70,8 +71,8 @@ export function criteriaForTestEvidence(report) {
|
|
|
70
71
|
return [...mapped];
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
73
|
-
* If test evidence is required
|
|
74
|
-
*
|
|
74
|
+
* If test evidence is required, run one bounded verification command in the attempt workspace
|
|
75
|
+
* and attach verbatim stdout/stderr with a Bridge witness marker.
|
|
75
76
|
*/
|
|
76
77
|
export async function ensureTestEvidence(input) {
|
|
77
78
|
if (!needsTestEvidence(input.report, input.spec, input.grants))
|
|
@@ -82,26 +83,32 @@ export async function ensureTestEvidence(input) {
|
|
|
82
83
|
}
|
|
83
84
|
const run = input.runCommand ?? defaultRunCommand;
|
|
84
85
|
const result = await run(command, input.workspace);
|
|
85
|
-
// Silent success is valid for bounded targets like `make test` (`@test -f …` prints nothing).
|
|
86
|
-
// Still record an explicit no-output line so control-plane verbatim-detail checks pass.
|
|
87
86
|
const stdoutLines = result.stdout.trim() ? result.stdout.trim().split("\n") : [];
|
|
88
87
|
const stderrLines = result.stderr.trim() ? result.stderr.trim().split("\n") : [];
|
|
88
|
+
if (result.code !== 0) {
|
|
89
|
+
// Prefix must match FINALIZE_CONTRACT_PATTERN ("Agent report") — not Bridge-fault laundering.
|
|
90
|
+
const failedDetails = [
|
|
91
|
+
`$ ${command}`,
|
|
92
|
+
...stdoutLines,
|
|
93
|
+
...stderrLines,
|
|
94
|
+
`exit ${result.code}`,
|
|
95
|
+
].map((line) => line.slice(0, 4_000)).slice(0, 100);
|
|
96
|
+
throw new Error(`Agent report: Verification failed (${command}): ${failedDetails.join("\n").slice(0, 2_000)}`);
|
|
97
|
+
}
|
|
98
|
+
// Empty exit-0 is not proof — a no-op recipe (`@true`) must not clear required_evidence: test.
|
|
99
|
+
if (stdoutLines.length === 0 && stderrLines.length === 0) {
|
|
100
|
+
throw new Error(`Agent report: Verification produced no output for test evidence (${command}). A bounded test command must print observable stdout or stderr.`);
|
|
101
|
+
}
|
|
89
102
|
const details = [
|
|
90
103
|
`$ ${command}`,
|
|
91
104
|
...stdoutLines,
|
|
92
105
|
...stderrLines,
|
|
93
|
-
...(stdoutLines.length === 0 && stderrLines.length === 0
|
|
94
|
-
? ["(no stdout/stderr — command exited with the status below)"]
|
|
95
|
-
: []),
|
|
96
106
|
`exit ${result.code}`,
|
|
97
107
|
].map((line) => line.slice(0, 4_000)).slice(0, 100);
|
|
98
|
-
if (result.code !== 0) {
|
|
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)}`);
|
|
101
|
-
}
|
|
102
108
|
if (details.join("\n").trim().length < TEST_EVIDENCE_DETAILS_MIN) {
|
|
103
109
|
throw new Error(`Agent report: Verification produced insufficient output for test evidence (${command})`);
|
|
104
110
|
}
|
|
111
|
+
// Keep agent test prose as supplement; Bridge witness is the authoritative entry.
|
|
105
112
|
const withoutThinTest = input.report.evidence.filter((item) => {
|
|
106
113
|
if (item.kind !== "test")
|
|
107
114
|
return true;
|
|
@@ -119,6 +126,7 @@ export async function ensureTestEvidence(input) {
|
|
|
119
126
|
name: command,
|
|
120
127
|
details,
|
|
121
128
|
acceptance_criteria: criteriaForTestEvidence(input.report),
|
|
129
|
+
witness: "bridge",
|
|
122
130
|
},
|
|
123
131
|
],
|
|
124
132
|
};
|
package/dist/execution.js
CHANGED
|
@@ -558,8 +558,8 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
558
558
|
}
|
|
559
559
|
catch (error) {
|
|
560
560
|
const message = error instanceof Error ? error.message : "Agent delivery report was invalid";
|
|
561
|
-
// classifyFinalizeFailure:
|
|
562
|
-
// unknown
|
|
561
|
+
// classifyFinalizeFailure: forge transport → retryable; contract/environment/unknown → fail closed
|
|
562
|
+
// (unknown prefixed as "Bridge finalize interrupted", not laundered as contract).
|
|
563
563
|
const classified = classifyFinalizeFailure(message);
|
|
564
564
|
await queueTerminal(client, taskId, {
|
|
565
565
|
action: "fail",
|
|
@@ -729,7 +729,11 @@ async function prepareDelivery(client, attemptId, taskId, report) {
|
|
|
729
729
|
.digest("hex")}`);
|
|
730
730
|
const response = await client.attemptRequest(taskId, "artifacts", {
|
|
731
731
|
kind: item.kind, name: item.name, uri, digest,
|
|
732
|
-
metadata: {
|
|
732
|
+
metadata: {
|
|
733
|
+
details: item.details,
|
|
734
|
+
acceptance_criteria: item.acceptance_criteria,
|
|
735
|
+
...(item.witness === "bridge" ? { witness: "bridge" } : {}),
|
|
736
|
+
},
|
|
733
737
|
idempotency_key: `bridge:artifact:${attemptId}:${index + 1}`,
|
|
734
738
|
});
|
|
735
739
|
const artifactId = z.string().uuid().parse(response.artifact_id);
|
package/dist/mcp.js
CHANGED
|
@@ -42,7 +42,13 @@ export function createBridgeMcpServer(client) {
|
|
|
42
42
|
server.registerTool("claim_assignment", { description: "Claim an assignment and store its lease", inputSchema: { task_id: z.string().uuid(), attempt_id: z.string().uuid() } }, async ({ task_id, attempt_id }) => result(await client.claim(task_id, attempt_id)));
|
|
43
43
|
server.registerTool("report_progress", { description: "Report the current execution phase and concise observable progress", inputSchema: { task_id: z.string().uuid(), phase: executionPhaseSchema, message: z.string().min(1), idempotency_key: z.string().min(1) } }, async (input) => result(await client.attemptRequest(input.task_id, "progress", { phase: input.phase, message: input.message, idempotency_key: input.idempotency_key })));
|
|
44
44
|
server.registerTool("raise_question", { description: "Raise a structured blocker for human judgment", inputSchema: { task_id: z.string().uuid(), question: z.string().min(1), impact: z.string().min(1), options: z.array(z.string()).default([]), recommendation: z.string().optional(), idempotency_key: z.string().min(1) } }, async (input) => result(await client.attemptRequest(input.task_id, "blockers", input)));
|
|
45
|
-
server.registerTool("add_artifact", { description: "Attach external delivery evidence metadata", inputSchema: { task_id: z.string().uuid(), kind: z.string().min(1), name: z.string().min(1), uri: z.string().url(), digest: z.string().optional(), metadata: z.record(z.unknown()).optional(), idempotency_key: z.string().min(1) } }, async (input) =>
|
|
45
|
+
server.registerTool("add_artifact", { description: "Attach external delivery evidence metadata", inputSchema: { task_id: z.string().uuid(), kind: z.string().min(1), name: z.string().min(1), uri: z.string().url(), digest: z.string().optional(), metadata: z.record(z.unknown()).optional(), idempotency_key: z.string().min(1) } }, async (input) => {
|
|
46
|
+
// Witness is Bridge-authored after a real command run — never accept an agent-claimed stamp.
|
|
47
|
+
const metadata = input.metadata && typeof input.metadata === "object"
|
|
48
|
+
? Object.fromEntries(Object.entries(input.metadata).filter(([key]) => key !== "witness"))
|
|
49
|
+
: input.metadata;
|
|
50
|
+
return result(await client.attemptRequest(input.task_id, "artifacts", { ...input, metadata }));
|
|
51
|
+
});
|
|
46
52
|
server.registerTool("submit_delivery", { description: "Submit a structured Delivery Packet for human acceptance", inputSchema: { task_id: z.string().uuid(), packet: deliveryPacketSchema, evidence: z.array(z.object({ artifact_id: z.string().uuid(), kind: z.string() })).default([]), idempotency_key: z.string().min(1) } }, async (input) => result(await client.attemptRequest(input.task_id, "complete", { delivery_packet: input.packet, evidence: input.evidence, idempotency_key: input.idempotency_key })));
|
|
47
53
|
server.registerTool("fail_assignment", { description: "Report a terminal failed attempt", inputSchema: { task_id: z.string().uuid(), error: z.string().min(1), idempotency_key: z.string().min(1) } }, async (input) => result(await client.attemptRequest(input.task_id, "fail", input)));
|
|
48
54
|
return server;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 2: when Connect queued a workspace intent, Bridge applies it once
|
|
3
|
+
* (ops.env + install/restart) then acks. Ack runs only after install succeeds so a
|
|
4
|
+
* failed apply leaves the intent queued for the next heartbeat (manual ops switch remains fallback).
|
|
5
|
+
* After a successful restart, currentWorkspace matches the target so we do not re-apply.
|
|
6
|
+
*/
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { defaultOpsEnvPath, expandOpsValue, loadOpsEnv, writeOpsEnvFile } from "./ops.js";
|
|
10
|
+
function pathsEqual(left, right) {
|
|
11
|
+
return resolve(expandOpsValue(left)) === resolve(expandOpsValue(right));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* If control-plane intent names a different workspace than this runner, apply and return true
|
|
15
|
+
* (caller should exit so install-service / LaunchAgent restarts).
|
|
16
|
+
*/
|
|
17
|
+
export async function maybeApplyOnShiftIntent(input) {
|
|
18
|
+
const intentPath = input.onShift?.on_shift_intent?.workspace_path?.trim();
|
|
19
|
+
if (!intentPath)
|
|
20
|
+
return false;
|
|
21
|
+
const target = resolve(expandOpsValue(intentPath));
|
|
22
|
+
const current = input.currentWorkspace ? resolve(expandOpsValue(input.currentWorkspace)) : null;
|
|
23
|
+
if (current && pathsEqual(current, target)) {
|
|
24
|
+
// Already on the declared path — leave intent for Bound clear on heartbeat.
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const repo = (input.onShift?.on_shift_intent?.repository_url
|
|
28
|
+
?? input.onShift?.expected_repository
|
|
29
|
+
?? "").trim();
|
|
30
|
+
if (input.writeEnv) {
|
|
31
|
+
input.writeEnv(target, repo);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
let envPath = defaultOpsEnvPath(homedir());
|
|
35
|
+
try {
|
|
36
|
+
envPath = loadOpsEnv().loadedFrom ?? envPath;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// First apply may create ops.env at the default path.
|
|
40
|
+
}
|
|
41
|
+
writeOpsEnvFile(envPath, {
|
|
42
|
+
CONDUIT_WORKSPACE: target,
|
|
43
|
+
...(repo ? { CONDUIT_REPO: repo } : {}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
console.log(`Applying on-shift workspace intent: ${target}${repo ? ` (${repo})` : ""}`);
|
|
47
|
+
await input.runInstall(target, repo);
|
|
48
|
+
// Ack only after install succeeds. A rebound service that already matches the target
|
|
49
|
+
// short-circuits above and will not re-apply; Bound clears any leftover intent.
|
|
50
|
+
await input.client.request("/runner/v1/machine/ack-on-shift-intent", {
|
|
51
|
+
method: "POST",
|
|
52
|
+
body: "{}",
|
|
53
|
+
});
|
|
54
|
+
return true;
|
|
55
|
+
}
|
package/dist/ops.js
CHANGED
|
@@ -232,7 +232,11 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
232
232
|
const client = new ConduitClient(config);
|
|
233
233
|
const result = await client.request("/runner/v1/machine/switch-project", {
|
|
234
234
|
method: "POST",
|
|
235
|
-
body: JSON.stringify({
|
|
235
|
+
body: JSON.stringify({
|
|
236
|
+
project_id: projectId,
|
|
237
|
+
workspace_path: workspace,
|
|
238
|
+
...(repo ? { repository_url: repo } : {}),
|
|
239
|
+
}),
|
|
236
240
|
});
|
|
237
241
|
console.log(`Control plane on-shift: ${String(result.project_id ?? projectId)} (${String(result.binding_status ?? "pending")})`);
|
|
238
242
|
writeOpsEnvFile(envPath, {
|
|
@@ -251,6 +255,13 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
251
255
|
CONDUIT_REPO: repo || switched.CONDUIT_REPO,
|
|
252
256
|
},
|
|
253
257
|
});
|
|
258
|
+
// Clear queued intent only after install succeeds so a failed apply can retry via heartbeat.
|
|
259
|
+
try {
|
|
260
|
+
await client.request("/runner/v1/machine/ack-on-shift-intent", { method: "POST", body: "{}" });
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// Older control planes omit ack — local ops.env still wins.
|
|
264
|
+
}
|
|
254
265
|
return;
|
|
255
266
|
}
|
|
256
267
|
if (verb === "connect") {
|
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.11",
|
|
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": {
|