crewhaus 0.1.3 → 0.1.4

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
@@ -11,7 +11,7 @@ CrewHaus compiles one spec into the shape each situation calls for — a CLI age
11
11
  The fastest path is the self-contained binary — one file, no Bun/Node required:
12
12
 
13
13
  ```bash
14
- brew install crewhaus/tap/crewhaus # macOS / Linux (Homebrew)
14
+ brew tap crewhaus/tap && brew install crewhaus # macOS / Linux (Homebrew)
15
15
  scoop install crewhaus # Windows (Scoop; see repo for the bucket)
16
16
  winget install CrewHaus.CLI # Windows (winget)
17
17
  # Debian / Ubuntu (apt): signed repo at https://crewhaus.github.io/apt
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crewhaus",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
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
6
  "keywords": [
@@ -28,69 +28,69 @@
28
28
  "test": "bun test src"
29
29
  },
30
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"
31
+ "@crewhaus/adapter-anthropic": "0.1.4",
32
+ "@crewhaus/agent-context-isolation": "0.1.4",
33
+ "@crewhaus/audit-log": "0.1.4",
34
+ "@crewhaus/eval-optimizer-orchestrator": "0.1.4",
35
+ "@crewhaus/prompt-optimizer": "0.1.4",
36
+ "@crewhaus/prompt-optimizer-claude": "0.1.4",
37
+ "@crewhaus/justification-judge-claude": "0.1.4",
38
+ "@crewhaus/spec-patch": "0.1.4",
39
+ "@crewhaus/canary-controller": "0.1.4",
40
+ "@crewhaus/compiler": "0.1.4",
41
+ "@crewhaus/computer-use-driver": "0.1.4",
42
+ "@crewhaus/context-bundle": "0.1.4",
43
+ "@crewhaus/deployment-controller": "0.1.4",
44
+ "@crewhaus/egress-classifier": "0.1.4",
45
+ "@crewhaus/egress-matcher-semantic": "0.1.4",
46
+ "@crewhaus/embedder": "0.1.4",
47
+ "@crewhaus/errors": "0.1.4",
48
+ "@crewhaus/eval-dataset": "0.1.4",
49
+ "@crewhaus/eval-grader": "0.1.4",
50
+ "@crewhaus/eval-report": "0.1.4",
51
+ "@crewhaus/eval-runner": "0.1.4",
52
+ "@crewhaus/hooks-engine": "0.1.4",
53
+ "@crewhaus/infra-utils": "0.1.4",
54
+ "@crewhaus/ir": "0.1.4",
55
+ "@crewhaus/logging": "0.1.4",
56
+ "@crewhaus/mcp-host": "0.1.4",
57
+ "@crewhaus/model-router": "0.1.4",
58
+ "@crewhaus/migration-engine": "0.1.4",
59
+ "@crewhaus/migration-runner": "0.1.4",
60
+ "@crewhaus/permission-engine": "0.1.4",
61
+ "@crewhaus/run-context": "0.1.4",
62
+ "@crewhaus/runtime-core": "0.1.4",
63
+ "@crewhaus/secrets-manager": "0.1.4",
64
+ "@crewhaus/session-store": "0.1.4",
65
+ "@crewhaus/spec-registry": "0.1.4",
66
+ "@crewhaus/skills-registry": "0.1.4",
67
+ "@crewhaus/slash-commands": "0.1.4",
68
+ "@crewhaus/spec": "0.1.4",
69
+ "@crewhaus/sub-agent-spawner": "0.1.4",
70
+ "@crewhaus/trace-event-bus": "0.1.4",
71
+ "@crewhaus/tool-bash": "0.1.4",
72
+ "@crewhaus/tool-builder": "0.1.4",
73
+ "@crewhaus/tool-catalog": "0.1.4",
74
+ "@crewhaus/tool-codegraph": "0.1.4",
75
+ "@crewhaus/tool-fetch": "0.1.4",
76
+ "@crewhaus/tool-fs": "0.1.4",
77
+ "@crewhaus/tool-image": "0.1.4",
78
+ "@crewhaus/tool-image-generation": "0.1.4",
79
+ "@crewhaus/tool-document-ingest": "0.1.4",
80
+ "@crewhaus/tool-mcp": "0.1.4",
81
+ "@crewhaus/tool-mouse-keyboard": "0.1.4",
82
+ "@crewhaus/tool-navigate": "0.1.4",
83
+ "@crewhaus/tool-screen-capture": "0.1.4",
84
+ "@crewhaus/tool-task": "0.1.4",
85
+ "@crewhaus/tool-todo": "0.1.4",
86
+ "@crewhaus/tool-vision-grounding": "0.1.4",
87
+ "@crewhaus/tool-web": "0.1.4",
88
+ "@crewhaus/docker-images": "0.1.4",
89
+ "@crewhaus/crewhaus-cloud": "0.1.4",
90
+ "@crewhaus/federation-discovery": "0.1.4",
91
+ "@crewhaus/sandbox": "0.1.4",
92
+ "@crewhaus/sandbox-image-registry": "0.1.4",
93
+ "@crewhaus/compliance-controls": "0.1.4"
94
94
  },
95
95
  "devDependencies": {
96
96
  "zod": "^3.23.8"
package/src/index.test.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { parseSpec } from "@crewhaus/spec";
@@ -213,6 +213,35 @@ describe("crewhaus compile", () => {
213
213
  expect(existsSync(join(outDir, "agent.ts"))).toBe(false);
214
214
  });
215
215
 
216
+ // A malformed credential env-ref (`$lowercase` on a Slack token) is a hard
217
+ // compile error from lowerCredential(). It must render as a clean die()
218
+ // one-liner — exit 1, `crewhaus: ` prefix, no uncaught stack trace — on BOTH
219
+ // the --emit-ir and bundle paths (the lower() catch routes CompilerError, not
220
+ // just SpecParseError, through die()).
221
+ const BAD_CRED_SPEC =
222
+ "name: status-bot\ntarget: channel\nagent:\n model: claude-sonnet-4-6\n instructions: announce status\nchannels:\n slack:\n botToken: $slack_bot_token\n signingSecret: $SLACK_SIGNING_SECRET\nrouting:\n sessionKey: thread\n";
223
+
224
+ for (const mode of ["--emit-ir", "bundle"] as const) {
225
+ test(`compile (${mode}) on a malformed credential $ref fails cleanly (die, not crash)`, async () => {
226
+ const specPath = join(tmp, "crewhaus.yaml");
227
+ writeFileSync(specPath, BAD_CRED_SPEC);
228
+ const outDir = join(tmp, "out");
229
+ const argv =
230
+ mode === "--emit-ir"
231
+ ? ["compile", specPath, "--emit-ir"]
232
+ : ["compile", specPath, "-o", outDir];
233
+ const result = await runCli(argv);
234
+ expect(result.exitCode).toBe(1);
235
+ expect(result.stderr).toContain(
236
+ 'crewhaus: channels.slack.botToken value "$slack_bot_token" looks like an environment reference',
237
+ );
238
+ // Not an uncaught crash: no error-class name, no "at file:line:col" frame.
239
+ expect(result.stderr).not.toContain("CompilerError");
240
+ expect(result.stderr).not.toMatch(/\bat .+:\d+:\d+/);
241
+ if (mode === "bundle") expect(existsSync(join(outDir, "agent.ts"))).toBe(false);
242
+ });
243
+ }
244
+
216
245
  // (c) CLEAN spec still compiles by default (no flag, no opt-out).
217
246
  test("compile (no flag) on a clean (toolless) spec still emits and exits 0", async () => {
218
247
  const result = await runCli(["compile", HELLO_SPEC, "-o", tmp]);
@@ -663,13 +692,21 @@ describe("crewhaus optimize --budget-usd", () => {
663
692
  });
664
693
 
665
694
  describe("crewhaus help and unknown subcommand", () => {
666
- test("--help prints usage listing all four subcommands", async () => {
667
- const result = await runCli(["--help"]);
695
+ for (const flag of ["--help", "-h"]) {
696
+ test(`${flag} prints usage to stdout and exits 0 (works in \`set -e\` health checks)`, async () => {
697
+ const result = await runCli([flag]);
698
+ expect(result.exitCode).toBe(0);
699
+ expect(result.stdout).toContain("compile");
700
+ expect(result.stdout).toContain("run");
701
+ expect(result.stdout).toContain("init");
702
+ expect(result.stdout).toContain("doctor");
703
+ });
704
+ }
705
+
706
+ test("no subcommand prints usage to stderr and exits 1 (usage error)", async () => {
707
+ const result = await runCli([]);
668
708
  expect(result.exitCode).toBe(1);
669
- expect(result.stderr).toContain("compile");
670
- expect(result.stderr).toContain("run");
671
- expect(result.stderr).toContain("init");
672
- expect(result.stderr).toContain("doctor");
709
+ expect(result.stderr).toContain("usage: crewhaus");
673
710
  });
