@haven_ai/connect 0.0.0-dev.202609181807.87b28b1 → 0.0.0-dev.202609181927.cd4620a

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 CHANGED
@@ -87,13 +87,39 @@ every credential directory on the machine and classifies each one:
87
87
  | `retired` | Tombstoned (see below); key material removed. |
88
88
  | `orphaned` | No usable identity and no tombstone. |
89
89
 
90
- The exit code is non-zero if **any** wired agent fails **any** check — not
90
+ The exit code is non-zero if **any** wired agent fails **any** check
91
+ (`level: failed` — see the next section; an advisory does not count) — not
91
92
  only the one the report's main section describes. In `--json`, the same
92
93
  information is on `agents[]`, each entry carrying `slug`, `agentId`,
93
94
  `directory`, `classification` and its own `checks[]`; the flat `checks` array
94
95
  is retained and still describes one agent, so a single-agent install reads as
95
96
  it always did.
96
97
 
98
+ ### Three verdicts, not two (#3121)
99
+
100
+ Every check carries a `level` — `ok`, `advisory` or `failed` — and the report
101
+ carries the rolled-up `level` over the flat checks and every wired agent's
102
+ checks. Only `failed` reaches the exit code; an advisory is printed with a `!`
103
+ marker, gets its own summary line ("No failures. N advisory finding(s)") and
104
+ exits 0. `ok` on a check and on the report is kept for `--json` consumers and
105
+ means "nothing is broken": it is `true` for `ok` and `advisory`, `false` only
106
+ for `failed`, so `report.ok` is always the exit code's predicate. The doctor
107
+ report stays `version: 1`; `level` is additive.
108
+
109
+ | Level | Meaning | Example |
110
+ | --- | --- | --- |
111
+ | `ok` | Nothing to say. | The installed signer matches the connector's pin. |
112
+ | `advisory` | Worth reading; nothing is broken. Exit 0. | `signer_runtime`: the install is intact but behind the pinned version ("intact, but outdated" — both versions named, `--repair` offered). `superseded_agents` on a recognised runtime with no config file the connector can read (Claude Code, `other`): a second live key is reported, and the check says why "wired" cannot be verified from this machine. |
113
+ | `failed` | A real failure with one repair action. Exit 1. | A stale or empty runtime directory; a live key in a directory the runtime's config demonstrably does not use. |
114
+
115
+ What stays blocking is live spend authority: a directory whose stored key
116
+ still authenticates and whose classification is not `wired` fails the run, for
117
+ every non-wired classification, whenever the runtime config could be read. The
118
+ classification itself does not change on Claude Code — only the severity of
119
+ the check that reads it — so `agents[]` reads as before. A runtime string the
120
+ connector does not recognise (`--runtime codex-clii`) is not "no config": it
121
+ fails `runtime_config` naming the allowed values, and demotes nothing.
122
+
97
123
  One check is worth calling out: **`identity_match`** compares the agent the
98
124
  stored API key actually authenticates as against the `delegate_address` in
99
125
  that directory's `signer.json`. A mismatch means the runtime would quote as
@@ -496,9 +522,9 @@ the agent credential files, the pinned signer runtime install (and, since
496
522
  see the last section of this file), the hosted MCP
497
523
  (authorized `tools/list`), and starts the local signer for a real stdio
498
524
  handshake — reporting its advertised compat versions. Every failing check
499
- prints one concrete repair action; the exit code is non-zero on any failure.
500
- Add `--json` for a machine-readable report. No secret material is ever
501
- printed.
525
+ prints one concrete repair action; the exit code is non-zero on any failure
526
+ and zero on an advisory (see "Three verdicts, not two" above). Add `--json`
527
+ for a machine-readable report. No secret material is ever printed.
502
528
 
503
529
  `--doctor` also probes every OTHER agent credential directory it did not
