@miraland-labs/conduit-bridge 0.16.137 → 0.16.140

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 CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { redactSecrets } from "./secret-redaction.js";
2
3
  import { parseArgs } from "node:util";
3
4
  import { resolve, dirname, join as pathJoin } from "node:path";
4
5
  import { hostname, homedir, platform, userInfo } from "node:os";
@@ -8,7 +9,7 @@ import { chmodSync, cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
8
9
  import { stdin as input, stdout as output } from "node:process";
9
10
  import { fileURLToPath } from "node:url";
10
11
  import { assignmentPollBehindServer, ConduitClient, ConduitRequestError } from "./client.js";
11
- import { BRIDGE_LEASE_CAPACITY, BRIDGE_MAX_LEASE_CAPACITY, clampLeaseCapacity, clearLocalConnection, clearPendingConnection, loadConfig, loadConfigIfPresent, loadOrCreateInstallationId, loadPendingConnection, redactSecrets, saveConfig, saveConfigPrefs, saveDriverQuota, savePendingConnection, suggestMachineName, } from "./config.js";
12
+ import { BRIDGE_LEASE_CAPACITY, BRIDGE_MAX_LEASE_CAPACITY, clampLeaseCapacity, clearLocalConnection, clearPendingConnection, loadConfig, loadConfigIfPresent, loadOrCreateInstallationId, loadPendingConnection, saveConfig, saveConfigPrefs, saveDriverQuota, savePendingConnection, suggestMachineName, } from "./config.js";
12
13
  import { runMcp } from "./mcp.js";
13
14
  import { detectInstalledClients, localFuelOnlyClients, suggestFuelSource } from "./detect.js";
14
15
  import { DRIVERS, MODEL_TIERS } from "./driver.js";
package/dist/config.js CHANGED
@@ -77,8 +77,10 @@ export async function loadConfigIfPresent() {
77
77
  // → config — so a new lane field has to be added in both, and forgetting here is silent: the
78
78
  // value survives in memory, then vanishes on the next load. `quota` was lost exactly that way.
79
79
  const quota = plainQuota(lane.quota);
80
+ const outcome = plainOutcome(lane.outcome);
80
81
  const models = plainTierModels(lane.models, `drivers.${id}.models`);
81
82
  cleaned[id] = { state, ...(modelsPolicy ? { models_policy: modelsPolicy } : {}), ...(fuel ? { fuel } : {}), ...(quota ? { quota } : {}),
83
+ ...(outcome ? { outcome } : {}),
82
84
  ...(models && Object.keys(models).length ? { models } : {}) };
83
85
  }
84
86
  config.drivers = cleaned;
@@ -136,6 +138,14 @@ function plainQuota(raw) {
136
138
  const vendor = typeof raw.vendor === "string" && raw.vendor.trim() ? raw.vendor : undefined;
137
139
  return { exhausted: raw.exhausted, resets_at: raw.resets_at, observed_at: raw.observed_at, ...(vendor ? { vendor } : {}) };
138
140
  }
141
+ /** The lane's timeout record. Dropped here, the count never passed 1 and a lane that kept timing out was never demoted. */
142
+ function plainOutcome(raw) {
143
+ if (!raw || typeof raw !== "object")
144
+ return undefined;
145
+ if (!Number.isFinite(raw.consecutive_timeouts) || typeof raw.last_timeout_at !== "string")
146
+ return undefined;
147
+ return { consecutive_timeouts: Math.max(0, Math.floor(raw.consecutive_timeouts)), last_timeout_at: raw.last_timeout_at };
148
+ }
139
149
  async function loadRuntime() {
140
150
  try {
141
151
  const raw = JSON.parse(await readFile(runtimePath, "utf8"));
@@ -224,9 +234,10 @@ function mergeCurrentDriverQuota(disk, snapshot) {
224
234
  return snapshot;
225
235
  const drivers = {};
226
236
  for (const [id, lane] of Object.entries(snapshot)) {
227
- const { quota: _staleQuota, ...prefs } = lane;
237
+ const { quota: _staleQuota, outcome: _staleOutcome, ...prefs } = lane;
228
238
  const quota = plainQuota(disk.drivers?.[id]?.quota);
229
- drivers[id] = { ...prefs, ...(quota ? { quota } : {}) };
239
+ const outcome = plainOutcome(disk.drivers?.[id]?.outcome);
240
+ drivers[id] = { ...prefs, ...(quota ? { quota } : {}), ...(outcome ? { outcome } : {}) };
230
241
  }
231
242
  return drivers;
232
243
  }
@@ -409,15 +420,3 @@ export function suggestMachineName(host, installationId) {
409
420
  const suffix = installationId.slice(-5).toLowerCase();
410
421
  return `${base.slice(0, 199 - suffix.length)}-${suffix}`;
411
422
  }
412
- export function redactSecrets(value) {
413
- // Conduit's own key classes, then the provider and forge shapes an agent's own environment
414
- // carries (ANTHROPIC_API_KEY, XAI_API_KEY, GEMINI_API_KEY, a GitHub token, a bearer header) —
415
- // an agent that prints its environment on failure must not leak them through this terminal.
416
- return value
417
- .replace(/(runner_sk_|gateway_sk_|lease_|connect_)[A-Za-z0-9-]+/g, "$1[redacted]")
418
- .replace(/sk-[A-Za-z0-9_-]{8,}/g, "sk-[redacted]")
419
- .replace(/xai-[A-Za-z0-9]+/g, "xai-[redacted]")
420
- .replace(/AIza[0-9A-Za-z_-]{35}/g, "AIza[redacted]")
421
- .replace(/(ghp_|gho_|github_pat_|glpat-)[A-Za-z0-9_-]+/g, "$1[redacted]")
422
- .replace(/Bearer\s+\S+/g, "Bearer [redacted]");
423
- }
package/dist/execution.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { redactSecrets, redactSecretsDeep } from "./secret-redaction.js";
1
3
  import { spawn } from "node:child_process";
2
4
  import { createHash } from "node:crypto";
3
5
  import { z } from "zod";
@@ -5,7 +7,7 @@ import { resolveAgentTimeout } from "./execution-budget.js";
5
7
  import { AttemptStalledError, createAttemptWatchdog, withTimeout } from "./attempt-watchdog.js";
6
8
  import { workPackageSchema } from "./work-package-schema.js";
7
9
  import { ConduitRequestError, reportAssignmentPollShapeMismatch } from "./client.js";
8
- import { redactSecrets, saveDriverOutcome, saveDriverQuota, tierCandidates } from "./config.js";
10
+ import { saveDriverOutcome, saveDriverQuota, tierCandidates } from "./config.js";
9
11
  import { DRIVERS, acceptanceCriteriaLines, acceptanceReportSkeleton, agentReportTemplate, buildAssignmentPrompt, evidenceKinds, extractAgentReportJsonText, fuelEndpoint, normalizeEvidenceDigest, parseAgentReport, parseJsonObjectCandidate, pickModelCandidate, isSafeModelName, driverExecutable, requireStampedExecutionClass, tierForRisk } from "./driver.js";
10
12
  import { assertClassFloor } from "./execution-class.js";
11
13
  import { pickDriverForClaim, recordDriverQuota, resolveDriverFuel, resolveDriverFuelProvenance, supportsReadOnlyDiagnosis, clearDriverOutcome, recordDriverTimeout } from "./drivers.js";
@@ -184,8 +186,22 @@ function timeoutFailureBody(resolution, attemptId) {
184
186
  }
185
187
  class AgentTurnTimeoutError extends Error {
186
188
  }
187
- /** Environment faults that must Hold — mirror CP ENVIRONMENT_FAILURES message patterns. */
188
- const FINALIZE_ENVIRONMENT_PATTERN = /base[_ ]not[_ ]ancestor|required base commit is not available|source[_ ]workspace[_ ]dirty|uncommitted changes|dirty workspace|workspace[_ ]head[_ ]changed|workspace[_ ]repository|workspace[_ ]unavailable|driver[_ ]not[_ ]authenticated|not logged in|no login|not authenticated|login required|no[_ ]online[_ ]driver|bridge[_ ]preflight|stale bridge|bridge version/i;
189
+ /**
190
+ * Computer faults found while finishing a run. Each Holds on its own named cause: without an
191
+ * envelope the control plane read them as unclassifiable and stopped the task instead (K3/K4).
192
+ * Order matters: the first match names the cause.
193
+ */
194
+ const FINALIZE_ENVIRONMENT_CAUSES = [
195
+ [/base[_ ]not[_ ]ancestor|required base commit is not available/i, "base_not_ancestor"],
196
+ [/source[_ ]workspace[_ ]dirty|uncommitted changes|dirty workspace/i, "workspace_dirty"],
197
+ [/workspace[_ ]head[_ ]changed/i, "workspace_head_changed"],
198
+ [/workspace[_ ]repository/i, "workspace_repository_mismatch"],
199
+ [/workspace[_ ]unavailable/i, "workspace_unavailable"],
200
+ [/driver[_ ]not[_ ]authenticated|not logged in|no login|not authenticated|login required/i, "driver_not_authenticated"],
201
+ [/no[_ ]online[_ ]driver/i, "no_online_driver"],
202
+ [/bridge[_ ]preflight/i, "bridge_preflight_missing"],
203
+ [/stale bridge|bridge version/i, "stale_bridge"],
204
+ ];
189
205
  /** Delivery-report / grant / evidence contract defects (non-retryable rework). */
190
206
  /** Two finalize messages with builders of their own in the twin and the cause table. */
191
207
  const CONTRACT_SCOPE_CONFLICT_MESSAGE = /Agent reported changes outside the approved scope:/i;
@@ -204,8 +220,9 @@ export function classifyFinalizeFailure(message) {
204
220
  // the owner reads the refusal rather than that Conduit could not classify the failure.
205
221
  if (message.startsWith(PULL_REQUEST_OPEN_FAILURE_PREFIX))
206
222
  return { retryable: false, error: message, failure: bridgeCause("pull_request_open_refused", message) };
207
- if (FINALIZE_ENVIRONMENT_PATTERN.test(message))
208
- return { retryable: false, error: message };
223
+ const environment = FINALIZE_ENVIRONMENT_CAUSES.find(([pattern]) => pattern.test(message))?.[1];
224
+ if (environment)
225
+ return { retryable: false, error: message, failure: bridgeCause(environment, message) };
209
226
  // Bridge names the contract defect it found (K3); an unknown throw below stays prose until its
210
227
  // own site names a code.
211
228
  if (FINALIZE_CONTRACT_PATTERN.test(message)) {
@@ -471,11 +488,6 @@ export function parseDiagnosticRepairBrief(text) {
471
488
  }
472
489
  return parsed.data;
473
490
  }
474
- /**
475
- * The gate command inside the witness line, for the diagnosis prompt. Repair names the gate from
476
- * the typed signal; this pattern stays until that prompt moves.
477
- */
478
- const VERIFICATION_GATE_PATTERN = /Verification failed \(([^)]+)\)/;
479
491
  /** Prompt for Option C: IDE-depth understanding in the retained tree, with write authority absent. */
480
492
  export function buildDiagnosticPrompt(input) {
481
493
  const pkg = input.workPackage;
@@ -488,7 +500,7 @@ export function buildDiagnosticPrompt(input) {
488
500
  // Bridge re-runs the gate itself after the next authoring run. Without this rule the diagnosis
489
501
  // reads the gate as the agent's own command and briefs the agent to run a different one, which
490
502
  // cannot change what Bridge runs: the briefed run then fails on identical output.
491
- const gate = VERIFICATION_GATE_PATTERN.exec(input.failureOutput)?.[1]?.trim();
503
+ const gate = input.gateCommand;
492
504
  return [
493
505
  "You are Conductor's read-only diagnosis lane. Diagnose the exact failed authoring run in the current retained worktree.",
494
506
  "Inspect the actual code, git diff, nearby callers, and relevant tests. Run only the supplied project verification commands when useful.",
@@ -760,7 +772,8 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
760
772
  console.error(`Slot recovery failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`);
761
773
  })
762
774
  .then(() => undefined)
763
- .finally(() => { running.delete(id); });
775
+ .finally(() => { if (running.get(id) === slot)
776
+ running.delete(id); });
764
777
  running.set(id, slot);
765
778
  }
766
779
  while (running.size < config.leaseCapacity) {
@@ -795,7 +808,8 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
795
808
  console.error(`Slot execution failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`);
796
809
  })
797
810
  .then(() => undefined)
798
- .finally(() => { running.delete(taskId); });
811
+ .finally(() => { if (running.get(taskId) === slot)
812
+ running.delete(taskId); });
799
813
  running.set(taskId, slot);
800
814
  }
801
815
  return progressed;
@@ -960,6 +974,13 @@ export async function executeNextAssignment(client, config, driver, workspace, b
960
974
  * hung: keep it instead of clearing it as an ordinary finished attempt.
961
975
  */
962
976
  const stalledAttempts = new Set();
977
+ /**
978
+ * The attempt the current run serves. The maps above are keyed by task, and an abandoned run (after
979
+ * a stall or a lost lease) keeps running while the task's next attempt starts on this machine — a
980
+ * repair diagnosis always does. Without this, the old run reported with the new attempt's lease and
981
+ * removed the new run's handles.
982
+ */
983
+ const runningAttempt = new AsyncLocalStorage();
963
984
  /** T90: the timers and the sleep guard of a running attempt, for a stall report to stop. */
964
985
  const attemptDisposers = new Map();
965
986
  /** T90: the watchdog of a running attempt, so any terminal path can name the phase it ends in. */
@@ -978,7 +999,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
978
999
  turnBudgetMs: resolveAgentTimeout({ machineCeilingMs: timeoutMs }).timeoutMs,
979
1000
  });
980
1001
  attemptWatchdogs.set(taskId, watchdog);
981
- const attempt = runAttempt(client, config, driver, workspace, brief, taskId, watchdog, timeoutMs, supervision, options);
1002
+ const attempt = runningAttempt.run(client.attempt(taskId).attemptId, () => runAttempt(client, config, driver, workspace, brief, taskId, watchdog, timeoutMs, supervision, options));
982
1003
  try {
983
1004
  await Promise.race([attempt, watchdog.stalled]);
984
1005
  }
@@ -1413,6 +1434,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1413
1434
  spec,
1414
1435
  workPackage,
1415
1436
  failureOutput: diagnosticFailure,
1437
+ gateCommand: workPackage.diagnostic_context?.gate_command ?? null,
1416
1438
  verificationCommands: attemptBrief?.verification ?? [],
1417
1439
  })
1418
1440
  : buildAssignmentPrompt({
@@ -2300,9 +2322,12 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
2300
2322
  // that never answered.
2301
2323
  if (stalledAttempts.delete(active.attemptId))
2302
2324
  retainAttemptWorktree = true;
2303
- attemptDisposers.delete(taskId);
2325
+ // Only this run's own handles: the task's next attempt may already have set its own.
2326
+ if (attemptDisposers.get(taskId) === disposeTimers)
2327
+ attemptDisposers.delete(taskId);
2304
2328
  disposeTimers();
2305
- slotAborts.delete(taskId);
2329
+ if (slotAborts.get(taskId) === abort)
2330
+ slotAborts.delete(taskId);
2306
2331
  if (retainAttemptWorktree) {
2307
2332
  console.log(`Retained failed-attempt worktree for Conductor diagnosis at ${attemptWorkspace}`);
2308
2333
  }
@@ -3096,11 +3121,19 @@ async function endAttemptForChangedFact(client, workspace, taskId, fact, observe
3096
3121
  }
3097
3122
  console.error(`Assignment ${taskId} ended: ${redactSecrets(detail)}`);
3098
3123
  }
3099
- async function queueTerminal(client, taskId, terminal) {
3124
+ async function queueTerminal(client, taskId, reported) {
3125
+ // Every terminal of every path passes here: its prose, envelope, signal tails and delivery packet
3126
+ // carry agent and gate output, so the whole report is redacted once, before it is stored or sent.
3127
+ const terminal = redactSecretsDeep(reported);
3128
+ const active = client.attempt(taskId);
3129
+ const owner = runningAttempt.getStore();
3130
+ if (owner && owner !== active.attemptId) {
3131
+ console.error(`Assignment ${taskId}: a run for attempt ${owner} answered after attempt ${active.attemptId} took the task; its report is dropped.`);
3132
+ return { accepted: false, status: "superseded" };
3133
+ }
3100
3134
  // Every terminal of every path passes here, so this is where the attempt's clock learns that the
3101
3135
  // work is over and only the report is left.
3102
3136
  attemptWatchdogs.get(taskId)?.phase("terminal");
3103
- const active = client.attempt(taskId);
3104
3137
  let executionFacts = active.executionFacts;
3105
3138
  if (executionFacts && executionFacts.final_commit === null && active.worktreePath) {
3106
3139
  const finalCommit = await headCommitOrNull(active.worktreePath);
@@ -5,12 +5,12 @@
5
5
  * held to repo_read + test_run by the same driver projection that enforces read-only diagnosis, and
6
6
  * the worktree it reads is deleted whichever way the run ends.
7
7
  */
8
+ import { redactSecrets } from "./secret-redaction.js";
8
9
  import { z } from "zod";
9
10
  import { boundedTail, runBoundedVerificationCommand } from "./ensure-test-evidence.js";
10
11
  import { createAttemptWorktree, removeAttemptWorktree } from "./attempt-worktree.js";
11
12
  import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl } from "./brief.js";
12
13
  import { headCommitOrNull } from "./git.js";
13
- import { redactSecrets } from "./config.js";
14
14
  import { learnDriverFuel, resolveAssignmentModel, changedPathsSince, runDeliveryVerificationGates, discardWorktreeWrites } from "./execution.js";
15
15
  import { DRIVERS, extractAgentReportJsonText, parseJsonObjectCandidate } from "./driver.js";
16
16
  import { pickDriverForClaim, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
@@ -52,6 +52,8 @@ export const investigationAssignmentSchema = z.object({
52
52
  acceptance: z.array(z.string().trim().min(1).max(4_000)).max(20).optional().default([]),
53
53
  /** Delivery verification: the brief's `[sN]` / `[kN]` lines. Absent or [] on a plain investigation. */
54
54
  brief_keys: z.array(z.string().trim().min(1).max(4_000)).max(40).optional().default([]),
55
+ /** Delivery verification: the owner's returns on this delivery, which amend the brief. */
56
+ owner_returns: z.array(z.string().trim().min(1).max(4_000)).max(10).optional().default([]),
55
57
  });
56
58
  /**
57
59
  * Enforcement is a vendor's claim; verification is ours. Both run on every investigation — this only
@@ -145,6 +147,10 @@ export function buildDeliveryVerificationPrompt(assignment, commit, context) {
145
147
  : "REGISTERED COMMANDS\n- none: this repository registers no verification command, so judge from the files only";
146
148
  const keys = (assignment.brief_keys ?? []).map((key) => key.trim()).filter(Boolean);
147
149
  const briefKeysBlock = keys.length ? `BRIEF KEYS\n${keys.join("\n")}` : "";
150
+ const returns = (assignment.owner_returns ?? []).map((text) => text.trim()).filter(Boolean);
151
+ const ownerReturnsBlock = returns.length
152
+ ? `OWNER RETURNS — the product owner's decisions on this delivery, in order. They amend the criteria and the brief: where a return changes a rule, judge and probe the rule as returned, never the original wording.\n${returns.map((text, index) => `${index + 1}. ${text}`).join("\n")}`
153
+ : "";
148
154
  const probeBlock = keys.length ? DELIVERY_VERIFICATION_PROBE_INSTRUCTION : "";
149
155
  return [
150
156
  "You are Conductor's independent delivery verification. A coding agent delivered this commit against approved acceptance criteria; you check it the way a careful reviewer would, and you never take the agent's word for anything.",
@@ -161,6 +167,7 @@ export function buildDeliveryVerificationPrompt(assignment, commit, context) {
161
167
  `PINNED COMMIT ${commit}`,
162
168
  commands,
163
169
  assignment.question,
170
+ ...(ownerReturnsBlock ? [ownerReturnsBlock] : []),
164
171
  ...(briefKeysBlock ? [briefKeysBlock] : []),
165
172
  ...(probeBlock ? [probeBlock] : []),
166
173
  `BUDGET\nAt most ${assignment.budget.max_files} files read in full; the change summary and command output are not counted.`,
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The one list of secret shapes, for the Bridge and the control plane.
3
+ *
4
+ * The Bridge source is packages/conduit-bridge/src/secret-redaction.ts. The control-plane copy at
5
+ * src/conductor/secret-redaction.ts is generated by `npm run sync:twins`. Do not edit that copy.
6
+ * Bridge cannot import from the control plane, and the other way round.
7
+ *
8
+ * Three lists used to exist (Bridge terminal, control-plane logs, forge diffs), and each missed
9
+ * shapes another caught. An agent that prints its environment on failure must not leak a provider,
10
+ * forge, cloud or Conduit credential through any of them.
11
+ */
12
+ export function redactSecrets(value) {
13
+ return value
14
+ .replace(/-----BEGIN [^-]+-----[\s\S]*?-----END [^-]+-----/g, "[redacted private key]")
15
+ .replace(/(authorization\s*:\s*bearer\s+)[^\s]+/gi, "$1[redacted]")
16
+ .replace(/Bearer\s+\S+/g, "Bearer [redacted]")
17
+ .replace(/((?:api[_-]?key|access[_-]?token|client[_-]?secret|password|secret)\s*[:=]\s*)["']?[^\s"']+/gi, "$1[redacted]")
18
+ .replace(/(runner_sk_|gateway_sk_|lease_|connect_)[A-Za-z0-9-]+/g, "$1[redacted]")
19
+ .replace(/sk-[A-Za-z0-9_-]{8,}/g, "sk-[redacted]")
20
+ .replace(/xai-[A-Za-z0-9]+/g, "xai-[redacted]")
21
+ .replace(/AIza[0-9A-Za-z_-]{35}/g, "AIza[redacted]")
22
+ .replace(/(gh[pousr]_|github_pat_|glpat-|xox[baprs]-)[A-Za-z0-9_-]+/g, "$1[redacted]")
23
+ .replace(/AKIA[0-9A-Z]{16}/g, "AKIA[redacted]");
24
+ }
25
+ /** Every string inside a JSON-shaped value, redacted; the shape is kept. */
26
+ export function redactSecretsDeep(value) {
27
+ if (typeof value === "string")
28
+ return redactSecrets(value);
29
+ if (Array.isArray(value))
30
+ return value.map((item) => redactSecretsDeep(item));
31
+ if (value && typeof value === "object") {
32
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactSecretsDeep(item)]));
33
+ }
34
+ return value;
35
+ }
@@ -56,6 +56,8 @@ export const workPackageSchema = z.object({
56
56
  diagnostic_context: z.object({
57
57
  source_attempt_id: z.string().uuid(),
58
58
  failure_output: z.string().min(1).max(12_000),
59
+ /** The gate Bridge re-runs, from the failed attempt's verification signal; null names no gate. */
60
+ gate_command: z.string().min(1).max(2_000).nullable(),
59
61
  }).strict().optional(),
60
62
  /** Working/Delivery language for Bridge assignment prose. */
61
63
  working_language: z.enum(["en", "zh"]).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.16.137",
3
+ "version": "0.16.140",
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": {