674
711
 
675
712
  test("unknown subcommand exits 1 with a clear message", async () => {
@@ -679,6 +716,89 @@ describe("crewhaus help and unknown subcommand", () => {
679
716
  });
680
717
  });
681
718
 
719
+ describe("crewhaus secrets backend selection", () => {
720
+ test("--backend vault errors clearly instead of silently using env-var", async () => {
721
+ const result = await runCli(["secrets", "doctor", "--backend", "vault"]);
722
+ expect(result.exitCode).toBe(1);
723
+ expect(result.stderr).toContain("vault backend is not wired into the CLI");
724
+ });
725
+
726
+ test("an unknown backend is rejected", async () => {
727
+ const result = await runCli(["secrets", "doctor", "--backend", "bogus"]);
728
+ expect(result.exitCode).toBe(1);
729
+ expect(result.stderr).toContain('unknown secrets backend "bogus"');
730
+ });
731
+
732
+ test("--backend file rotates into a not-yet-existing root dir (auto-mkdir)", async () => {
733
+ const root = join(tmp, "nested", "secrets");
734
+ const result = await runCli([
735
+ "secrets",
736
+ "rotate",
737
+ "demo",
738
+ "--backend",
739
+ "file",
740
+ "--root-dir",
741
+ root,
742
+ ]);
743
+ expect(result.exitCode).toBe(0);
744
+ expect(result.stdout).toContain("rotated demo");
745
+ expect(existsSync(join(root, "demo"))).toBe(true);
746
+ });
747
+ });
748
+
749
+ describe("crewhaus cost-summary", () => {
750
+ const SESSION_ID = "sess_00000000000000ab";
751
+
752
+ function seedSession(lines: ReadonlyArray<unknown>): void {
753
+ const dir = join(tmp, ".crewhaus", "sessions");
754
+ mkdirSync(dir, { recursive: true });
755
+ writeFileSync(
756
+ join(dir, `${SESSION_ID}.jsonl`),
757
+ `${lines.map((l) => JSON.stringify(l)).join("\n")}\n`,
758
+ );
759
+ }
760
+
761
+ test("sums cost_accrual events written in the real event-log envelope shape", async () => {
762
+ seedSession([
763
+ { ts: 1, version: 1, kind: "user_message", payload: { content: "hi" } },
764
+ {
765
+ ts: 2,
766
+ version: 1,
767
+ kind: "cost_accrual",
768
+ payload: { provider: "anthropic", modelId: "claude-sonnet-4-6", costUsdMicros: 450 },
769
+ },
770
+ {
771
+ ts: 3,
772
+ version: 1,
773
+ kind: "cost_accrual",
774
+ payload: { provider: "anthropic", modelId: "claude-sonnet-4-6", costUsdMicros: 300 },
775
+ },
776
+ ]);
777
+ const result = await runCli(["cost-summary", "--session", SESSION_ID, "--format", "json"], {
778
+ cwd: tmp,
779
+ });
780
+ expect(result.exitCode).toBe(0);
781
+ const out = JSON.parse(result.stdout) as {
782
+ count: number;
783
+ totalUsdMicros: number;
784
+ byProvider: Record<string, number>;
785
+ };
786
+ expect(out.count).toBe(2);
787
+ expect(out.totalUsdMicros).toBe(750);
788
+ expect(out.byProvider["anthropic"]).toBe(750);
789
+ });
790
+
791
+ test("still reads flat (non-enveloped) cost_accrual lines for back-compat", async () => {
792
+ seedSession([{ kind: "cost_accrual", provider: "openai", costUsdMicros: 1000 }]);
793
+ const result = await runCli(["cost-summary", "--session", SESSION_ID, "--format", "json"], {
794
+ cwd: tmp,
795
+ });
796
+ expect(result.exitCode).toBe(0);
797
+ const out = JSON.parse(result.stdout) as { totalUsdMicros: number };
798
+ expect(out.totalUsdMicros).toBe(1000);
799
+ });
800
+ });
801
+
682
802
  describe("crewhaus version", () => {
683
803
  const pkgVersion = (
684
804
  JSON.parse(readFileSync(join(import.meta.dir, "..", "package.json"), "utf-8")) as {
package/src/index.ts CHANGED
@@ -296,62 +296,76 @@ const FEDERATION_SCHEMA: ParseArgsSchema = {
296
296
  ],
297
297
  };
298
298
 
299
+ function usageText(): string {
300
+ return [
301
+ "usage: crewhaus <subcommand> [args]",
302
+ "",
303
+ "subcommands:",
304
+ " compile <spec.yaml> -o <out-dir> compile a spec to a runnable bundle",
305
+ " (fails if an outward tool is left non-external — FR-002)",
306
+ " [--allow-unmarked-sinks] opt out of the external-sink scope gate",
307
+ " compile <spec.yaml> --emit-ir print the lowered IR as JSON (debug)",
308
+ " run <spec.yaml> [--model <model>] compile in-memory and execute the agent",
309
+ " [--resume <id>] resume a specific session (cli targets only)",
310
+ " [--continue] resume the most-recent session (cli targets only)",
311
+ " [--prompt <text>] initial user prompt (browser targets; defaults to stdin)",
312
+ " [--justification-judge rule-based|claude] Pillar 3 intent-gate judge (FR-004)",
313
+ " [--egress-matcher substring|semantic] Pillar 3 sink-side matcher (FR-006)",
314
+ " [--egress-embedder <model>] embedder for --egress-matcher semantic",
315
+ " eval <spec.yaml> --dataset <data> run the agent against a dataset and grade",
316
+ " --graders <graders.yaml> (deterministic graders + LLM-as-judge)",
317
+ " [--judge-model <model>] [--concurrency N] [--seed N] -o <out-dir>",
318
+ " eval-report diff <prev> <new> compare two eval runs and emit a diff report",
319
+ " [-o <out-dir>]",
320
+ " optimize <spec.yaml> --dataset <data> --graders <graders.yaml>",
321
+ " [--mutator rule-based|claude] [--iterations N] [--seed N]",
322
+ " [--budget-usd N] stop a model-driven run before it exceeds $N (FR-003)",
323
+ " [--write-back] [-o <out-dir>] active eval-driven optimization (Pillar 2)",
324
+ " init [name] scaffold a new crewhaus.yaml",
325
+ " doctor check environment health",
326
+ " context --bundle [-o <file>] emit a single-markdown orientation manifest",
327
+ " [--factory-root <p>] [--docs-root <p>] [--demos-root <p>]",
328
+ " cost-summary --session <id> summarize cost_accrual events for a session",
329
+ " secrets doctor list known secrets via the configured backend",
330
+ " secrets rotate <name> [--value V] rotate a named secret (file backend)",
331
+ " spec put|list|get|pin|alias ... versioned spec storage (Section 28 spec-registry)",
332
+ " deploy promote|rollback ... re-pin a spec for an environment (Section 28)",
333
+ " migrate-all --from N --to N batch-migrate every spec in the registry",
334
+ " build-image <target> --tag <tag> build the docker image for a target shape (Section 32)",
335
+ " [--platform <p>] [--push]",
336
+ " cloud deploy --provider <p> deploy a managed CrewHaus cluster (Section 32)",
337
+ " --region <r> [--tier <t>] [--image-tag <tag>]",
338
+ " cloud teardown --provider <p> tear down a managed cluster",
339
+ " --region <r>",
340
+ " federation discover <deployment> resolve a federated peer's endpoint + cert fingerprint (Section 34)",
341
+ " [--srv-domain <d>] [--format json|yaml]",
342
+ " sandbox doctor [--probe] list registered sandbox images + healthcheck status (Section 36)",
343
+ " [--format json|table]",
344
+ " compliance evidence collect SOC 2 / ISO 27001 / HIPAA evidence (Section 39)",
345
+ " --framework <id> [--control <id>] --period <p> [--audit-dir <d>]",
346
+ " [--out-dir <d>] [--signing-key-env <ENV>]",
347
+ " version print the CLI version (also: --version, -v)",
348
+ "",
349
+ ].join("\n");
350
+ }
351
+
352
+ /** No subcommand given (or a parse error) — usage to stderr, exit 1. */
299
353
  function usage(): never {
300
- process.stderr.write(
301
- [
302
- "usage: crewhaus <subcommand> [args]",
303
- "",
304
- "subcommands:",
305
- " compile <spec.yaml> -o <out-dir> compile a spec to a runnable bundle",
306
- " (fails if an outward tool is left non-external — FR-002)",
307
- " [--allow-unmarked-sinks] opt out of the external-sink scope gate",
308
- " compile <spec.yaml> --emit-ir print the lowered IR as JSON (debug)",
309
- " run <spec.yaml> [--model <model>] compile in-memory and execute the agent",
310
- " [--resume <id>] resume a specific session (cli targets only)",
311
- " [--continue] resume the most-recent session (cli targets only)",
312
- " [--prompt <text>] initial user prompt (browser targets; defaults to stdin)",
313
- " [--justification-judge rule-based|claude] Pillar 3 intent-gate judge (FR-004)",
314
- " [--egress-matcher substring|semantic] Pillar 3 sink-side matcher (FR-006)",
315
- " [--egress-embedder <model>] embedder for --egress-matcher semantic",
316
- " eval <spec.yaml> --dataset <data> run the agent against a dataset and grade",
317
- " --graders <graders.yaml> (deterministic graders + LLM-as-judge)",
318
- " [--judge-model <model>] [--concurrency N] [--seed N] -o <out-dir>",
319
- " eval-report diff <prev> <new> compare two eval runs and emit a diff report",
320
- " [-o <out-dir>]",
321
- " optimize <spec.yaml> --dataset <data> --graders <graders.yaml>",
322
- " [--mutator rule-based|claude] [--iterations N] [--seed N]",
323
- " [--budget-usd N] stop a model-driven run before it exceeds $N (FR-003)",
324
- " [--write-back] [-o <out-dir>] active eval-driven optimization (Pillar 2)",
325
- " init [name] scaffold a new crewhaus.yaml",
326
- " doctor check environment health",
327
- " context --bundle [-o <file>] emit a single-markdown orientation manifest",
328
- " [--factory-root <p>] [--docs-root <p>] [--demos-root <p>]",
329
- " cost-summary --session <id> summarize cost_accrual events for a session",
330
- " secrets doctor list known secrets via the configured backend",
331
- " secrets rotate <name> [--value V] rotate a named secret (file or vault backend)",
332
- " spec put|list|get|pin|alias ... versioned spec storage (Section 28 spec-registry)",
333
- " deploy promote|rollback ... re-pin a spec for an environment (Section 28)",
334
- " migrate-all --from N --to N batch-migrate every spec in the registry",
335
- " build-image <target> --tag <tag> build the docker image for a target shape (Section 32)",
336
- " [--platform <p>] [--push]",
337
- " cloud deploy --provider <p> deploy a managed CrewHaus cluster (Section 32)",
338
- " --region <r> [--tier <t>] [--image-tag <tag>]",
339
- " cloud teardown --provider <p> tear down a managed cluster",
340
- " --region <r>",
341
- " federation discover <deployment> resolve a federated peer's endpoint + cert fingerprint (Section 34)",
342
- " [--srv-domain <d>] [--format json|yaml]",
343
- " sandbox doctor [--probe] list registered sandbox images + healthcheck status (Section 36)",
344
- " [--format json|table]",
345
- " compliance evidence collect SOC 2 / ISO 27001 / HIPAA evidence (Section 39)",
346
- " --framework <id> [--control <id>] --period <p> [--audit-dir <d>]",
347
- " [--out-dir <d>] [--signing-key-env <ENV>]",
348
- " version print the CLI version (also: --version, -v)",
349
- "",
350
- ].join("\n"),
351
- );
354
+ process.stderr.write(usageText());
352
355
  process.exit(1);
353
356
  }
354
357
 
358
+ /**
359
+ * Explicit `-h`/`--help` at the top level — usage to stdout, exit 0. Help was
360
+ * requested, so it is not an error; this matches every subcommand's own
361
+ * `--help` (stdout, exit 0) and lets `crewhaus --help` work in `set -e` health
362
+ * checks instead of looking like a broken CLI.
363
+ */
364
+ function help(): never {
365
+ process.stdout.write(usageText());
366
+ process.exit(0);
367
+ }
368
+
355
369
  function die(message: string): never {
356
370
  process.stderr.write(`crewhaus: ${message}\n`);
357
371
  process.exit(1);
@@ -464,7 +478,10 @@ async function runCompile(args: ParsedArgs): Promise<void> {
464
478
  try {
465
479
  ir = lower(parseSpec(yamlText));
466
480
  } catch (err) {
467
- if (err instanceof SpecParseError) die(err.message);
481
+ // parseSpec throws SpecParseError; lower() can throw CompilerError (e.g. a
482
+ // malformed credential env-ref). Both extend CrewhausError — route the
483
+ // family through die() for a clean one-liner instead of a raw stack trace.
484
+ if (err instanceof CrewhausError) die(err.message);
468
485
  throw err;
469
486
  }
470
487
  const json = `${JSON.stringify(ir, null, 2)}\n`;
@@ -524,7 +541,10 @@ async function runStrictScopeGate(yamlText: string): Promise<void> {
524
541
  try {
525
542
  ir = lower(parseSpec(yamlText));
526
543
  } catch (err) {
527
- if (err instanceof SpecParseError) die(err.message);
544
+ // SpecParseError (parse) and CompilerError (lower, e.g. a malformed
545
+ // credential env-ref) both extend CrewhausError — render as a clean die()
546
+ // one-liner rather than an uncaught stack trace.
547
+ if (err instanceof CrewhausError) die(err.message);
528
548
  throw err;
529
549
  }
530
550
  const toolNames = collectToolNames(ir);
@@ -1828,13 +1848,21 @@ async function runCostSummary(args: ParsedArgs): Promise<void> {
1828
1848
  "kind" in parsed &&
1829
1849
  (parsed as { kind?: string }).kind === "cost_accrual"
1830
1850
  ) {
1831
- const e = parsed as unknown as {
1832
- provider: string;
1833
- costUsdMicros: number;
1851
+ // event-log writes a `{ ts, version, kind, payload }` envelope, so the
1852
+ // cost fields live under `payload`. Fall back to top-level fields so a
1853
+ // hand-written/flat cost_accrual line still aggregates.
1854
+ const e = parsed as {
1855
+ provider?: string;
1856
+ costUsdMicros?: number;
1857
+ payload?: { provider?: string; costUsdMicros?: number };
1834
1858
  };
1835
- totalMicros += e.costUsdMicros;
1836
- byProvider[e.provider] = (byProvider[e.provider] ?? 0) + e.costUsdMicros;
1837
- count++;
1859
+ const provider = e.payload?.provider ?? e.provider;
1860
+ const micros = e.payload?.costUsdMicros ?? e.costUsdMicros;
1861
+ if (typeof provider === "string" && typeof micros === "number") {
1862
+ totalMicros += micros;
1863
+ byProvider[provider] = (byProvider[provider] ?? 0) + micros;
1864
+ count++;
1865
+ }
1838
1866
  }
1839
1867
  }
1840
1868
  const totalDollars = totalMicros / 1_000_000;
@@ -1855,13 +1883,13 @@ async function runCostSummary(args: ParsedArgs): Promise<void> {
1855
1883
  /**
1856
1884
  * Section 27 — `crewhaus secrets <action> <name> [opts]`. Two actions:
1857
1885
  * doctor list configured secrets and report missing
1858
- * rotate <name> rotate the named secret (file or vault backends)
1886
+ * rotate <name> rotate the named secret (file backend)
1859
1887
  */
1860
1888
  async function runSecrets(args: ParsedArgs, action: string): Promise<void> {
1861
1889
  if (args.flags["help"]) {
1862
1890
  process.stdout.write(
1863
1891
  "usage:\n" +
1864
- " crewhaus secrets doctor [--backend env-var|file|vault] [--root-dir <dir>]\n" +
1892
+ " crewhaus secrets doctor [--backend env-var|file] [--root-dir <dir>]\n" +
1865
1893
  " crewhaus secrets rotate <name> [--value <new-value>] [--backend ...]\n",
1866
1894
  );
1867
1895
  return;
@@ -1874,7 +1902,23 @@ async function runSecrets(args: ParsedArgs, action: string): Promise<void> {
1874
1902
  const { createSecrets, createEnvVarBackend, createFileBackend } = await import(
1875
1903
  "@crewhaus/secrets-manager"
1876
1904
  );
1877
- const backend = backendId === "file" ? createFileBackend({ rootDir }) : createEnvVarBackend();
1905
+ // The `vault` backend exists in @crewhaus/secrets-manager but is not wired
1906
+ // into the CLI (it needs an address/auth story the CLI does not yet expose).
1907
+ // Fail loudly rather than silently degrading a security-sensitive flag to the
1908
+ // env-var backend — a silent fallback would, e.g., make `secrets doctor`
1909
+ // cheerfully list process env var names as if they were Vault secrets.
1910
+ let backend: ReturnType<typeof createFileBackend>;
1911
+ if (backendId === "env-var") {
1912
+ backend = createEnvVarBackend();
1913
+ } else if (backendId === "file") {
1914
+ backend = createFileBackend({ rootDir });
1915
+ } else if (backendId === "vault") {
1916
+ die(
1917
+ "vault backend is not wired into the CLI in this build — construct it programmatically via createVaultBackend(), or use --backend env-var|file",
1918
+ );
1919
+ } else {
1920
+ die(`unknown secrets backend "${backendId}" (expected: env-var | file)`);
1921
+ }
1878
1922
  const secrets = createSecrets({ backend });
1879
1923
 
1880
1924
  if (action === "doctor") {
@@ -2503,9 +2547,11 @@ switch (subcommand) {
2503
2547
  case "--version":
2504
2548
  printVersion();
2505
2549
  break;
2506
- case "":
2507
2550
  case "-h":
2508
2551
  case "--help":
2552
+ help();
2553
+ break;
2554
+ case "":
2509
2555
  usage();
2510
2556
  break;
2511
2557
  default: