stitchkit 0.54.0 → 0.55.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.
Files changed (100) hide show
  1. package/README.md +4 -1
  2. package/dist/browser/socket-io.d.ts +11 -1
  3. package/dist/browser/socket-io.d.ts.map +1 -1
  4. package/dist/cli.js +14 -9
  5. package/dist/contract/file-ref.d.ts +13 -0
  6. package/dist/contract/file-ref.d.ts.map +1 -0
  7. package/dist/contract/index.d.ts +1 -0
  8. package/dist/contract/index.d.ts.map +1 -1
  9. package/dist/contract/index.js +21 -17
  10. package/dist/files/boundary.d.ts +52 -0
  11. package/dist/files/boundary.d.ts.map +1 -0
  12. package/dist/files.d.ts +3 -0
  13. package/dist/files.d.ts.map +1 -0
  14. package/dist/files.js +16 -0
  15. package/dist/{index-7vb44hy8.js → index-0gv7k0ra.js} +127 -92
  16. package/dist/index-1bx83sw4.js +4 -0
  17. package/dist/index-1k16zv57.js +269 -0
  18. package/dist/index-6k1937bx.js +20 -0
  19. package/dist/{index-bmnhqtya.js → index-7eq9va6e.js} +1 -4
  20. package/dist/{index-yxpe3phd.js → index-cby4ar3v.js} +1 -18
  21. package/dist/index-h60df7rj.js +110 -0
  22. package/dist/{index-yez419px.js → index-js7yexng.js} +19 -6
  23. package/dist/index-k4dftwf7.js +66 -0
  24. package/dist/{index-z7761jk2.js → index-npye5kt7.js} +16 -22
  25. package/dist/{index-b15rtha1.js → index-pzc32v75.js} +61 -3
  26. package/dist/{index-e1es808r.js → index-qh4xevf5.js} +1 -1
  27. package/dist/{index-gvha32jh.js → index-qx84bqzk.js} +2 -2
  28. package/dist/{index-pfqjb5xy.js → index-rgd18hx3.js} +23 -5
  29. package/dist/index-sbdmyz75.js +18 -0
  30. package/dist/{index-173tvqcp.js → index-v4bbq3p4.js} +9 -114
  31. package/dist/index.js +162 -35
  32. package/dist/node.js +30 -27
  33. package/dist/observability/index.js +28 -25
  34. package/dist/react.js +2 -2
  35. package/dist/realtime/contract.d.ts +9 -0
  36. package/dist/realtime/contract.d.ts.map +1 -1
  37. package/dist/realtime/index.d.ts +2 -1
  38. package/dist/realtime/index.d.ts.map +1 -1
  39. package/dist/realtime/request.d.ts +21 -0
  40. package/dist/realtime/request.d.ts.map +1 -0
  41. package/dist/realtime/socket.d.ts +3 -0
  42. package/dist/realtime/socket.d.ts.map +1 -1
  43. package/dist/remote.d.ts +9 -0
  44. package/dist/remote.d.ts.map +1 -0
  45. package/dist/remote.js +79 -0
  46. package/dist/server/context-contribution.d.ts +10 -0
  47. package/dist/server/context-contribution.d.ts.map +1 -0
  48. package/dist/server/context.d.ts.map +1 -1
  49. package/dist/server/index.d.ts +2 -1
  50. package/dist/server/index.d.ts.map +1 -1
  51. package/dist/server/index.js +133 -88
  52. package/dist/server/lifecycle.d.ts +4 -0
  53. package/dist/server/lifecycle.d.ts.map +1 -0
  54. package/dist/server/middleware/auth.d.ts +35 -7
  55. package/dist/server/middleware/auth.d.ts.map +1 -1
  56. package/dist/server/types.d.ts +1 -1
  57. package/dist/server/types.d.ts.map +1 -1
  58. package/dist/testing/surface-conformance.d.ts +47 -0
  59. package/dist/testing/surface-conformance.d.ts.map +1 -0
  60. package/dist/testing/surface-manifest.d.ts +156 -0
  61. package/dist/testing/surface-manifest.d.ts.map +1 -0
  62. package/dist/testing.d.ts +2 -0
  63. package/dist/testing.d.ts.map +1 -1
  64. package/dist/testing.js +366 -3
  65. package/dist/tools/agent.d.ts.map +1 -1
  66. package/dist/tools/async-operation.d.ts +168 -0
  67. package/dist/tools/async-operation.d.ts.map +1 -0
  68. package/dist/tools/cli-args.d.ts +1 -1
  69. package/dist/tools/cli-args.d.ts.map +1 -1
  70. package/dist/tools/cli-format.d.ts +2 -2
  71. package/dist/tools/cli-format.d.ts.map +1 -1
  72. package/dist/tools/cli-wait.d.ts +11 -2
  73. package/dist/tools/cli-wait.d.ts.map +1 -1
  74. package/dist/tools/cli.d.ts +2 -0
  75. package/dist/tools/cli.d.ts.map +1 -1
  76. package/dist/tools/define-download-tool.d.ts +6 -5
  77. package/dist/tools/define-download-tool.d.ts.map +1 -1
  78. package/dist/tools/define-upload-tool.d.ts +3 -1
  79. package/dist/tools/define-upload-tool.d.ts.map +1 -1
  80. package/dist/tools/define-wait-tool.d.ts +1 -1
  81. package/dist/tools/define-wait-tool.d.ts.map +1 -1
  82. package/dist/tools/download-core.d.ts +5 -10
  83. package/dist/tools/download-core.d.ts.map +1 -1
  84. package/dist/tools/lifecycle.d.ts +4 -0
  85. package/dist/tools/lifecycle.d.ts.map +1 -0
  86. package/dist/tools/mount-download.d.ts +5 -4
  87. package/dist/tools/mount-download.d.ts.map +1 -1
  88. package/dist/tools/mount-upload.d.ts +4 -2
  89. package/dist/tools/mount-upload.d.ts.map +1 -1
  90. package/dist/tools/upload-core.d.ts +3 -2
  91. package/dist/tools/upload-core.d.ts.map +1 -1
  92. package/dist/tools/view-file.d.ts +4 -3
  93. package/dist/tools/view-file.d.ts.map +1 -1
  94. package/dist/tools/wait-core.d.ts +6 -1
  95. package/dist/tools/wait-core.d.ts.map +1 -1
  96. package/dist/tools.d.ts +3 -2
  97. package/dist/tools.d.ts.map +1 -1
  98. package/dist/tools.js +385 -244
  99. package/llms-full.txt +417 -27
  100. package/package.json +10 -2
