@miraland-labs/conduit-bridge 0.16.149 → 0.16.151
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/attempt-worktree.js +40 -1
- package/dist/cli.js +5 -2
- package/dist/ensure-test-evidence.js +24 -0
- package/dist/execution.js +28 -2
- package/dist/failure-signal.js +9 -3
- package/dist/preflight.js +75 -3
- package/package.json +1 -1
package/dist/attempt-worktree.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* F-07 slice 1: one Git worktree per attempt. The install-service source checkout is never the
|
|
3
3
|
* agent cwd; retries must not inherit dirty files from a failed run.
|
|
4
4
|
*/
|
|
5
|
-
import { access, appendFile, mkdir, readFile, rm } from "node:fs/promises";
|
|
5
|
+
import { access, appendFile, mkdir, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
6
6
|
import { constants } from "node:fs";
|
|
7
7
|
import { basename, join, resolve } from "node:path";
|
|
8
8
|
import { execFile } from "node:child_process";
|
|
@@ -118,6 +118,45 @@ export async function removeAttemptWorktree(sourceWorkspace, worktreePath) {
|
|
|
118
118
|
}
|
|
119
119
|
return true;
|
|
120
120
|
}
|
|
121
|
+
/** A retained worktree the control plane never released is swept after this long. */
|
|
122
|
+
export const STALE_ATTEMPT_WORKTREE_MS = 7 * 24 * 60 * 60_000;
|
|
123
|
+
/**
|
|
124
|
+
* Removes every retained attempt worktree older than seven days that no attempt of this Bridge is
|
|
125
|
+
* running in. The control plane releases a retained tree only when its repair settles; a repair
|
|
126
|
+
* that never settles left the tree on disk for ever (four September trees deleted by hand,
|
|
127
|
+
* 2026-10-03). A diagnosis runs inside its source attempt's tree, so that tree counts as in use.
|
|
128
|
+
* Answers the paths it removed.
|
|
129
|
+
*/
|
|
130
|
+
export async function sweepStaleAttemptWorktrees(sourceWorkspace, activeAttempts, now = Date.now()) {
|
|
131
|
+
const root = join(sourceWorkspace, ".conduit", "attempts");
|
|
132
|
+
let entries;
|
|
133
|
+
try {
|
|
134
|
+
entries = await readdir(root);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
const inUse = new Set();
|
|
140
|
+
for (const attempt of Object.values(activeAttempts)) {
|
|
141
|
+
inUse.add(resolve(root, attempt.attemptId));
|
|
142
|
+
if (attempt.sourceAttemptId)
|
|
143
|
+
inUse.add(resolve(root, attempt.sourceAttemptId));
|
|
144
|
+
if (attempt.worktreePath)
|
|
145
|
+
inUse.add(resolve(attempt.worktreePath));
|
|
146
|
+
}
|
|
147
|
+
const removed = [];
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
const path = join(root, entry);
|
|
150
|
+
if (inUse.has(resolve(path)))
|
|
151
|
+
continue;
|
|
152
|
+
const modified = await stat(path).then((info) => info.isDirectory() ? info.mtimeMs : null, () => null);
|
|
153
|
+
if (modified === null || now - modified < STALE_ATTEMPT_WORKTREE_MS)
|
|
154
|
+
continue;
|
|
155
|
+
if (await removeAttemptWorktree(sourceWorkspace, path))
|
|
156
|
+
removed.push(path);
|
|
157
|
+
}
|
|
158
|
+
return removed;
|
|
159
|
+
}
|
|
121
160
|
/**
|
|
122
161
|
* F-07 safe resume: prove the interrupted attempt still owns this worktree path.
|
|
123
162
|
* Does not check session identity — caller requires config.sessions[taskId] separately.
|
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ import { buildOnShiftIntentProof, managedWorkspacePath, maybeApplyOnShiftIntent
|
|
|
22
22
|
import { expandOpsValue, loadOpsEnv, OPS_VERBS, runOps } from "./ops.js";
|
|
23
23
|
import { discardWorktreeWrites, pumpExecutionSlots, renewLeases } from "./execution.js";
|
|
24
24
|
import { applyRunnerToolPath, installRunnerService, uninstallRunnerService } from "./service.js";
|
|
25
|
-
import { BRIDGE_PROTOCOL_VERSION, cachedBridgePreflight, planGateRehearsals, PREFLIGHT_CACHE_TTL_MS, runGateRehearsal, unavailableWorkspacePreflight } from "./preflight.js";
|
|
25
|
+
import { BRIDGE_PROTOCOL_VERSION, cachedBridgePreflight, gateRehearsalLockHeld, planGateRehearsals, PREFLIGHT_CACHE_TTL_MS, runGateRehearsal, unavailableWorkspacePreflight } from "./preflight.js";
|
|
26
26
|
import { refreshSourceWorkspace } from "./workspace-refresh.js";
|
|
27
27
|
import { executeNextInvestigation } from "./investigation.js";
|
|
28
28
|
import { bridgeVersion } from "./version.js";
|
|
@@ -666,8 +666,9 @@ async function runner() {
|
|
|
666
666
|
await heartbeat(client, config, workspace, currentBrief, preflight, intentProof, bootstrapResult);
|
|
667
667
|
}
|
|
668
668
|
await renewLeases(client, config);
|
|
669
|
+
// Another runner on this computer rehearsing its gate: claim as usual, rehearse on a later cycle.
|
|
669
670
|
if (workspace && onlineDriverIds(config).length && !gateRehearsalRunning && rehearsalPlan?.due
|
|
670
|
-
&& running.size === 0 && Object.keys(config.activeAttempts).length === 0) {
|
|
671
|
+
&& running.size === 0 && Object.keys(config.activeAttempts).length === 0 && !gateRehearsalLockHeld()) {
|
|
671
672
|
startGateRehearsal(rehearsalPlan.due.workspace, rehearsalPlan.due.plan);
|
|
672
673
|
}
|
|
673
674
|
// No claim while the gate rehearses: the rehearsal measures the gate alone, never beside an attempt.
|
|
@@ -767,6 +768,8 @@ function startGateRehearsal(workspace, plan) {
|
|
|
767
768
|
gateRehearsalRunning = true;
|
|
768
769
|
console.log(`Rehearsing the gate in ${workspace}: ${plan.commands.join(", ")}`);
|
|
769
770
|
void runGateRehearsal(workspace, plan)
|
|
771
|
+
.then((rehearsal) => { if (rehearsal === "busy")
|
|
772
|
+
console.log("Gate rehearsal deferred: another runner on this computer is rehearsing"); })
|
|
770
773
|
.catch((error) => console.error(`Gate rehearsal failed: ${redactSecrets(error instanceof Error ? error.message : "unknown error")}`))
|
|
771
774
|
.finally(async () => {
|
|
772
775
|
if (!await workspaceIsClean(workspace))
|
|
@@ -569,6 +569,23 @@ export class VerificationGateRanNothingError extends Error {
|
|
|
569
569
|
this.vendorMessage = vendorMessage;
|
|
570
570
|
}
|
|
571
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* A declared gate the verification timeout stopped after the agent run: it never exited, so it says
|
|
574
|
+
* nothing about the change. Carries the command and the output tails the timeout signal needs — the
|
|
575
|
+
* classifier holds this on the computer operator and never diagnoses or repairs it.
|
|
576
|
+
*/
|
|
577
|
+
export class VerificationGateTimedOutError extends Error {
|
|
578
|
+
command;
|
|
579
|
+
stdout;
|
|
580
|
+
stderr;
|
|
581
|
+
constructor(command, stdout, stderr, detail) {
|
|
582
|
+
super(`Agent report: Verification gate did not finish (${command}): ${detail}`);
|
|
583
|
+
this.name = "VerificationGateTimedOutError";
|
|
584
|
+
this.command = command;
|
|
585
|
+
this.stdout = stdout;
|
|
586
|
+
this.stderr = stderr;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
572
589
|
/**
|
|
573
590
|
* If test evidence is required, run the bounded verification commands in the attempt workspace
|
|
574
591
|
* and attach verbatim stdout/stderr with Bridge witness markers.
|
|
@@ -593,6 +610,13 @@ export async function ensureTestEvidence(input) {
|
|
|
593
610
|
const result = await run(command, input.workspace);
|
|
594
611
|
const stdoutLines = result.stdout.trim() ? result.stdout.trim().split("\n") : [];
|
|
595
612
|
const stderrLines = result.stderr.trim() ? result.stderr.trim().split("\n") : [];
|
|
613
|
+
// First, as in the dry run: a gate the timeout stopped never exited, so its partial output and
|
|
614
|
+
// exit code say nothing about the change. Read as a failed test, it sent a slow computer's
|
|
615
|
+
// timeout to diagnosis and repair (2026-10-10).
|
|
616
|
+
if (result.timedOut) {
|
|
617
|
+
const detail = boundedFailureDetail(command, outputLines(result), result.code);
|
|
618
|
+
throw new VerificationGateTimedOutError(command, result.stdout, result.stderr, detail);
|
|
619
|
+
}
|
|
596
620
|
// Checked ahead of both the exit-code and the pass path: `go test` on an empty package exits 0
|
|
597
621
|
// and would otherwise be witnessed as a passing test run that tested nothing.
|
|
598
622
|
const ranNothing = detectGateRanNothing(command, result.stdout, result.stderr, result.code);
|
package/dist/execution.js
CHANGED
|
@@ -11,7 +11,7 @@ import { saveDriverOutcome, saveDriverQuota, tierCandidates } from "./config.js"
|
|
|
11
11
|
import { DRIVERS, acceptanceCriteriaLines, acceptanceReportSkeleton, agentReportTemplate, buildAssignmentPrompt, evidenceKinds, extractAgentReportJsonText, fuelEndpoint, normalizeEvidenceDigest, parseAgentReport, parseJsonObjectCandidate, pickModelCandidate, isSafeModelName, driverExecutable, requireStampedExecutionClass, tierForRisk } from "./driver.js";
|
|
12
12
|
import { assertClassFloor } from "./execution-class.js";
|
|
13
13
|
import { pickDriverForClaim, recordDriverQuota, resolveDriverFuel, resolveDriverFuelProvenance, supportsReadOnlyDiagnosis, clearDriverOutcome, recordDriverTimeout } from "./drivers.js";
|
|
14
|
-
import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, removeAttemptWorktree, } from "./attempt-worktree.js";
|
|
14
|
+
import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, removeAttemptWorktree, sweepStaleAttemptWorktrees, } from "./attempt-worktree.js";
|
|
15
15
|
import { execFile } from "node:child_process";
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
17
|
import { buildWorkspaceBrief, ensureCommitAvailable, fetchOrigin, normalizeRepositoryUrl, resolveAttemptStartCommit } from "./brief.js";
|
|
@@ -28,7 +28,7 @@ import { checkpointWorkInProgress, ensureLandCommit } from "./ensure-land-commit
|
|
|
28
28
|
import { AGENT_NO_LAND_COMMIT_PREFIX, AgentNoLandCommitError, agentNoLandCommitMessage, requiresLandCommit } from "./land-contract.js";
|
|
29
29
|
import { agentClaimsRepositoryWork, claimsVsGitMismatch, isLandTreeEmpty, reportClaimsRepositoryWork, } from "./land-git-reality.js";
|
|
30
30
|
const execFileAsync = promisify(execFile);
|
|
31
|
-
import { boundedTail, captureVerificationFailure, DeliveryReportDefectError, detectGateRanNothing, ensureTestEvidence, needsTestEvidence, runBoundedVerificationCommand, verificationEvidenceCommands, VerificationFailedError, VerificationGateRanNothingError, VERIFICATION_TIMEOUT_MS } from "./ensure-test-evidence.js";
|
|
31
|
+
import { boundedTail, captureVerificationFailure, DeliveryReportDefectError, detectGateRanNothing, ensureTestEvidence, needsTestEvidence, runBoundedVerificationCommand, verificationEvidenceCommands, VerificationFailedError, VerificationGateRanNothingError, VerificationGateTimedOutError, VERIFICATION_TIMEOUT_MS } from "./ensure-test-evidence.js";
|
|
32
32
|
import { ensureChangeEvidence } from "./ensure-change-evidence.js";
|
|
33
33
|
import { ensureDemonstrationEvidence } from "./ensure-demonstration-evidence.js";
|
|
34
34
|
import { ensureNormativeEvidence } from "./ensure-normative-evidence.js";
|
|
@@ -856,6 +856,14 @@ export async function claimNextAssignment(client, config, workspace, brief, driv
|
|
|
856
856
|
console.error(`Diagnostic worktree release failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`);
|
|
857
857
|
}
|
|
858
858
|
}
|
|
859
|
+
try {
|
|
860
|
+
for (const swept of await sweepStaleAttemptWorktrees(workspace, config.activeAttempts)) {
|
|
861
|
+
console.log(`Released stale diagnostic worktree ${swept}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
catch (error) {
|
|
865
|
+
console.error(`Stale worktree sweep failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`);
|
|
866
|
+
}
|
|
859
867
|
const assignment = assignments[0];
|
|
860
868
|
if (!assignment)
|
|
861
869
|
return null;
|
|
@@ -2244,6 +2252,24 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
|
|
|
2244
2252
|
validateDeliveryReport(report, spec, grants, actualPaths ?? undefined, deliveryBaseCommit);
|
|
2245
2253
|
}
|
|
2246
2254
|
catch (error) {
|
|
2255
|
+
// A gate the timeout stopped never exited, so nothing says the change is at fault: the same
|
|
2256
|
+
// timeout signal the dry run emits, held on the computer operator — never a diagnosis or a
|
|
2257
|
+
// repair brief. Task ce673ef1 (2026-10-10) ran two diagnoses on a CPU-starved `npm run test`.
|
|
2258
|
+
if (error instanceof VerificationGateTimedOutError) {
|
|
2259
|
+
const response = await queueTerminal(client, taskId, {
|
|
2260
|
+
action: "fail",
|
|
2261
|
+
body: {
|
|
2262
|
+
error: error.message,
|
|
2263
|
+
retryable: false,
|
|
2264
|
+
signal: verificationGateTimeoutSignal({ command: error.command, stderr: error.stderr, stdout: error.stdout }),
|
|
2265
|
+
...(report.pull_request_url ? { pull_request_url: report.pull_request_url } : {}),
|
|
2266
|
+
idempotency_key: `bridge:verification-gate-timeout:${active.attemptId}`,
|
|
2267
|
+
},
|
|
2268
|
+
});
|
|
2269
|
+
retainAttemptWorktree = response.retain_worktree === true;
|
|
2270
|
+
console.error(`Assignment ${taskId} verification gate did not finish: ${redactSecrets(error.message)}`);
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2247
2273
|
// A gate that ran no tests is a misconfigured gate, not a failed verification: the delivery
|
|
2248
2274
|
// pack may be entirely correct. Emit the T1 typed signal and Hold on the computer operator —
|
|
2249
2275
|
// never a repair attempt or a blind retry, which is what burned six deliveries on 2026-09-04
|
package/dist/failure-signal.js
CHANGED
|
@@ -403,6 +403,9 @@ export function verificationRedBaseSignal(input) {
|
|
|
403
403
|
* The signal Bridge emits when the verification timeout stops a gate on the base commit, before the
|
|
404
404
|
* agent starts. The gate never exited, so it says nothing about the base: read as exit 1, a gate
|
|
405
405
|
* that hung (waiting for input or the network, or on a slow computer) was a red base on the author.
|
|
406
|
+
*
|
|
407
|
+
* The delivery gate after the agent run emits it without a base commit: the gate ran on the change,
|
|
408
|
+
* and a gate that never exited says nothing about the change either.
|
|
406
409
|
*/
|
|
407
410
|
export function verificationGateTimeoutSignal(input) {
|
|
408
411
|
return {
|
|
@@ -410,7 +413,7 @@ export function verificationGateTimeoutSignal(input) {
|
|
|
410
413
|
witness: "bridge",
|
|
411
414
|
kind: "timeout",
|
|
412
415
|
command: input.command,
|
|
413
|
-
base_commit: input.baseCommit,
|
|
416
|
+
...(input.baseCommit ? { base_commit: input.baseCommit } : {}),
|
|
414
417
|
exit_code: null,
|
|
415
418
|
...(input.stderr?.trim() ? { stderr_tail: bounded2k(input.stderr) } : {}),
|
|
416
419
|
...(input.stdout?.trim() ? { stdout_tail: bounded2k(input.stdout) } : {}),
|
|
@@ -562,13 +565,16 @@ export function classifyFailureSignal(signal) {
|
|
|
562
565
|
// base commit nor a change is known to be at fault. The generic rule read it as a failed test.
|
|
563
566
|
if (signal.phase === "verification" && signal.kind === "timeout") {
|
|
564
567
|
const command = signal.command ?? "the declared gate";
|
|
565
|
-
|
|
568
|
+
// No base commit: the delivery gate after the agent run, on the agent's change.
|
|
569
|
+
const message = signal.base_commit
|
|
570
|
+
? `The verification gate \`${command}\` did not finish on the base commit ${shortCommit(signal.base_commit)} within Bridge's 10-minute limit, so Bridge stopped it before any change was made.`
|
|
571
|
+
: `The verification gate \`${command}\` did not finish on the agent's change within Bridge's 10-minute limit, so Bridge stopped it before delivery.`;
|
|
566
572
|
return {
|
|
567
573
|
code: "verification_gate_timeout",
|
|
568
574
|
class: "environment",
|
|
569
575
|
disposition: "hold",
|
|
570
576
|
responsible_party: "computer_operator",
|
|
571
|
-
message
|
|
577
|
+
message,
|
|
572
578
|
next_action: `Run \`${command}\` on this computer and find where it hangs — a test that waits for input or the network, or a computer too slow for the gate — then fix it and Recheck. Conduit does not retry or repair a gate that does not finish.`,
|
|
573
579
|
diagnostic_detail: [`$ ${command}`, "stopped after 10 minutes", signal.stderr_tail, signal.stdout_tail].filter(Boolean).join("\n"),
|
|
574
580
|
};
|
package/dist/preflight.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import {
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { link, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
4
6
|
import { dirname, join } from "node:path";
|
|
5
7
|
import { promisify } from "node:util";
|
|
6
8
|
import { buildWorkspaceBrief, normalizeRepositoryUrl } from "./brief.js";
|
|
@@ -352,8 +354,78 @@ export async function planGateRehearsals(checkouts, options = {}) {
|
|
|
352
354
|
}
|
|
353
355
|
return { reports, due };
|
|
354
356
|
}
|
|
355
|
-
/**
|
|
357
|
+
/**
|
|
358
|
+
* One gate rehearsal at a time on a computer. The VPS runs two runner services (conduit-runner and
|
|
359
|
+
* forge-runner); on 2026-10-03 both rehearsed together, each gate slowed the other, and the times
|
|
360
|
+
* recorded were wrong. Each installation has its own config directory, so the lock lives in the
|
|
361
|
+
* user's cache directory, which every runner of that user shares. The lock names its holder's
|
|
362
|
+
* process; a lock whose process is gone is taken over.
|
|
363
|
+
*/
|
|
364
|
+
export function gateRehearsalLockPath() {
|
|
365
|
+
return join(homedir(), ".cache", "conduit", "gate-rehearsal.lock");
|
|
366
|
+
}
|
|
367
|
+
/** A live process holds the gate rehearsal lock. */
|
|
368
|
+
export function gateRehearsalLockHeld(lockPath = gateRehearsalLockPath()) {
|
|
369
|
+
let pid;
|
|
370
|
+
try {
|
|
371
|
+
pid = Number.parseInt(readFileSync(lockPath, "utf8"), 10);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
377
|
+
return false;
|
|
378
|
+
try {
|
|
379
|
+
process.kill(pid, 0);
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
return error.code === "EPERM";
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/** Take the lock, or answer false when a live process holds it. */
|
|
387
|
+
async function acquireGateRehearsalLock(lockPath) {
|
|
388
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
389
|
+
// Linked into place whole, so the lock never exists without its holder's pid in it.
|
|
390
|
+
const claim = `${lockPath}.${randomUUID()}`;
|
|
391
|
+
await writeFile(claim, `${process.pid}\n`);
|
|
392
|
+
try {
|
|
393
|
+
for (let tries = 0; tries < 2; tries += 1) {
|
|
394
|
+
try {
|
|
395
|
+
await link(claim, lockPath);
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
if (error.code !== "EEXIST")
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
if (gateRehearsalLockHeld(lockPath))
|
|
403
|
+
return false;
|
|
404
|
+
await unlink(lockPath).catch(() => undefined);
|
|
405
|
+
}
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
await unlink(claim).catch(() => undefined);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Run the planned gate once and remember it for the day. Null when no command could start.
|
|
414
|
+
* "busy" when another rehearsal on this computer holds the lock: nothing is run or remembered, so
|
|
415
|
+
* the gate is still due and the next cycle tries again.
|
|
416
|
+
*/
|
|
356
417
|
export async function runGateRehearsal(workspace, plan, options = {}) {
|
|
418
|
+
const lockPath = options.lockPath ?? gateRehearsalLockPath();
|
|
419
|
+
if (!await acquireGateRehearsalLock(lockPath))
|
|
420
|
+
return "busy";
|
|
421
|
+
try {
|
|
422
|
+
return await rehearseGate(workspace, plan, options);
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
await unlink(lockPath).catch(() => undefined);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async function rehearseGate(workspace, plan, options) {
|
|
357
429
|
const now = options.now ?? Date.now;
|
|
358
430
|
const run = options.runCommand ?? runBoundedVerificationCommand;
|
|
359
431
|
const commands = [];
|
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.151",
|
|
4
4
|
"description": "Conduit Bridge CLI \u2014 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": {
|