crewhaus 0.1.3

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 ADDED
@@ -0,0 +1,84 @@
1
+ # crewhaus
2
+
3
+ The [factory](https://github.com/crewhaus/factory) command line: compile, run, eval, and deploy agent harness specs (`crewhaus.yaml`).
4
+
5
+ CrewHaus compiles one spec into the shape each situation calls for — a CLI agent, channel bot, RAG pipeline, multi-agent crew, eval bundle, and more. Every factory workflow starts with a `crewhaus` subcommand.
6
+
7
+ > This is the bare, unscoped `crewhaus` package. (The earlier `@crewhaus/cli` name is deprecated and points here; the supporting libraries remain scoped under `@crewhaus/*`.)
8
+
9
+ ## Install
10
+
11
+ The fastest path is the self-contained binary — one file, no Bun/Node required:
12
+
13
+ ```bash
14
+ brew install crewhaus/tap/crewhaus # macOS / Linux (Homebrew)
15
+ scoop install crewhaus # Windows (Scoop; see repo for the bucket)
16
+ winget install CrewHaus.CLI # Windows (winget)
17
+ # Debian / Ubuntu (apt): signed repo at https://crewhaus.github.io/apt
18
+ ```
19
+
20
+ Or install from npm — this package runs on [Bun](https://bun.sh) ≥ 1.2:
21
+
22
+ ```bash
23
+ npm install -g crewhaus # global
24
+ bun add -d crewhaus # project-local dev dependency
25
+ ```
26
+
27
+ Confirm it:
28
+
29
+ ```bash
30
+ crewhaus --version
31
+ ```
32
+
33
+ ## Quickstart
34
+
35
+ ```bash
36
+ # Scaffold a starter spec
37
+ crewhaus init my-agent
38
+ cd my-agent
39
+
40
+ # Inspect the lowered IR — a quick sanity check of what the compiler sees
41
+ crewhaus compile crewhaus.yaml --emit-ir
42
+
43
+ # Set a provider credential (the starter spec uses an Anthropic model;
44
+ # --model accepts other providers, e.g. openai/<m>, gemini/<m>, local/<m>@<url>)
45
+ export ANTHROPIC_API_KEY=sk-ant-...
46
+
47
+ # Compile in-memory and run the agent
48
+ crewhaus run crewhaus.yaml
49
+ ```
50
+
51
+ To emit a runnable bundle on disk instead, use `crewhaus compile crewhaus.yaml -o build`.
52
+
53
+ ## Subcommands
54
+
55
+ Run `crewhaus` with no arguments for the full usage text, or any subcommand with `--help` for its flags.
56
+
57
+ | Subcommand | What it does |
58
+ |---|---|
59
+ | `compile <spec.yaml> -o <out-dir>` | Compile a spec to a runnable bundle; `--emit-ir` prints the lowered IR as JSON instead |
60
+ | `run <spec.yaml>` | Compile in-memory and execute the agent (`--model`, `--resume <id>` / `--continue`, `--prompt <text>`, …) |
61
+ | `eval <spec.yaml> --dataset <data> --graders <graders.yaml>` | Run the agent against a dataset and grade (deterministic graders + LLM-as-judge) |
62
+ | `eval-report diff <prev> <new>` | Compare two eval runs and emit a diff report |
63
+ | `optimize <spec.yaml> --dataset <data> --graders <graders.yaml>` | Active eval-driven optimization; `--write-back` rewrites the spec in place |
64
+ | `init [name]` | Scaffold a new `crewhaus.yaml` |
65
+ | `doctor` | Check environment health |
66
+ | `context --bundle` | Emit a single-markdown orientation manifest |
67
+ | `cost-summary --session <id>` | Summarize cost-accrual events for a session |
68
+ | `secrets doctor` / `secrets rotate <name>` | List or rotate secrets via the configured backend |
69
+ | `spec put\|list\|get\|pin\|alias …` | Versioned spec storage |
70
+ | `deploy promote\|rollback …` | Re-pin a spec for an environment |
71
+ | `migrate-all --from N --to N` | Batch-migrate every spec in the registry |
72
+ | `build-image <target> --tag <tag>` | Build the docker image for a target shape |
73
+ | `cloud deploy` / `cloud teardown` | Deploy or tear down a managed CrewHaus cluster |
74
+ | `federation discover <deployment>` | Resolve a federated peer's endpoint and cert fingerprint |
75
+ | `sandbox doctor` | List registered sandbox images and healthcheck status |
76
+ | `compliance evidence` | Collect SOC 2 / ISO 27001 / HIPAA evidence |
77
+ | `version` | Print the CLI version (also `--version`, `-v`) |
78
+
79
+ ## Docs
80
+
81
+ - [Documentation](https://crewhaus.ai/docs) — start with the [quickstart](https://crewhaus.ai/docs/quickstart) and the [CLI target guide](https://crewhaus.ai/docs/targets/cli)
82
+ - [factory repository](https://github.com/crewhaus/factory) — architecture, contributing, and the full target-shape table
83
+
84
+ Apache-2.0.
package/package.json ADDED
@@ -0,0 +1,117 @@
1
+ {
2
+ "name": "crewhaus",
3
+ "version": "0.1.3",
4
+ "type": "module",
5
+ "description": "CrewHaus — the meta-harness compiler for AI agents. Compile one crewhaus.yaml spec into a CLI agent, channel bot, RAG pipeline, multi-agent crew, eval harness, voice or browser agent, and more.",
6
+ "keywords": [
7
+ "crewhaus",
8
+ "ai",
9
+ "agent",
10
+ "agents",
11
+ "llm",
12
+ "compiler",
13
+ "meta-harness",
14
+ "cli",
15
+ "claude",
16
+ "anthropic",
17
+ "rag",
18
+ "eval"
19
+ ],
20
+ "main": "src/index.ts",
21
+ "bin": {
22
+ "crewhaus": "src/index.ts"
23
+ },
24
+ "exports": {
25
+ ".": "./src/index.ts"
26
+ },
27
+ "scripts": {
28
+ "test": "bun test src"
29
+ },
30
+ "dependencies": {
31
+ "@crewhaus/adapter-anthropic": "0.1.3",
32
+ "@crewhaus/agent-context-isolation": "0.1.3",
33
+ "@crewhaus/audit-log": "0.1.3",
34
+ "@crewhaus/eval-optimizer-orchestrator": "0.1.3",
35
+ "@crewhaus/prompt-optimizer": "0.1.3",
36
+ "@crewhaus/prompt-optimizer-claude": "0.1.3",
37
+ "@crewhaus/justification-judge-claude": "0.1.3",
38
+ "@crewhaus/spec-patch": "0.1.3",
39
+ "@crewhaus/canary-controller": "0.1.3",
40
+ "@crewhaus/compiler": "0.1.3",
41
+ "@crewhaus/computer-use-driver": "0.1.3",
42
+ "@crewhaus/context-bundle": "0.1.3",
43
+ "@crewhaus/deployment-controller": "0.1.3",
44
+ "@crewhaus/egress-classifier": "0.1.3",
45
+ "@crewhaus/egress-matcher-semantic": "0.1.3",
46
+ "@crewhaus/embedder": "0.1.3",
47
+ "@crewhaus/errors": "0.1.3",
48
+ "@crewhaus/eval-dataset": "0.1.3",
49
+ "@crewhaus/eval-grader": "0.1.3",
50
+ "@crewhaus/eval-report": "0.1.3",
51
+ "@crewhaus/eval-runner": "0.1.3",
52
+ "@crewhaus/hooks-engine": "0.1.3",
53
+ "@crewhaus/infra-utils": "0.1.3",
54
+ "@crewhaus/ir": "0.1.3",
55
+ "@crewhaus/logging": "0.1.3",
56
+ "@crewhaus/mcp-host": "0.1.3",
57
+ "@crewhaus/model-router": "0.1.3",
58
+ "@crewhaus/migration-engine": "0.1.3",
59
+ "@crewhaus/migration-runner": "0.1.3",
60
+ "@crewhaus/permission-engine": "0.1.3",
61
+ "@crewhaus/run-context": "0.1.3",
62
+ "@crewhaus/runtime-core": "0.1.3",
63
+ "@crewhaus/secrets-manager": "0.1.3",
64
+ "@crewhaus/session-store": "0.1.3",
65
+ "@crewhaus/spec-registry": "0.1.3",
66
+ "@crewhaus/skills-registry": "0.1.3",
67
+ "@crewhaus/slash-commands": "0.1.3",
68
+ "@crewhaus/spec": "0.1.3",
69
+ "@crewhaus/sub-agent-spawner": "0.1.3",
70
+ "@crewhaus/trace-event-bus": "0.1.3",
71
+ "@crewhaus/tool-bash": "0.1.3",
72
+ "@crewhaus/tool-builder": "0.1.3",
73
+ "@crewhaus/tool-catalog": "0.1.3",
74
+ "@crewhaus/tool-codegraph": "0.1.3",
75
+ "@crewhaus/tool-fetch": "0.1.3",
76
+ "@crewhaus/tool-fs": "0.1.3",
77
+ "@crewhaus/tool-image": "0.1.3",
78
+ "@crewhaus/tool-image-generation": "0.1.3",
79
+ "@crewhaus/tool-document-ingest": "0.1.3",
80
+ "@crewhaus/tool-mcp": "0.1.3",
81
+ "@crewhaus/tool-mouse-keyboard": "0.1.3",
82
+ "@crewhaus/tool-navigate": "0.1.3",
83
+ "@crewhaus/tool-screen-capture": "0.1.3",
84
+ "@crewhaus/tool-task": "0.1.3",
85
+ "@crewhaus/tool-todo": "0.1.3",
86
+ "@crewhaus/tool-vision-grounding": "0.1.3",
87
+ "@crewhaus/tool-web": "0.1.3",
88
+ "@crewhaus/docker-images": "0.1.3",
89
+ "@crewhaus/crewhaus-cloud": "0.1.3",
90
+ "@crewhaus/federation-discovery": "0.1.3",
91
+ "@crewhaus/sandbox": "0.1.3",
92
+ "@crewhaus/sandbox-image-registry": "0.1.3",
93
+ "@crewhaus/compliance-controls": "0.1.3"
94
+ },
95
+ "devDependencies": {
96
+ "zod": "^3.23.8"
97
+ },
98
+ "license": "Apache-2.0",
99
+ "author": {
100
+ "name": "Max Meier",
101
+ "email": "max@crewhaus.ai",
102
+ "url": "https://crewhaus.ai"
103
+ },
104
+ "repository": {
105
+ "type": "git",
106
+ "url": "git+https://github.com/crewhaus/factory.git",
107
+ "directory": "apps/cli"
108
+ },
109
+ "homepage": "https://github.com/crewhaus/factory/tree/main/apps/cli#readme",
110
+ "bugs": {
111
+ "url": "https://github.com/crewhaus/factory/issues"
112
+ },
113
+ "publishConfig": {
114
+ "access": "public"
115
+ },
116
+ "files": ["src", "README.md", "LICENSE", "NOTICE"]
117
+ }
@@ -0,0 +1,122 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { buildCredentialChecks, extractSpecModel, selectedProvider } from "./doctor-checks";
3
+
4
+ const yamlFor = (model: string): string =>
5
+ `name: t\ntarget: cli\nagent:\n model: ${model}\n instructions: hi\n`;
6
+
7
+ describe("extractSpecModel", () => {
8
+ test("extracts agent.model from a valid spec", () => {
9
+ expect(extractSpecModel(yamlFor("openai/gpt-4o-mini"))).toBe("openai/gpt-4o-mini");
10
+ });
11
+
12
+ test("returns undefined for an unparseable spec (doctor must not crash)", () => {
13
+ expect(extractSpecModel("name: t\ntarget: cli\n")).toBeUndefined();
14
+ expect(extractSpecModel(":::not yaml at all")).toBeUndefined();
15
+ });
16
+ });
17
+
18
+ describe("selectedProvider", () => {
19
+ test("maps the full router grammar to doctor providers", () => {
20
+ expect(selectedProvider("claude-sonnet-4-5")).toBe("anthropic");
21
+ expect(selectedProvider("openai/gpt-4o-mini")).toBe("openai");
22
+ expect(selectedProvider("gemini/gemini-2.5-flash")).toBe("gemini");
23
+ expect(selectedProvider("bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0")).toBe(
24
+ "bedrock",
25
+ );
26
+ // local/<m>@<url> parses to openai WITH a baseUrl → credential-free.
27
+ expect(selectedProvider("local/llama3.2@http://localhost:11434/v1")).toBe("local");
28
+ });
29
+
30
+ test("returns undefined for unrecognised model strings", () => {
31
+ expect(selectedProvider("gpt-4o-mini")).toBeUndefined();
32
+ });
33
+ });
34
+
35
+ describe("buildCredentialChecks — model-aware", () => {
36
+ test("openai spec + OPENAI_API_KEY passes; missing Anthropic creds do NOT fail", () => {
37
+ const checks = buildCredentialChecks("openai/gpt-4o-mini", { OPENAI_API_KEY: "sk-x" });
38
+ const selected = checks[0];
39
+ expect(selected?.label).toContain("OpenAI credentials");
40
+ expect(selected?.pass).toBe(true);
41
+ expect(checks.every((c) => c.pass)).toBe(true);
42
+ expect(checks.some((c) => c.label.includes("Anthropic") && !c.pass)).toBe(false);
43
+ });
44
+
45
+ test("openai spec accepts OPENAI_BASE_URL as the credential surface", () => {
46
+ const checks = buildCredentialChecks("openai/gpt-4o-mini", {
47
+ OPENAI_BASE_URL: "http://vllm:8000/v1",
48
+ });
49
+ expect(checks[0]?.pass).toBe(true);
50
+ });
51
+
52
+ test("openai spec without provider env FAILS the selected check", () => {
53
+ const checks = buildCredentialChecks("openai/gpt-4o-mini", {});
54
+ expect(checks[0]?.pass).toBe(false);
55
+ expect(checks[0]?.reason).toContain("OPENAI_API_KEY");
56
+ });
57
+
58
+ test("gemini spec: GEMINI_API_KEY, GOOGLE_API_KEY, or Vertex env all satisfy", () => {
59
+ expect(buildCredentialChecks("gemini/gemini-2.5-flash", { GEMINI_API_KEY: "g" })[0]?.pass).toBe(
60
+ true,
61
+ );
62
+ expect(buildCredentialChecks("gemini/gemini-2.5-flash", { GOOGLE_API_KEY: "g" })[0]?.pass).toBe(
63
+ true,
64
+ );
65
+ expect(
66
+ buildCredentialChecks("gemini/gemini-2.5-flash", {
67
+ GOOGLE_GENAI_USE_VERTEXAI: "1",
68
+ GOOGLE_CLOUD_PROJECT: "p",
69
+ })[0]?.pass,
70
+ ).toBe(true);
71
+ const missing = buildCredentialChecks("gemini/gemini-2.5-flash", {});
72
+ expect(missing[0]?.pass).toBe(false);
73
+ });
74
+
75
+ test("bedrock spec is INFORMATIONAL — never fails even with no AWS env (SDK chain is authoritative)", () => {
76
+ const none = buildCredentialChecks("bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0", {});
77
+ expect(none[0]?.pass).toBe(true);
78
+ expect(none[0]?.warn).toBe(true);
79
+ expect(none[0]?.reason).toContain("SDK default credential chain");
80
+ const some = buildCredentialChecks("bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0", {
81
+ AWS_PROFILE: "dev",
82
+ });
83
+ expect(some[0]?.pass).toBe(true);
84
+ });
85
+
86
+ test("local spec needs no credentials", () => {
87
+ const checks = buildCredentialChecks("local/llama3.2@http://localhost:11434/v1", {});
88
+ expect(checks[0]?.pass).toBe(true);
89
+ expect(checks[0]?.label).toContain("Local endpoint");
90
+ });
91
+
92
+ test("non-selected providers with env set surface as informational warn lines, not failures", () => {
93
+ const checks = buildCredentialChecks("openai/gpt-4o-mini", {
94
+ OPENAI_API_KEY: "sk-x",
95
+ ANTHROPIC_API_KEY: "sk-ant",
96
+ });
97
+ const anthropicLine = checks.find((c) => c.label.includes("Anthropic"));
98
+ expect(anthropicLine?.pass).toBe(true);
99
+ expect(anthropicLine?.warn).toBe(true);
100
+ expect(anthropicLine?.label).toContain("not required");
101
+ });
102
+ });
103
+
104
+ describe("buildCredentialChecks — no spec model (legacy fallback)", () => {
105
+ test("Anthropic creds present → plain pass (unchanged behaviour)", () => {
106
+ const checks = buildCredentialChecks(undefined, { ANTHROPIC_API_KEY: "sk-ant" });
107
+ expect(checks).toEqual([{ label: "Anthropic credentials", pass: true }]);
108
+ });
109
+
110
+ test("no creds at all → hard fail (unchanged behaviour)", () => {
111
+ const checks = buildCredentialChecks(undefined, {});
112
+ expect(checks[0]?.pass).toBe(false);
113
+ expect(checks[0]?.reason).toContain("ANTHROPIC_AUTH_TOKEN");
114
+ });
115
+
116
+ test("missing Anthropic creds downgrade to informational when another provider env is clearly set", () => {
117
+ const checks = buildCredentialChecks(undefined, { OPENAI_API_KEY: "sk-x" });
118
+ expect(checks[0]?.pass).toBe(true);
119
+ expect(checks[0]?.warn).toBe(true);
120
+ expect(checks[0]?.reason).toContain("OpenAI");
121
+ });
122
+ });
@@ -0,0 +1,220 @@
1
+ import { resolveAuth } from "@crewhaus/adapter-anthropic";
2
+ import { parseModelString } from "@crewhaus/model-router";
3
+ import { parseSpec } from "@crewhaus/spec";
4
+
5
+ /**
6
+ * Model-aware credential checks for `crewhaus doctor`, factored out of the
7
+ * entry file `index.ts` (which runs a top-level argv switch and so cannot be
8
+ * imported by a test without executing the CLI). Side-effect-free and
9
+ * directly unit-testable, mirroring `scope-audit.ts` / `justification-gate.ts`.
10
+ *
11
+ * The old doctor checked ONLY Anthropic credentials and exited 1 when they
12
+ * were absent — for every spec, including openai/gemini/bedrock/local ones
13
+ * that never touch Anthropic. The fix: parse the cwd spec's `agent.model`
14
+ * with the model-router grammar and check the matching provider's env,
15
+ * reporting the others informationally instead of failing on them.
16
+ */
17
+
18
+ /** Doctor-level provider id: the router's four providers plus "local"
19
+ * (`local/<m>@<url>` parses to openai WITH a baseUrl — credential-free). */
20
+ export type DoctorProviderId = "anthropic" | "openai" | "gemini" | "bedrock" | "local";
21
+
22
+ export type DoctorCredentialCheck = {
23
+ readonly label: string;
24
+ readonly pass: boolean;
25
+ /** warn+pass renders as "~" — informational, never fails doctor. */
26
+ readonly warn?: boolean;
27
+ readonly reason?: string;
28
+ };
29
+
30
+ function isSet(env: NodeJS.ProcessEnv, name: string): boolean {
31
+ const v = env[name];
32
+ return typeof v === "string" && v !== "";
33
+ }
34
+
35
+ /**
36
+ * Tolerantly extract `agent.model` from a crewhaus.yaml text. Returns
37
+ * undefined when the spec doesn't parse or carries no agent.model (e.g.
38
+ * workflow shapes with per-step models) — the caller then falls back to
39
+ * the legacy Anthropic-first behaviour.
40
+ */
41
+ export function extractSpecModel(yamlText: string): string | undefined {
42
+ try {
43
+ const spec = parseSpec(yamlText) as unknown as { agent?: { model?: unknown } };
44
+ const m = spec.agent?.model;
45
+ return typeof m === "string" && m.length > 0 ? m : undefined;
46
+ } catch {
47
+ return undefined;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Map a model string to the doctor-level provider whose credentials the
53
+ * run will need. Returns undefined for unparseable strings.
54
+ */
55
+ export function selectedProvider(model: string): DoctorProviderId | undefined {
56
+ try {
57
+ const parsed = parseModelString(model);
58
+ if (parsed.providerId === "openai" && parsed.baseUrl !== undefined) return "local";
59
+ return parsed.providerId;
60
+ } catch {
61
+ return undefined;
62
+ }
63
+ }
64
+
65
+ type EnvStatus = { readonly satisfied: boolean; readonly detail: string };
66
+
67
+ function anthropicStatus(env: NodeJS.ProcessEnv): EnvStatus {
68
+ const auth = resolveAuth(env);
69
+ return auth.mode !== "none"
70
+ ? { satisfied: true, detail: `${auth.mode} credentials found` }
71
+ : {
72
+ satisfied: false,
73
+ detail: "set ANTHROPIC_AUTH_TOKEN (Claude subscription) or ANTHROPIC_API_KEY",
74
+ };
75
+ }
76
+
77
+ function openaiStatus(env: NodeJS.ProcessEnv): EnvStatus {
78
+ if (isSet(env, "OPENAI_API_KEY")) return { satisfied: true, detail: "OPENAI_API_KEY set" };
79
+ if (isSet(env, "OPENAI_BASE_URL")) {
80
+ return { satisfied: true, detail: "OPENAI_BASE_URL set (OpenAI-compatible endpoint)" };
81
+ }
82
+ return {
83
+ satisfied: false,
84
+ detail: "set OPENAI_API_KEY (or OPENAI_BASE_URL for an OpenAI-compatible endpoint)",
85
+ };
86
+ }
87
+
88
+ function geminiStatus(env: NodeJS.ProcessEnv): EnvStatus {
89
+ if (isSet(env, "GEMINI_API_KEY")) return { satisfied: true, detail: "GEMINI_API_KEY set" };
90
+ if (isSet(env, "GOOGLE_API_KEY")) return { satisfied: true, detail: "GOOGLE_API_KEY set" };
91
+ if (isSet(env, "GOOGLE_GENAI_USE_VERTEXAI") || isSet(env, "GOOGLE_CLOUD_PROJECT")) {
92
+ return {
93
+ satisfied: true,
94
+ detail: "Vertex AI env set (GOOGLE_GENAI_USE_VERTEXAI/GOOGLE_CLOUD_PROJECT)",
95
+ };
96
+ }
97
+ return {
98
+ satisfied: false,
99
+ detail:
100
+ "set GEMINI_API_KEY (or GOOGLE_API_KEY; Vertex users set GOOGLE_GENAI_USE_VERTEXAI + GOOGLE_CLOUD_PROJECT)",
101
+ };
102
+ }
103
+
104
+ const BEDROCK_ENV_NAMES = [
105
+ "AWS_BEARER_TOKEN_BEDROCK",
106
+ "AWS_ACCESS_KEY_ID",
107
+ "AWS_PROFILE",
108
+ "AWS_REGION",
109
+ ] as const;
110
+
111
+ function bedrockStatus(env: NodeJS.ProcessEnv): EnvStatus {
112
+ const found = BEDROCK_ENV_NAMES.filter((n) => isSet(env, n));
113
+ return found.length > 0
114
+ ? { satisfied: true, detail: `${found.join(", ")} set` }
115
+ : {
116
+ satisfied: false,
117
+ detail:
118
+ "no AWS env vars visible (AWS_BEARER_TOKEN_BEDROCK/AWS_ACCESS_KEY_ID/AWS_PROFILE/AWS_REGION) — the SDK default credential chain (IRSA, instance profile, ~/.aws) may still apply",
119
+ };
120
+ }
121
+
122
+ function statusFor(provider: DoctorProviderId, env: NodeJS.ProcessEnv): EnvStatus {
123
+ switch (provider) {
124
+ case "anthropic":
125
+ return anthropicStatus(env);
126
+ case "openai":
127
+ return openaiStatus(env);
128
+ case "gemini":
129
+ return geminiStatus(env);
130
+ case "bedrock":
131
+ return bedrockStatus(env);
132
+ case "local":
133
+ return {
134
+ satisfied: true,
135
+ detail: "local endpoint baked into the model string — no credentials needed",
136
+ };
137
+ }
138
+ }
139
+
140
+ const PROVIDER_LABEL: Record<DoctorProviderId, string> = {
141
+ anthropic: "Anthropic credentials",
142
+ openai: "OpenAI credentials",
143
+ gemini: "Gemini credentials",
144
+ bedrock: "Bedrock (AWS) credentials",
145
+ local: "Local endpoint",
146
+ };
147
+
148
+ /** Providers whose env check is INFORMATIONAL even when selected: the AWS
149
+ * SDK's default credential chain is authoritative for bedrock (env vars are
150
+ * only one of its sources), and local/ endpoints need no credentials. */
151
+ const INFORMATIONAL_WHEN_SELECTED: ReadonlySet<DoctorProviderId> = new Set(["bedrock", "local"]);
152
+
153
+ /** Non-selected providers whose env is set get an informational line so the
154
+ * operator sees what else is configured. */
155
+ const ALL_PROVIDERS: readonly DoctorProviderId[] = ["anthropic", "openai", "gemini", "bedrock"];
156
+
157
+ /**
158
+ * Build the credential section of `crewhaus doctor`.
159
+ *
160
+ * With a parseable spec model: the matching provider gets a real pass/fail
161
+ * check (informational for bedrock/local), and any OTHER provider with env
162
+ * visibly set gets a "~" informational line. Without one (no spec file, or
163
+ * no agent.model): legacy Anthropic-first behaviour, EXCEPT that a missing
164
+ * Anthropic credential downgrades to informational when another provider's
165
+ * env is clearly set (the operator is plainly not on Anthropic).
166
+ */
167
+ export function buildCredentialChecks(
168
+ specModel: string | undefined,
169
+ env: NodeJS.ProcessEnv,
170
+ ): DoctorCredentialCheck[] {
171
+ const provider = specModel !== undefined ? selectedProvider(specModel) : undefined;
172
+ const checks: DoctorCredentialCheck[] = [];
173
+
174
+ if (provider !== undefined && specModel !== undefined) {
175
+ const status = statusFor(provider, env);
176
+ const informational = INFORMATIONAL_WHEN_SELECTED.has(provider);
177
+ checks.push({
178
+ label: `${PROVIDER_LABEL[provider]} (model ${specModel})`,
179
+ pass: informational ? true : status.satisfied,
180
+ ...(informational && !status.satisfied ? { warn: true } : {}),
181
+ ...(status.satisfied && !informational ? {} : { reason: status.detail }),
182
+ });
183
+ for (const other of ALL_PROVIDERS) {
184
+ if (other === provider) continue;
185
+ const otherStatus = statusFor(other, env);
186
+ if (otherStatus.satisfied) {
187
+ checks.push({
188
+ label: `${PROVIDER_LABEL[other]} (not required for this spec)`,
189
+ pass: true,
190
+ warn: true,
191
+ reason: otherStatus.detail,
192
+ });
193
+ }
194
+ }
195
+ return checks;
196
+ }
197
+
198
+ // No spec model — legacy Anthropic-first behaviour with one softening:
199
+ // when Anthropic creds are absent but another provider's env is clearly
200
+ // set, report informationally instead of hard-failing.
201
+ const anthropic = anthropicStatus(env);
202
+ if (anthropic.satisfied) {
203
+ checks.push({ label: "Anthropic credentials", pass: true });
204
+ return checks;
205
+ }
206
+ const others = ALL_PROVIDERS.filter((p) => p !== "anthropic").filter(
207
+ (p) => statusFor(p, env).satisfied,
208
+ );
209
+ if (others.length > 0) {
210
+ checks.push({
211
+ label: "Anthropic credentials",
212
+ pass: true,
213
+ warn: true,
214
+ reason: `not set, but ${others.map((p) => PROVIDER_LABEL[p]).join(" + ")} detected — claude-* models will not run until ANTHROPIC_AUTH_TOKEN/ANTHROPIC_API_KEY is set`,
215
+ });
216
+ return checks;
217
+ }
218
+ checks.push({ label: "Anthropic credentials", pass: false, reason: anthropic.detail });
219
+ return checks;
220
+ }