package/llms-full.txt CHANGED
@@ -33,7 +33,7 @@ realtime, `@tanstack/react-query` + `react-query-kit` for React). See
33
33
 
34
34
  ## Entrypoints
35
35
 
36
- stitchkit ships nine entrypoints. Each is import-safe for one environment —
36
+ stitchkit ships eleven entrypoints. Each is import-safe for one environment —
37
37
  keeping server-only code (`Bun.serve`, the MCP SDK) out of browser bundles.
38
38
 
39
39
  | Import | Use in | Holds |
@@ -44,12 +44,15 @@ keeping server-only code (`Bun.serve`, the MCP SDK) out of browser bundles.
44
44
  | `stitchkit/node` | server (Node ≥ 22) | `serveNode` + the runtime-agnostic core — the Node mirror of `/server` |
45
45
  | `stitchkit/tools` | server | `createMcpHandler`, `mountMcp`, `mountAgent`, the OAuth provider, native tools |
46
46
  | `stitchkit/cli` | server | `createCli` — the CLI transport, light (no MCP SDK / `ai`) |
47
+ | `stitchkit/remote` | browser **and** server | peer-free `implementRemote` for thin HTTP proxy processes |
48
+ | `stitchkit/files` | server (Bun or Node) | peer-free managed local-file boundary |
47
49
  | `stitchkit/observability` | server | request/tool event projections — `createObservability`, trace context, sanitisation |
48
50
  | `stitchkit/testing` | tests on Bun or Node | in-process generated clients over a real Fetch handler, without a TCP port |
49
51
  | `stitchkit/react` | browser | `createCursorQuery`, `createCacheBridge` |
50
52
 
51
53
  Rule of thumb: browser code imports `stitchkit` and `stitchkit/react`; server
52
- code adds `stitchkit/server` (or `stitchkit/node` on Node) and `stitchkit/tools`.
54
+ code adds `stitchkit/server` (or `stitchkit/node` on Node) and opts into
55
+ `stitchkit/tools`, `stitchkit/remote` or `stitchkit/files` by capability.
53
56
  The full export list of each is in the [API reference](../api/reference.md).
54
57
 
55
58
  ## Project layout
@@ -2777,7 +2780,8 @@ through the contract's typed client:
2777
2780
 
2778
2781
  ```ts
2779
2782
  import { createHttpClient } from 'stitchkit'
2780
- import { createStdioMcpServer, implementRemote } from 'stitchkit/tools'
2783
+ import { implementRemote } from 'stitchkit/remote'
2784
+ import { createStdioMcpServer } from 'stitchkit/tools'
2781
2785
 
2782
2786
  const http = createHttpClient({
2783
2787
  baseUrl: 'https://api.example.com',
@@ -3017,8 +3021,11 @@ import {
3017
3021
  defineViewFileTool,
3018
3022
  defineWaitTool,
3019
3023
  } from 'stitchkit/tools'
3024
+ import { createManagedFileBoundary } from 'stitchkit/files'
3020
3025
  import { z } from 'zod'
3021
3026
 
3027
+ const files = await createManagedFileBoundary({ root: '/srv/job-media' })
3028
+
3022
3029
  const waitForJob = defineWaitTool({
3023
3030
  name: 'wait_for_job',
3024
3031
  description: 'Wait for a job to finish',
@@ -3035,10 +3042,10 @@ const downloadResult = defineDownloadTool({
3035
3042
  name: 'download_result',
3036
3043
  description: 'Download a completed result',
3037
3044
  identity: { serviceName: 'jobs', action: 'downloadResult', scope: 'user' },
3038
- input: z.object({ id: z.string(), dir: z.string().optional() }),
3045
+ input: z.object({ id: z.string() }),
3039
3046
  resolveUrl: ({ id }) => resolveResultUrl(id),
3040
- defaultDir: './downloads',
3041
- dirFromInput: ({ dir }) => dir,
3047
+ files,
3048
+ pathFromInput: ({ id }) => `results/${id}.bin`,
3042
3049
  })
3043
3050
 
3044
3051
  const uploadInput = defineUploadTool({
@@ -3046,16 +3053,17 @@ const uploadInput = defineUploadTool({
3046
3053
  description: 'Upload a local input file',
3047
3054
  identity: { serviceName: 'jobs', action: 'uploadInput', scope: 'user' },
3048
3055
  output: z.object({ url: z.url() }),
3049
- upload: (path, context) => uploadFile(path, context.signal),
3056
+ files,
3057
+ upload: (source, context) => uploadBytes(source.bytes, context.signal),
3050
3058
  })
3051
3059
 
3052
3060
  const viewResult = defineViewFileTool({
3053
3061
  name: 'view_result',
3054
3062
  description: 'Inspect generated media',
3055
3063
  identity: { serviceName: 'jobs', action: 'viewResult', scope: 'user' },
3056
- // Local files are disabled when baseDir is omitted. URL fetches retain SSRF
3064
+ // Local files are disabled when files is omitted. URL fetches retain SSRF
3057
3065
  // protection; use allowPrivateHosts only for an explicitly trusted network.
3058
- baseDir: '/srv/job-media',
3066
+ files,
3059
3067
  })
3060
3068
 
3061
3069
  const runtimeTools = [waitForJob, downloadResult, uploadInput, viewResult]
@@ -3078,6 +3086,100 @@ same neutral mechanics but do not gain lifecycle or hooks. The raw view-file
3078
3086
  adapter preserves its content-only MCP envelope while using the same bounded
3079
3087
  batch operation.
3080
3088
 
3089
+ ### Async-operation protocol
3090
+
3091
+ For a complete long-running operation, define the linked runtime-only surface
3092
+ once. Stitchkit owns transport names, validation and wait mechanics; the
3093
+ application still owns storage, execution, retries and domain states:
3094
+
3095
+ ```ts
3096
+ import {
3097
+ createAsyncOperationSnapshotSchema,
3098
+ defineAsyncOperation,
3099
+ } from 'stitchkit/tools'
3100
+
3101
+ const id = z.object({ id: z.string() })
3102
+ const state = z.object({
3103
+ phase: z.enum(['pending', 'running', 'succeeded']),
3104
+ value: z.string().optional(),
3105
+ })
3106
+ const snapshot = createAsyncOperationSnapshotSchema({
3107
+ failure: z.object({ code: z.string() }),
3108
+ progress: z.object({ current: z.number(), total: z.number() }),
3109
+ })
3110
+
3111
+ const exportOperation = defineAsyncOperation({
3112
+ mode: 'runtime-only',
3113
+ name: 'export',
3114
+ description: 'Export project data',
3115
+ identity: { serviceName: 'exports', action: 'export', scope: 'user' },
3116
+ startInput: z.object({ projectId: z.string() }),
3117
+ id,
3118
+ state,
3119
+ snapshot,
3120
+ start: ({ projectId }) => enqueueExport(projectId),
3121
+ authorize: ({ id }, capability, ctx) => authorizeExport(id, capability, ctx),
3122
+ inspect: ({ id }, ctx) => readExport(id, ctx.signal),
3123
+ classify: (job) => ({ phase: job.phase }),
3124
+ result: {
3125
+ output: z.object({ value: z.string() }),
3126
+ handler: (job) => ({ value: job.value ?? '' }),
3127
+ },
3128
+ })
3129
+
3130
+ const runtimeTools = exportOperation.runtimeTools
3131
+ ```
3132
+
3133
+ Every follow-up repeats `authorize`; an opaque id is never authority. Aborting
3134
+ `wait` only stops waiting and never calls optional domain `cancel`. A
3135
+ contract-backed operation uses `bindContractAsyncOperation` with literal methods
3136
+ from a dedicated existing contract, so it creates no second HTTP router or
3137
+ duplicate schemas. Capability keys are narrowed by schema-compatible TypeScript
3138
+ input/output types. At runtime, the binder additionally requires the exact same
3139
+ id and snapshot Zod instances; declare each once and reuse it:
3140
+
3141
+ ```ts
3142
+ import { defineContract } from 'stitchkit/contract'
3143
+ import { bindContractAsyncOperation } from 'stitchkit/tools'
3144
+ import { z } from 'zod'
3145
+
3146
+ const operationId = z.object({ id: z.string() })
3147
+ const operationSnapshot = z.object({
3148
+ phase: z.enum(['pending', 'running', 'succeeded', 'failed', 'cancelled']),
3149
+ })
3150
+
3151
+ const operations = defineContract(
3152
+ { prefix: 'exports' },
3153
+ {
3154
+ start: {
3155
+ method: 'POST', path: '/', desc: 'Start export',
3156
+ input: z.object({ projectId: z.string() }), output: operationId,
3157
+ },
3158
+ status: {
3159
+ method: 'POST', path: '/status', desc: 'Read export status',
3160
+ input: operationId, output: operationSnapshot,
3161
+ },
3162
+ wait: {
3163
+ method: 'POST', path: '/wait', desc: 'Wait for export',
3164
+ input: operationId, output: operationSnapshot,
3165
+ },
3166
+ },
3167
+ )
3168
+
3169
+ const operationHandlers = {
3170
+ start: () => ({ id: 'example' }),
3171
+ status: (): z.output<typeof operationSnapshot> => ({ phase: 'pending' }),
3172
+ wait: (): z.output<typeof operationSnapshot> => ({ phase: 'succeeded' }),
3173
+ }
3174
+
3175
+ const bound = bindContractAsyncOperation({
3176
+ mode: 'contract-backed',
3177
+ contract: operations,
3178
+ capabilities: { start: 'start', status: 'status', wait: 'wait' },
3179
+ handlers: operationHandlers,
3180
+ })
3181
+ ```
3182
+
3081
3183
  ### Explicit raw SDK registration
3082
3184
 
3083
3185
  `rawTools` is deliberately named as an escape hatch. A tool registered there
@@ -3093,7 +3195,7 @@ const handleMcp = createMcpHandler({
3093
3195
  serverInfo: { name: 'my-app', version: '1.0.0' },
3094
3196
  auth,
3095
3197
  services: [service],
3096
- rawTools: (server) => mountViewFile(server, { baseDir: '/srv/uploads' }),
3198
+ rawTools: (server) => mountViewFile(server, { files }),
3097
3199
  })
3098
3200
  ```
