@socketsecurity/lib 6.0.8 → 6.0.9

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.
Files changed (119) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/ai/route.d.mts +21 -8
  3. package/dist/ai/route.js +21 -6
  4. package/dist/ai/spawn.d.mts +32 -0
  5. package/dist/ai/spawn.js +91 -2
  6. package/dist/ai/types.d.mts +10 -0
  7. package/dist/bin/prim.cjs +18797 -13627
  8. package/dist/bin/resolve.js +1 -1
  9. package/dist/cache/ttl/store.js +1 -1
  10. package/dist/compression/brotli.js +1 -1
  11. package/dist/compression/gzip.js +1 -1
  12. package/dist/config/layers.d.ts +53 -0
  13. package/dist/config/layers.js +83 -0
  14. package/dist/constants/socket.js +1 -1
  15. package/dist/debug/caller-info.js +1 -1
  16. package/dist/dlx/binary-cache.js +1 -1
  17. package/dist/dlx/binary-resolution.js +1 -1
  18. package/dist/dlx/firewall.js +1 -1
  19. package/dist/dlx/lockfile.js +1 -1
  20. package/dist/eco/npm/npm/parse-lockfile.js +1 -1
  21. package/dist/eco/npm/yarnpkg/yarn/parse-lockfile.js +1 -1
  22. package/dist/env/rewire.d.ts +5 -3
  23. package/dist/env/rewire.js +6 -7
  24. package/dist/env/xdg.d.ts +17 -0
  25. package/dist/env/xdg.js +21 -1
  26. package/dist/errors/stack.js +1 -2
  27. package/dist/external/@npmcli/package-json.js +3455 -4881
  28. package/dist/external/@yarnpkg/extensions.js +1 -1
  29. package/dist/external/external-pack.js +1 -1
  30. package/dist/external/npm-pack.js +29164 -31799
  31. package/dist/external/tar-fs.js +63 -16
  32. package/dist/external-tools/jre/detect-platform-arch.js +1 -1
  33. package/dist/external-tools/manifest.js +1 -1
  34. package/dist/external-tools/python/asset-names.js +1 -1
  35. package/dist/external-tools/python/uv-install.d.ts +89 -0
  36. package/dist/external-tools/python/uv-install.js +165 -0
  37. package/dist/external-tools/skillspector/from-uv.d.ts +30 -0
  38. package/dist/external-tools/skillspector/from-uv.js +56 -0
  39. package/dist/external-tools/skillspector/resolve.d.ts +18 -3
  40. package/dist/external-tools/skillspector/resolve.js +21 -8
  41. package/dist/external-tools/skillspector/types.d.ts +3 -1
  42. package/dist/external-tools/uv/asset-names.d.ts +36 -0
  43. package/dist/external-tools/uv/asset-names.js +73 -0
  44. package/dist/external-tools/uv/from-download.d.ts +17 -0
  45. package/dist/external-tools/uv/from-download.js +50 -0
  46. package/dist/external-tools/uv/from-path.d.ts +5 -0
  47. package/dist/external-tools/uv/from-path.js +22 -0
  48. package/dist/external-tools/uv/from-vfs.d.ts +7 -0
  49. package/dist/external-tools/uv/from-vfs.js +26 -0
  50. package/dist/external-tools/uv/resolve.d.ts +25 -0
  51. package/dist/external-tools/uv/resolve.js +61 -0
  52. package/dist/external-tools/uv/types.d.ts +24 -0
  53. package/dist/external-tools/uv/types.js +2 -0
  54. package/dist/fleet/repo-config.d.ts +53 -0
  55. package/dist/fleet/repo-config.js +79 -0
  56. package/dist/fs/allowed-dirs-cache.js +3 -5
  57. package/dist/fs/copy.d.ts +88 -0
  58. package/dist/fs/copy.js +89 -0
  59. package/dist/fs/safe.js +1 -1
  60. package/dist/git/_internal.js +2 -2
  61. package/dist/github/refs.js +10 -15
  62. package/dist/globs/_internal.js +1 -1
  63. package/dist/http-request/browser.js +1 -1
  64. package/dist/http-request/checksum-file.js +1 -1
  65. package/dist/http-request/headers.js +1 -1
  66. package/dist/integrity.d.ts +125 -78
  67. package/dist/integrity.js +168 -84
  68. package/dist/logger/colors.d.ts +1 -2
  69. package/dist/logger/colors.js +2 -2
  70. package/dist/logger/symbols-builder.js +7 -8
  71. package/dist/logger/symbols.js +8 -8
  72. package/dist/node/module.d.ts +73 -2
  73. package/dist/node/module.js +97 -12
  74. package/dist/objects/predicates.js +1 -1
  75. package/dist/packages/exports.js +2 -2
  76. package/dist/packages/isolation.js +1 -1
  77. package/dist/packages/manifest.d.ts +29 -0
  78. package/dist/packages/manifest.js +38 -2
  79. package/dist/packages/normalize.js +1 -1
  80. package/dist/packages/provenance.js +2 -2
  81. package/dist/packages/specs.js +1 -1
  82. package/dist/paths/_internal.d.ts +1 -7
  83. package/dist/paths/_internal.js +9 -15
  84. package/dist/paths/conversion.js +1 -1
  85. package/dist/paths/normalize.js +1 -1
  86. package/dist/paths/predicates.js +2 -1
  87. package/dist/paths/socket.d.ts +74 -23
  88. package/dist/paths/socket.js +97 -23
  89. package/dist/perf/metrics.js +1 -1
  90. package/dist/perf/report.js +1 -1
  91. package/dist/regexps/spec.js +1 -1
  92. package/dist/schema/validate.js +1 -1
  93. package/dist/secrets/broker.d.ts +35 -0
  94. package/dist/secrets/broker.js +72 -0
  95. package/dist/secrets/find.d.ts +6 -4
  96. package/dist/secrets/find.js +15 -1
  97. package/dist/secrets/rc.js +1 -1
  98. package/dist/smol/detect.js +1 -1
  99. package/dist/smol/http.js +1 -1
  100. package/dist/smol/https.js +1 -1
  101. package/dist/smol/manifest.js +1 -1
  102. package/dist/smol/path.js +1 -1
  103. package/dist/smol/primordial.js +1 -1
  104. package/dist/smol/purl.js +1 -1
  105. package/dist/smol/versions.js +1 -1
  106. package/dist/smol/vfs.js +1 -1
  107. package/dist/sorts/_internal.d.ts +1 -2
  108. package/dist/sorts/_internal.js +2 -6
  109. package/dist/sorts/semver.js +2 -2
  110. package/dist/spinner/create-spinner-class.js +1 -1
  111. package/dist/stdio/progress.js +1 -1
  112. package/dist/stdio/prompts.d.ts +0 -8
  113. package/dist/stdio/prompts.js +10 -23
  114. package/dist/strings/format.js +1 -1
  115. package/dist/strings/search.js +1 -1
  116. package/dist/themes/context.d.ts +5 -3
  117. package/dist/themes/context.js +5 -6
  118. package/llms.txt +750 -0
  119. package/package.json +79 -13