504
530
  select (#1688). A re-run of setup mints a NEW agent and, unless it ran with
package/dist/cli.cjs CHANGED
@@ -567,9 +567,9 @@ var init_runtime_manifest = __esm({
567
567
  mcpPackage: "@haven_ai/mcp",
568
568
  mcpVersion: mcp.MCP_VERSION,
569
569
  sdkPackage: "@haven_ai/sdk",
570
- sdkVersion: "0.0.0-dev.202609181807.87b28b1",
570
+ sdkVersion: "0.0.0-dev.202609181927.cd4620a",
571
571
  signerPackage: "@haven_ai/signer",
572
- signerVersion: "0.0.0-dev.202609181807.87b28b1",
572
+ signerVersion: "0.0.0-dev.202609181927.cd4620a",
573
573
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
574
574
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
575
575
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -3527,9 +3527,18 @@ var init_rekey_restart = __esm({
3527
3527
  var doctor_exports = {};
3528
3528
  __export(doctor_exports, {
3529
3529
  describeAccountAddressKey: () => describeAccountAddressKey,
3530
+ rollUpLevel: () => rollUpLevel,
3530
3531
  runDoctor: () => runDoctor,
3531
3532
  runRepair: () => runRepair
3532
3533
  });
3534
+ function finalizeCheck(check) {
3535
+ return { ...check, ok: check.level !== "failed" };
3536
+ }
3537
+ function rollUpLevel(checks) {
3538
+ if (checks.some((check) => check.level === "failed")) return "failed";
3539
+ if (checks.some((check) => check.level === "advisory")) return "advisory";
3540
+ return "ok";
3541
+ }
3533
3542
  function runtimeFlagFor(runtime) {
3534
3543
  return normalizeRuntimeName(runtime) ? ` --runtime ${runtime}` : "";
3535
3544
  }
@@ -3604,13 +3613,16 @@ function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bar
3604
3613
  return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
3605
3614
  }
3606
3615
  function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
3616
+ return finalizeCheck(rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug));
3617
+ }
3618
+ function rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug) {
3607
3619
  const label = "Pending re-key";
3608
3620
  const nameFlag = slug ? ` --name ${slug}` : "";
3609
3621
  if (status.state === "unreadable") {
3610
3622
  return {
3611
3623
  id: "rekey_pending",
3612
3624
  label,
3613
- ok: false,
3625
+ level: "failed",
3614
3626
  detail: `A re-key was started here but ${status.path} does not parse, so neither the address it generated nor when it started can be read. The file still holds what was a private key.`,
3615
3627
  repair: `Delete ${status.path}, then start again: ${RERUN} --rekey${nameFlag}`
3616
3628
  };
@@ -3622,7 +3634,7 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
3622
3634
  return {
3623
3635
  id: "rekey_pending",
3624
3636
  label,
3625
- ok: false,
3637
+ level: "failed",
3626
3638
  detail: `A re-key started ${started} has COMPLETED on Haven \u2014 the agent's signing address is already ${address}, the one this machine generated \u2014 but the local half was never finished, so the credential files here still hold the old key. Parked at ${status.path}.` + (status.state === "expired" ? " The local file is also past its 24h TTL, which --rekey-finish refuses, so the finish command below will not accept it any more." : ""),
3627
3639
  repair: status.state === "expired" ? `The parked key expired. Start again \u2014 ${RERUN} --rekey${nameFlag} \u2014 and re-run "Replace signing key" on the Haven agent page with the new address it prints.` : `Run: ${RERUN} --rekey-finish${nameFlag} --api-key <the key the agent page showed you>${runtimeFlagFor(runtime)}`
3628
3640
  };
@@ -3632,7 +3644,7 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
3632
3644
  return {
3633
3645
  id: "rekey_pending",
3634
3646
  label,
3635
- ok: false,
3647
+ level: "failed",
3636
3648
  detail: `A re-key started ${started} EXPIRED ${status.expiresAt ?? ""} without being finished. Its address was ${address}; the private half it generated is still on disk at ${status.path}. ` + wedgeNote,
3637
3649
  repair: `Either delete ${status.path} to drop the parked key, or start over: ${RERUN} --rekey${nameFlag}. Connect never deletes it for you \u2014 an expired TTL is a refusal to USE the key, not a licence to destroy key material you may still be mid-flow on.`
3638
3650
  };
@@ -3640,7 +3652,7 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
3640
3652
  return {
3641
3653
  id: "rekey_pending",
3642
3654
  label,
3643
- ok: true,
3655
+ level: "ok",
3644
3656
  detail: `A re-key started ${started} is still open (expires ${status.expiresAt ?? "unknown"}). Paste this address into "Replace signing key" on the Haven agent page: ${address}. Parked at ${status.path}. ` + wedgeNote
3645
3657
  };
3646
3658
  }
@@ -3668,13 +3680,13 @@ async function runtimeSpecOverrideCheck(directory, sidecar, env) {
3668
3680
  if (shell) facts.push(shell);
3669
3681
  if (facts.length === 0) return void 0;
3670
3682
  const variables = Object.values(RUNTIME_SPEC_ENV).join(" / ");
3671
- return {
3683
+ return finalizeCheck({
3672
3684
  id: "runtime_spec_override",
3673
3685
  label: "Runtime spec override",
3674
- ok: false,
3686
+ level: "failed",
3675
3687
  detail: `runtime spec overridden \u2014 not the pinned manifest (${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}, ${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}). ${facts.join(". ")}.`,
3676
3688
  repair: `Developer override (#2424). To return to the pinned manifest: unset ${variables}, then run ${RERUN} --doctor --repair --runtime <runtime>. If the override is intentional, this finding is the record of it.`
3677
- };
3689
+ });
3678
3690
  }
3679
3691
  async function readMcpSidecarOverride(directory) {
3680
3692
  try {
@@ -3701,6 +3713,13 @@ function describeAccountAddressKey(identity, signerFile) {
3701
3713
  return "no account address stored";
3702
3714
  }
3703
3715
  async function checksForAgent(entry, input, deps) {
3716
+ const verdicts = await verdictsForAgent(entry, input, deps);
3717
+ return {
3718
+ checks: verdicts.checks.map(finalizeCheck),
3719
+ ...verdicts.signerCapabilities ? { signerCapabilities: verdicts.signerCapabilities } : {}
3720
+ };
3721
+ }
3722
+ async function verdictsForAgent(entry, input, deps) {
3704
3723
  const { directory, identity, sidecar } = entry;
3705
3724
  const checks = [];
3706
3725
  let signerCapabilities;
@@ -3715,7 +3734,7 @@ async function checksForAgent(entry, input, deps) {
3715
3734
  checks.push({
3716
3735
  id: "credentials",
3717
3736
  label: "Agent credentials",
3718
- ok: credentialsOk,
3737
+ level: credentialsOk ? "ok" : "failed",
3719
3738
  detail: credentialsOk ? `identity.json and signer.json parse (agent ${identity?.agent_id ?? "unknown"}; ${describeAccountAddressKey(identity, signerFile)})` : "identity.json or signer.json is missing or unparseable.",
3720
3739
  ...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
3721
3740
  });
@@ -3723,7 +3742,7 @@ async function checksForAgent(entry, input, deps) {
3723
3742
  checks.push({
3724
3743
  id: "signer_runtime",
3725
3744
  label: "Signer runtime (preinstalled wrapper)",
3726
- ok: false,
3745
+ level: "failed",
3727
3746
  detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
3728
3747
  repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}`
3729
3748
  });
@@ -3735,7 +3754,7 @@ async function checksForAgent(entry, input, deps) {
3735
3754
  checks.push({
3736
3755
  id: "signer_runtime",
3737
3756
  label: "Signer runtime (preinstalled wrapper)",
3738
- ok: matches,
3757
+ level: matches ? "ok" : "failed",
3739
3758
  detail: matches ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory} (override install \u2014 see runtime_spec_override)` : `Override runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
3740
3759
  ...matches ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)} with the same HAVEN_*_SPEC variables set.` }
3741
3760
  });
@@ -3749,7 +3768,7 @@ async function checksForAgent(entry, input, deps) {
3749
3768
  checks.push({
3750
3769
  id: "signer_runtime",
3751
3770
  label: "Signer runtime (preinstalled wrapper)",
3752
- ok,
3771
+ level: ok ? "ok" : intact ? "advisory" : "failed",
3753
3772
  detail: ok ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory}` : intact ? `Installed version ${sidecar.signer_version} does not match the connector's pinned ${MCP_RUNTIME_MANIFEST.signerVersion} \u2014 intact, but outdated.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
3754
3773
  ...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}` }
3755
3774
  });
@@ -3762,7 +3781,7 @@ async function checksForAgent(entry, input, deps) {
3762
3781
  checks.push({
3763
3782
  id: "hosted_mcp",
3764
3783
  label: "Hosted Haven MCP",
3765
- ok: probe.status === "ok",
3784
+ level: probe.status === "ok" ? "ok" : "failed",
3766
3785
  detail: probe.status === "ok" ? `MCP tools endpoint is reachable (${hostedUrl}).` : `MCP tools endpoint probe failed: ${probe.status} (${hostedUrl}).`,
3767
3786
  ...probe.status === "ok" ? {} : {
3768
3787
  repair: "Check network access and runtime configuration for the hosted MCP URL, then re-run --doctor."
@@ -3772,7 +3791,7 @@ async function checksForAgent(entry, input, deps) {
3772
3791
  checks.push({
3773
3792
  id: "hosted_mcp",
3774
3793
  label: "Hosted Haven MCP",
3775
- ok: false,
3794
+ level: "failed",
3776
3795
  detail: "No stored API key / hosted MCP URL to probe with.",
3777
3796
  repair: `Re-run the full setup: ${RERUN} --setup <token>.`
3778
3797
  });
@@ -3790,7 +3809,7 @@ async function checksForAgent(entry, input, deps) {
3790
3809
  checks.push({
3791
3810
  id: "identity_match",
3792
3811
  label: "Hosted identity matches the local signing key",
3793
- ok: false,
3812
+ level: "failed",
3794
3813
  detail: probe.status === "unauthorized" ? "The stored API key was rejected, so the agent it authenticates as cannot be compared with the local signing key." : `Could not read the hosted identity (${probe.status}) \u2014 the comparison did not happen, so it cannot be reported as a match.`,
3795
3814
  repair: probe.status === "unauthorized" ? `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` : `Restore network access to the Haven API, then re-run: ${RERUN} --doctor${runtimeFlagFor(input.runtime)}`
3796
3815
  });
@@ -3798,7 +3817,7 @@ async function checksForAgent(entry, input, deps) {
3798
3817
  checks.push({
3799
3818
  id: "identity_match",
3800
3819
  label: "Hosted identity matches the local signing key",
3801
- ok: false,
3820
+ level: "failed",
3802
3821
  detail: "signer.json holds no delegate_address to compare against the hosted identity.",
3803
3822
  repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
3804
3823
  });
@@ -3807,7 +3826,7 @@ async function checksForAgent(entry, input, deps) {
3807
3826
  checks.push({
3808
3827
  id: "identity_match",
3809
3828
  label: "Hosted identity matches the local signing key",
3810
- ok: same,
3829
+ level: same ? "ok" : "failed",
3811
3830
  detail: same ? `The stored API key authenticates as the agent whose signing key is in this directory (${shortAddress(localDelegate)}).` : `MISMATCH: the stored API key authenticates as agent ${probe.agentId ?? "unknown"} with delegate ${shortAddress(probe.delegateAddress ?? "unknown")}, but signer.json here holds ${shortAddress(localDelegate)}. This runtime would quote as one agent and sign as another.`,
3812
3831
  ...same ? {} : {
3813
3832
  repair: `Re-run setup for this agent so its API key and signing key come from one run: ${RERUN} --setup <token>. Do not hand-edit either file.`
@@ -3825,7 +3844,7 @@ async function checksForAgent(entry, input, deps) {
3825
3844
  checks.push({
3826
3845
  id: "signer_process",
3827
3846
  label: "Signer stdio handshake",
3828
- ok: false,
3847
+ level: "failed",
3829
3848
  detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
3830
3849
  repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original connector command with --ack-local-tools).`
3831
3850
  });
@@ -3842,7 +3861,7 @@ async function checksForAgent(entry, input, deps) {
3842
3861
  checks.push({
3843
3862
  id: "signer_process",
3844
3863
  label: "Signer stdio handshake",
3845
- ok: probe.status === "ok",
3864
+ level: probe.status === "ok" ? "ok" : "failed",
3846
3865
  detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
3847
3866
  ...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}` }
3848
3867
  });
@@ -3851,7 +3870,7 @@ async function checksForAgent(entry, input, deps) {
3851
3870
  checks.push({
3852
3871
  id: "signer_process",
3853
3872
  label: "Signer stdio handshake",
3854
- ok: false,
3873
+ level: "failed",
3855
3874
  detail: "Skipped \u2014 no prepared signer runtime to probe.",
3856
3875
  repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}`
3857
3876
  });
@@ -3866,7 +3885,8 @@ async function runDoctor(input, deps = {}) {
3866
3885
  const resolution = await resolveDoctorRuntime(input, directory);
3867
3886
  const runtime = resolution.runtime;
3868
3887
  const input2 = { ...input, runtime };
3869
- const configPath = runtimeConfigPathFor(input2.runtime, homeDir);
3888
+ const normalizedRuntime = normalizeRuntimeName(input2.runtime);
3889
+ const configPath = runtimeConfigPathFor(normalizedRuntime ?? input2.runtime, homeDir);
3870
3890
  let configText = null;
3871
3891
  if (configPath !== null) {
3872
3892
  try {
@@ -3937,7 +3957,7 @@ async function runDoctor(input, deps = {}) {
3937
3957
  checks.push({
3938
3958
  id: "credentials",
3939
3959
  label: "Agent credentials",
3940
- ok: false,
3960
+ level: "failed",
3941
3961
  detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
3942
3962
  repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
3943
3963
  });
@@ -3958,11 +3978,20 @@ async function runDoctor(input, deps = {}) {
3958
3978
  if (check) checks.push(check);
3959
3979
  }
3960
3980
  }
3961
- if (configPath === null && input2.runtime === "") {
3981
+ const runtimeOwnsNoConfig = normalizedRuntime !== null && configPath === null;
3982
+ if (configPath === null && input2.runtime !== "" && normalizedRuntime === null) {
3962
3983
  checks.push({
3963
3984
  id: "runtime_config",
3964
3985
  label: "Runtime MCP config",
3965
- ok: false,
3986
+ level: "failed",
3987
+ detail: `Runtime '${input2.runtime}' is not one the connector recognises. The runtime config was NOT checked. Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`,
3988
+ repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
3989
+ });
3990
+ } else if (configPath === null && input2.runtime === "") {
3991
+ checks.push({
3992
+ id: "runtime_config",
3993
+ label: "Runtime MCP config",
3994
+ level: "failed",
3966
3995
  detail: `Runtime is unknown \u2014 no runtime flag was given and the connector's record in ${primaryDirectory ?? input.credentialsDir ?? "~/.haven/agents"} carries no resolvable ${CONNECT_OUTCOME_FILENAME} runtime. The runtime config was NOT checked. Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`,
3967
3996
  repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
3968
3997
  });
@@ -3970,14 +3999,14 @@ async function runDoctor(input, deps = {}) {
3970
3999
  checks.push({
3971
4000
  id: "runtime_config",
3972
4001
  label: "Runtime MCP config",
3973
- ok: true,
4002
+ level: "ok",
3974
4003
  detail: `Runtime '${input2.runtime}' is configured through its own CLI or by hand (${input2.runtime === "other" ? "manual runtime" : "CLI-managed"}) and has no file-based config the connector owns \u2014 skipping the file check.`
3975
4004
  });
3976
4005
  } else if (configText === null) {
3977
4006
  checks.push({
3978
4007
  id: "runtime_config",
3979
4008
  label: "Runtime MCP config",
3980
- ok: false,
4009
+ level: "failed",
3981
4010
  detail: `No runtime config at ${configPath}.`,
3982
4011
  repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input2.runtime)}`
3983
4012
  });
@@ -3991,7 +4020,7 @@ async function runDoctor(input, deps = {}) {
3991
4020
  checks.push({
3992
4021
  id: "runtime_config",
3993
4022
  label: "Runtime MCP config",
3994
- ok,
4023
+ level: ok ? "ok" : "failed",
3995
4024
  detail: ok ? `Config at ${configPath} references the hosted server and the prepared signer wrapper.` : signerViaNpx ? `Config at ${configPath} still launches the signer via npx \u2014 the pre-#1586 shape that cannot start under a 120s startup timeout.` : `Config at ${configPath} is missing the Haven entries${primarySidecar && !wrapperReferenced ? " (or references a different signer wrapper)" : ""}.`,
3996
4025
  ...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input2.runtime)}` }
3997
4026
  });
@@ -4040,13 +4069,17 @@ async function runDoctor(input, deps = {}) {
4040
4069
  );
4041
4070
  }
4042
4071
  const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
4072
+ const classificationUnreliable = configText === null && runtimeOwnsNoConfig;
4073
+ const supersededLevel = supersededLive.length === 0 ? "ok" : classificationUnreliable ? "advisory" : "failed";
4043
4074
  checks.push({
4044
4075
  id: "superseded_agents",
4045
4076
  label: "Superseded agent credentials",
4046
- ok: supersededLive.length === 0,
4047
- detail: supersededLive.length > 0 ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. A host started before your latest setup keeps authenticating (and spending) as the old agent.` : `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}.`,
4048
- ...supersededLive.length > 0 ? {
4077
+ level: supersededLevel,
4078
+ detail: supersededLevel === "failed" ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. A host started before your latest setup keeps authenticating (and spending) as the old agent.` : supersededLevel === "advisory" ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. Runtime '${input2.runtime}' has no config file the connector can read, so which of these agents are wired cannot be verified from this machine: a live key here may be an agent you use deliberately, or one a host started before your latest setup is still spending as.` : `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}.`,
4079
+ ...supersededLevel === "failed" ? {
4049
4080
  repair: `Revoke ${supersededLive.join(", ")} on the Haven agent page, then remove the old director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
4081
+ } : supersededLevel === "advisory" ? {
4082
+ repair: `Check ${supersededLive.join(", ")} on the Haven agent page: revoke the ones you no longer use, then remove their director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
4050
4083
  } : {}
4051
4084
  });
4052
4085
  }
@@ -4057,7 +4090,7 @@ async function runDoctor(input, deps = {}) {
4057
4090
  checks.push({
4058
4091
  id: "rekey_pending_elsewhere",
4059
4092
  label: "Parked re-keys in other credential directories",
4060
- ok: abandoned.length === 0,
4093
+ level: abandoned.length === 0 ? "ok" : "failed",
4061
4094
  detail: abandoned.length > 0 ? `ABANDONED re-key key material outside the agent this report describes: ${abandoned.map(describe).join(", ")}. Each holds a private key that was generated for a re-key nobody finished.` : `${parkedElsewhere.length} other director(y/ies) hold an open pending re-key: ${parkedElsewhere.map(describe).join(", ")}.`,
4062
4095
  ...abandoned.length > 0 ? {
4063
4096
  repair: "Check the Haven agent page for each before deleting: if its on-chain revoke already ran, the agent has no spend authority until you re-grant it (#1868), and that is not visible from this machine. Connect never deletes key material for you."
@@ -4070,16 +4103,19 @@ async function runDoctor(input, deps = {}) {
4070
4103
  checks.push({
4071
4104
  id: "restart",
4072
4105
  label: "Runtime restart",
4073
- ok: true,
4106
+ level: "ok",
4074
4107
  detail: restart ? "This runtime loads MCP config at startup \u2014 restart it after any repair before expecting the tools to appear." : input2.runtime === "" ? "Runtime is unknown \u2014 whether a restart is needed cannot be determined. Re-run the doctor naming the runtime for a definitive answer." : "No restart requirement known for this runtime."
4075
4108
  });
4076
- const wiredOk = inventory.filter((entry) => entry.classification === "wired").every((entry) => entry.checks.every((check) => check.ok));
4109
+ const wiredChecks = inventory.filter((entry) => entry.classification === "wired").flatMap((entry) => entry.checks);
4110
+ const finalChecks = checks.map(finalizeCheck);
4111
+ const level = rollUpLevel([...finalChecks, ...wiredChecks]);
4077
4112
  return {
4078
4113
  version: 1,
4079
- ok: checks.every((check) => check.ok) && wiredOk,
4114
+ ok: level !== "failed",
4115
+ level,
4080
4116
  runtime: input2.runtime,
4081
4117
  credentialDirectory: primaryDirectory,
4082
- checks,
4118
+ checks: finalChecks,
4083
4119
  agents: inventory,
4084
4120
  ...signerCapabilities ? { signerCapabilities } : {}
4085
4121
  };
@@ -4121,7 +4157,7 @@ async function runRepair(input, deps = {}) {
4121
4157
  if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
4122
4158
  return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
4123
4159
  }
4124
- const configPath = runtimeConfigPathFor(input2.runtime, homeDir);
4160
+ const configPath = runtimeConfigPathFor(normalizeRuntimeName(input2.runtime) ?? input2.runtime, homeDir);
4125
4161
  if (configPath) {
4126
4162
  try {
4127
4163
  const existing = await promises.readFile(configPath, "utf8");
@@ -4148,7 +4184,10 @@ async function runRepair(input, deps = {}) {
4148
4184
  const names = serverNamesFor(serverName);
4149
4185
  messages.push(`Rewriting MCP entries ${names.hosted} / ${names.signer}${serverName ? ` (agent "${serverName}")` : " (unnamed pair)"} \u2014 no other pair is touched.`);
4150
4186
  const configResult = await writeRuntimeConfig({
4151
- runtime: input2.runtime,
4187
+ // Normalized for the WRITE too (#3145 review round 3): `writeRuntimeConfig`
4188
+ // switches on the id, and the raw alias fell to its "manual runtime" arm
4189
+ // — a repair that reported success while writing nothing.
4190
+ runtime: normalizeRuntimeName(input2.runtime) ?? input2.runtime,
4152
4191
  hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
4153
4192
  apiKey: identity.api_key,
4154
4193
  identityPath: path.join(directory, "identity.json"),
@@ -4479,7 +4518,7 @@ async function pathExists2(path) {
4479
4518
  init_unwire();
4480
4519
  init_local_mcp_runtime();
4481
4520
  init_runtime_manifest();
4482
- var CONNECTOR_VERSION = "0.0.0-dev.202609181807.87b28b1";
4521
+ var CONNECTOR_VERSION = "0.0.0-dev.202609181927.cd4620a";
4483
4522
  var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
4484
4523
  var failureOutcomesByError = /* @__PURE__ */ new WeakMap();
4485
4524
  function failureOutcomeFor(runtimeHint, error) {
@@ -5377,7 +5416,7 @@ function helpText() {
5377
5416
  " Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
5378
5417
  " --json Emit one versioned, secret-free result object on stdout; progress stays on stderr.",
5379
5418
  " --doctor Diagnose an existing setup (read-only, no token): config, credentials,",
5380
- " signer runtime, hosted MCP, and a live signer handshake. Exits non-zero on any failure.",
5419
+ " signer runtime, hosted MCP, and a live signer handshake. Exits non-zero only on a failed check; an advisory (!) exits 0.",
5381
5420
  " --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
5382
5421
  " runtime, rewrite the wrapper and runtime config from stored credentials.",
5383
5422
  " Hosted topology only (refuses to touch a --local config). No keys, no token.",
@@ -5443,6 +5482,13 @@ function failSubcommand(io, json, err, envelope, fallback) {
5443
5482
  }
5444
5483
  return 1;
5445
5484
  }
5485
+ function levelMarker(level) {
5486
+ return level === "ok" ? "\u2713" : level === "advisory" ? "!" : "\u2717";
5487
+ }
5488
+ function advisoryCount(report) {
5489
+ const others = report.agents.filter((agent) => agent.classification === "wired" && agent.directory !== report.credentialDirectory).flatMap((agent) => agent.checks);
5490
+ return [...report.checks, ...others].filter((check) => check.level === "advisory").length;
5491
+ }
5446
5492
  async function runCli(argv, io = {
5447
5493
  stdout: (message) => process.stdout.write(message),
5448
5494
  stderr: (message) => process.stderr.write(message)
@@ -5649,7 +5695,7 @@ async function runCli(argv, io = {
5649
5695
  `);
5650
5696
  } else {
5651
5697
  for (const check of report.checks) {
5652
- io.stdout(redactSecrets(`${check.ok ? "\u2713" : "\u2717"} ${check.label}: ${check.detail}
5698
+ io.stdout(redactSecrets(`${levelMarker(check.level)} ${check.label}: ${check.detail}
5653
5699
  `));
5654
5700
  if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
5655
5701
  `));
@@ -5659,21 +5705,25 @@ async function runCli(argv, io = {
5659
5705
  io.stdout("\nOther agents on this machine:\n");
5660
5706
  for (const agent of otherAgents) {
5661
5707
  const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
5662
- const failed = agent.checks.filter((check) => !check.ok);
5663
- const verdict = agent.classification === "wired" ? failed.length === 0 ? "wired, all checks passed" : `wired, ${failed.length} check(s) FAILED` : agent.classification === "parked" ? "parked re-key only \u2014 no identity.json in this directory, but key material is still there" : agent.classification;
5664
- io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : "\u2022"} ${name}: ${verdict}
5708
+ const failed = agent.checks.filter((check) => check.level === "failed");
5709
+ const advised = agent.checks.filter((check) => check.level === "advisory");
5710
+ const verdict = agent.classification === "wired" ? failed.length > 0 ? `wired, ${failed.length} check(s) FAILED` : advised.length > 0 ? `wired, ${advised.length} advisory finding(s)` : "wired, all checks passed" : agent.classification === "parked" ? "parked re-key only \u2014 no identity.json in this directory, but key material is still there" : agent.classification;
5711
+ io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : advised.length > 0 ? "!" : "\u2022"} ${name}: ${verdict}
5665
5712
  `));
5666
- for (const check of failed) {
5667
- io.stdout(redactSecrets(` \u2717 ${check.label}: ${check.detail}
5713
+ for (const check of [...failed, ...advised]) {
5714
+ io.stdout(redactSecrets(` ${levelMarker(check.level)} ${check.label}: ${check.detail}
5668
5715
  `));
5669
5716
  if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
5670
5717
  `));
5671
5718
  }
5672
5719
  }
5673
5720
  }
5674
- io.stdout(report.ok ? "All checks passed.\n" : "One or more checks FAILED \u2014 see repairs above.\n");
5721
+ io.stdout(
5722
+ report.level === "failed" ? "One or more checks FAILED \u2014 see repairs above.\n" : report.level === "advisory" ? `No failures. ${advisoryCount(report)} advisory finding(s) \u2014 see the ! line(s) above.
5723
+ ` : "All checks passed.\n"
5724
+ );
5675
5725
  }
5676
- return report.ok ? 0 : 1;
5726
+ return report.level === "failed" ? 1 : 0;
5677
5727
  } catch (err) {
5678
5728
  return failSubcommand(io, parsed.json, err, { doctor: "failed" }, {
5679
5729
  code: "doctor_failed",