crewhaus 0.2.4 → 0.3.0

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/fleet.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type FailureClass } from "@crewhaus/errors";
1
2
  import type { Manifest } from "@crewhaus/spec-registry";
2
3
  /** The spec file that roots a standalone harness. */
3
4
  export declare const HARNESS_SPEC_FILENAME = "crewhaus.yaml";
@@ -165,6 +166,41 @@ export type FleetRunner = (opts: {
165
166
  readonly exitCode: number;
166
167
  readonly tail: string;
167
168
  }>;
169
+ /**
170
+ * Is `entryPath` the Bun standalone-executable virtual-FS sentinel — i.e. are
171
+ * we running a `bun build --compile` single-file binary rather than
172
+ * `bun run …/index.ts`? Bun rewrites the entry (`process.argv[1]` / `Bun.main`)
173
+ * to a path inside its embedded root: `/$bunfs/root/<name>` on POSIX,
174
+ * `B:\~BUN\root\<name>` on Windows.
175
+ */
176
+ export declare function isCompiledEntryPath(entryPath: string): boolean;
177
+ /** The runtime facts the self-invoke argv depends on, injected so the builder
178
+ * is a pure function: production passes the live `process.execPath` +
179
+ * `Bun.main`; tests pass fixed strings for either mode. */
180
+ export type SelfInvokeContext = {
181
+ /** The real running executable — `bun` in dev, the compiled binary itself
182
+ * when compiled (`process.execPath`, NOT `process.argv[0]`, which the
183
+ * compiled runtime reports as the literal string `"bun"`). */
184
+ readonly execPath: string;
185
+ /** The entry path as the runtime sees it (`process.argv[1]` / `Bun.main`):
186
+ * the real `…/index.ts` in dev, the `/$bunfs/…` sentinel when compiled. */
187
+ readonly entryPath: string;
188
+ };
189
+ /**
190
+ * Build the argv to re-invoke THIS running CLI in a child process with
191
+ * `childArgv` (e.g. `["doctor"]`) — correct under BOTH `bun run
192
+ * apps/cli/src/index.ts …` and a `bun build --compile` single-file binary.
193
+ *
194
+ * The compiled case is the subtle one. A standalone binary RE-INJECTS its own
195
+ * embedded entry every time it is spawned, so the child already receives
196
+ * `[<exe>, /$bunfs/root/<name>, ...childArgv]`. If we ALSO passed our own
197
+ * `entryPath` (the `/$bunfs/…` sentinel) it would land in the child's user
198
+ * args and be parsed as the subcommand — the v0.2.4 bug where `fleet run
199
+ * doctor` reported `unknown subcommand: /$bunfs/root/crewhaus-…` for every
200
+ * harness. So: compiled → `[execPath, ...childArgv]` and let Bun supply the
201
+ * entry; dev → `[execPath, entryPath, ...childArgv]` with the real script path.
202
+ */
203
+ export declare function fleetSelfInvokeArgv(ctx: SelfInvokeContext, childArgv: ReadonlyArray<string>): string[];
168
204
  /** Per-harness confirm seam for `--allow-mutating` (interactive prompt in the
169
205
  * CLI; a predicate in tests). Returning false skips that harness. */
170
206
  export type ConfirmMutating = (inv: HarnessInventory, argv: ReadonlyArray<string>) => Promise<boolean>;
@@ -203,5 +239,19 @@ export type RunFleetBulkReport = {
203
239
  * runner is the sole side-effect surface, injected for testability.
204
240
  */
205
241
  export declare function runFleetBulk(opts: RunFleetBulkOptions): Promise<RunFleetBulkReport>;
242
+ /**
243
+ * v0.3.0 Goal 6 — decode a child process's exit code back into its failure
244
+ * class + report title. The coded exits are the documented `EXIT_CODES`
245
+ * contract every terminal surface (die(), the compiled-bundle catch
246
+ * wrappers) now honours, so fleet can say "out of funding" instead of
247
+ * "exit 31" without parsing any output. Titles mirror the
248
+ * `BUILTIN_FAILURE_CLASSES` wording (recovery-engine) for the provider
249
+ * classes and the `EXIT_CODES` doc comments for the rest. Exit 0 and
250
+ * unmapped codes return undefined (a plain `exit N` line).
251
+ */
252
+ export declare function describeFleetExit(exitCode: number): {
253
+ readonly class: FailureClass;
254
+ readonly title: string;
255
+ } | undefined;
206
256
  /** Render a bulk-run report as summary lines for `fleet run`. */
207
257
  export declare function formatBulkReport(report: RunFleetBulkReport): ReadonlyArray<string>;
package/dist/fleet.js CHANGED
@@ -49,6 +49,7 @@
49
49
  */
50
50
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
51
51
  import { join, resolve } from "node:path";
52
+ import { EXIT_CODES } from "@crewhaus/errors";
52
53
  import { extractFeedbackRecords, mergeFeedback } from "./feedback";
53
54
  /** The spec file that roots a standalone harness. */
54
55
  export const HARNESS_SPEC_FILENAME = "crewhaus.yaml";
@@ -426,6 +427,35 @@ export function matchesFilter(inv, filter) {
426
427
  const glob = new Bun.Glob(filter);
427
428
  return glob.match(inv.specName) || glob.match(basenameOf(inv.dir));
428
429
  }
430
+ /**
431
+ * Is `entryPath` the Bun standalone-executable virtual-FS sentinel — i.e. are
432
+ * we running a `bun build --compile` single-file binary rather than
433
+ * `bun run …/index.ts`? Bun rewrites the entry (`process.argv[1]` / `Bun.main`)
434
+ * to a path inside its embedded root: `/$bunfs/root/<name>` on POSIX,
435
+ * `B:\~BUN\root\<name>` on Windows.
436
+ */
437
+ export function isCompiledEntryPath(entryPath) {
438
+ return (entryPath.includes("/$bunfs/") || entryPath.includes("\\~BUN\\") || entryPath.includes("/~BUN/"));
439
+ }
440
+ /**
441
+ * Build the argv to re-invoke THIS running CLI in a child process with
442
+ * `childArgv` (e.g. `["doctor"]`) — correct under BOTH `bun run
443
+ * apps/cli/src/index.ts …` and a `bun build --compile` single-file binary.
444
+ *
445
+ * The compiled case is the subtle one. A standalone binary RE-INJECTS its own
446
+ * embedded entry every time it is spawned, so the child already receives
447
+ * `[<exe>, /$bunfs/root/<name>, ...childArgv]`. If we ALSO passed our own
448
+ * `entryPath` (the `/$bunfs/…` sentinel) it would land in the child's user
449
+ * args and be parsed as the subcommand — the v0.2.4 bug where `fleet run
450
+ * doctor` reported `unknown subcommand: /$bunfs/root/crewhaus-…` for every
451
+ * harness. So: compiled → `[execPath, ...childArgv]` and let Bun supply the
452
+ * entry; dev → `[execPath, entryPath, ...childArgv]` with the real script path.
453
+ */
454
+ export function fleetSelfInvokeArgv(ctx, childArgv) {
455
+ return isCompiledEntryPath(ctx.entryPath)
456
+ ? [ctx.execPath, ...childArgv]
457
+ : [ctx.execPath, ctx.entryPath, ...childArgv];
458
+ }
429
459
  /**
430
460
  * Run one subcommand across the filtered fleet. Read-only commands run
431
461
  * unconditionally on every matched harness; a mutating command (only reachable
@@ -462,6 +492,35 @@ export async function runFleetBulk(opts) {
462
492
  }
463
493
  return { plan, results, failed, passed, skipped };
464
494
  }
495
+ /**
496
+ * v0.3.0 Goal 6 — decode a child process's exit code back into its failure
497
+ * class + report title. The coded exits are the documented `EXIT_CODES`
498
+ * contract every terminal surface (die(), the compiled-bundle catch
499
+ * wrappers) now honours, so fleet can say "out of funding" instead of
500
+ * "exit 31" without parsing any output. Titles mirror the
501
+ * `BUILTIN_FAILURE_CLASSES` wording (recovery-engine) for the provider
502
+ * classes and the `EXIT_CODES` doc comments for the rest. Exit 0 and
503
+ * unmapped codes return undefined (a plain `exit N` line).
504
+ */
505
+ export function describeFleetExit(exitCode) {
506
+ const table = {
507
+ [EXIT_CODES.generic]: { class: "unknown", title: "unclassified failure" },
508
+ [EXIT_CODES.spec]: { class: "spec", title: "spec parse/validation failed" },
509
+ [EXIT_CODES.config]: { class: "config", title: "config / missing env" },
510
+ [EXIT_CODES.auth]: { class: "auth", title: "provider rejected the credentials" },
511
+ [EXIT_CODES.billing]: { class: "billing", title: "provider account out of funding" },
512
+ [EXIT_CODES.rate_limit]: {
513
+ class: "rate_limit",
514
+ title: "provider rate limit still exceeded after retries",
515
+ },
516
+ [EXIT_CODES.crewhaus_budget]: {
517
+ class: "crewhaus_budget",
518
+ title: "run stopped by the configured budget cap",
519
+ },
520
+ [EXIT_CODES.tool]: { class: "tool", title: "tool / MCP failure" },
521
+ };
522
+ return table[exitCode];
523
+ }
465
524
  /** Render a bulk-run report as summary lines for `fleet run`. */
466
525
  export function formatBulkReport(report) {
467
526
  const cmd = report.plan.argv.join(" ");
@@ -475,7 +534,12 @@ export function formatBulkReport(report) {
475
534
  continue;
476
535
  }
477
536
  const mark = r.exitCode === 0 ? "✓" : "✗";
478
- lines.push(`${mark} ${r.inv.specName} — exit ${r.exitCode}`);
537
+ // v0.3.0 Goal 6 a coded exit renders its failure title inline:
538
+ // ✗ support-bot — provider account out of funding · exit 31
539
+ const described = r.exitCode !== 0 ? describeFleetExit(r.exitCode ?? 0) : undefined;
540
+ lines.push(described !== undefined
541
+ ? `${mark} ${r.inv.specName} — ${described.title} · exit ${r.exitCode}`
542
+ : `${mark} ${r.inv.specName} — exit ${r.exitCode}`);
479
543
  const tail = (r.tail ?? "").trim();
480
544
  if (tail !== "") {
481
545
  for (const line of tail.split("\n").slice(-3))
@@ -483,6 +547,18 @@ export function formatBulkReport(report) {
483
547
  }
484
548
  }
485
549
  lines.push("");
486
- lines.push(`summary: ${report.passed} passed, ${report.failed} failed, ${report.skipped} skipped`);
550
+ // v0.3.0 Goal 6 — class-keyed failure rollup: `2 failed (billing ×1,
551
+ // unknown ×1)` so a fleet-wide funding outage reads at a glance.
552
+ const classCounts = new Map();
553
+ for (const r of report.results) {
554
+ if (!r.ran || r.exitCode === 0 || r.exitCode === undefined)
555
+ continue;
556
+ const cls = describeFleetExit(r.exitCode)?.class ?? "unknown";
557
+ classCounts.set(cls, (classCounts.get(cls) ?? 0) + 1);
558
+ }
559
+ const rollup = classCounts.size > 0
560
+ ? ` (${[...classCounts.entries()].map(([cls, n]) => `${cls} ×${n}`).join(", ")})`
561
+ : "";
562
+ lines.push(`summary: ${report.passed} passed, ${report.failed} failed${rollup}, ${report.skipped} skipped`);
487
563
  return lines;
488
564
  }