3099
3201
 
@@ -3234,6 +3336,12 @@ await createCli({
3234
3336
  })
3235
3337
  ```
3236
3338
 
3339
+ `createCli({ signal })` and its wait loop honor an explicit caller
3340
+ `AbortSignal`. Stitchkit does not install a process-global SIGINT handler for
3341
+ ordinary CLI programs: if desired, the application binds SIGINT to an
3342
+ `AbortController` and passes its signal. Aborting wait stops polling only; it
3343
+ does not cancel the underlying job.
3344
+
3237
3345
  ```json
3238
3346
  // package.json
3239
3347
  { "bin": { "myapp": "./dist/cli.js" } }
@@ -3347,7 +3455,7 @@ same Zod schema an HTTP or MCP call does.
3347
3455
 
3348
3456
  | Flag | Effect |
3349
3457
  | --------------------- | ---------------------------------------------------------- |
3350
- | `--json` | Raw JSON on stdout for piping / scripts |
3458
+ | `--json` | Compact success/error JSON records for scripts |
3351
3459
  | `--wait` | Block-poll an async result to a terminal state |
3352
3460
  | `--wait-timeout <s>` | Override the `--wait` timeout |
3353
3461
  | `--output-dir <dir>` | Download result media into a directory |
@@ -3355,10 +3463,18 @@ same Zod schema an HTTP or MCP call does.
3355
3463
  | `--dry-run` | Print the resolved call without executing |
3356
3464
  | `--help`, `-h` | Usage — top-level or per-command flag table |
3357
3465
 
3358
- stdout carries the result; errors and progress go to stderr, so `--json` stays
3359
- pipeable and `2>/dev/null` stays clean. The process exit code carries the error
3360
- class (`0` ok, `VALIDATION_ERROR 1`, `UNAUTHORIZED 2`, `FORBIDDEN → 3`,
3361
- `NOT_FOUND 4`, …) override per app with `exitCodes`.
3466
+ stdout carries the result; structured errors and progress go to stderr. With
3467
+ `--json`, a success or structured failure is exactly one compact,
3468
+ newline-terminated JSON record on its respective stream; progress and CLI usage
3469
+ diagnostics remain ordinary stderr text. This keeps stdout pipeable and
3470
+ `2>/dev/null` clean. The process exit code carries the error class (`0` ok,
3471
+ `VALIDATION_ERROR → 1`, `UNAUTHORIZED → 2`, `FORBIDDEN → 3`, `NOT_FOUND → 4`,
3472
+ …) — override per app with `exitCodes`.
3473
+
3474
+ Per-command help derives the positional form from the same schema order as the
3475
+ argv parser. For example, a required `action` and optional `profile` render as
3476
+ `Usage: myapp skill <action> [profile] [--flags]`; the argument table also shows
3477
+ `<action> | --action` and `[profile] | --profile`. Boolean fields remain flags.
3362
3478
 