package/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [6.0.9](https://github.com/SocketDev/socket-lib/releases/tag/v6.0.9) - 2026-06-18
9
+
10
+ ### Added
11
+
12
+ - **`external-tools/uv` — uv resolver.** Resolves Astral's `uv` Python package manager across three tiers (embedded VFS, then system PATH, then a downloaded GitHub release), matching the shape of the other `external-tools/*` tool resolvers. Exports `resolveUv`, `ResolvedUv`, `UvSource`, plus the per-platform asset map and download helpers.
13
+ - **`external-tools/python/uv-install` — reproducible uv-project install helpers.** `uvSyncProject` installs a uv project at its exact pinned versions and refuses to proceed when those have drifted, so every machine gets the same result. `uvExportMaterialize` installs the same pinned versions into a content-addressed directory with no virtualenv, so the result is relocatable and embeddable in a single-file build. Concurrent callers serialize so two installs can't collide. The Python analog of the npm dlx install model.
14
+ - **`external-tools/skillspector` — pinned-project resolution tier.** `skillspectorFromUv` installs SkillSpector from a fully pinned uv project (every version fixed) and returns its entry point, resolved ahead of the existing git-SHA fallback when a project directory and `uv` binary are supplied. Adds a `'uv'` source to the resolution result.
15
+ - **`config/layers` — generic layered-config reader.** `readConfigLayers(name, { dirs })` reads a named config file from an ordered list of layer directories (lowest precedence first) and returns the layers that exist; absent or unparseable layers are skipped. `mergeConfigArray` concatenates one array-valued key across all layers, for lists that higher layers extend rather than replace. It carries no project-convention knowledge: the caller supplies the directories and the merge policy.
16
+ - **`paths` — `_wheelhouse` tool-layout dirs and the agent-clone dir.** `getSocketRackDir` and `getSocketRackToolDir` locate the racked tool store, `getSocketWheelhouseBinDir` the PATH-handle directory that points into it, and `getSocketRepoClonesDir` the directory where agents clone external repos for reference (kept out of the projects tree so sibling-walking tooling never treats a clone as a fleet member).
17
+ - **`ai` — offline/gated-model detection and fall-over.** `spawnTierWithFallback` walks a tier's cross-engine equivalence chain and runs the first engine that is both installed and authenticated, so a request still completes when the preferred model is down, gated, or unkeyed. `isModelUnavailable` recognizes a down-or-gated model from the engine's actual output rather than a brittle literal-string match. The `ai/route` resolver and `ai/subagent-status` reader are exposed as their own entry points.
18
+ - **`fs/copy` — recursive copy with three destination modes.** `copy(from, to, { mode })` copies a file or directory tree. `CopyMode` chooses how an existing destination is treated: `'overlay'` (the default: overwrite collisions, keep destination-only files), `'pave'` (the destination becomes an exact mirror of the source via an atomic sibling-temp-then-rename swap, so no stale files survive and a partial tree is never observed), or `'fill'` (no-clobber: add only what is missing, never overwrite). Also accepts `filter`, `dereference`, and an abort `signal`.
19
+ - **`node/requireFrom` — relative `require` bound to the caller's directory.** `requireFrom(fromUrl, specifier)` resolves a relative specifier (`./x`, `../y`) from the caller's own directory, passed as `import.meta.url`, so relative loads bind to the right base when modules run unbundled (for example AOT-compiled from source, where each module sits at its own nested path). Builtins and bare packages resolve as before.
20
+ - **`secrets` — proteus broker credential tier.** The credential resolver gains a broker layer in the documented order (explicit, then env, then broker, then keychain): it connects to the broker's runtime socket, requests the value, and self-gates. No socket means no broker, so it returns `undefined` and falls through to the keychain. Async only (absent from `resolveSync`) and skipped under `allowEnvOnly`. Existing `resolveProviderCredential` call sites are unchanged.
21
+ - **`paths` — `getRuntimeSocketPath` and `getXdgRuntimeDir` for daemon sockets.** One resolver a daemon and its clients both call to locate a runtime socket: the XDG runtime dir when present, a `$TMPDIR/<name>-<uid>` fallback, or a named pipe on Windows.
22
+ - **`llms.txt` — discovery index for AI agents.** `pnpm run docs` now also emits a publish-safe `llms.txt` at the package root that links each export subpath to its shipped `.d.mts` declaration, giving an agent one file to read after installing. It ships in the published tarball.
23
+ - **`packages/manifest` — `trimPublishManifest`.** Returns a shallow copy of a `package.json` that omits dev/build-only top-level fields (`devDependencies`, `scripts` by default) so a published tarball and its npm metadata stay lean. `drop` overrides the field set; `keep` retains a field even when dropped (e.g. a runtime `postinstall`). The original object is left intact.
24
+
25
+ ### Changed
26
+
27
+ - **`integrity` — one `Hash` currency (breaking).** Hashes are modeled as a single concept, a `Hash` carrying algorithm, hex, and sri, rather than an integrity-vs-checksum type duality, so the algorithm is explicit on every value. Adds `parseHash` (encoding- and length-aware), `computeHash` (sha512 default), `verifyHash(bytes, expected)` (verifies against the expected hash's declared algorithm), `equalHashes` (encoding-agnostic, and never equal across algorithms), `makeHash`, and `HashMismatchError`. Breaking: `verifyHash` now takes `(bytes, expected)`, not `(expected, computed)`, and `DlxHashMismatchError` is a deprecated alias of `HashMismatchError`. Back-compat shims remain: `checksumToIntegrity`, `integrityToChecksum`, `normalizeHash`, `computeHashes`, `isChecksum`, `isIntegrity`, `parseIntegrity`.
28
+ - **`fleet/repo-config` is now a thin wrapper over `config/layers`.** `resolveRepoConfig` (the fleet default layered under a per-repo override) and `mergeRepoConfigArray` stay at `fleet/repo-config`; the generic, convention-free primitives moved to the new `config/layers` entry. `resolveRepoConfig` callers see no behavior change.
29
+
8
30
  ## [6.0.8](https://github.com/SocketDev/socket-lib/releases/tag/v6.0.8) - 2026-06-11
9
31
 
10
32
  ### Added
@@ -32,15 +32,16 @@ export interface TierCandidate {
32
32
  }
33
33
  /**
34
34
  * Why the resolver returned what it did. - `preferred` — the tier's
35
- * first-choice engine was available + keyed. - `fellback` — the preferred
36
- * engine was missing/unkeyed; an equivalent on another engine was used (`from`
37
- * names the original tier).
35
+ * first-choice engine was available + keyed. - `fell-over` — the preferred
36
+ * engine was missing/unkeyed; an equivalent on another engine was used
37
+ * (`requestedTier` names the tier originally asked for). Matches the
38
+ * `fellOver` vocabulary `spawnTierWithFallback` (spawn.mts) reports.
38
39
  */
39
- export type TierResolveReason = 'fellback' | 'preferred';
40
+ export type TierResolveReason = 'fell-over' | 'preferred';
40
41
  export interface TierResolution {
41
42
  readonly candidate: TierCandidate;
42
43
  readonly reason: TierResolveReason;
43
- readonly from?: AiTier | undefined;
44
+ readonly requestedTier?: AiTier | undefined;
44
45
  }
45
46
  /**
46
47
  * The context a caller probes once and passes in: which engine CLIs exist, and
@@ -62,8 +63,20 @@ export declare function isCandidateUsable(candidate: TierCandidate, ctx: RouteCo
62
63
  * Resolve a tier to the best available concrete target. Prefers the tier's
63
64
  * first-choice (Claude) candidate; if its engine is missing or unkeyed, walks
64
65
  * the cross-engine equivalence ladder and returns the first usable equivalent,
65
- * tagging the result `fellback` with the original tier in `from`. Returns
66
- * `undefined` only when NOTHING in the chain is usable — the caller then skips
67
- * the work or surfaces a "no AI engine available" message.
66
+ * tagging the result `fell-over` with the original tier in `requestedTier`.
67
+ * Returns `undefined` only when NOTHING in the chain is usable — the caller
68
+ * then skips the work or surfaces a "no AI engine available" message.
68
69
  */
69
70
  export declare function resolveTier(tier: AiTier, ctx: RouteContext): TierResolution | undefined;
71
+ /**
72
+ * The ORDERED list of usable candidates for a tier — the runtime-fallback
73
+ * sequence. `resolveTier` returns only the single best pick (good when you
74
+ * trust it will work); this returns every usable candidate in preference order
75
+ * so a caller can advance to the next when one fails AT RUNTIME — e.g. the
76
+ * preferred model is installed + keyed (so it passes `isCandidateUsable`) yet
77
+ * its CLI reports it offline at spawn ("Claude Fable 5 is currently
78
+ * unavailable"). The static check can't predict an outage; only the spawn
79
+ * result can, so the caller walks this list until a spawn returns without the
80
+ * `unavailable` flag. Empty when nothing in the chain is usable.
81
+ */
82
+ export declare function usableTierCandidates(tier: AiTier, ctx: RouteContext): TierCandidate[];
package/dist/ai/route.js CHANGED
@@ -131,9 +131,9 @@ function isCandidateUsable(candidate, ctx) {
131
131
  * Resolve a tier to the best available concrete target. Prefers the tier's
132
132
  * first-choice (Claude) candidate; if its engine is missing or unkeyed, walks
133
133
  * the cross-engine equivalence ladder and returns the first usable equivalent,
134
- * tagging the result `fellback` with the original tier in `from`. Returns
135
- * `undefined` only when NOTHING in the chain is usable — the caller then skips
136
- * the work or surfaces a "no AI engine available" message.
134
+ * tagging the result `fell-over` with the original tier in `requestedTier`.
135
+ * Returns `undefined` only when NOTHING in the chain is usable — the caller
136
+ * then skips the work or surfaces a "no AI engine available" message.
137
137
  */
138
138
  function resolveTier(tier, ctx) {
139
139
  const chain = TIER_CHAINS[tier] ?? TIER_CHAINS.sonnet;
@@ -144,13 +144,28 @@ function resolveTier(tier, ctx) {
144
144
  reason: "preferred"
145
145
  } : {
146
146
  candidate,
147
- from: tier,
148
- reason: "fellback"
147
+ requestedTier: tier,
148
+ reason: "fell-over"
149
149
  };
150
150
  }
151
151
  }
152
+ /**
153
+ * The ORDERED list of usable candidates for a tier — the runtime-fallback
154
+ * sequence. `resolveTier` returns only the single best pick (good when you
155
+ * trust it will work); this returns every usable candidate in preference order
156
+ * so a caller can advance to the next when one fails AT RUNTIME — e.g. the
157
+ * preferred model is installed + keyed (so it passes `isCandidateUsable`) yet
158
+ * its CLI reports it offline at spawn ("Claude Fable 5 is currently
159
+ * unavailable"). The static check can't predict an outage; only the spawn
160
+ * result can, so the caller walks this list until a spawn returns without the
161
+ * `unavailable` flag. Empty when nothing in the chain is usable.
162
+ */
163
+ function usableTierCandidates(tier, ctx) {
164
+ return (TIER_CHAINS[tier] ?? TIER_CHAINS.sonnet).filter((c) => isCandidateUsable(c, ctx));
165
+ }
152
166
 
153
167
  //#endregion
154
168
  exports.TIER_CHAINS = TIER_CHAINS;
155
169
  exports.isCandidateUsable = isCandidateUsable;
156
- exports.resolveTier = resolveTier;
170
+ exports.resolveTier = resolveTier;
171
+ exports.usableTierCandidates = usableTierCandidates;
@@ -11,6 +11,8 @@
11
11
  * SDK installs. Retry: 3 attempts on overload (HTTP 529 / "Overloaded"), exp.
12
12
  * backoff (5s / 15s / 45s). Each retry is a fresh subprocess.
13
13
  */
14
+ import type { RouteContext, TierCandidate } from './route.mts';
15
+ import type { AiTier } from './tier.mts';
14
16
  import type { AgentSpawnResult, AiAgentName, SpawnAiAgentOptions } from './types.mts';
15
17
  export declare function backoffFor(attempt: number): number;
16
18
  /**
@@ -31,6 +33,7 @@ export declare function buildArgs(agent: AiAgentName, options: SpawnAiAgentOptio
31
33
  * shapes (`fable`, `claude-fable-5`, `mythos`, `claude-mythos-5`).
32
34
  */
33
35
  export declare function isAdaptiveOnlyModel(model: string): boolean;
36
+ export declare function isModelUnavailable(stdout: string, stderr: string): boolean;
34
37
  export declare function isOverloaded(stdout: string, stderr: string): boolean;
35
38
  export declare function pickAgent(requested: AiAgentName | undefined, cwd: string): Promise<AiAgentName>;
36
39
  /**
@@ -55,3 +58,32 @@ export declare function pickAgent(requested: AiAgentName | undefined, cwd: strin
55
58
  * is requested, when none of the known agents are on PATH).
56
59
  */
57
60
  export declare function spawnAiAgent(options: SpawnAiAgentOptions): Promise<AgentSpawnResult>;
61
+ /**
62
+ * Result of a tier spawn that may have fallen over one or more offline models.
63
+ * `result` is the spawn that actually ran (the first whose model was not
64
+ * `unavailable`, or the last attempt if every candidate was down). `candidate`
65
+ * is the engine/model that produced it. `fellOver` lists the candidates that
66
+ * reported their model offline before this one — empty on a first-try success.
67
+ */
68
+ export interface TierSpawnResult {
69
+ readonly candidate: TierCandidate;
70
+ readonly fellOver: readonly TierCandidate[];
71
+ readonly result: AgentSpawnResult;
72
+ }
73
+ /**
74
+ * Spawn a tier's work, automatically FALLING OVER to the next agent when a
75
+ * model is offline. Walks the tier's usable candidates (Claude → Codex →
76
+ * open-weight) in order; if a spawn comes back with `unavailable` (the model is
77
+ * down or gated — e.g. "Claude Fable 5 is currently unavailable"), it advances
78
+ * to the next candidate instead of failing. This is the runtime complement to
79
+ * `route.mts`'s static availability check: that check can't predict an outage,
80
+ * so the live spawn result drives the fallback.
81
+ *
82
+ * Returns the first non-`unavailable` spawn (success OR a genuine work failure
83
+ * on a model that WAS reachable — a real failure shouldn't silently retry on a
84
+ * weaker model). If every candidate is offline, returns the last attempt with
85
+ * its `unavailable` flag set, so the caller can report "all models down".
86
+ * Throws only when the tier has no usable candidate at all (nothing installed +
87
+ * keyed) — same contract as `resolveTier` returning undefined.
88
+ */
89
+ export declare function spawnTierWithFallback(tier: AiTier, ctx: RouteContext, options: Omit<SpawnAiAgentOptions, 'agent' | 'effort' | 'model'>): Promise<TierSpawnResult>;
package/dist/ai/spawn.js CHANGED
@@ -9,6 +9,7 @@ const require_process_spawn_errors = require('../process/spawn/errors.js');
9
9
  const require_process_spawn_child = require('../process/spawn/child.js');
10
10
  const require_ai_discover = require('./discover.js');
11
11
  const require_primordials_promise = require('../primordials/promise.js');
12
+ const require_ai_route = require('./route.js');
12
13
 
13
14
  //#region src/ai/spawn.mts
14
15
  /**
@@ -113,6 +114,45 @@ function buildArgs(agent, options) {
113
114
  function isAdaptiveOnlyModel(model) {
114
115
  return /\b(?:fable|mythos)\b/i.test(model) || /claude-(?:fable|mythos)/i.test(model);
115
116
  }
117
+ const MODEL_UNAVAILABLE_PHRASES = [
118
+ "access denied",
119
+ "currently unavailable",
120
+ "forbidden",
121
+ "have access",
122
+ "is unavailable",
123
+ "isn’t available",
124
+ "isn't available",
125
+ "may not exist",
126
+ "no access to",
127
+ "no such model",
128
+ "not authorized",
129
+ "not authorised",
130
+ "not available",
131
+ "permission denied",
132
+ "permission_denied",
133
+ "temporarily unavailable",
134
+ "unauthorized",
135
+ "unauthorised"
136
+ ];
137
+ const MODEL_EXISTENCE_PHRASES = [
138
+ "does not exist",
139
+ "doesn't exist",
140
+ "no such",
141
+ "not exist",
142
+ "not found",
143
+ "unavailable",
144
+ "unknown",
145
+ "unreachable"
146
+ ];
147
+ function isModelUnavailable(stdout, stderr) {
148
+ const text = `${stdout}\n${stderr}`.toLowerCase();
149
+ for (let i = 0, { length } = MODEL_UNAVAILABLE_PHRASES; i < length; i += 1) if (text.includes(MODEL_UNAVAILABLE_PHRASES[i])) return true;
150
+ if (/\bmodel[_-]?not[_-]?found\b/i.test(text) || /\bapi error:\s*(?:403|404)\b/i.test(text)) return true;
151
+ if (text.includes("model")) {
152
+ for (let i = 0, { length } = MODEL_EXISTENCE_PHRASES; i < length; i += 1) if (text.includes(MODEL_EXISTENCE_PHRASES[i])) return true;
153
+ }
154
+ return false;
155
+ }
116
156
  function isOverloaded(stdout, stderr) {
117
157
  const re = /API Error: 529|Overloaded/i;
118
158
  return re.test(stdout) || re.test(stderr);
@@ -200,7 +240,54 @@ async function spawnAiAgent(options) {
200
240
  exitCode,
201
241
  overloaded: isOverloaded(stdout, stderr),
202
242
  stderr,
203
- stdout
243
+ stdout,
244
+ unavailable: isModelUnavailable(stdout, stderr)
245
+ };
246
+ }
247
+ /**
248
+ * Spawn a tier's work, automatically FALLING OVER to the next agent when a
249
+ * model is offline. Walks the tier's usable candidates (Claude → Codex →
250
+ * open-weight) in order; if a spawn comes back with `unavailable` (the model is
251
+ * down or gated — e.g. "Claude Fable 5 is currently unavailable"), it advances
252
+ * to the next candidate instead of failing. This is the runtime complement to
253
+ * `route.mts`'s static availability check: that check can't predict an outage,
254
+ * so the live spawn result drives the fallback.
255
+ *
256
+ * Returns the first non-`unavailable` spawn (success OR a genuine work failure
257
+ * on a model that WAS reachable — a real failure shouldn't silently retry on a
258
+ * weaker model). If every candidate is offline, returns the last attempt with
259
+ * its `unavailable` flag set, so the caller can report "all models down".
260
+ * Throws only when the tier has no usable candidate at all (nothing installed +
261
+ * keyed) — same contract as `resolveTier` returning undefined.
262
+ */
263
+ async function spawnTierWithFallback(tier, ctx, options) {
264
+ const candidates = require_ai_route.usableTierCandidates(tier, ctx);
265
+ if (candidates.length === 0) throw new require_primordials_error.ErrorCtor(`spawnTierWithFallback: no usable agent for tier "${tier}". No candidate engine is both installed and keyed (checked: ${candidates.length}). Install/authenticate one of the tier's engines, or pick a different tier.`);
266
+ const fellOver = [];
267
+ let last;
268
+ for (let i = 0, { length } = candidates; i < length; i += 1) {
269
+ const candidate = candidates[i];
270
+ const result = await spawnAiAgent({
271
+ ...options,
272
+ agent: candidate.engine,
273
+ effort: candidate.effort,
274
+ model: candidate.model
275
+ });
276
+ last = {
277
+ candidate,
278
+ result
279
+ };
280
+ if (!result.unavailable) return {
281
+ candidate,
282
+ fellOver,
283
+ result
284
+ };
285
+ fellOver.push(candidate);
286
+ }
287
+ return {
288
+ candidate: last.candidate,
289
+ fellOver: fellOver.slice(0, -1),
290
+ result: last.result
204
291
  };
205
292
  }
206
293
 
@@ -208,6 +295,8 @@ async function spawnAiAgent(options) {
208
295
  exports.backoffFor = backoffFor;
209
296
  exports.buildArgs = buildArgs;
210
297
  exports.isAdaptiveOnlyModel = isAdaptiveOnlyModel;
298
+ exports.isModelUnavailable = isModelUnavailable;
211
299
  exports.isOverloaded = isOverloaded;
212
300
  exports.pickAgent = pickAgent;
213
- exports.spawnAiAgent = spawnAiAgent;
301
+ exports.spawnAiAgent = spawnAiAgent;
302
+ exports.spawnTierWithFallback = spawnTierWithFallback;
@@ -56,6 +56,16 @@ export interface AgentSpawnResult {
56
56
  readonly overloaded: boolean;
57
57
  readonly stderr: string;
58
58
  readonly stdout: string;
59
+ /**
60
+ * True when the selected MODEL could not serve the request — it is offline
61
+ * (e.g. "Claude Fable 5 is currently unavailable") or gated/absent for this
62
+ * account ("issue with the selected model … may not exist or you may not have
63
+ * access"). Distinct from `overloaded`: the model is not coming back within a
64
+ * retry window, so the right response is to FALL OVER to the next agent in
65
+ * the tier chain rather than retry the same one. Both surface as a non-zero
66
+ * `exitCode`; this flag is how a caller knows to route elsewhere.
67
+ */
68
+ readonly unavailable: boolean;
59
69
  }
60
70
  /**
61
71
  * Inputs to a single agent spawn.