3363
3479
  ## `--wait` — background-friendly generation
3364
3480
 
@@ -3375,12 +3491,19 @@ await createCli({
3375
3491
  generate: {
3376
3492
  tool: 'get_generation',
3377
3493
  poll: (r) => (isRecord(r) && typeof r.id === 'string' ? { id: r.id } : null),
3378
- done: (r) => isRecord(r) && r.status === 'COMPLETED',
3494
+ done: (r) => isRecord(r) && ['COMPLETED', 'FAILED'].includes(String(r.status)),
3495
+ failed: (r) => isRecord(r) && r.status === 'FAILED',
3379
3496
  },
3380
3497
  },
3381
3498
  })
3382
3499
  ```
3383
3500
 
3501
+ `failed` is optional. When it matches either the initial result or a later poll,
3502
+ polling stops and the CLI emits `WAIT_FAILED` on stderr with a non-zero exit;
3503
+ the terminal payload is retained under `details.result`. `failed` is checked
3504
+ before `done`, so overlapping predicates fail closed. A failed poll tool call
3505
+ keeps its own error code, and an elapsed deadline remains `TIMEOUT`.
3506
+
3384
3507
  ```bash
3385
3508
  # foreground
3386
3509
  myapp generate "a fox" --wait --output-dir ./out
@@ -3486,6 +3609,57 @@ runtime. A malformed inbound tuple never reaches the application handler;
3486
3609
  invalid outbound data throws before Socket.IO publishes it. Rejections call the
3487
3610
  optional `onRejected` hook with event, direction, phase and the Zod error.
3488
3611
 
3612
+ ### Protocol generations
3613
+
3614
+ For a distributed producer/consumer pair, put a literal generation first in the
3615
+ first payload object. Zod validates object fields in declaration order, so an
3616
+ incompatible peer is identified before the rest of its payload is interpreted:
3617
+
3618
+ ```ts
3619
+ const ReplicationMessage = z.object({
3620
+ v: z.literal(2),
3621
+ item: ReplicatedItem,
3622
+ })
3623
+
3624
+ const realtimeContract = defineRealtimeContract({
3625
+ serverToClient: {
3626
+ replicated: { args: z.tuple([ReplicationMessage]) },
3627
+ },
3628
+ clientToServer: {},
3629
+ })
3630
+ ```
3631
+
3632
+ The original `z.ZodError` is retained as `RealtimeRejectedEvent.error.cause`.
3633
+ Because event arguments are a tuple, the first payload's `v` path is
3634
+ `[0, 'v']` (not `['v']`):
3635
+
3636
+ ```ts
3637
+ import type { RealtimeRejectedEvent } from 'stitchkit'
3638
+ import { z } from 'zod'
3639
+
3640
+ function isProtocolGenerationMismatch(rejected: RealtimeRejectedEvent): boolean {
3641
+ const cause = rejected.error.cause
3642
+ if (!(cause instanceof z.ZodError)) return false
3643
+ const first = cause.issues[0]
3644
+ return first?.code === 'invalid_value'
3645
+ && first.path.length === 2
3646
+ && first.path[0] === 0
3647
+ && first.path[1] === 'v'
3648
+ }
3649
+
3650
+ const socket = createRealtimeClient(realtimeContract, {
3651
+ url,
3652
+ onRejected: (rejected) => {
3653
+ if (isProtocolGenerationMismatch(rejected)) schedulePeerUpgrade()
3654
+ else reportMalformedRealtimePayload(rejected)
3655
+ },
3656
+ })
3657
+ ```
3658
+
3659
+ A generation mismatch means “upgrade the peer”; another schema rejection means
3660
+ “fix the producer or payload”. This stays an application convention, not a core
3661
+ API: only the application knows which field denotes protocol compatibility.
3662
+
3489
3663
  ## Server — `createSocketIOServer`
3490
3664
 
3491
3665
  ```ts
@@ -3584,6 +3758,35 @@ socket.on('note:created', (note) => { /* typed note */ })
3584
3758
  socket.emit('room:join', 'general', ({ joined }) => { /* typed + validated */ })
3585
3759
  ```
3586
3760
 
3761
+ ### Request-response over realtime
3762
+
3763
+ For an event with an `ack` schema, `request()` is the Promise form of the same
3764
+ native Socket.IO acknowledgement. Arguments and the resolved value are inferred
3765
+ from the contract and validated on both sides:
3766
+
3767
+ ```ts
3768
+ const result = await socket.request(
3769
+ 'room:join',
3770
+ 'general',
3771
+ { timeoutMs: 5_000 },
3772
+ )
3773
+ // result: { joined: boolean }
3774
+ ```
3775
+
3776
+ An event without `ack` cannot be passed to `request()` by type. A disconnected
3777
+ client rejects immediately with `RealtimeRequestDisconnectedError`; an in-flight
3778
+ disconnect uses the same error; an elapsed native acknowledgement timeout uses
3779
+ `RealtimeRequestTimeoutError`. These are distinct stable classes and codes, so
3780
+ never parse Socket.IO error text. An invalid acknowledgement still fires the
3781
+ existing `onRejected` hook with phase `acknowledgement`, then rejects with
3782
+ `RealtimeRequestInvalidAcknowledgementError`.
3783
+
3784
+ `timeoutMs` must be finite and greater than zero. Use acknowledgements only for
3785
+ bounded request-response work. A job that runs for minutes, progress streaming,
3786
+ or resumable delivery should use separate correlated events or the async
3787
+ operation protocol; keeping one acknowledgement open is not a durable RPC/job
3788
+ transport.
3789
+
3587
3790
  ## Low-level transport
3588
3791
 
3589
3792
  `createSocketIOClient` remains the low-level Socket.IO transport wrapper for
@@ -4068,10 +4271,11 @@ The value of each `rules` entry, keyed by scope:
4068
4271
 
4069
4272
  - **`'public'`** — always passes; the identity is attached if present.
4070
4273
  - **`'authenticated'`** — any resolved identity passes; no identity ⇒ 401.
4071
- - **a function** `(identity, ctx) => boolean | Promise<boolean>` — a custom
4274
+ - **a function** `(identity, ctx) => boolean | contribution`, sync or async — a custom
4072
4275
  check. It receives request metadata and validated path params, so a
4073
4276
  resource-scoped rule can do a DB lookup. It cannot read `input` or files: the
4074
- body has deliberately not been consumed yet. May be async.
4277
+ body has deliberately not been consumed yet. `false` denies, `true` passes,
4278
+ and a returned plain object passes and contributes inferred handler fields.
4075
4279
 
4076
4280
  #### Resource-scoped rule — reading a path/prefix param
4077
4281
 
@@ -4101,6 +4305,30 @@ identity *and* any derived values (role, parent-id) on `ctx` for handlers. Read
4101
4305
  them back as `ctx.tenantRole` (typed `unknown` — narrow at the read site). The
4102
4306
  endpoints under the group declare `scope: 'tenant'`.
4103
4307
 
4308
+ When the async authorization lookup already produced handler data, return it
4309
+ instead of repeating the lookup in `inject` and a handwritten scope map:
4310
+
4311
+ ```ts
4312
+ const auth = createAuthHook({
4313
+ resolve: sessionResolver,
4314
+ rules: {
4315
+ project: async (user, ctx) => {
4316
+ const membership = await findMembership(user.id, String(ctx.projectId))
4317
+ return membership
4318
+ ? { userId: user.id, projectId: membership.projectId, role: membership.role }
4319
+ : false
4320
+ },
4321
+ },
4322
+ })
4323
+
4324
+ const implementFor = createScopedImplement<AuthScopes<typeof auth>>()
4325
+ ```
4326
+
4327
+ Contributions are merged only after full validation. Runtime-owned context keys,
4328
+ arrays, class instances, accessors, symbols, unsafe prototypes and partial proxy
4329
+ reads fail without mutating context. `false | object` contributes required
4330
+ fields; `true | object` correctly makes them optional.
4331
+
4104
4332
  ### `AuthHookConfig`
4105
4333
 
4106
4334
  | Field | Purpose |
@@ -4968,6 +5196,25 @@ observe. (This is also why it lives on `ToolCallHooks` rather than being an
4968
5196
  `Response`, which a tool call has no use for, and a whole `createServer` hooks
4969
5197
  object must stay assignable to `ToolLifecycle`.)
4970
5198
 
5199
+ ### Compose lifecycle policy explicitly
5200
+
5201
+ When auth, a feature policy and audit identity are independent modules, compose
5202
+ their existing hooks instead of forwarding each phase by hand:
5203
+
5204
+ ```ts
5205
+ import { composeLifecycleHooks } from 'stitchkit/server'
5206
+ import { composeToolLifecycle } from 'stitchkit/tools'
5207
+
5208
+ const httpLifecycle = composeLifecycleHooks(authLifecycle, featurePolicy, auditIdentity)
5209
+ const toolLifecycle = composeToolLifecycle(authHook, featureToolPolicy, auditToolIdentity)
5210
+ ```
5211
+
5212
+ Phases run left to right. `onRequest` stops on the first `Response`; `onError`
5213
+ falls through until a hook handles it; after transforms feed the current value
5214
+ forward and preserve it when a hook returns `undefined`. Throws remain owned by
5215
+ the existing outer error boundary, and the original context/signal identity is
5216
+ never replaced.
5217
+
4971
5218
  **Do not reach for `setRequestError` here.** It writes to the *request* context,
4972
5219
  which the built-in **tool** row does not read: a tool event takes
4973
5220
  `errorCode` / `errorMessage` / `errorDetail` from the `ToolResult`, and only
@@ -5083,6 +5330,38 @@ contract registry. Both helpers are Fetch-only: they construct ordinary
5083
5330
  absolute `Request` objects and call `createHandler` directly, so Bun and Node
5084
5331
  exercise the same framework pipeline.
5085
5332
 
5333
+ ### Transport conformance
5334
+
5335
+ `buildSurfaceManifest` snapshots actual HTTP topology, MCP/Agent/CLI names,
5336
+ CLI-only commands, extensions and canonical schema digests. Compare real runner
5337
+ discovery with `assertSurfaceDiscovery`, then use `runSurfaceProbes` only for
5338
+ the transports you explicitly provide:
5339
+
5340
+ ```ts
5341
+ import {
5342
+ assertSurfaceDiscovery,
5343
+ buildSurfaceManifest,
5344
+ runSurfaceProbes,
5345
+ } from 'stitchkit/testing'
5346
+
5347
+ const manifest = buildSurfaceManifest({ services, runtimeTools, cliCommands })
5348
+ assertSurfaceDiscovery(manifest, {
5349
+ openApi,
5350
+ MCP: (await mcpClient.listTools()).tools.map((tool) => tool.name),
5351
+ AGENT: Object.keys(agentTools),
5352
+ CLI: cliHelpNames,
5353
+ })
5354
+
5355
+ await runSurfaceProbes({
5356
+ probes,
5357
+ drivers: { HTTP: httpDriver, MCP: mcpDriver, AGENT: agentDriver, CLI: cliDriver },
5358
+ })
5359
+ ```
5360
+
5361
+ The kit never starts a server or invents credentials. Each probe declares its
5362
+ expected normalized outcome and has bounded timeout, setup/teardown and an
5363
+ AbortSignal. A missing driver is unsupported, not silently marked conformant.
5364
+
5086
5365
  ### Test handlers with raw Requests
5087
5366
 
5088
5367
  `createHandler` is the router as a plain `(req) => Promise<Response>` function —
@@ -5642,6 +5921,45 @@ current one *up to* your target, and apply each snippet.
5642
5921
  runtime): bootstrap the server, one HTTP request, and any feature you rely on
5643
5922
  (Socket.IO connect, an MCP tool call, a multipart upload, …).
5644
5923
 
5924
+ ## Unreleased migration: peer-free `implementRemote`
5925
+
5926
+ `implementRemote` now has one canonical, optional-peer-free owner. This keeps
5927
+ MCP SDK and AI SDK modules out of CLI bundles that only proxy HTTP calls:
5928
+
5929
+ ```ts
5930
+ // before
5931
+ import { implementRemote } from 'stitchkit/tools'
5932
+ // after
5933
+ import { implementRemote } from 'stitchkit/remote'
5934
+ ```
5935
+
5936
+ ## Unreleased migration: managed file boundary and strict auth returns
5937
+
5938
+ Create one boundary during application bootstrap and pass the capability, never
5939
+ a per-call directory or host path:
5940
+
5941
+ ```ts
5942
+ import { createManagedFileBoundary } from 'stitchkit/files'
5943
+
5944
+ const files = await createManagedFileBoundary({ root: '/srv/app-files' })
5945
+
5946
+ // before
5947
+ defineDownloadTool({ defaultDir: '/srv/app-files', resolveUrl, ...common })
5948
+ defineUploadTool({ upload: (path) => provider.uploadFile(path), ...common })
5949
+ defineViewFileTool({ baseDir: '/srv/app-files', ...common })
5950
+
5951
+ // after
5952
+ defineDownloadTool({ files, resolveUrl, ...common })
5953
+ defineUploadTool({ files, upload: ({ bytes }) => provider.upload(bytes), ...common })
5954
+ defineViewFileTool({ files, ...common })
5955
+ ```
5956
+
5957
+ Downloaded `path` is now relative to the boundary and MIME metadata is named
5958
+ `mediaType`. Update raw `mountDownload`/`mountUpload`/`mountViewFile` configs the
5959
+ same way. Auth predicates must explicitly return `true`, `false`, or a plain
5960
+ object of context fields; replace accidental `undefined` fallthroughs with the
5961
+ intended boolean.
5962
+
5645
5963
  ## Released migration: 0.53.0
5646
5964
 
5647
5965
  ### Realtime `emit` returns `boolean` instead of `void`
@@ -6611,11 +6929,18 @@ The browser-and-server entrypoint. Re-exports everything from
6611
6929
  | `createRealtimeClient` | function | inferred, runtime-validated Socket.IO client — [guide](../guide/realtime.md#client--createrealtimeclient) |
6612
6930
  | `createRetainedTopics` | function | retained last-value store for sticky events — [guide](../guide/realtime.md#sticky-events) |
6613
6931
  | `parseSSE` | function | parse an SSE `Response` into an async generator — [guide](../guide/client.md#sse) |
6614
- | `SocketIOClient` | _type_ | the client handle; `emit` returns `false` for an emit dropped while disconnected |
6932
+ | `SocketIOClient` | _type_ | low-level client handle; `emit` reports disconnected drops and `emitWithAck` exposes the native Promise primitive used by validated `request()` |
6615
6933
  | `SocketIOClientConfig` | _type_ | config for `createSocketIOClient` (incl. `retain`, `onConnectError`, `onDroppedEmit`) |
6616
6934
  | `SocketEventMap` | _type_ | the shape of an event map |
6617
6935
  | `RealtimeClient` | _type_ | validated client inferred from a realtime contract |
6618
6936
  | `RealtimeClientOptions` | _type_ | transport options and the rejected-event hook for `createRealtimeClient` |
6937
+ | `RealtimeAcknowledgedEvent` | _type_ | event-name union restricted to definitions with an `ack` schema |
6938
+ | `RealtimeAcknowledgement` | _type_ | validated acknowledgement output inferred from an event definition |
6939
+ | `RealtimeRequestArguments` | _type_ | request arguments inferred from an acknowledged event tuple |
6940
+ | `RealtimeRequestOptions` | _type_ | finite positive native acknowledgement `timeoutMs` |
6941
+ | `RealtimeRequestTimeoutError` | class | stable `REALTIME_REQUEST_TIMEOUT` rejection |
6942
+ | `RealtimeRequestDisconnectedError` | class | stable `REALTIME_REQUEST_DISCONNECTED` rejection, including an immediate disconnected call |
6943
+ | `RealtimeRequestInvalidAcknowledgementError` | class | invalid ack was reported through `onRejected` and the request rejected |
6619
6944
  | `RealtimeContract` | _type_ | shared server-to-client and client-to-server event registries |
6620
6945
  | `RealtimeEventRegistry` | _type_ | string-keyed registry of event definitions |
6621
6946
  | `RealtimeEventDefinition` | _type_ | one tuple-shaped event and optional acknowledgement schema |
@@ -6796,6 +7121,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
6796
7121
  | `OperationIdentity` | _type_ | path-free service/action/scope/method identity shared by contract and native tool operations |
6797
7122
  | `Handlers` | _type_ | the typed handler map `implement` expects |
6798
7123
  | `LifecycleHooks` | _type_ | `onRequest` / pre-body `authorize` / `beforeHandle` / `afterHandle` / `onError` |
7124
+ | `composeLifecycleHooks` | function | compose HTTP lifecycle phases in declaration order with short-circuit/fallthrough semantics |
6799
7125
  | `AuthorizationContext` | _type_ | HTTP pre-body context with validated params, `input: undefined` and no files |
6800
7126
  | `RouteGroup` | _type_ | a prefixed group of services with its own hooks |
6801
7127
  | `RawRoute` | _type_ | a non-contract `Request → Response` route with a concrete `BunServer` context |
@@ -6827,6 +7153,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
6827
7153
  | `AuthHook` | _type_ | the hook `createAuthHook` returns |
6828
7154
  | `AuthHookConfig` | _type_ | config for `createAuthHook` |
6829
7155
  | `AuthRule` | _type_ | `'public' \| 'authenticated' \| predicate` |
7156
+ | `AuthRuleContribution` | _type_ | plain context fields a sync/async rule may contribute after authorizing |
6830
7157
  | `ScopedAuthRule` | _type_ | a rule plus its typed context contribution — `{ rule, inject? }` |
6831
7158
  | `AuthRules` | _type_ | the `rules` map: bare rules or scoped rules |
6832
7159
  | `RuleScopes` | _type_ | scope→context map derived from a `rules` object; `'public'` fields become optional |
@@ -6995,6 +7322,18 @@ audit event. See the [Observability guide](../guide/observability.md).
6995
7322
 
6996
7323
  ---
6997
7324
 
7325
+ ## `stitchkit/remote`
7326
+
7327
+ Peer-free remote implementation boundary. Importing it does not load the MCP
7328
+ SDK, `ai`, or other optional tool peers, so it is safe in a thin CLI bundle.
7329
+
7330
+ | Export | Kind | Summary |
7331
+ |--------|------|---------|
7332
+ | `implementRemote` | function | bind a contract to a remote HTTP API — [guide](../guide/mcp-and-agents.md#proxying-a-remote-api--implementremote) |
7333
+ | `ImplementRemoteOptions` | _type_ | optional argument-rewrite hook for `implementRemote` |
7334
+
7335
+ ---
7336
+
6998
7337
  ## `stitchkit/tools`
6999
7338
 
7000
7339
  Server-only. Turns contracts into MCP and AI-agent tools. MCP server surfaces
@@ -7014,7 +7353,6 @@ payload.
7014
7353
  | `bindStdioProcessSignals` | function | explicitly bind OS signals to one close-only stdio handle — [guide](../guide/testing-and-deployment.md#stdio-process-signals) |
7015
7354
  | `buildMcpServer` | function | build an `McpServer` from contract/runtime surfaces; no-auth configs omit the second argument |
7016
7355
  | `mountMcp` | function | add contract tools to an existing `McpServer` — [guide](../guide/mcp-and-agents.md#mountmcp) |
7017
- | `implementRemote` | function | bind a contract to a remote HTTP API — [guide](../guide/mcp-and-agents.md#proxying-a-remote-api--implementremote) |
7018
7356
  | `mountAgent` | function | a Vercel AI SDK `ToolSet` from a service — [guide](../guide/mcp-and-agents.md#ai-agents--mountagent) |
7019
7357
  | `defineRuntimeTool` | function | define one validated pathless operation for explicit MCP, Agent and/or CLI surfaces — [guide](../guide/mcp-and-agents.md#pathless-runtime-tools-and-multimodal-results) |
7020
7358
  | `createRuntimeToolFactory` | function | bind shared identity and Zod-validated per-call context for runtime tools — [guide](../guide/mcp-and-agents.md#pathless-runtime-tools-and-multimodal-results) |
@@ -7046,7 +7384,6 @@ payload.
7046
7384
  | `McpSurfaceDefinition` | _type_ | one immutable `{ services, runtimeTools }` MCP surface |
7047
7385
  | `McpSurfaceRegistry` | _type_ | finite keyed surface registry for eager preparation |
7048
7386
  | `StdioAuthConfig` | _type_ | startup identity composed into `StdioMcpServerConfig` |
7049
- | `ImplementRemoteOptions` | _type_ | options for `implementRemote` |
7050
7387
  | `McpMountConfig` | _type_ | config for `mountMcp` |
7051
7388
  | `McpSchemaValidationConfig` | _type_ | shared `{ policy, requireTypedProperties, allowUntyped, requirePortableFormats, allowFormats }` profile |
7052
7389
  | `ValidateMcpSchemasConfig` | _type_ | standalone validation profile plus `services`, `extend`, flattening and logger |
@@ -7102,7 +7439,7 @@ payload.
7102
7439
  | `ToolInvocationOptions` | _type_ | per-call source, context, lifecycle, hooks and output-strip reporter |
7103
7440
  | `ToolInvokerTransport` | _type_ | invoker exposure policy: `MCP \| AGENT \| CLI` |
7104
7441
  | `ToolCallContext` | _type_ | the context every tool hook receives — `{ source, mcp? }` plus whatever the mount's `context` added |
7105
- | `ViewFileOptions` | _type_ | shared URL/local-sandbox policy for `defineViewFileTool`, `mountViewFile` and `resolveMedia` |
7442
+ | `ViewFileOptions` | _type_ | shared URL/managed-file-boundary policy for `defineViewFileTool`, `mountViewFile` and `resolveMedia` |
7106
7443
  | `ViewFileOutput` | _type_ | neutral managed batch result with multimodal `content` and per-item `errors` |
7107
7444
  | `ViewFileInputSchema` | constant | fixed one-or-many media path/URL input schema |
7108
7445
  | `ViewFileOutputSchema` | constant | Zod schema for the neutral managed view-file batch result |
@@ -7138,9 +7475,25 @@ runtime-tool runner, plus deliberate raw MCP adapters over the same mechanics.
7138
7475
  | `ManagedNativeToolConfig` | _type_ | shared name, description, identity, exposure and annotations for managed native factories |
7139
7476
  | `NativeToolIdentity` | _type_ | pathless service/action/scope/meta identity; semantic method is factory-owned |
7140
7477
  | `ManagedWaitRender` | _type_ | optional managed wait terminal text and failure classification |
7141
- | `DownloadResultSchema` | constant | Zod schema for a saved download's path, byte size and MIME type |
7142
- | `DownloadResult` | _type_ | validated output of `defineDownloadTool` |
7143
7478
  | `UploadToolInputSchema` | constant | fixed `{ path: string }` input schema for `defineUploadTool` |
7479
+ | `defineAsyncOperation` | function | runtime-only start/status/wait plus configured cancel/result/artifacts definitions |
7480
+ | `bindContractAsyncOperation` | function | bind literal methods from an existing contract without creating another HTTP surface |
7481
+ | `createAsyncOperationSnapshotSchema` | function | canonical pending/running/succeeded/failed/cancelled Zod snapshot |
7482
+ | `AsyncOperationCancelResultSchema` | constant | validated accepted/already_terminal/rejected cancellation result |
7483
+ | `AsyncOperationCancelResult` | _type_ | validated cancel capability result |
7484
+ | `AsyncOperationCancelCapability` | _type_ | optional typed domain cancellation callback |
7485
+ | `AsyncOperationCapability` | _type_ | generated capability-name union |
7486
+ | `AsyncOperationStartDefinition` / `AsyncOperationFollowDefinition` | _type_ | generated runtime definition types |
7487
+ | `AsyncOperationIdentity` | _type_ | shared service/action/scope/meta identity for an operation |
7488
+ | `AsyncOperationOutputCapability` | _type_ | optional result/artifact schema plus handler |
7489
+ | `RuntimeAsyncOperationConfig` | _type_ | runtime-only descriptor configuration |
7490
+ | `RuntimeAsyncOperation` | _type_ | inferred generated definitions and schemas |
7491
+ | `ContractAsyncOperationConfig` | _type_ | literal contract method binding and handlers |
7492
+ | `ContractAsyncOperationKeys` | _type_ | literal method-key union of a bound contract |
7493
+ | `ContractAsyncOperationStartKey` | _type_ | contract keys with a declared output schema, valid for `start` |
7494
+ | `ContractAsyncOperationFollowKey` | _type_ | contract keys whose input schema type matches the selected start output |
7495
+ | `ContractAsyncOperationWaitKey` | _type_ | follow-up keys whose output schema type also matches the selected status output |
7496
+ | `composeToolLifecycle` | function | ordered composition of tool before/after phases |
7144
7497
  | `mountDownload` | function | raw MCP "download a URL to disk" adapter (SSRF-guarded, size-capped) |
7145
7498
  | `mountUpload` | function | raw MCP "upload a local file" adapter |
7146
7499
  | `mountWait` | function | raw MCP generic `--wait`-style polling adapter |
@@ -7220,6 +7573,43 @@ handler pipeline without opening a TCP port.
7220
7573
  | `HandlerTestClientConfig` | _type_ | handler, contract, path prefix, scoped config and client request defaults |
7221
7574
  | `HandlerTestClientsConfig` | _type_ | batch helper configuration |
7222
7575
  | `HandlerTestTransportConfig` | _type_ | shared in-process handler, origin, prefix, client defaults and optional server handle |
7576
+ | `buildSurfaceManifest` | function | deterministic actual HTTP/tool/CLI/extension topology with versioned schema digests |
7577
+ | `assertSurfaceManifestSnapshot` | function | bounded deterministic manifest drift assertion |
7578
+ | `assertSurfaceDiscovery` | function | compare real OpenAPI/MCP/Agent/CLI discovery to a manifest |
7579
+ | `runSurfaceProbes` | function | execute explicit transport drivers with per-probe setup, teardown, timeout and cancellation |
7580
+ | `TransportObservationSchema` | constant | normalized success/validation/domain-error/aborted probe outcome |
7581
+ | `ConformanceTransport` | _type_ | explicit probe transport union |
7582
+ | `RunSurfaceProbesConfig` | _type_ | probes, drivers and diagnostic byte cap |
7583
+ | `SurfaceDiscoveryObservation` | _type_ | real OpenAPI/tool/CLI/extension discovery values |
7584
+ | `SurfaceProbe` / `SurfaceProbeDriver` | _type_ | one bounded scenario and its consumer-supplied runner |
7585
+ | `TransportObservation` | _type_ | validated normalized driver result |
7586
+ | `SurfaceManifest` / `SurfaceManifestConfig` | _type_ | deterministic surface snapshot and its inputs |
7587
+ | `SurfaceManifestOperation` / `SurfaceManifestOperationSchema` | _type_ / schema | one contract or runtime operation row |
7588
+ | `SurfaceManifestExtension` / `SurfaceManifestExtensionSchema` | _type_ / schema | declared transport extension row |
7589
+ | `SurfaceManifestSchema` | schema | complete deterministic manifest schema |
7590
+ | `SurfaceRuntimeToolDefinition` | _type_ | peer-free structural runtime-operation subset accepted by manifests |
7591
+ | `SurfaceSchemaDigestsSchema` | schema | params/input/output/multipart digest object |
7592
+ | `serializeSurfaceValue` | function | canonical versioned serialization used for deterministic digests |
7593
+
7594
+ ---
7595
+
7596
+ ## `stitchkit/files`
7597
+
7598
+ Peer-free Bun/Node filesystem capability. Browser/contract-safe refs are also
7599
+ available from `stitchkit/contract`.
7600
+
7601
+ | Export | Kind | Summary |
7602
+ |--------|------|---------|
7603
+ | `createManagedFileBoundary` | function | bind one existing application-owned root for capped reads and atomic writes |
7604
+ | `ManagedFileBoundary` | _type_ | non-reopenable `read`/`write` capability over canonical relative paths |
7605
+ | `ManagedFileBoundaryConfig` | _type_ | bound root, finite limits, inspector and cleanup observer |
7606
+ | `ManagedFileRefSchema` / `ManagedFileRef` | schema / _type_ | transport-safe relative path, measured size and optional media metadata |
7607
+ | `ManagedFilePathSchema` / `ManagedFilePath` | schema / _type_ | canonical POSIX relative managed-file path |
7608
+ | `ManagedFileSource` | _type_ | bounded immutable bytes plus validated ref passed to upload callbacks |
7609
+ | `ManagedFileReadOptions` / `ManagedFileWriteOptions` | _type_ | per-operation byte cap, signal and atomic write policy |
7610
+ | `ManagedFileError` / `ManagedFileErrorCode` | class / _type_ | stable caller-safe boundary failures |
7611
+ | `ManagedFileInspector` | _type_ | bounded-prefix content inspection callback that cannot own path or size |
7612
+ | `ManagedFileInspectionInput` / `ManagedFileInspection` | _type_ | inspector input and normalized metadata-only result |
7223
7613
 
7224
7614
  ---
7225
7615
 
@@ -7258,7 +7648,7 @@ SDK nor the `ai` peer.
7258
7648
  | `defineCliCommand` | function | define one Zod-typed CLI-only executable command |
7259
7649
  | `parseCliArgs` | function | argv → typed tool args against a schema (advanced) |
7260
7650
  | `pollUntilDone` | function | the generic `--wait` poller (advanced) |
7261
- | `emitResult` | function | write a `ToolResult` to stdout/stderr + exit code (advanced) |
7651
+ | `emitResult` | function | write a pretty or compact `ToolResult` record to stdout/stderr + exit code (advanced) |
7262
7652
  | `DEFAULT_EXIT_CODES` | const | the default `ToolResult.code` → exit-code map |
7263
7653
  | `CliConfig` | _type_ | config for `createCli` |
7264
7654
  | `CliSurfaceSource` | _type_ | static service/runtime array or identity-dependent factory |
@@ -7267,9 +7657,9 @@ SDK nor the `ai` peer.
7267
7657
  | `CliCommandDefinitionWithOutput` | _type_ | native command with validated declared output |
7268
7658
  | `CliCommandDefinitionWithoutOutput` | _type_ | native void command without an output contract |
7269
7659
  | `CliCommandContext` | _type_ | parsed input, global options and stdout/stderr writers |
7270
- | `CliRunOptions` | _type_ | parsed global flags (`--json`, `--wait`, …) |
7660
+ | `CliRunOptions` | _type_ | parsed global flags (`--json` compacts success/error records, `--wait`, …) |
7271
7661
  | `ParsedCliArgs` | _type_ | result of `parseCliArgs` |
7272
- | `CliWaitConfig` | _type_ | per-command `--wait` polling config |
7662
+ | `CliWaitConfig` | _type_ | per-command `--wait` polling config; optional `failed(result)` maps a terminal domain failure to `WAIT_FAILED` and a non-zero exit |
7273
7663
  | `ExitCodeMap` | _type_ | `ToolResult.code` → process exit code |
7274
7664
  | `PollParams` | _type_ | params for `pollUntilDone` |
7275
7665
  | `CliWriters` | _type_ | stdout/stderr sinks for `emitResult` |