stitchkit 0.104.1 → 0.105.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 (429) hide show
  1. package/CHANGELOG.md +381 -12
  2. package/darwin-native.cjs +4 -2
  3. package/dist/agent-runtime/browser-control-contract.d.ts +32 -2
  4. package/dist/agent-runtime/browser-control-contract.d.ts.map +1 -1
  5. package/dist/agent-runtime/browser-controller.d.ts +21 -3
  6. package/dist/agent-runtime/browser-controller.d.ts.map +1 -1
  7. package/dist/agent-runtime/coding-shell-process.d.ts.map +1 -1
  8. package/dist/agent-runtime/coding-tool-refusals.d.ts +1 -0
  9. package/dist/agent-runtime/coding-tool-refusals.d.ts.map +1 -1
  10. package/dist/agent-runtime/coding-tool-shell.d.ts.map +1 -1
  11. package/dist/agent-runtime/compaction.d.ts.map +1 -1
  12. package/dist/agent-runtime/contained-write.d.ts.map +1 -1
  13. package/dist/agent-runtime/control-schema.d.ts +48 -1
  14. package/dist/agent-runtime/control-schema.d.ts.map +1 -1
  15. package/dist/agent-runtime/deferred-tool-search.d.ts +3 -2
  16. package/dist/agent-runtime/deferred-tool-search.d.ts.map +1 -1
  17. package/dist/agent-runtime/harness-control.d.ts.map +1 -1
  18. package/dist/agent-runtime/harness-realtime.d.ts +34 -4
  19. package/dist/agent-runtime/harness-realtime.d.ts.map +1 -1
  20. package/dist/agent-runtime/runtime-recovery.d.ts.map +1 -1
  21. package/dist/agent-runtime/runtime-state.d.ts.map +1 -1
  22. package/dist/agent-runtime/sandbox-contract.d.ts.map +1 -1
  23. package/dist/agent-runtime/sandbox-process-owner.d.ts +9 -1
  24. package/dist/agent-runtime/sandbox-process-owner.d.ts.map +1 -1
  25. package/dist/agent-runtime/sandbox.d.ts +32 -1
  26. package/dist/agent-runtime/sandbox.d.ts.map +1 -1
  27. package/dist/agent-runtime/schedule-records.d.ts.map +1 -1
  28. package/dist/agent-runtime/schedules.d.ts.map +1 -1
  29. package/dist/agent-runtime/store-events.d.ts +9 -4
  30. package/dist/agent-runtime/store-events.d.ts.map +1 -1
  31. package/dist/agent-runtime/testing.d.ts.map +1 -1
  32. package/dist/application/diagnostic-journal-contract.d.ts +50 -0
  33. package/dist/application/diagnostic-journal-contract.d.ts.map +1 -1
  34. package/dist/application/diagnostic-journal-generations.d.ts +24 -0
  35. package/dist/application/diagnostic-journal-generations.d.ts.map +1 -0
  36. package/dist/application/diagnostic-journal-manager.d.ts.map +1 -1
  37. package/dist/application/diagnostic-journal-open.d.ts +10 -0
  38. package/dist/application/diagnostic-journal-open.d.ts.map +1 -0
  39. package/dist/application/diagnostic-journal-quarantine.d.ts +28 -0
  40. package/dist/application/diagnostic-journal-quarantine.d.ts.map +1 -0
  41. package/dist/application/diagnostic-journal-read-contract.d.ts +73 -3
  42. package/dist/application/diagnostic-journal-read-contract.d.ts.map +1 -1
  43. package/dist/application/diagnostic-journal-reader.d.ts +17 -2
  44. package/dist/application/diagnostic-journal-reader.d.ts.map +1 -1
  45. package/dist/application/diagnostic-journal-recovery.d.ts +18 -2
  46. package/dist/application/diagnostic-journal-recovery.d.ts.map +1 -1
  47. package/dist/application/diagnostic-journal-storage.d.ts +6 -2
  48. package/dist/application/diagnostic-journal-storage.d.ts.map +1 -1
  49. package/dist/application/diagnostic-journal-tail.d.ts +12 -0
  50. package/dist/application/diagnostic-journal-tail.d.ts.map +1 -0
  51. package/dist/application/diagnostic-journal.d.ts +3 -0
  52. package/dist/application/diagnostic-journal.d.ts.map +1 -1
  53. package/dist/application/directory-inbox-accept.d.ts +3 -3
  54. package/dist/application/directory-inbox-accept.d.ts.map +1 -1
  55. package/dist/application/directory-inbox-contract.d.ts +38 -10
  56. package/dist/application/directory-inbox-contract.d.ts.map +1 -1
  57. package/dist/application/directory-inbox-delivery.d.ts +3 -3
  58. package/dist/application/directory-inbox-delivery.d.ts.map +1 -1
  59. package/dist/application/directory-inbox-files.d.ts +17 -2
  60. package/dist/application/directory-inbox-files.d.ts.map +1 -1
  61. package/dist/application/directory-inbox-state.d.ts +1 -1
  62. package/dist/application/directory-inbox-state.d.ts.map +1 -1
  63. package/dist/application/directory-inbox.d.ts +1 -1
  64. package/dist/application/directory-inbox.d.ts.map +1 -1
  65. package/dist/application/file-state-store.d.ts +21 -1
  66. package/dist/application/file-state-store.d.ts.map +1 -1
  67. package/dist/application/grammy.d.ts.map +1 -1
  68. package/dist/application/kernel-contract.d.ts.map +1 -1
  69. package/dist/application/revision-signal.d.ts.map +1 -1
  70. package/dist/application/state-store.d.ts +3 -0
  71. package/dist/application/state-store.d.ts.map +1 -1
  72. package/dist/browser/api-error.d.ts +38 -0
  73. package/dist/browser/api-error.d.ts.map +1 -0
  74. package/dist/browser/chunked-upload.d.ts.map +1 -1
  75. package/dist/browser/client-executors.d.ts.map +1 -1
  76. package/dist/browser/client-url.d.ts.map +1 -1
  77. package/dist/browser/contract-stream.d.ts.map +1 -1
  78. package/dist/browser/http.d.ts +0 -25
  79. package/dist/browser/http.d.ts.map +1 -1
  80. package/dist/browser/resumable.d.ts.map +1 -1
  81. package/dist/contract/define-validation.d.ts +0 -1
  82. package/dist/contract/define-validation.d.ts.map +1 -1
  83. package/dist/contract/define.d.ts.map +1 -1
  84. package/dist/contract/errors-factory.d.ts.map +1 -1
  85. package/dist/contract/errors.d.ts +15 -18
  86. package/dist/contract/errors.d.ts.map +1 -1
  87. package/dist/contract/normalize.d.ts.map +1 -1
  88. package/dist/durability/effect.d.ts.map +1 -1
  89. package/dist/durability/scheduler.d.ts.map +1 -1
  90. package/dist/entrypoints/agent-runtime/browser.js +29 -18
  91. package/dist/entrypoints/agent-runtime/coding-tools.js +8 -7
  92. package/dist/entrypoints/agent-runtime/harness.js +15 -14
  93. package/dist/entrypoints/agent-runtime/realtime.d.ts +1 -1
  94. package/dist/entrypoints/agent-runtime/realtime.d.ts.map +1 -1
  95. package/dist/entrypoints/agent-runtime/realtime.js +119 -54
  96. package/dist/entrypoints/agent-runtime/sandbox.js +23 -17
  97. package/dist/entrypoints/agent-runtime/sqlite/bun.js +4 -3
  98. package/dist/entrypoints/agent-runtime/sqlite/node.js +4 -3
  99. package/dist/entrypoints/agent-runtime/testing.js +8 -6
  100. package/dist/entrypoints/agent-runtime.js +37 -38
  101. package/dist/entrypoints/application/diagnostic-journal.js +305 -111
  102. package/dist/entrypoints/application/directory-inbox.js +174 -77
  103. package/dist/entrypoints/application/grammy.js +5 -2
  104. package/dist/entrypoints/application/opentelemetry.js +2 -2
  105. package/dist/entrypoints/application.d.ts +2 -2
  106. package/dist/entrypoints/application.d.ts.map +1 -1
  107. package/dist/entrypoints/application.js +17 -8
  108. package/dist/entrypoints/cli/publish.d.ts +16 -0
  109. package/dist/entrypoints/cli/publish.d.ts.map +1 -0
  110. package/dist/entrypoints/cli/publish.js +527 -0
  111. package/dist/entrypoints/cli.d.ts +1 -2
  112. package/dist/entrypoints/cli.d.ts.map +1 -1
  113. package/dist/entrypoints/cli.js +59 -966
  114. package/dist/entrypoints/contract.js +3 -3
  115. package/dist/entrypoints/files/packaging.js +196 -56
  116. package/dist/entrypoints/files.d.ts +1 -0
  117. package/dist/entrypoints/files.d.ts.map +1 -1
  118. package/dist/entrypoints/files.js +17 -10
  119. package/dist/entrypoints/index.d.ts +2 -1
  120. package/dist/entrypoints/index.d.ts.map +1 -1
  121. package/dist/entrypoints/index.js +31 -60
  122. package/dist/entrypoints/node.js +8 -8
  123. package/dist/entrypoints/observability.js +3 -3
  124. package/dist/entrypoints/primitives.js +67 -83
  125. package/dist/entrypoints/process.d.ts +1 -1
  126. package/dist/entrypoints/process.d.ts.map +1 -1
  127. package/dist/entrypoints/process.js +2 -2
  128. package/dist/entrypoints/react.js +4 -2
  129. package/dist/entrypoints/remote.js +37 -13
  130. package/dist/entrypoints/server.js +18 -16
  131. package/dist/entrypoints/telegram/screens.js +5 -3
  132. package/dist/entrypoints/telegram.js +31 -46
  133. package/dist/entrypoints/testing.d.ts +1 -1
  134. package/dist/entrypoints/testing.d.ts.map +1 -1
  135. package/dist/entrypoints/testing.js +444 -448
  136. package/dist/entrypoints/tools/connections.js +268 -105
  137. package/dist/entrypoints/tools/contract.js +2 -2
  138. package/dist/entrypoints/tools/invoker.js +8 -7
  139. package/dist/entrypoints/tools/mcp.d.ts +2 -2
  140. package/dist/entrypoints/tools/mcp.d.ts.map +1 -1
  141. package/dist/entrypoints/tools/mcp.js +10 -9
  142. package/dist/entrypoints/tools.d.ts +0 -1
  143. package/dist/entrypoints/tools.d.ts.map +1 -1
  144. package/dist/entrypoints/tools.js +118 -53
  145. package/dist/entrypoints/tracking.js +2 -3
  146. package/dist/files/boundary.d.ts +2 -2
  147. package/dist/files/boundary.d.ts.map +1 -1
  148. package/dist/files/chunk-spool.d.ts.map +1 -1
  149. package/dist/files/file-io.d.ts +2 -2
  150. package/dist/files/file-io.d.ts.map +1 -1
  151. package/dist/files/native-packaging-layout.d.ts +50 -4
  152. package/dist/files/native-packaging-layout.d.ts.map +1 -1
  153. package/dist/files/native-packaging.d.ts +104 -21
  154. package/dist/files/native-packaging.d.ts.map +1 -1
  155. package/dist/{index-r5hsgkz5.js → index-0gs4ymfj.js} +6 -3
  156. package/dist/{index-zax7resm.js → index-0gxvns6s.js} +1 -13
  157. package/dist/index-0s4xebbg.js +8 -0
  158. package/dist/{index-r4r70zmh.js → index-12zsej7b.js} +5 -2
  159. package/dist/{index-kemm0ej5.js → index-1pwwan01.js} +65 -22
  160. package/dist/{index-vgkbcnkc.js → index-299sm5tr.js} +1 -1
  161. package/dist/{index-2vnh05w4.js → index-2k91mewf.js} +31 -74
  162. package/dist/{index-269zqj8m.js → index-2rc6jd9n.js} +28 -12
  163. package/dist/{index-wh0ecvyk.js → index-37asdw3a.js} +1 -1
  164. package/dist/index-3h0ztg0z.js +285 -0
  165. package/dist/{index-g95fk660.js → index-4vsby8n8.js} +8 -15
  166. package/dist/index-556g5hsh.js +17 -0
  167. package/dist/{index-adhgd4pe.js → index-5p55xreg.js} +2 -2
  168. package/dist/index-5w6t91ze.js +47 -0
  169. package/dist/{index-9pw0a51a.js → index-6yxhe58g.js} +40 -7
  170. package/dist/{index-6d9xaxw1.js → index-8ks9b85e.js} +33 -5
  171. package/dist/{index-bhb950qs.js → index-916yh4e2.js} +25 -12
  172. package/dist/index-9tqbxv5p.js +44 -0
  173. package/dist/{index-5csmzesf.js → index-azx12jgv.js} +4 -3
  174. package/dist/{index-mbnnw3f2.js → index-b0kddvzw.js} +15 -5
  175. package/dist/{index-n5yqxd3d.js → index-b0w7rbpt.js} +23 -2
  176. package/dist/index-b56nnscm.js +132 -0
  177. package/dist/{index-0ndk4555.js → index-c0hffcmj.js} +2 -2
  178. package/dist/{index-pptyv13k.js → index-c17xcx4k.js} +42 -6
  179. package/dist/{index-g2j7pnak.js → index-cmzvhhtd.js} +5 -2
  180. package/dist/index-cq59agna.js +51 -0
  181. package/dist/{index-ssmd41kh.js → index-d2h99n7d.js} +1 -1
  182. package/dist/{index-z94wht4y.js → index-dky0dcmf.js} +34 -5
  183. package/dist/{index-za6j7nwy.js → index-dsnjzb62.js} +104 -51
  184. package/dist/index-eemvahkc.js +21 -0
  185. package/dist/index-ef8gdaq0.js +105 -0
  186. package/dist/{index-fa4wg312.js → index-f199nfq5.js} +10 -6
  187. package/dist/{index-ck71tbcf.js → index-fsw1ybf3.js} +11 -15
  188. package/dist/{index-f330qqyg.js → index-g2vfg7ak.js} +217 -63
  189. package/dist/{index-fk18k56k.js → index-gr5g3xfw.js} +11 -18
  190. package/dist/{index-jn9g8xdt.js → index-hjvje0pf.js} +6 -4
  191. package/dist/{index-x928baz2.js → index-j9gxbztg.js} +18 -14
  192. package/dist/index-kfjkb396.js +37 -0
  193. package/dist/{index-6drst9sg.js → index-ksfagmyz.js} +45 -345
  194. package/dist/index-m616f0yz.js +120 -0
  195. package/dist/{index-fm1c1a08.js → index-m6w6jtda.js} +40 -21
  196. package/dist/{index-ptn0g0gj.js → index-ma963wz7.js} +21 -20
  197. package/dist/{index-bakgedh8.js → index-msfpyy2f.js} +2 -4
  198. package/dist/{index-ywppsaby.js → index-n1nszyk0.js} +23 -34
  199. package/dist/{index-v14qqmys.js → index-n4689krb.js} +1 -1
  200. package/dist/{index-a0zzh8ak.js → index-pz3y6b6g.js} +3 -3
  201. package/dist/{index-43p08g7t.js → index-pzb63z66.js} +12 -8
  202. package/dist/{index-h97tqqp1.js → index-qaet40b2.js} +5 -3
  203. package/dist/{index-t1sr4aks.js → index-qaqbdm7s.js} +52 -6
  204. package/dist/{index-77nxk6ay.js → index-r393tnfp.js} +16 -10
  205. package/dist/{index-kb8styz3.js → index-rpzewktb.js} +15 -5
  206. package/dist/{index-27a2pj09.js → index-sj28htvf.js} +1 -1
  207. package/dist/index-szybbezc.js +455 -0
  208. package/dist/{index-2hyavjjj.js → index-wbhdzxar.js} +3 -3
  209. package/dist/{index-4e0ah203.js → index-wdy68sh6.js} +47 -24
  210. package/dist/{index-hbd32c6a.js → index-wqmvtnkp.js} +38 -89
  211. package/dist/{index-hp9d77wk.js → index-x7w1z6pf.js} +15 -4
  212. package/dist/{index-20yvtn9a.js → index-yt7gyg5x.js} +1 -1
  213. package/dist/{index-cvda079a.js → index-yvfk3y36.js} +12 -2
  214. package/dist/{index-5z6jn897.js → index-z23rfxhj.js} +1 -1
  215. package/dist/internal/abort-race.d.ts +8 -2
  216. package/dist/internal/abort-race.d.ts.map +1 -1
  217. package/dist/internal/atomic-file.d.ts +55 -9
  218. package/dist/internal/atomic-file.d.ts.map +1 -1
  219. package/dist/internal/atomic-publication.d.ts +39 -4
  220. package/dist/internal/atomic-publication.d.ts.map +1 -1
  221. package/dist/internal/atomic-staging.d.ts +46 -0
  222. package/dist/internal/atomic-staging.d.ts.map +1 -0
  223. package/dist/internal/bounded-file-read.d.ts +19 -11
  224. package/dist/internal/bounded-file-read.d.ts.map +1 -1
  225. package/dist/internal/bounded-lines.d.ts.map +1 -1
  226. package/dist/internal/close-after.d.ts +11 -0
  227. package/dist/internal/close-after.d.ts.map +1 -0
  228. package/dist/internal/deadline.d.ts +10 -0
  229. package/dist/internal/deadline.d.ts.map +1 -1
  230. package/dist/internal/exclusive-lock-read.d.ts +10 -12
  231. package/dist/internal/exclusive-lock-read.d.ts.map +1 -1
  232. package/dist/internal/exclusive-lock.d.ts +2 -2
  233. package/dist/internal/exclusive-lock.d.ts.map +1 -1
  234. package/dist/internal/file-observation.d.ts +14 -0
  235. package/dist/internal/file-observation.d.ts.map +1 -0
  236. package/dist/internal/json-data.d.ts +22 -5
  237. package/dist/internal/json-data.d.ts.map +1 -1
  238. package/dist/internal/positive-integer.d.ts +9 -0
  239. package/dist/internal/positive-integer.d.ts.map +1 -0
  240. package/dist/internal/process-instance.d.ts +35 -5
  241. package/dist/internal/process-instance.d.ts.map +1 -1
  242. package/dist/internal/secure-fetch.d.ts.map +1 -1
  243. package/dist/internal/timers.d.ts +11 -0
  244. package/dist/internal/timers.d.ts.map +1 -0
  245. package/dist/internal/with-exclusive-lock.d.ts +5 -4
  246. package/dist/internal/with-exclusive-lock.d.ts.map +1 -1
  247. package/dist/observability/bounded-sink.d.ts.map +1 -1
  248. package/dist/primitives/canonical-json.d.ts +23 -2
  249. package/dist/primitives/canonical-json.d.ts.map +1 -1
  250. package/dist/primitives/delivery.d.ts.map +1 -1
  251. package/dist/primitives/index.d.ts +1 -1
  252. package/dist/primitives/index.d.ts.map +1 -1
  253. package/dist/primitives/money.d.ts.map +1 -1
  254. package/dist/process/cleanup.d.ts +6 -1
  255. package/dist/process/cleanup.d.ts.map +1 -1
  256. package/dist/process/command-lifetime.d.ts +4 -0
  257. package/dist/process/command-lifetime.d.ts.map +1 -1
  258. package/dist/process/command-owner.d.ts.map +1 -1
  259. package/dist/process/command.d.ts +4 -0
  260. package/dist/process/command.d.ts.map +1 -1
  261. package/dist/process/contract.d.ts +67 -2
  262. package/dist/process/contract.d.ts.map +1 -1
  263. package/dist/process/group.d.ts +40 -3
  264. package/dist/process/group.d.ts.map +1 -1
  265. package/dist/process/launch.d.ts +12 -1
  266. package/dist/process/launch.d.ts.map +1 -1
  267. package/dist/process/terminate.d.ts +39 -0
  268. package/dist/process/terminate.d.ts.map +1 -0
  269. package/dist/react/query-client.d.ts.map +1 -1
  270. package/dist/realtime/rejection.d.ts.map +1 -1
  271. package/dist/server/contract-stream.d.ts.map +1 -1
  272. package/dist/server/handler-state.d.ts.map +1 -1
  273. package/dist/server/implement.d.ts.map +1 -1
  274. package/dist/server/request-body.d.ts +0 -1
  275. package/dist/server/request-body.d.ts.map +1 -1
  276. package/dist/server/unix-client.d.ts.map +1 -1
  277. package/dist/telegram/broadcast-delivery.d.ts.map +1 -1
  278. package/dist/telegram/broadcast-failure.d.ts +10 -2
  279. package/dist/telegram/broadcast-failure.d.ts.map +1 -1
  280. package/dist/telegram/operator-channel.d.ts.map +1 -1
  281. package/dist/telegram/screens/outcome.d.ts.map +1 -1
  282. package/dist/testing/surface-conformance.d.ts +1 -1
  283. package/dist/testing/surface-conformance.d.ts.map +1 -1
  284. package/dist/testing/surface-manifest.d.ts +0 -6
  285. package/dist/testing/surface-manifest.d.ts.map +1 -1
  286. package/dist/tools/agent.d.ts +1 -1
  287. package/dist/tools/agent.d.ts.map +1 -1
  288. package/dist/tools/cli/args.d.ts +1 -1
  289. package/dist/tools/cli/args.d.ts.map +1 -1
  290. package/dist/tools/cli/help.d.ts.map +1 -1
  291. package/dist/tools/cli/invoke.d.ts +2 -2
  292. package/dist/tools/cli/invoke.d.ts.map +1 -1
  293. package/dist/tools/cli/profile.d.ts.map +1 -1
  294. package/dist/tools/cli/publication-assets.d.ts.map +1 -1
  295. package/dist/tools/cli/publication-contract.d.ts +8 -0
  296. package/dist/tools/cli/publication-contract.d.ts.map +1 -1
  297. package/dist/tools/cli/publication-control.d.ts.map +1 -1
  298. package/dist/tools/cli/publication-storage.d.ts +23 -2
  299. package/dist/tools/cli/publication-storage.d.ts.map +1 -1
  300. package/dist/tools/cli/publication.d.ts +8 -1
  301. package/dist/tools/cli/publication.d.ts.map +1 -1
  302. package/dist/tools/connections/errors.d.ts +18 -6
  303. package/dist/tools/connections/errors.d.ts.map +1 -1
  304. package/dist/tools/connections/limits.d.ts +8 -2
  305. package/dist/tools/connections/limits.d.ts.map +1 -1
  306. package/dist/tools/connections/mcp-client.d.ts.map +1 -1
  307. package/dist/tools/connections/mcp-envelope.d.ts.map +1 -1
  308. package/dist/tools/connections/openapi.d.ts.map +1 -1
  309. package/dist/tools/connections/schema-budget.d.ts +1 -1
  310. package/dist/tools/connections/schema-budget.d.ts.map +1 -1
  311. package/dist/tools/connections/sse-session.d.ts.map +1 -1
  312. package/dist/tools/execute-args.d.ts.map +1 -1
  313. package/dist/tools/execute-result.d.ts.map +1 -1
  314. package/dist/tools/internal/surface-projector.d.ts +17 -3
  315. package/dist/tools/internal/surface-projector.d.ts.map +1 -1
  316. package/dist/tools/internal/tool-error-projection.d.ts +16 -0
  317. package/dist/tools/internal/tool-error-projection.d.ts.map +1 -0
  318. package/dist/tools/internal/tool-view.d.ts.map +1 -1
  319. package/dist/tools/invoker.d.ts.map +1 -1
  320. package/dist/tools/list-names.d.ts +3 -2
  321. package/dist/tools/list-names.d.ts.map +1 -1
  322. package/dist/tools/manifest.d.ts +3 -2
  323. package/dist/tools/manifest.d.ts.map +1 -1
  324. package/dist/tools/mcp/handler.d.ts +7 -0
  325. package/dist/tools/mcp/handler.d.ts.map +1 -1
  326. package/dist/tools/mcp/mount.d.ts +18 -2
  327. package/dist/tools/mcp/mount.d.ts.map +1 -1
  328. package/dist/tools/mcp/prepare.d.ts +3 -3
  329. package/dist/tools/mcp/prepare.d.ts.map +1 -1
  330. package/dist/tools/mcp/register.d.ts.map +1 -1
  331. package/dist/tools/mcp/runtime-tools.d.ts.map +1 -1
  332. package/dist/tools/mcp/stdio-signals.d.ts +14 -0
  333. package/dist/tools/mcp/stdio-signals.d.ts.map +1 -1
  334. package/dist/tools/mcp/stdio.d.ts +7 -0
  335. package/dist/tools/mcp/stdio.d.ts.map +1 -1
  336. package/dist/tools/operations/async-operation-runtime-tools.d.ts.map +1 -1
  337. package/dist/tools/operations/async-operation-runtime-types.d.ts +3 -3
  338. package/dist/tools/operations/async-operation-runtime-types.d.ts.map +1 -1
  339. package/dist/tools/operations/define-wait-tool.d.ts +1 -1
  340. package/dist/tools/operations/define-wait-tool.d.ts.map +1 -1
  341. package/dist/tools/operations/wait-core.d.ts +0 -6
  342. package/dist/tools/operations/wait-core.d.ts.map +1 -1
  343. package/dist/tools/registry.d.ts +1 -1
  344. package/dist/tools/registry.d.ts.map +1 -1
  345. package/dist/tools/remote.d.ts +1 -1
  346. package/dist/tools/remote.d.ts.map +1 -1
  347. package/dist/tools/{runtime-tool-execution.d.ts → runtime-tool-declaration.d.ts} +32 -9
  348. package/dist/tools/runtime-tool-declaration.d.ts.map +1 -0
  349. package/dist/tools/runtime-tool-mcp.d.ts +6 -15
  350. package/dist/tools/runtime-tool-mcp.d.ts.map +1 -1
  351. package/dist/tools/runtime-tool.d.ts +14 -12
  352. package/dist/tools/runtime-tool.d.ts.map +1 -1
  353. package/dist/tools/schema/coerce.d.ts +4 -0
  354. package/dist/tools/schema/coerce.d.ts.map +1 -1
  355. package/dist/tools/schema/flatten-join.d.ts.map +1 -1
  356. package/dist/tools/surface.d.ts +7 -11
  357. package/dist/tools/surface.d.ts.map +1 -1
  358. package/dist/tools/transfer/define-download-tool.d.ts +1 -1
  359. package/dist/tools/transfer/define-download-tool.d.ts.map +1 -1
  360. package/dist/tools/transfer/define-upload-tool.d.ts +1 -1
  361. package/dist/tools/transfer/define-upload-tool.d.ts.map +1 -1
  362. package/dist/tools/transfer/define-view-file-tool.d.ts +1 -1
  363. package/dist/tools/transfer/define-view-file-tool.d.ts.map +1 -1
  364. package/dist/tools/transfer/managed-file-error.d.ts.map +1 -1
  365. package/dist/tools/transfer/view-file-contract.d.ts.map +1 -1
  366. package/dist/tools/transfer/view-file.d.ts +1 -1
  367. package/dist/tools/transfer/view-file.d.ts.map +1 -1
  368. package/dist/tools/transports.d.ts +3 -2
  369. package/dist/tools/transports.d.ts.map +1 -1
  370. package/llms/agent-runtime-browser.txt +10 -4
  371. package/llms/agent-runtime-harness.txt +32 -31
  372. package/llms/agent-runtime-react.txt +1 -1
  373. package/llms/agent-runtime-realtime.txt +14 -5
  374. package/llms/agent-runtime-sandbox.txt +3 -11
  375. package/llms/agent-runtime.3.txt +2 -2
  376. package/llms/agent-runtime.txt +0 -9
  377. package/llms/application.2.txt +3 -12
  378. package/llms/application.3.txt +7 -4
  379. package/llms/application.txt +44 -15
  380. package/llms/cli-publish.txt +29 -0
  381. package/llms/cli.2.txt +9 -11
  382. package/llms/cli.txt +26 -14
  383. package/llms/declaration.txt +0 -9
  384. package/llms/files-packaging.txt +22 -7
  385. package/llms/files.txt +4 -2
  386. package/llms/geo.txt +0 -9
  387. package/llms/live.txt +4 -13
  388. package/llms/observability.txt +31 -5
  389. package/llms/primitives.txt +38 -20
  390. package/llms/process.txt +305 -154
  391. package/llms/react-keyboard.txt +0 -9
  392. package/llms/react.txt +0 -18
  393. package/llms/server.4.txt +1 -1
  394. package/llms/server.5.txt +1 -1
  395. package/llms/server.txt +1 -1
  396. package/llms/stitchkit.txt +6 -7
  397. package/llms/telegram-screens.txt +2 -2
  398. package/llms/testing.txt +0 -1
  399. package/llms/tools-mcp.txt +2 -3
  400. package/llms/tools.2.txt +12 -0
  401. package/llms/tools.3.txt +5 -6
  402. package/llms/tools.txt +6 -4
  403. package/llms/upgrading-0.37.0-0.50.0.txt +947 -0
  404. package/llms/{upgrading-0.44.0-0.61.0.txt → upgrading-0.53.0-0.67.0.txt} +576 -506
  405. package/llms/{upgrading-0.62.0-0.80.0.txt → upgrading-0.68.0-0.94.0.txt} +562 -576
  406. package/llms/{upgrading-0.81.0-0.104.0.txt → upgrading-0.95.0-0.105.0.txt} +452 -567
  407. package/llms/upgrading.txt +4 -4
  408. package/llms-full.txt +1053 -405
  409. package/llms.txt +24 -24
  410. package/native-assets.json +11 -3
  411. package/package.json +6 -6
  412. package/dist/agent-runtime/harness-tools.d.ts +0 -11
  413. package/dist/agent-runtime/harness-tools.d.ts.map +0 -1
  414. package/dist/entrypoints/agent-runtime/harness-tools.d.ts +0 -2
  415. package/dist/entrypoints/agent-runtime/harness-tools.d.ts.map +0 -1
  416. package/dist/entrypoints/agent-runtime/harness-tools.js +0 -32
  417. package/dist/index-1a6jtnd9.js +0 -88
  418. package/dist/index-8a9ksm91.js +0 -95
  419. package/dist/index-a3wma1hk.js +0 -48
  420. package/dist/index-jd470k6q.js +0 -109
  421. package/dist/index-n1dqr7gf.js +0 -20
  422. package/dist/index-nggfa2yn.js +0 -34
  423. package/dist/index-s4evrmp5.js +0 -98
  424. package/dist/index-ze1eph1k.js +0 -20
  425. package/dist/tools/connections/error-projection.d.ts +0 -4
  426. package/dist/tools/connections/error-projection.d.ts.map +0 -1
  427. package/dist/tools/runtime-tool-execution.d.ts.map +0 -1
  428. package/llms/agent-runtime-harness-tools.txt +0 -25
  429. package/llms/upgrading-0.37.0-0.44.0.txt +0 -444
package/llms-full.txt CHANGED
@@ -59,6 +59,7 @@ own, recorded as an ADR.
59
59
  | `stitchkit/tools/invoker` | server | stable | peer-free `createToolInvoker` over the canonical contract tool runner |
60
60
  | `stitchkit/tools/connections` | server (Bun or Node) | evolving | consume external MCP servers and a bounded OpenAPI subset — typed reauthorization, a per-request host fence and per-call credentials |
61
61
  | `stitchkit/cli` | server | stable | `createCli` / `createCliInvoker` — runtime and strict NodeNext declarations without MCP SDK / `ai` |
62
+ | `stitchkit/cli/publish` | build and release tooling (Bun or Node, POSIX) | evolving | `publishCli` — commit a verified CLI distribution and move its public manifest last; see [CLI guide](cli.md#publishing-a-complete-version) |
62
63
  | `stitchkit/remote` | browser **and** server | stable | peer-free `implementRemote` for thin HTTP proxy processes |
63
64
  | `stitchkit/process` | server (Bun or Node, POSIX) | evolving | finite native commands, byte sinks, bounded capture and process-group cancellation |
64
65
  | `stitchkit/files/packaging` | build time (Bun or Node) | evolving | installed native asset graph, integrity and Bun packaging plugin; no runtime install policy |
@@ -73,25 +74,24 @@ own, recorded as an ADR.
73
74
  | `stitchkit/geo` | server (Bun or Node) | evolving | managed GeoIP reader generations, last-known-good reload and the optional MaxMind adapter |
74
75
  | `stitchkit/oauth` | browser **and** server | evolving | provider-neutral Authorization Code + PKCE request and one-shot callback transaction mechanics |
75
76
  | `stitchkit/google` | server (Bun or Node) | evolving | optional Google code exchange and verified OIDC identity adapter |
76
- | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 49 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
77
+ | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 50 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
77
78
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the managed-resource conformance kit |
78
79
  | `stitchkit/declaration` | browser + build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
79
80
  | `stitchkit/react` | browser + server rendering | stable | `createCursorQuery`, `createCacheBridge`, QueryClient and `ApiError` retry policy |
80
81
  | `stitchkit/react/keyboard` | browser | evolving | which part of a screen gets a key — ordered layers, one Escape per level, list zones with real focus; see [keyboard guide](keyboard.md) |
81
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 22 of the 49 minors since 0.56.2, most recently 0.97.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
82
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 23 of the 50 minors since 0.56.2, most recently 0.105.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
82
83
  | `stitchkit/agent-runtime/sandbox` | server | evolving | optional Linux Bubblewrap sessions, durable workspaces and a host HTTP credential gateway; see [sandbox guide](sandbox.md) |
83
84
  | `stitchkit/agent-runtime/testing` | tests on Bun or Node | evolving | credential-free replay, scripted provider faults, deterministic race controls and the agent-store conformance kit |
84
85
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
85
86
  | `stitchkit/agent-runtime/coding-tools` | server (Bun or Node) | evolving | bounded host-authorized direct file and shell tools; a root boundary, not an OS sandbox |
86
87
  | `stitchkit/agent-runtime/openrouter` | server | evolving | isolated OpenRouter language-model adapter |
87
88
  | `stitchkit/agent-runtime/browser` | browser + server | evolving | canonical agent records, events, reconnect cursor and optional conversation controller |
88
- | `stitchkit/agent-runtime/harness-tools` | server | evolving | full mountAgent configuration with the current run fence |
89
- | `stitchkit/agent-runtime/react` | browser + server | evolving | React subscription to an application-owned agent controller |
90
- | `stitchkit/agent-runtime/realtime` | server (Bun or Node) | evolving | authorized agent control over the existing Socket.IO server |
89
+ | `stitchkit/agent-runtime/react` | browser + server | evolving, experimental: no committed consumer | React subscription to an application-owned agent controller |
90
+ | `stitchkit/agent-runtime/realtime` | server (Bun or Node) | evolving, experimental: no committed consumer | authorized agent control over the existing Socket.IO server |
91
91
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
92
92
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
93
93
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
94
- | `stitchkit/application` | browser + server | evolving<br>_redefined in 12 of the 49 minors since 0.56.2, most recently 0.104.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
94
+ | `stitchkit/application` | browser + server | evolving<br>_redefined in 13 of the 50 minors since 0.56.2, most recently 0.105.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
95
95
  | `stitchkit/application/grammy` | server | evolving | a grammY bot as application resources — configuration, then polling with updates admitted by batch — and the webhook adapter |
96
96
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
97
97
  | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
@@ -197,7 +197,6 @@ map — feature → packages:
197
197
  | MCP / agent adapters (`stitchkit/tools`) | `@modelcontextprotocol/server` `ai` |
198
198
  | Agent application runtime (`stitchkit/agent-runtime`) | `ai` |
199
199
  | Headless Agent harness (`stitchkit/agent-runtime/harness`) | `ai` |
200
- | Harness tools (`stitchkit/agent-runtime/harness-tools`) | `ai` `@modelcontextprotocol/server` (tool declarations) |
201
200
  | Agent coding tools (`stitchkit/agent-runtime/coding-tools`) | — |
202
201
  | OpenRouter runtime adapter (`stitchkit/agent-runtime/openrouter`) | `ai` `@openrouter/ai-sdk-provider` |
203
202
  | SQLite agent store (`stitchkit/agent-runtime/sqlite/bun` or `/node`) | — (runtime built-in) |
@@ -1207,7 +1206,7 @@ Three boundaries worth knowing:
1207
1206
 
1208
1207
  - A field of another scope is not a compile error on access — `RuntimeContext`
1209
1208
  keeps its `[key: string]: unknown` index signature (transports write through
1210
- it). It degrades to `unknown`, so it can no longer pose as a `string`; using it
1209
+ it). It degrades to `unknown`, so it cannot pose as a `string`; using it
1211
1210
  in a typed position fails.
1212
1211
  - The map states what **your** `beforeHandle` / `createAuthHook.inject` puts on
1213
1212
  the context. The framework does not verify a hand-written claim (→ ADR 0075) —
@@ -4229,15 +4228,17 @@ For example, an optional object `value` in `single` and a required array `value`
4229
4228
  in `group` advertise:
4230
4229
 
4231
4230
  ```text
4232
- When kind = group: Two to ten references.
4233
4231
  When kind = single: One reference.
4234
- Available if kind = group | single. Required if kind = group
4232
+ When kind = group: Two to ten references.
4233
+ Available if kind = single | group. Required if kind = group
4235
4234
  ```
4236
4235
 
4237
4236
  A third branch without `value` is absent from those labels. The object and array
4238
4237
  retain their separate shapes; the array's minimum length does not apply to the object.
4239
- Discriminator values, branch explanation labels and joined property order are
4240
- deterministic under branch reordering. → ADR [0221](../decisions/0221-flat-field-explanations-retain-branch-meaning.md).
4238
+ Discriminator values, branch explanation labels, enum values and joined
4239
+ properties keep the order of their first appearance across the variants, so the
4240
+ order you declare is the order the model reads. Alternatives of different kinds
4241
+ keep one canonical order, so one declaration always yields one document. → ADR [0221](../decisions/0221-flat-field-explanations-retain-branch-meaning.md).
4241
4242
 
4242
4243
  **A field several variants declare keeps its type.** Flattening puts every
4243
4244
  variant's fields side by side, so a key two variants share has to be advertised
@@ -4487,6 +4488,18 @@ An explicit `AppError.retryable` boolean also survives this remote hop. `false`
4487
4488
  `true` on409 remain the application's recommendation through HTTP, CLI, MCP and AI tools.
4488
4489
  Absent metadata keeps the previous defaults and does not alter HTTP transport retries.
4489
4490
 
4491
+ A remote failure that carries no stitchkit error envelope — an HTML `503` from a proxy
4492
+ during a deploy, a `429` from a rate limiter, a `404` page, a refused connection — keeps
4493
+ what the caller can act on and drops what it must not see. A response with a status
4494
+ becomes `HTTP_ERROR` with that status and a fixed message; a failure with no response
4495
+ becomes `CONNECTION_REQUEST_FAILED` (`502`). Neither carries `details`: the upstream
4496
+ body, URL and credentials stay in the raw cause that `onToolError` and `AgentToolError.cause`
4497
+ expose in-process. One rule decides `retryable` for every origin: the declared value wins,
4498
+ otherwise the status decides (`408`, `429`, `502`, `503`, `504` are retryable, as for any
4499
+ local tool). A call the caller cancelled (`REQUEST_ABORTED`) declares `retryable: false`;
4500
+ a deadline the remote client hit (`REQUEST_TIMEOUT`, `408`) follows the status like a
4501
+ `408` from a local handler.
4502
+
4490
4503
  ## Structured output
4491
4504
 
4492
4505
  When a contract endpoint or runtime tool declares `output`, its MCP tool
@@ -5147,15 +5160,6 @@ identical. Write the handler once; it serves every surface.
5147
5160
  # Guide: Agent application runtime (docs/guide/agent-runtime.md)
5148
5161
  ==============================================================================
5149
5162
 
5150
- ---
5151
- title: "Agent application runtime"
5152
- description: Configure Stitchkit's optional durable history, stream loop, run coordination and managed-tool fencing.
5153
- type: architecture
5154
- status: active
5155
- created: 2026-08-22
5156
- updated: 2026-10-02 16:28 +07:00
5157
- ---
5158
-
5159
5163
  # Agent application runtime
5160
5164
 
5161
5165
  > **Maturity: evolving.** This surface is still finding its shape and may be
@@ -6697,20 +6701,6 @@ existing runtime configurations need no migration.
6697
6701
  # Guide: Agent composition (docs/guide/agent-composition.md)
6698
6702
  ==============================================================================
6699
6703
 
6700
- ---
6701
- title: "Agent composition without a second runtime"
6702
- description: "Compose contract tools, authenticated realtime control, React views and exact-run behavioral checks."
6703
- type: architecture
6704
- status: active
6705
- created: 2026-09-30 22:45 +07:00
6706
- updated: 2026-09-30 23:03 +07:00
6707
- participants:
6708
- - role: authored
6709
- harness: Codex
6710
- model: GPT-6
6711
- at: 2026-09-30 22:45 +07:00
6712
- ---
6713
-
6714
6704
  # Compose an agent application
6715
6705
 
6716
6706
  Use these pieces when an application wants the existing headless harness and a generic
@@ -6726,7 +6716,7 @@ import {
6726
6716
  createAgentHarnessFileResources,
6727
6717
  createHeadlessAgentHarness,
6728
6718
  } from 'stitchkit/agent-runtime/harness';
6729
- import { createAgentHarnessTools } from 'stitchkit/agent-runtime/harness-tools';
6719
+ import { composeToolLifecycle, mountAgent } from 'stitchkit/tools';
6730
6720
 
6731
6721
  const resources = createAgentHarnessFileResources({
6732
6722
  roots: [{ id: 'guide', kind: 'instruction', path: instructionRoot }],
@@ -6735,27 +6725,31 @@ const resources = createAgentHarnessFileResources({
6735
6725
  const harness = createHeadlessAgentHarness({
6736
6726
  protocol, store, models, promptBudget,
6737
6727
  resources,
6738
- tools: createAgentHarnessTools(({ context, run }) => ({
6739
- services: [catalogService],
6740
- context: { ...context, messageId: run.assistantMessageId },
6741
- runtimeTools: resources.runtimeTools,
6742
- lifecycle: authorizeTools,
6743
- hooks: observeTools,
6744
- })),
6728
+ tools: ({ context, run, toolFenceLifecycle }) =>
6729
+ mountAgent([catalogService], {
6730
+ context: { ...context, messageId: run.assistantMessageId },
6731
+ runtimeTools: resources.runtimeTools,
6732
+ lifecycle: composeToolLifecycle(authorizeTools, toolFenceLifecycle),
6733
+ hooks: observeTools,
6734
+ }),
6745
6735
  });
6746
6736
  ```
6747
6737
 
6748
- The factory composes application authorization then the runtime fence through `mountAgent`.
6749
- The optional harness-tools leaf preserves the complete tool types, including MCP presenter
6750
- declarations; install the same `ai` and `@modelcontextprotocol/server` peers as for
6751
- `stitchkit/tools`. The base harness stays AI-only. Every mount option remains available, including registry, runtime tools, extension,
6752
- coercion, output stripping, hooks and durability. Async per-run configuration is supported.
6753
- The original tools callback still accepts arbitrary typed AI SDK tools and prepareStep.
6738
+ `tools` is called for each run with its validated context. Mount through `mountAgent` and
6739
+ compose your authorization with the run's `toolFenceLifecycle` by `composeToolLifecycle`: the
6740
+ fence rejects a call from a run that no longer owns the conversation, and it must run exactly
6741
+ once. Every mount option remains available, including registry, runtime tools, extension,
6742
+ coercion, output stripping, hooks and durability, and the callback may be async. The same callback
6743
+ still accepts arbitrary typed AI SDK tools and prepareStep. `stitchkit/tools` needs the same `ai` and
6744
+ `@modelcontextprotocol/server` peers as any `mountAgent` use; the base harness stays AI-only.
6754
6745
  The resource reader is explicitly selected: filtered allowlists do not gain tools merely
6755
6746
  because a loader exists. Root IDs, kinds, provenance and limits are unchanged.
6756
6747
 
6757
6748
  ## Server and browser
6758
6749
 
6750
+ `stitchkit/agent-runtime/realtime` and `stitchkit/agent-runtime/react` are evolving and
6751
+ experimental: no application has committed to them yet, so expect their shape to change.
6752
+
6759
6753
  ```ts
6760
6754
  import { bindAgentHarnessRealtime } from 'stitchkit/agent-runtime/realtime';
6761
6755
 
@@ -6767,11 +6761,19 @@ const binding = bindAgentHarnessRealtime(harness, socket, {
6767
6761
  },
6768
6762
  onError: reportServerError,
6769
6763
  });
6764
+
6765
+ // When an application withdraws someone's access, tell the binding.
6766
+ await removeMember(conversationId, userId);
6767
+ binding.revoke(conversationId, (identity) => identity.userId === userId);
6770
6768
  ```
6771
6769
 
6772
6770
  `socket` is the existing `createSocketIOServer` handle, attached to `createServer` (Bun)
6773
- or the Node adapter. The access policy checks every operation, file/reference and metadata
6774
- as well as snapshot/event reads. Session handshake authentication alone is insufficient.
6771
+ or the Node adapter. The access policy checks every request, file/reference and metadata, and
6772
+ the attach that opens a stream. Events then flow under that grant without a call per token;
6773
+ Without a `revoke()` call a grant lasts until the socket disconnects or the conversation is detached: events keep flowing even if your own permission data changed. That fail-open default is deliberate (one check per request, not one per token), so call `revoke` whenever access is withdrawn. `revoke` ends the grant, the next event is re-authorized (`source: 'delivery'`), and a refusal
6774
+ or failure detaches the conversation and delivers `access-denied`, which puts the controller in
6775
+ `error` with code `FORBIDDEN` instead of leaving a frozen view. Session handshake
6776
+ authentication alone is insufficient.
6775
6777
  Conversation ownership and observer/controller roles are application policy; the canonical
6776
6778
  control server additionally enforces its exclusive controller lease.
6777
6779
 
@@ -6809,7 +6811,10 @@ view does not interrupt its remote run. Use explicit `interrupt` for cancellatio
6809
6811
  Reconnect reattaches and reads durable state. Mutations are not retried automatically:
6810
6812
  if an acknowledgement is lost, retain the original idempotency key. Incoming event
6811
6813
  buffers and outgoing requests have finite limits; capacity/overflow/lease failures stay
6812
- visible. A server overflow deliberately disconnects the transport; reconnect explicitly.
6814
+ visible, each with a code from the one `AgentControlErrorCode` vocabulary. A reconnect
6815
+ rejects the requests of the previous connection at once and attaches again, so they
6816
+ cannot hold capacity the new attach needs. A server overflow deliberately disconnects the
6817
+ transport; reconnect explicitly.
6813
6818
 
6814
6819
  ## Check behavior, not just admission
6815
6820
 
@@ -6837,15 +6842,6 @@ The helper does not invoke a model or poll; the existing runtime ticket owns exe
6837
6842
  # Guide: Managed application kernel (docs/guide/application-kernel.md)
6838
6843
  ==============================================================================
6839
6844
 
6840
- ---
6841
- title: Managed application kernel
6842
- description: Compose process-local resources, schedules, readiness, drain and optional provider adapters without building a second job platform.
6843
- type: architecture
6844
- status: active
6845
- created: 2026-08-23
6846
- updated: 2026-10-04 13:28 +07:00
6847
- ---
6848
-
6849
6845
  # Managed application kernel
6850
6846
 
6851
6847
  > **Maturity: evolving.** This surface is still finding its shape and may be
@@ -7029,9 +7025,8 @@ lifecycle context; the application decides when a database/provider probe runs.
7029
7025
 
7030
7026
  **A `reportHealth` call inside `start` is kept.** A resource that says nothing
7031
7027
  is assumed healthy once it is ready; one that reports its own health has already
7032
- answered the question, and the answer stands. (It used to be overwritten — and
7033
- the example above hides that, because `healthy` is the same value that
7034
- overwrote it.)
7028
+ answered the question, and the answer stands. The readiness default never
7029
+ overwrites it.
7035
7030
 
7036
7031
  **Reporting the same health again is not a change.** A resource may confirm its
7037
7032
  health on a timer; a report with the value it already reported publishes
@@ -7527,9 +7522,48 @@ The caller owns file selection and coordination with an active rotator.
7527
7522
  Writer startup inspects retained and active files under its lock. On damage,
7528
7523
  `journal.getStatus().recovery` contains counts and first/last anomaly locations, reconstructed
7529
7524
  again on restart from the preserved bytes. This describes startup observations, not live
7530
- continuous inspection. `maxFiles: 1` cannot preserve a torn active file by rotation and throws
7531
- `DiagnosticJournalRecoveryError` before removing any evidence; configure at least two slots.
7532
- Normal rotation retains the existing finite eviction policy. → [ADR 0219](../decisions/0219-journal-damage-is-data-and-io-failure-is-failure.md).
7525
+ continuous inspection. Normal rotation retains the existing finite eviction policy.
7526
+ → [ADR 0219](../decisions/0219-journal-damage-is-data-and-io-failure-is-failure.md).
7527
+
7528
+ A file opening cannot keep in place — a torn active file with `maxFiles: 1`, a generation name
7529
+ that is not a regular file, a retained file it cannot read — is renamed to
7530
+ `<file>.quarantined-<epoch>` beside it, and the journal starts (`onStartupRefusal: 'quarantine'`,
7531
+ the default). Nothing is deleted; quarantined files are outside retention and yours to remove.
7532
+ Every open lists them in `getStatus().recovery.quarantined` until you do: the ones it moved
7533
+ first, with their `reason`, then the ones earlier runs left, without one — at most 32, and
7534
+ `quarantinedUnlisted` counts the rest.
7535
+ A journal whose damage must stop the process — one that is a source of truth, not diagnostics —
7536
+ declares `onStartupRefusal: 'fail'` and gets a typed `DiagnosticJournalRecoveryError`:
7537
+
7538
+ ```ts
7539
+ import { DiagnosticJournalRecoveryError } from 'stitchkit/application'
7540
+
7541
+ // A diagnostic journal: start, then say what is set aside.
7542
+ const journal = await createDiagnosticJournal({ ...config })
7543
+ const { quarantined = [], quarantinedUnlisted = 0 } = journal.getStatus().recovery ?? {}
7544
+ for (const entry of quarantined) {
7545
+ // `reason` is present only for a file this start moved; an earlier run's has none.
7546
+ internalLogger.warn({ file: entry.file, movedTo: entry.quarantinedAs, reason: entry.reason })
7547
+ }
7548
+ if (quarantinedUnlisted > 0) internalLogger.warn({ quarantinedUnlisted })
7549
+
7550
+ // A journal that is a source of truth: refuse to start over damage.
7551
+ try {
7552
+ await createDiagnosticJournal({ ...config, onStartupRefusal: 'fail' })
7553
+ } catch (error) {
7554
+ if (error instanceof DiagnosticJournalRecoveryError) {
7555
+ // error.reason: 'torn-without-retention-slot' | 'not-a-regular-file' | 'unreadable'
7556
+ }
7557
+ throw error
7558
+ }
7559
+ ```
7560
+
7561
+ A quarantining journal writes, so the managed resource that owns it reports `healthy`, and its
7562
+ dependants read `recovery` from the value they `use()`. There is no degraded journal: the kernel
7563
+ degrades only optional resources, and a required resource cannot depend on one.
7564
+ `DiagnosticJournalRecoveryStatusSchema` is evolving and `.strict()`; if you embed it in your own
7565
+ protocol, every change to its shape is listed under breaking changes.
7566
+ → [ADR 0240](../decisions/0240-a-diagnostic-journal-quarantines-what-it-cannot-keep.md).
7533
7567
 
7534
7568
  Shutdown performs one phase barrier at a time: stop admission everywhere,
7535
7569
  cancel future schedules, drain admitted work, then close in reverse stable
@@ -7983,17 +8017,6 @@ boundary is required. Send and projection callbacks own their external deadlines
7983
8017
  # Guide: Optional process sandbox (docs/guide/sandbox.md)
7984
8018
  ==============================================================================
7985
8019
 
7986
- ---
7987
- title: Optional process sandbox
7988
- description: Linux namespace isolation, durable workspaces and a host credential gateway.
7989
- updated: 2026-10-03 14:47 +07:00
7990
- participants:
7991
- - role: implemented
7992
- harness: Codex
7993
- model: GPT-6
7994
- at: 2026-10-03 14:47 +07:00
7995
- ---
7996
-
7997
8020
  # Optional process sandbox
7998
8021
 
7999
8022
  `stitchkit/agent-runtime/sandbox` is a server-only, evolving API. Existing coding
@@ -8057,6 +8080,9 @@ access the same workspace through the host's contained-file implementation;
8057
8080
  commands run inside the namespace with `/workspace` as their root. The host-only
8058
8081
  `handle.coding` binding never belongs in model input or persisted state.
8059
8082
  Unsupported backends omit the binding and the factory refuses explicitly.
8083
+ When the adapter's `probe()` or `prepare()` fails, the agent gets `SANDBOX_UNAVAILABLE` with a
8084
+ fixed reason; the adapter's own error is the refusal's `cause`, delivered to the mount's
8085
+ `hooks.onToolError`, which is where an application logs it.
8060
8086
 
8061
8087
  Stop also kills coding commands. Both launch paths share the configured
8062
8088
  `maxConcurrentCommands` admission limit (default 8); an occupied slot gives
@@ -8125,37 +8151,25 @@ be refused explicitly by the backend.
8125
8151
  # Guide: Native IO (docs/guide/native-io.md)
8126
8152
  ==============================================================================
8127
8153
 
8128
- ---
8129
- title: Конечные команды и файловые гарантии
8130
- description: Native IO для Bun и Node, с явно выбранной durability, безопасными bounds и caller lifetime.
8131
- type: guide
8132
- status: active
8133
- created: 2026-10-01 20:44 +07:00
8134
- updated: 2026-10-04 20:04 +07:00
8135
- participants:
8136
- - role: authored
8137
- harness: Codex
8138
- model: GPT-6
8139
- at: 2026-10-01 20:44 +07:00
8140
- - role: implemented
8141
- harness: Codex
8142
- model: GPT-6
8143
- at: 2026-10-02 00:08 +07:00
8144
- - role: implemented
8145
- harness: Codex
8146
- model: GPT-6
8147
- at: 2026-10-03 13:36 +07:00
8148
- - role: implemented
8149
- harness: Codex
8150
- model: GPT-6
8151
- at: 2026-10-04 09:21 +07:00
8152
- ---
8153
-
8154
8154
  # Native IO
8155
8155
 
8156
- Примеры используют публичные imports и одинаково исполняются в Bun и Node ≥22.
8156
+ Shortest working example: publish a file atomically, then run one bounded command.
8157
+
8158
+ ```ts
8159
+ import { writeFileAtomic } from 'stitchkit/files'
8160
+ import { runNativeCommand } from 'stitchkit/process'
8161
+
8162
+ await writeFileAtomic('/srv/app/state.json', '{"version":2}')
8163
+ const { exitCode, stdout } = await runNativeCommand({
8164
+ executable: '/usr/bin/git', args: ['rev-parse', 'HEAD'], cwd: '/srv/app',
8165
+ capture: true, maxOutputBytes: 4096, timeoutMs: 5000,
8166
+ })
8167
+ console.log(exitCode, new TextDecoder().decode(stdout))
8168
+ ```
8157
8169
 
8158
- ## Запись файла
8170
+ The examples use public imports and run the same in Bun and Node >= 22.
8171
+
8172
+ ## Writing a file
8159
8173
 
8160
8174
  ```ts
8161
8175
  import { writeFileAtomic, AtomicFilePublicationError } from 'stitchkit/files'
@@ -8168,7 +8182,7 @@ try {
8168
8182
  })
8169
8183
  } catch (error) {
8170
8184
  if (error instanceof AtomicFilePublicationError) {
8171
- // target уже виден. Проверьте receipt, прежде чем решать о повторе.
8185
+ // The target is already visible. Check the receipt before deciding whether to retry.
8172
8186
  console.error(error.phase, error.cause)
8173
8187
  } else throw error
8174
8188
  }
@@ -8177,25 +8191,50 @@ await writeFileAtomic('/srv/app/state.json', '{"version":2}', {
8177
8191
  })
8178
8192
  ```
8179
8193
 
8180
- По умолчанию `replace: true`, `durability: 'file'`, `mode: 0o600`: existing вызовы сохраняют
8181
- file-fsync и atomic replacement. `durability: 'none'` отключает sync; `'directory'` выполняет
8182
- полную запись → exact descriptor chmod → file fsync → rename (replace) либо link (create) →
8183
- удаление staging link → parent fsync. `writeFileAtomicSync` имеет те же options и порядок.
8184
- Create отказывает с native `EEXIST` при существующем файле, symlink или concurrent winner.
8185
- Никогда не переводите этот отказ в replace автоматически.
8194
+ The defaults are `replace: true`, `durability: 'file'` and `mode: 0o600`: file fsync and atomic
8195
+ replacement. `durability: 'none'` skips every sync. `'directory'` runs the full sequence: write all
8196
+ bytes, exact descriptor chmod, file fsync, rename (replace) or link (create), remove the staging
8197
+ link, parent directory fsync. `writeFileAtomicSync` takes the same options and runs the same order.
8198
+ Create fails with the native `EEXIST` when the target exists, is a symlink, or a concurrent writer
8199
+ won. Never turn that refusal into a replace automatically.
8200
+
8201
+ `AtomicFilePublicationError.published === true` tells a visible target from an unconfirmed
8202
+ acknowledgement. `phase` is `cleanup`, `directory-sync` or `directory-close`; the primary cause is
8203
+ kept in `cause`. An error before publication leaves the previous target untouched. Cleanup errors are
8204
+ secondary to the primary error. While a create is in flight a second hardlink briefly exists, so a
8205
+ strict single-link reader may refuse in that window.
8206
+
8207
+ ### Abandoned staging files
8208
+
8209
+ The bytes are staged beside the target under `.stitchkit-<24 lowercase hex>.tmp`, the same name for
8210
+ `writeFileAtomic`, `writeFileAtomicSync`, the managed writer and the chunk spool. A process killed
8211
+ between the staging and the publication (SIGKILL, OOM, power loss) leaves that file behind, and no
8212
+ later write removes it: a random name alone cannot tell an abandoned write from one in flight.
8213
+ Removing it is the caller's responsibility:
8214
+
8215
+ ```ts
8216
+ import { isAtomicStagingName, sweepAtomicStaging } from 'stitchkit/files'
8217
+
8218
+ // At startup, or before trusting that a directory holds only your own records:
8219
+ const removed = await sweepAtomicStaging({ directory: '/srv/app/registry', olderThanMs: 60 * 60_000 })
8186
8220
 
8187
- `AtomicFilePublicationError.published === true` отличает видимость от неподтверждённого
8188
- acknowledgement. `phase` — `cleanup`, `directory-sync` или `directory-close`; первичная причина
8189
- сохранена в `cause`. Ошибка до publish оставляет прежний target. Cleanup вторичен к первичной ошибке.
8190
- При create кратко существует второй hardlink; strict single-link reader вправе отказать в этом окне.
8221
+ // Skip staging files when listing a directory of your records:
8222
+ const records = (await readdir('/srv/app/registry')).filter((name) => !isAtomicStagingName(name))
8223
+ ```
8191
8224
 
8192
- Directory fsync — native POSIX/filesystem capability. Windows явно отказывает; иной filesystem
8193
- может отказать при open/sync. Проверка на Linux не подтверждает поведение macOS, сетевой FS или
8194
- сохранность при отключении питания. Состояние диска определяется гарантиями самой FS/ОС.
8195
- `createManagedFileBoundary.write(..., { durable: true })` использует тот же publish owner и parent sync;
8196
- его create по умолчанию остаётся create, а `replace: true` явно разрешает replacement.
8225
+ `olderThanMs` is required and must exceed the longest write in flight; a younger staging file is
8226
+ left alone. The sweep removes only regular files, never follows a symlink and never enters a
8227
+ subdirectory. The name form is a stable public contract: changing it is a breaking change of
8228
+ `stitchkit/files`. An exclusive lock's `.lock-*.tmp` file is not an atomic-write staging file and
8229
+ does not match.
8197
8230
 
8198
- ## Строгое чтение
8231
+ Directory fsync is a native POSIX filesystem capability. Windows refuses it explicitly, and another
8232
+ filesystem may refuse at open or sync. A check on Linux does not confirm macOS behavior, network
8233
+ filesystems or survival of a power loss; the disk state is whatever the filesystem and OS guarantee.
8234
+ `createManagedFileBoundary.write(..., { durable: true })` uses the same publish owner and parent sync;
8235
+ its create stays a create, and `replace: true` allows replacement explicitly.
8236
+
8237
+ ## Strict reads
8199
8238
 
8200
8239
  ```ts
8201
8240
  import { createManagedFileBoundary } from 'stitchkit/files'
@@ -8206,25 +8245,27 @@ const source = await files.read('receipt.json', {
8206
8245
  })
8207
8246
  console.log(source.observation) // dev, ino, size, nlink, mtimeMs, ctimeMs
8208
8247
  const receipt = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(source.bytes))
8209
- // schema.parse(receipt) и проверка digest принадлежат приложению.
8248
+ // Schema parsing and digest checks belong to the application.
8210
8249
  ```
8211
8250
 
8212
- Без новых options ответ сохраняет `{ ref, bytes }`, допускает trusted in-root symlink и hardlink.
8213
- `rejectSymlinks` запрещает requested leaf, `singleLink` проверяет `nlink` открытого descriptor,
8214
- `stable` сравнивает dev/ino/size/mtime/ctime/nlink вокруг byte read; `observe` добавляет metadata.
8215
- `FILE_NOT_FOUND`, `FILE_NOT_REGULAR`, `FILE_UNSAFE_LINK`, `FILE_TOO_LARGE`, `FILE_CHANGED`,
8216
- `FILE_UNSUPPORTED` и `FILE_IO_ERROR` различаются. Abort сохраняет caller reason.
8217
- Nonblocking open позволяет отказать FIFO без ожидания writer. Bytes ограничены во время чтения,
8218
- включая рост файла, не только initial stat. Chunk allocation ≤ maxBytes+1+64 KiB, flatten ≤ maxBytes;
8219
- FD закрывается при success, отказе, abort и IO error.
8220
- Ошибка close сохраняет первичную ошибку чтения; после успешного чтения ошибка close возвращается caller.
8251
+ Without options the result is `{ ref, bytes }` and a trusted in-root symlink or hardlink is
8252
+ allowed. `rejectSymlinks` refuses a symlink at the requested leaf, `singleLink` checks `nlink` of
8253
+ the opened descriptor, `stable` compares dev/ino/size/mtime/ctime/nlink around the byte read, and
8254
+ `observe` adds the metadata. These refusals are distinct: `FILE_NOT_FOUND`, `FILE_NOT_REGULAR`,
8255
+ `FILE_UNSAFE_LINK`, `FILE_TOO_LARGE`, `FILE_CHANGED`, `FILE_UNSUPPORTED` and `FILE_IO_ERROR`. Abort
8256
+ keeps the caller's reason. A nonblocking open lets a FIFO be refused without waiting for a writer.
8257
+ Bytes are bounded while reading, including a file that grows, not only by the initial stat. Chunk
8258
+ allocation is at most `maxBytes + 1 + 64 KiB` and the flattened result at most `maxBytes`. The
8259
+ descriptor closes on success, refusal, abort and IO error. A close error keeps the primary read error;
8260
+ after a successful read a close error is returned to the caller.
8221
8261
 
8222
- Root и ancestors принадлежат trusted actor. Эти options не дают descriptor-relative containment
8223
- при hostile concurrent ancestor replacement. Отдельного переключателя «считать ancestors безопасными»
8224
- нет: portable owner не предоставляет такую capability. Metadata stability не доказывает immutable
8225
- snapshot против произвольного hostile writer; используйте application lock/immutable publication.
8262
+ The root and its ancestors belong to a trusted actor. These options do not give
8263
+ descriptor-relative containment against a hostile concurrent ancestor replacement, and there is no
8264
+ switch to treat ancestors as safe: the portable owner has no such capability. Metadata stability does
8265
+ not prove an immutable snapshot against an arbitrary hostile writer; use an application lock or
8266
+ immutable publication.
8226
8267
 
8227
- ## Команды
8268
+ ## Commands
8228
8269
 
8229
8270
  ```ts
8230
8271
  import { runNativeCommand } from 'stitchkit/process'
@@ -8246,21 +8287,23 @@ await runNativeCommand({
8246
8287
  })
8247
8288
  ```
8248
8289
 
8249
- Команда требует caller signal или finite `timeoutMs` (1–2147483647 ms: native timer range; большее значение отказывается, не превращается в 1 ms). Streaming имеет caller lifetime, без скрытого
8250
- 60-second timeout или 1-MiB output limit. `capture` по умолчанию false; true требует
8251
- `maxOutputBytes` для суммарных stdout+stderr. Лимит считает исходные bytes; API возвращает
8252
- `Uint8Array`, не преобразует invalid UTF-8. Result содержит наблюдаемый `exitCode: number | null`
8253
- и `signal: string | null`; signal termination не превращается в выдуманный exit code. `maxOutputBytes` может ограничивать и streaming.
8254
- `stdin` — bytes с `maxStdinBytes` (default 1 MiB). `envPolicy` default `declared-only`: отсутствие env
8255
- означает пустой environment; `ambient` явно разрешает наследование. Cwd/executable policy остаётся
8256
- приложению; этот primitive запускает native executable и сам не создаёт shell.
8290
+ A command needs a caller `signal` or a finite `timeoutMs` (1 to 2147483647 ms, the native timer
8291
+ range; a larger value is refused, never turned into 1 ms). Streaming runs for the caller's lifetime,
8292
+ with no hidden 60-second timeout or 1 MiB output limit. `capture` defaults to false; `true` requires
8293
+ `maxOutputBytes` for the combined stdout and stderr. The limit counts source bytes; the API returns
8294
+ `Uint8Array` and does not decode invalid UTF-8. The result holds the observed `exitCode: number | null`
8295
+ and `signal: string | null`; a signal termination is never turned into an invented exit code.
8296
+ `maxOutputBytes` may bound streaming too. `stdin` is bytes bounded by `maxStdinBytes` (default 1 MiB).
8297
+ `envPolicy` defaults to `declared-only`: no `env` means an empty environment, and `ambient` inherits
8298
+ explicitly. The application owns cwd and executable policy; the primitive launches a native
8299
+ executable and never creates a shell.
8257
8300
 
8258
- Машинная классификация limit error использует `NativeCommandError.reason`: `deadline` при
8259
- истечении собственного `timeoutMs`, `output-budget` при превышении суммарных bytes stdout+stderr
8260
- в capture или streaming. Оба случая сохраняют `code: 'COMMAND_LIMIT'`; `message` служит
8261
- диагностике, не протоколу. У `COMMAND_UNAVAILABLE` и `COMMAND_CLEANUP` reason отсутствует;
8262
- caller abort и sink failure сохраняют исходную ошибку. При cleanup failure первоначальный
8263
- limit вместе с reason остаётся внутри `AggregateError` cause.
8301
+ `NativeCommandError.reason` classifies a limit error by machine: `deadline` when the command's own
8302
+ `timeoutMs` expires, `output-budget` when the combined stdout and stderr bytes exceed the limit, in
8303
+ capture or streaming. Both keep `code: 'COMMAND_LIMIT'`; `message` is for diagnosis, not for
8304
+ protocols. `COMMAND_UNAVAILABLE` and `COMMAND_CLEANUP` have no reason. A caller abort and a sink
8305
+ failure keep the original error. When cleanup fails, the original limit and its reason stay inside the
8306
+ `AggregateError` cause.
8264
8307
 
8265
8308
  ```ts
8266
8309
  import { NativeCommandError } from 'stitchkit/process'
@@ -8276,38 +8319,112 @@ try {
8276
8319
  }
8277
8320
  ```
8278
8321
 
8279
- Конструктор сохраняет вызов `new NativeCommandError(code, message, { cause })` и принимает
8280
- `{ cause, reason: 'deadline' | 'output-budget' }` только для `COMMAND_LIMIT`. Owner всегда
8281
- заполняет reason для собственных limits. Созданная вызывающей стороной ошибка без reason
8282
- не получает выдуманную причину из текста; её reason остаётся `undefined`.
8283
-
8284
- Вывод читается с backpressure отдельно для stdout/stderr: один выполняющийся sink на канал плюс
8285
- bounded native Readable buffer; накопления всей истории при streaming нет. Callback получает
8286
- lifetime signal. Abort освобождает ожидание даже игнорирующего signal callback, но не способен
8287
- отменить сторонние действия, которые сам callback продолжит после abort. Sink обязан отменять свои
8288
- IO, не сохранять бесконечную историю и не изменять shared descriptor flags. После abort новых
8289
- callbacks owner не запускает. Normal completion ждёт всех bytes и sinks.
8290
-
8291
- Cleanup: POSIX process group, TERM, `killGraceMs` (default 100, maximum 10 000), затем KILL;
8292
- `cleanupTimeoutMs` (default 2000, maximum 30 000) ограничивает ожидание close после сигналов.
8293
- На Darwin временный `EPERM` при сигнале группе может означать zombies, ещё не убранные
8294
- OS reaper. Повтор ограничен `cleanupTimeoutMs`; успех требует доставленного сигнала или
8295
- `ESRCH`, постоянный отказ сохраняется в cause. Закрытие каждого owned pipe и выход leader
8296
- наблюдаются отдельно, включая отсутствие общего `child.close` при teardown.
8297
- Сохраняется caller/sink причина; failure cleanup — `NativeCommandError` с `COMMAND_CLEANUP` и
8298
- AggregateError cause. Даже вышедший parent не исключает helper, удерживающий pipe. Windows
8299
- отказывает с `COMMAND_UNAVAILABLE`; потомок, самостоятельно покинувший группу через setsid,
8300
- лежит за границей этой capability. Это one-shot execution, не PTY, supervisor или daemon host.
8301
-
8302
- ## Механическая адаптация
8303
-
8304
- Atomic file wrapper заменяется одним вызовом `writeFileAtomic` с выбранными options. Bounded
8305
- regular-file read заменяется `files.read`; JSON/schema, receipt conflicts, journal rotation и
8306
- application locks сохраняются. Spawn/drain/capture/cancellation wrapper заменяется `runNativeCommand`;
8307
- shell policy, command admission, логирование, redaction, bytes→text decoder и operator sink сохраняются.
8308
- Длительные signal-only команды остаются streaming; bounded tar/capture получают свои явные limits.
8309
- Импорт leaf не делает установку всего npm package маленькой; выбирайте dependency после измерения
8310
- packed размера и dependency closure.
8322
+ The constructor is `new NativeCommandError(code, message, { cause })`; it accepts
8323
+ `{ cause, reason: 'deadline' | 'output-budget' }` only for `COMMAND_LIMIT`. The owner always sets the
8324
+ reason for its own limits. An error a caller constructs without a reason gets none inferred from the
8325
+ text; its `reason` is `undefined`.
8326
+
8327
+ Output is read with backpressure, stdout and stderr separately: one running sink per channel plus a
8328
+ bounded native Readable buffer, with no accumulation of the whole history while streaming. The
8329
+ callback receives the lifetime signal. Abort releases the wait even for a callback that ignores the
8330
+ signal, but it cannot cancel third-party work the callback continues after the abort. A sink must
8331
+ cancel its own IO, must not keep an unbounded history and must not change shared descriptor flags.
8332
+ After an abort the owner starts no new callbacks. Normal completion waits for all bytes and sinks.
8333
+
8334
+ Cleanup uses the POSIX process group under the `stop` policy, then KILL; `cleanupTimeoutMs`
8335
+ (default 2000, maximum 30 000) bounds the wait for close after the signals. On
8336
+ Darwin a transient `EPERM` when signalling a group can mean zombies the OS reaper has not collected
8337
+ yet. The retry is bounded by `cleanupTimeoutMs`; success needs a delivered signal or `ESRCH`, and a
8338
+ permanent refusal is kept in the cause. The close of every owned pipe and the leader's exit are
8339
+ observed separately, including the absence of a shared `child.close` during teardown. The caller or
8340
+ sink cause is kept; a cleanup failure is a `NativeCommandError` with `COMMAND_CLEANUP` and an
8341
+ `AggregateError` cause. Even an exited parent does not exclude a helper that holds a pipe. Windows
8342
+ refuses with `COMMAND_UNAVAILABLE`; a descendant that leaves the group on its own with `setsid` is
8343
+ outside this capability. This is one-shot execution, not a PTY, a supervisor or a daemon host.
8344
+
8345
+ ### Stopping a command: the `stop` policy
8346
+
8347
+ A caller abort, the `timeoutMs` deadline, an exceeded output budget and a failing sink all stop the
8348
+ command the same way, by its `stop` policy:
8349
+
8350
+ | field | meaning |
8351
+ |---|---|
8352
+ | `target` | `'group'`: `signal` goes to every member of the group. `'leader'`: `signal` goes to the leader alone; the leader's exit ends the grace at once. Required. |
8353
+ | `signal` | `SIGTERM` (default), `SIGINT`, `SIGHUP`, `SIGQUIT`, `SIGUSR1` or `SIGUSR2`. KILL is not a stop signal: it is what the grace ends in. |
8354
+ | `graceMs` | How long the leader (or the group) has to leave before the whole group gets KILL. 0 to 3 600 000 (one hour). Required. |
8355
+ | `killOn` | Optional `AbortSignal`. Its abort stops the command with KILL to the whole group at once, also in the middle of a grace. |
8356
+
8357
+ Omitting `stop` means `{ target: 'group', signal: 'SIGTERM', graceMs: 100 }`.
8358
+
8359
+ ```ts
8360
+ // Cooperative cancellation: the leader rolls its work back, its helpers are not asked.
8361
+ await runNativeCommand({
8362
+ executable: '/usr/local/bin/migrate', args: ['apply'], signal: request.signal,
8363
+ stop: { target: 'leader', signal: 'SIGINT', graceMs: 15 * 60_000, killOn: shutdown.signal },
8364
+ })
8365
+ ```
8366
+
8367
+ With `target: 'leader'` the rest of the group never receives the cooperative signal: they are
8368
+ processes the leader started, and the leader decides what happens to them. Whatever is left in the
8369
+ group when the leader exits, or when the grace ends, gets KILL, including a helper with its stdio
8370
+ closed that no pipe would have revealed. A leader that has already exited is never signalled again.
8371
+
8372
+ The output pipes stay open until the stop sequence ends. Output a stopping command writes is read and
8373
+ dropped, never passed to `onOutput` and never captured, so a leader that writes while it shuts down
8374
+ does not meet a broken pipe. The result rejects with the abort reason once the group is gone and the
8375
+ pipes are closed, so it can take `graceMs` plus `cleanupTimeoutMs` after the abort.
8376
+
8377
+ The grace is a timer inside the process that runs the command. If that process exits first, the
8378
+ timer is gone and the group, which is detached into its own session, keeps running: nothing in this
8379
+ package can bound it from outside. Abort `killOn` in your shutdown path, so the group is killed
8380
+ before the process exits, and let the supervisor own the case where the process dies without a
8381
+ shutdown. Under systemd, keep `KillMode=control-group` (the default) so the stop of the unit
8382
+ reaches every process in its cgroup, whatever its process group; launchd only kills the job's own
8383
+ process group, which a command's group is not.
8384
+
8385
+ ### Stopping a group and its descendants
8386
+
8387
+ A group is signalled only while the kernel still reports members. `ESRCH` from the first signal,
8388
+ or from the existence probe that follows it during the grace, means no member is left: no KILL
8389
+ is sent afterwards, because a numeric group id that has vanished can already belong to an
8390
+ unrelated group. A group that is still visible after the grace period receives KILL.
8391
+
8392
+ `descendants` declares what happens to processes the leader left running once it has exited
8393
+ successfully:
8394
+
8395
+ | `descendants` | After the leader exits |
8396
+ |---|---|
8397
+ | `'terminate-after-leader'` (the default) | The group is stopped right after `onLeaderSettled` returns and before the pipes drain: with `target: 'group'` by the same signal, grace, KILL sequence as cancellation; with `target: 'leader'` by KILL at once, since the leader has already exited. Descendants that left the group with `setsid` stay out of reach. |
8398
+ | `'leave'` | Nothing. A helper started with its stdio detached keeps running. One that inherited stdout or stderr keeps the pipes open, and the command then waits for it until its deadline or abort. |
8399
+
8400
+ Omitting the option means `'terminate-after-leader'`: a command does not leak the helpers it
8401
+ started. A real daemon detaches into its own session (`setsid`, as `daemon(3)` does), is no member
8402
+ of the group and is unaffected. Declare `'leave'` only for a helper that has to outlive the
8403
+ command while staying in its group:
8404
+
8405
+ ```ts
8406
+ await runNativeCommand({
8407
+ executable: '/usr/local/bin/start-helper', timeoutMs: 60_000,
8408
+ // the helper started here outlives this command on purpose
8409
+ descendants: 'leave',
8410
+ })
8411
+ ```
8412
+
8413
+ A command whose group this package did not create (a structural
8414
+ launcher supplied by a host) has no group to stop, and `descendants` has nothing to act on there.
8415
+
8416
+
8417
+ ## Replacing hand-written wrappers
8418
+
8419
+ An atomic-file wrapper becomes one `writeFileAtomic` call with the chosen options. A bounded
8420
+ regular-file read becomes `files.read`; JSON and schema parsing, receipt conflicts, journal rotation
8421
+ and application locks stay with the application. A spawn, drain, capture and cancellation wrapper
8422
+ becomes `runNativeCommand`; shell policy, command admission, logging, redaction, the bytes-to-text
8423
+ decoder and the operator sink stay with the application. Long signal-only commands stay streaming;
8424
+ bounded tar or capture commands get their own explicit limits. Importing one leaf does not make
8425
+ installing the whole npm package smaller; choose a dependency after measuring packed size and its
8426
+ dependency closure.
8427
+
8311
8428
  ## Resource-scoped launchers and shared owners
8312
8429
 
8313
8430
  `onLeaderSettled(event, signal)` observes leader exit before inherited stdout/stderr pipes
@@ -8349,8 +8466,23 @@ syscall interruptible or provide a snapshot against a hostile writer.
8349
8466
  An exclusive lock applies exact requested permissions through its creating descriptor,
8350
8467
  independent of umask. Default `0600` stays private. Shared `0640` requires a common group and
8351
8468
  traversable directories; another UID also needs directory write permission to reclaim a
8352
- proven-dead owner's file. Set `ownerlessGraceMs: null` to refuse every age-only reclaim,
8353
- including ownerless reclaim guards. The default remains 5000 ms for existing clients.
8469
+ proven-dead owner's file.
8470
+
8471
+ The lock is published with its owner already recorded: the owner record is written to a
8472
+ private temporary file in the lock's directory, then hard-linked to the lock name. `link`
8473
+ fails with `EEXIST` for every caller but one, and the name never exists without a complete
8474
+ record, so a holder that stalls or dies before publishing leaves no lock and cannot be
8475
+ displaced. The directory must therefore be on a filesystem that supports hard links. A crash
8476
+ between the temporary write and the link can leave a `.lock-*.tmp` file behind; it is not a
8477
+ lock and nothing waits on it.
8478
+
8479
+ `ownerlessGraceMs` defaults to `null`: a lock file with no readable owner is never taken
8480
+ because of its age. This library does not produce such a file; only a writer that creates the name
8481
+ before its owner record can leave one (for example an empty file from a crashed process).
8482
+ `ownerlessGraceMs: <ms>` opts in to reclaiming an empty lock file older than that, and also governs
8483
+ abandoned empty reclaim guards.
8484
+ When unset, an empty reclaim guard left by an older writer is taken after 5 000 ms so recovery
8485
+ is never disabled for good; `null` refuses that too.
8354
8486
  Stale reclaim guards use the same lock owner and a child guard for the inode check and
8355
8487
  unlink. Recovery depth is capped at 16; a deeper stale chain refuses recovery with a depth diagnosis retained as the cause of
8356
8488
  the bounded `LOCK_TIMEOUT`. Live owners, unknown process-lifetime evidence and unsafe records are never
@@ -8376,27 +8508,26 @@ version: carry their reachable relative `.d.ts` closure, rather than emitting re
8376
8508
  instead emit declarations for only that DTO contract. Keep the library's manifest at runtime
8377
8509
  `dependencies: {}` and `peerDependencies: { zod: ... }` when that is its declared promise.
8378
8510
 
8379
- The packaged Darwin loader has static references to both architecture addons. Bun compilation
8380
- embeds the matching `.node` asset; ordinary Bun bundling emits native assets alongside its JS
8381
- output. Publish **every output returned by `Bun.build`**, preserving relative paths. Copying only
8382
- the JS file drops its native dependency. No runtime Stitchkit installation or manually chosen
8383
- native path is required for these bundled artifacts.
8511
+ The packaged Darwin loader names its addons by a path computed at run time, so a bundler
8512
+ never follows it: bundling `stitchkit/server`, `stitchkit/files` or `stitchkit/process` yields
8513
+ the same single JS file on every operating system, with no `.node` output, and
8514
+ `bun build --outfile` works. Such a bundle looks for the addon beside the original package
8515
+ files, never inside itself. Where that file is absent (an artifact moved to another machine)
8516
+ the Darwin backend is `unavailable`: process identity cannot be recorded, so a lock held by a
8517
+ crashed owner is not reclaimed. An artifact that must carry the Darwin addon uses the packaging
8518
+ plugin of the next section; it is the only way a static loader enters a bundle.
8384
8519
 
8385
8520
  ```sh
8386
- bun build src/native.ts --target=bun --minify --outdir=dist
8387
- bun build src/native.ts --compile --bytecode --format=esm --outfile=dist/native
8521
+ bun build src/native.ts --target=bun --minify --outfile=dist/native.js
8388
8522
  ```
8389
8523
 
8390
- For a JS build that previously used `--outfile=dist/native.js`, use
8391
- `--outdir=dist --entry-naming=native.js` instead: the output can now include native assets.
8392
- The standalone executable still uses `--outfile` because those assets are embedded inside it.
8393
-
8394
8524
  Qualify the complete output outside its build tree and installed dependency graph. Test process
8395
- identity, live-owner refusal, dead-owner recovery and contained file operations in the resulting
8396
- JS bundle and standalone executable. Bun and Node package imports retain the same lazy loader;
8397
- importing a portable leaf does not load a Darwin addon on another OS. Test declarations too,
8398
- and check that importing an unbundled Stitchkit leaf fails in a deliberately isolated library
8399
- distribution. A separate schema-only entry must not import the native entry.
8525
+ identity, live-owner refusal, dead-owner recovery and contained file operations in artifacts built
8526
+ with the packaging plugin, and check that an unpackaged build reports the backend `unavailable`
8527
+ rather than certifying a dead owner. Bun and Node package imports retain the same
8528
+ lazy loader; importing a portable leaf does not load a Darwin addon on another OS. Test
8529
+ declarations too, and check that importing an unbundled Stitchkit leaf fails in a deliberately
8530
+ isolated library distribution. A separate schema-only entry must not import the native entry.
8400
8531
 
8401
8532
  If the Darwin backend cannot load, `observeProcessInstance` still returns `unavailable`.
8402
8533
  Its backend `Error` preserves the original `cause`; JSON serialization carries only the stable
@@ -8412,17 +8543,17 @@ Stitchkit installation or create an independent lightweight npm package.
8412
8543
  ## Public native packaging
8413
8544
 
8414
8545
  Custom archives and installers use `stitchkit/files/packaging` at **build time**.
8415
- `createNativePackaging` resolves the installed version and target addon with its original SHA256;
8416
- its Bun-compatible plugin integrates that asset with the same lazy native loader.
8546
+ `createNativePackaging` resolves the installed version and target addon, reads the addon once and
8547
+ checks it against the size and SHA256 that Stitchkit published in its `native-assets.json` when the
8548
+ package was built. A ready asset carries those verified `bytes` with the published `size` and
8549
+ `sha256`; its Bun-compatible plugin integrates the same bytes with the same lazy native loader.
8550
+ Each call reads and hashes the selected addons, so call it once per build and reuse the result.
8417
8551
  The leaf is evolving. It requires no optional peer or Bun ambient declarations for Node imports.
8418
8552
  It must run unbundled from its installed package, never from an application's runtime artifact.
8419
- This contract is available from 0.104.1; versions 0.103.13–0.104.0 use the complete-output recipe
8420
- in the previous section.
8421
8553
 
8422
8554
  ```ts
8423
8555
  import { createNativePackaging } from 'stitchkit/files/packaging'
8424
- import { createHash } from 'node:crypto'
8425
- import { copyFileSync, mkdirSync, readFileSync } from 'node:fs'
8556
+ import { mkdirSync, writeFileSync } from 'node:fs'
8426
8557
  import { dirname, join } from 'node:path'
8427
8558
 
8428
8559
  const native = createNativePackaging({
@@ -8436,14 +8567,11 @@ const result = await Bun.build({
8436
8567
  plugins: [native.plugin],
8437
8568
  })
8438
8569
  if (!result.success) throw new AggregateError(result.logs, 'Build failed')
8439
- // Preserve every result.outputs file under dist, then add the selected companion.
8570
+ // Preserve every result.outputs file under dist, then write the verified companion bytes.
8440
8571
  for (const asset of native.assets) {
8441
- const bytes = readFileSync(asset.sourcePath)
8442
- if (createHash('sha256').update(bytes).digest('hex') !== asset.sha256)
8443
- throw new Error('Native asset changed during build')
8444
8572
  const destination = join('dist', asset.outputPath)
8445
8573
  mkdirSync(dirname(destination), { recursive: true })
8446
- copyFileSync(asset.sourcePath, destination)
8574
+ writeFileSync(destination, asset.bytes)
8447
8575
  }
8448
8576
  ```
8449
8577
 
@@ -8452,23 +8580,67 @@ absolute paths, traversal or overlapping file/directory paths. `entryPath` is fi
8452
8580
  Bun naming templates such as `[dir]` or `[name]`. Companion JS builds require one entry, no splitting,
8453
8581
  and `naming.entry` exactly matching `entryPath`; the plugin rejects a mismatching layout.
8454
8582
  Choose any application layout; the framework does not prescribe an app directory or installer.
8455
- Keep every bundler output, then verify copied addon bytes against the original SHA256.
8456
- Archive the complete output directory with an integrity manifest, unpack to a clean directory,
8457
- verify the same hashes there, and run offline without the build tree or `node_modules`.
8458
- A digest provides integrity, not authenticity: signature and trust policy remain application-owned.
8583
+ Keep every bundler output and write `asset.bytes`; never read the addon from `node_modules` a
8584
+ second time. An installed addon whose size or SHA256 differs from the published manifest refuses
8585
+ with `mismatch` / `NATIVE_ASSET_DIGEST_MISMATCH`, so a file substituted after installation never
8586
+ reaches an artifact. Archive the complete output directory with an integrity manifest that records
8587
+ `asset.sha256`, unpack to a clean directory, verify the same hashes there, and run offline without
8588
+ the build tree or `node_modules`. The published digest ties the bytes to the package Stitchkit
8589
+ built; the package itself is only as trusted as its tarball (the lockfile integrity of the install).
8590
+ Signature and trust policy remain application-owned.
8591
+
8592
+ One JS artifact can run on both Darwin architectures; change only the packaging inputs:
8593
+
8594
+ ```ts
8595
+ const native = createNativePackaging({
8596
+ platform: 'darwin', architecture: ['arm64', 'x64'], delivery: 'companion',
8597
+ entryPath: 'app/native.js',
8598
+ assetPath: { arm64: 'addons/arm.node', x64: 'addons/intel.node' },
8599
+ })
8600
+ ```
8601
+
8602
+ Use the same single plugin and write loop above. A single-target loader deliberately refuses the
8603
+ other architecture; two competing plugins cannot produce a universal loader. The array form
8604
+ requires exactly one distinct output path per declared target. A ready result carries the target
8605
+ array and both verified assets with their published digests; `NativePackagingOptions<true>` /
8606
+ `NativePackagingResult<true>` describe this form when explicitly annotating variables.
8607
+ The loader selects only `process.arch`: a missing or wrong-architecture selected addon refuses,
8608
+ even when the other valid addon is present. Linux runtime imports remain lazy and use Linux
8609
+ process primitives without loading Darwin assets. Qualify the exact same JS bytes on real
8610
+ Darwin arm64 and x64 machines, with both companions preserved through archive delivery.
8611
+ Build-machine architecture and cross-build success cannot establish universal native support.
8459
8612
 
8460
8613
  For a standalone executable use `delivery: 'embedded'` and the same plugin in `Bun.build`
8461
- with `compile: { outfile: ... }`. Bun embeds the selected addon; `assets` identifies its
8462
- original bytes for qualification, not a companion that must be installed beside the executable.
8614
+ with `compile: { outfile: ... }`. Bun embeds the selected addon from the verified bytes the
8615
+ plugin holds, not from a second read of the file; `assets` identifies those bytes and their
8616
+ published digest for qualification, not a companion that must be installed beside the executable.
8463
8617
  Cross-builds must select the requested architecture explicitly; the artifact must run on that target.
8464
8618
  The installed package must contain that target's addon. No automatic fallback/downgrade occurs.
8465
8619
 
8466
- The API returns `unsupported` / `NATIVE_TARGET_UNSUPPORTED` for non-Darwin or unsupported
8467
- architectures, and `missing` / `NATIVE_ASSET_MISSING` for a missing addon. Portable Linux libraries
8468
- use their normal build, without a Darwin plugin; merely importing the build leaf loads no addon.
8469
- Malformed inputs, unsupported metadata versions and other IO errors throw before packaging.
8470
- Runtime missing/corrupt companions preserve the native `unavailable` result and safe stage/code
8471
- from the existing observer; they never certify a dead owner. Preserve the raw cause internally.
8620
+ `platform` is the closed set of platforms with native addons, today `'darwin'`: another name is a
8621
+ type error and a schema refusal that throws, so pass the literal (or narrow `process.platform`
8622
+ with `=== 'darwin'`) rather than a free string. The API returns `unsupported` /
8623
+ `NATIVE_TARGET_UNSUPPORTED` for an unsupported architecture, `missing` / `NATIVE_ASSET_MISSING` for
8624
+ an addon the package does not publish or whose file is gone, and `mismatch` /
8625
+ `NATIVE_ASSET_DIGEST_MISMATCH` for an addon whose bytes differ from the published size or SHA256.
8626
+ A `mismatch` names the first differing `architecture` and carries `expected` (the published
8627
+ `{ size, sha256 }`) and `actual` (the installed file's size and the SHA256 of the bytes read), so
8628
+ a build log can tell a truncated copy from a substituted one:
8629
+
8630
+ ```ts
8631
+ if (native.state === 'mismatch')
8632
+ throw new Error(
8633
+ `${native.architecture} addon: expected ${native.expected.size} bytes ` +
8634
+ `${native.expected.sha256}, found ${native.actual.size} bytes ${native.actual.sha256}`,
8635
+ )
8636
+ ```
8637
+
8638
+ Portable libraries
8639
+ and Linux builds use their normal build, without a Darwin plugin; merely importing the build leaf
8640
+ loads no addon.
8641
+ Malformed inputs, a manifest whose `formatVersion` is not 2 and other IO errors throw before packaging.
8642
+ A missing or corrupt companion at runtime yields the native `unavailable` result with the safe
8643
+ stage and code from the observer; it never certifies a dead owner. Preserve the raw cause internally.
8472
8644
  The plugin does not parse consumer JS or establish a second runtime native implementation.
8473
8645
 
8474
8646
  ## Qualification boundaries
@@ -8487,24 +8659,19 @@ the qualification for native Darwin behavior.
8487
8659
 
8488
8660
  Local filesystem tests do not attest delayed NFS visibility, shared-volume microVM or gVisor
8489
8661
  ownership, actual PGID reuse, power-loss durability or macOS full-sync behavior. Those
8490
- require their actual topology or failure environment. The default ownerless grace remains
8491
- 5000 ms; stricter callers can retain `ownerlessGraceMs: null`. No age-only observation
8492
- proves a partially published owner's process dead.
8662
+ require their actual topology or failure environment. No age-only observation proves a
8663
+ process dead; the lock's owner record exists before its name does.
8664
+
8665
+ A sandbox admission slot is released once a direct command's pipes have closed and its owner has settled.
8666
+ A `COMMAND_CLEANUP` outcome means the death of its group was not proven, so the slot stays
8667
+ occupied, `stop` keeps rejecting with the cleanup error, and the sandbox session is recreated to
8668
+ recover.
8493
8669
 
8494
8670
 
8495
8671
  ==============================================================================
8496
8672
  # Guide: Generic application primitives (docs/guide/primitives.md)
8497
8673
  ==============================================================================
8498
8674
 
8499
- ---
8500
- title: Generic application primitives
8501
- description: Declare lifecycle, access, exact values, audit, delivery and exports without moving application infrastructure into Stitchkit.
8502
- type: guide
8503
- status: active
8504
- created: 2026-09-01
8505
- updated: 2026-10-01 21:03 +07:00
8506
- ---
8507
-
8508
8675
  # Generic application primitives
8509
8676
 
8510
8677
  `stitchkit/primitives` is an optional browser-safe leaf. It declares values and policies; it does
@@ -8607,33 +8774,50 @@ const bytes = canonicalJson({ '2': 'two', '10': 'ten', optional: undefined, nest
8607
8774
  // Native crypto or browser WebCrypto hashes these same UTF-8 bytes.
8608
8775
  ```
8609
8776
 
8610
- `canonicalJson` использует единый serializer исторических digest: object keys в UTF-16 code-unit
8611
- order, включая integer-like keys; array order сохраняется. Optional object members с undefined
8612
- опускаются. Top-level/array undefined, sparse arrays, functions/symbols/BigInt, non-finite numbers,
8613
- negative zero, cycles, user prototypes, accessors, non-enumerable/symbol members отказаны до
8614
- serialization. Приложение валидирует свою schema первым; canonical boundary не заменяет её.
8777
+ `canonicalJson` is the one serializer behind the digests: object keys sort by UTF-16 code unit,
8778
+ integer-like keys included; array order is kept; object members that are `undefined` are omitted.
8779
+ It refuses, before serializing, a top-level or array `undefined`, sparse arrays, functions, symbols,
8780
+ BigInt, non-finite numbers, negative zero, cycles, user prototypes, accessors and non-enumerable or
8781
+ symbol members. Validate your own schema first; the canonical boundary does not replace it.
8782
+
8783
+ Defaults: depth 100 (the maximum), nodes 100 000, UTF-8 bytes 1 MiB. Invalid options throw a Zod
8784
+ validation error. A refused value throws `CanonicalJsonError`, a `TypeError` whose `reason` is a
8785
+ `CanonicalJsonRefusal`; branch on the reason, never on the message:
8615
8786
 
8616
- Defaults: depth 100 (maximum 100), nodes 100 000, UTF-8 bytes 1 MiB. Превышение или не-JSON data —
8617
- TypeError; неверные options — Zod validation error. Это browser-safe leaf без Node/Bun/crypto,
8618
- agent-runtime/testing или optional peers. Native и browser используют один algorithm; hashing
8619
- остаётся platform adapter. Переносите только serializer recursion, сохраняя proof schemas и
8620
- verification boundary. Существующие внутренние historical normalization/digest fixtures не меняются;
8621
- новый public boundary сознательно отказывает неоднозначным values вместо фиктивной совместимости.
8787
+ | `reason` | Meaning |
8788
+ |---|---|
8789
+ | `depth` | The value is nested deeper than `maxDepth`. |
8790
+ | `nodes` | The value has more nodes than `maxNodes`. |
8791
+ | `bytes` | The output is longer than `maxBytes` in UTF-8. |
8792
+ | `cycle` | The value contains a cycle. |
8793
+ | `negative-zero` | The value contains `-0`, which JSON cannot carry. |
8794
+ | `not-json` | The value is not bounded plain JSON: sparse arrays, non-finite numbers, non-plain objects, accessors. |
8795
+
8796
+ `depth`, `nodes` and `bytes` are the caller's limits being exceeded; the other three say the value is
8797
+ not JSON data at all.
8798
+
8799
+ ```ts
8800
+ import { CanonicalJsonError, canonicalJson } from 'stitchkit/primitives'
8801
+
8802
+ try {
8803
+ canonicalJson(payload, { maxBytes: 4096 })
8804
+ } catch (error) {
8805
+ if (error instanceof CanonicalJsonError && error.reason === 'bytes') {
8806
+ // too large: reject the request
8807
+ } else throw error
8808
+ }
8809
+ ```
8810
+
8811
+ The leaf is browser-safe: no Node, Bun or crypto, no `agent-runtime/testing`, no optional peers. Native
8812
+ and browser code run the same algorithm; hashing stays a platform adapter. A consumer that needs the
8813
+ same bytes moves only the serializer recursion and keeps its own proof schemas and verification
8814
+ boundary.
8622
8815
 
8623
8816
 
8624
8817
  ==============================================================================
8625
8818
  # Guide: Application migration recipes (docs/guide/application-migration-recipes.md)
8626
8819
  ==============================================================================
8627
8820
 
8628
- ---
8629
- title: Application migration recipes
8630
- description: Executable patterns for moving database, poller, queue and operational publishing lifecycle into the managed application kernel.
8631
- type: architecture
8632
- status: active
8633
- created: 2026-08-24
8634
- updated: 2026-10-04 13:28 +07:00
8635
- ---
8636
-
8637
8821
  # Application migration recipes
8638
8822
 
8639
8823
  These recipes are the migration companion to the
@@ -8785,10 +8969,10 @@ guard must refuse. Filesystem mutations made inside inbox transactions call
8785
8969
  `assertHeld` immediately before publication or cleanup.
8786
8970
 
8787
8971
  For `createFileStateStore`, remove `staleLockMs` and `retryMs` from configuration.
8788
- `lockTimeoutMs` remains the acquisition budget. Timestamp age no longer revokes
8972
+ `lockTimeoutMs` is the acquisition budget. Timestamp age never revokes
8789
8973
  a live or unknown writer. Stop and verify old writers before recovering a legacy
8790
- pid-only lock; do not invent owner metadata. Historical temporary files are no
8791
- longer deleted by age. See the [durable intake contract](application-kernel.md#durable-process-facts-and-owner-notifications).
8974
+ pid-only lock; do not invent owner metadata. Temporary files are never
8975
+ deleted by age. See the [durable intake contract](application-kernel.md#durable-process-facts-and-owner-notifications).
8792
8976
 
8793
8977
  ## Long-running poller
8794
8978
 
@@ -9036,9 +9220,10 @@ declarations do not require `ai`, the MCP SDK or Bun globals. Node consumers can
9036
9220
  use strict `NodeNext` resolution with `skipLibCheck: false` and ordinary Node
9037
9221
  types; the package keeps its required dependencies separate from optional SDKs.
9038
9222
 
9039
- CLI runtime tools use the shared executable contract: identity, input and output
9040
- schemas, handler, and explicit CLI exposure. SDK presenters belong to the MCP/AI
9041
- definitions in `stitchkit/tools`, which require the corresponding peers. A typed
9223
+ CLI runtime tools use the one runtime-tool declaration: identity, input and output
9224
+ schemas, handler, and explicit CLI exposure. SDK presenters are the adapter's extension
9225
+ of that declaration (the fourth type argument of `RuntimeToolDefinitionWithOutput`, or
9226
+ `defineRuntimeTool` in `stitchkit/tools`), which requires the corresponding peers. A typed
9042
9227
  definition returned by `defineRuntimeTool` or `createRuntimeToolFactory` can be
9043
9228
  passed directly to both CLI and SDK mounts.
9044
9229
 
@@ -9052,7 +9237,7 @@ For a CLI-only managed tool, use the neutral generic construction contract to
9052
9237
  infer the handler from its schemas:
9053
9238
 
9054
9239
  ```ts
9055
- import type { RuntimeToolExecutionWithOutput } from 'stitchkit/cli'
9240
+ import type { RuntimeToolDefinitionWithOutput } from 'stitchkit/cli'
9056
9241
  import { z } from 'zod'
9057
9242
 
9058
9243
  const input = z.object({ text: z.string() })
@@ -9062,12 +9247,14 @@ const measure = {
9062
9247
  identity: { serviceName: 'text', action: 'measure', method: 'POST' },
9063
9248
  transports: ['CLI'], input, output,
9064
9249
  handler: ({ input }) => ({ size: input.text.length }),
9065
- } satisfies RuntimeToolExecutionWithOutput<typeof input, typeof output>
9250
+ } satisfies RuntimeToolDefinitionWithOutput<typeof input, typeof output>
9066
9251
  ```
9067
9252
 
9068
- Pass `measure` to `runtimeTools`. The heterogeneous registration type preserves
9069
- the definition without inventing a common handler input; execute it through
9070
- `createCli` or `createCliInvoker` so validation remains on the canonical path.
9253
+ Pass `measure` to `runtimeTools`. `RuntimeToolDefinition`, the heterogeneous
9254
+ registration type, accepts it unchanged, and types an inline handler written directly
9255
+ in `runtimeTools` by the parsed object (use the construction type above to type the
9256
+ input from the schema). Execute it through `createCli` or `createCliInvoker` so
9257
+ validation remains on the canonical path.
9071
9258
 
9072
9259
  ## Exposure is opt-in
9073
9260
 
@@ -9257,7 +9444,9 @@ the values agree. Duplicates are refused before command execution. Array
9257
9444
  options accumulate across long and short forms; unknown non-dotted passthrough
9258
9445
  options retain their raw-list grammar. Tokens after `--` are literal data.
9259
9446
  Command `--help` and `-h` keep their diagnostic precedence over malformed
9260
- operation arguments; application globals are validated before routing.
9447
+ operation arguments; application globals are validated before routing. Help is a
9448
+ request, not a setting, so repeating it in any spelling (`-h -h`, `-h --help`) asks once
9449
+ and never errors.
9261
9450
 
9262
9451
  Without presentation configuration, positional arguments fill non-boolean
9263
9452
  fields in declaration order, so `myapp generate "a fox"` is
@@ -9484,8 +9673,10 @@ Declared aliases render beside their canonical options, for example
9484
9673
  The argument table and `Application options:` show bounds from their JSON Schema,
9485
9674
  for example `<integer> [>=1, <=50]`, `<string> [length >=1, length <=12]`, and
9486
9675
  `<value…> [items >=2, items <=4]`. Numeric `>` and `<` are exclusive, while `>=`
9487
- and `<=` include the endpoint; a zero limit is displayed too. Unbounded fields
9488
- keep just their type and description. Bounds on an array's items are not bounds
9676
+ and `<=` include the endpoint; a zero limit is displayed too. Only limits the
9677
+ schema declares are shown: the safe-integer range every `z.int()` carries is the
9678
+ type, not a bound, so a bare integer prints `<integer>`, `.min(1)` prints `>=1` and
9679
+ `.positive()` prints `>0`. Unbounded fields keep just their type and description. Bounds on an array's items are not bounds
9489
9680
  on the array itself.
9490
9681
 
9491
9682
  Composed schemas retain their meaning: `any of:` and `one of:` separate
@@ -9713,11 +9904,16 @@ exists to distrust.
9713
9904
 
9714
9905
  `publishCli` is an opt-in publisher for a local application-owned distribution
9715
9906
  directory. It commits every target and its version manifest before atomically
9716
- replacing the public `manifest.json`. Your application selects the CLI source
9717
- identity and admits that snapshot at each phase:
9907
+ replacing the public `manifest.json`. It ships from its own evolving entrypoint,
9908
+ `stitchkit/cli/publish`: release infrastructure follows how distributions are
9909
+ published, so it is not held to the stable promise of `stitchkit/cli`
9910
+ ([ADR 0245](../decisions/0245-cli-publication-is-an-evolving-leaf.md)). The
9911
+ manifest, signatures, installer and updater it writes for stay in
9912
+ `stitchkit/cli`. Your application selects the CLI source identity and admits
9913
+ that snapshot at each phase:
9718
9914
 
9719
9915
  ```ts
9720
- import { publishCli } from 'stitchkit/cli'
9916
+ import { publishCli } from 'stitchkit/cli/publish'
9721
9917
 
9722
9918
  const result = await publishCli({
9723
9919
  storageRoot: '/srv/myapp/cli',
@@ -10042,7 +10238,7 @@ await createCli({ name: 'myapp', version, runtimeTools: discovered, commands: [.
10042
10238
  commands, and a connection without it still contributes nothing to the CLI —
10043
10239
  exposure stays explicit, as it is everywhere else in the framework.
10044
10240
 
10045
- One unconvertible schema no longer takes the connection down with it. The tool
10241
+ One unconvertible schema does not take the connection down. The tool
10046
10242
  is skipped and **named** (`onSkippedTool`, or a stderr line by default), so a
10047
10243
  surface of two hundred tools is not lost to one.
10048
10244
 
@@ -10075,13 +10271,12 @@ credential, body, identity and stack stay with the original in-process cause in
10075
10271
  Unknown throws remain `INTERNAL_SERVER_ERROR`. A timeout is not proof that a
10076
10272
  write did not happen: verify the remote outcome before repeating an operation.
10077
10273
 
10078
- **A remote refusal keeps its code.** A failed `tools/call` used to become a
10079
- one-sentence `Error` with the result discarded, which cost three things at once:
10080
- the code (so `exitCodes` had nothing to map and every remote failure exited `1`),
10081
- the message the operator needed, and the error's own class — a plain `Error` is
10082
- an *unexpected* error to the runner, so it printed a code frame of the framework
10083
- bundle before the JSON failure and was then scrubbed to `INTERNAL_SERVER_ERROR`.
10084
- A structured `{ error, details }` body is now relayed as the contract error it
10274
+ **A remote refusal keeps its code.** A failed `tools/call` must keep three things:
10275
+ the code (so `exitCodes` can map it instead of every remote failure exiting `1`),
10276
+ the message the operator needs, and the error's own class — a plain `Error` is
10277
+ an *unexpected* error to the runner, which would print a code frame of the framework
10278
+ bundle before the JSON failure and scrub it to `INTERNAL_SERVER_ERROR`.
10279
+ A structured `{ error, details }` body is relayed as the contract error it
10085
10280
  is, on every transport; anything else fails as `UPSTREAM_TOOL_ERROR` carrying
10086
10281
  what the server did send. Not `INTERNAL_SERVER_ERROR`, because nothing of ours
10087
10282
  broke.
@@ -11230,15 +11425,6 @@ watch client; silent lost notifications are repaired by hub reconciliation.
11230
11425
  # Guide: Live data (docs/guide/live.md)
11231
11426
  ==============================================================================
11232
11427
 
11233
- ---
11234
- title: Live data — announcements, watched reads, keyspaces and the trust fence
11235
- description: How to declare events beside the operation contract, share one server-side read between every browser asking the same question, keep authoritative memory in front of a durable backend, and refuse a request whose authority you never agreed to answer on.
11236
- type: guide
11237
- status: active
11238
- created: 2026-09-02
11239
- updated: 2026-09-02
11240
- ---
11241
-
11242
11428
  # Live data
11243
11429
 
11244
11430
  Four things an application builds by hand the second time it needs them.
@@ -11650,11 +11836,11 @@ Every refusal answers the same bare 403. A response that said *which* rule
11650
11836
  refused would let a caller learn your trusted list one guess at a time; the
11651
11837
  reason goes to `onRefused` and the log, where you are.
11652
11838
 
11653
- ### One thing that changed
11839
+ ### Group hooks
11654
11840
 
11655
- A route group can no longer declare `onRequest`. It never worked — the framework
11656
- dispatched only the server-level hook — and it could not be made to work without
11657
- matching group prefixes a second time, ahead of the real router. Use the
11841
+ A route group cannot declare `onRequest`: the framework dispatches only the
11842
+ server-level hook, and a group hook would need group prefixes matched a second
11843
+ time, ahead of the real router. Use the
11658
11844
  server-level `hooks.onRequest` to refuse before dispatch, or the group's
11659
11845
  `authorize` to gate once the endpoint is known.
11660
11846
 
@@ -12329,7 +12515,7 @@ whether the code declares `details`:
12329
12515
 
12330
12516
  The model-facing envelope is `{ error, retryable, details?, _hint? }`; for a code with no
12331
12517
  details schema the framework fills `details` with `{ message }`, so declaring a
12332
- message changes what the model reads there — it used to be the code itself. Put
12518
+ message changes what the model reads there; without one it is the code itself. Put
12333
12519
  anything the model must reliably read in `details` or `hint`, not in `message`.
12334
12520
 
12335
12521
  With no `details` schema, the options object forbids `details`. A required
@@ -12842,6 +13028,32 @@ Without an `observability` the work still runs inside a context, so trace ids,
12842
13028
  decision from having a context. `traceparent` continues the trace of whatever
12843
13029
  scheduled the work, instead of starting an unrelated one.
12844
13030
 
13031
+ ### CPU is not attributed to work
13032
+
13033
+ Request rows, `runUnitOfWork` rows and schedule status carry durations and
13034
+ counts, never CPU. One JavaScript thread interleaves every operation, so a
13035
+ `process.cpuUsage()` delta across an operation's start and end measures
13036
+ everything the process ran in that time — a long wait reads as the CPU of the
13037
+ timers that fired during it. The correct basis is the synchronous segment, and
13038
+ Bun gives no callback when a continuation is entered or left
13039
+ ([ADR 0244](../decisions/0244-cpu-is-not-attributed-to-operations.md)).
13040
+
13041
+ Process CPU per window is the number you can observe:
13042
+
13043
+ ```ts
13044
+ let last = process.cpuUsage()
13045
+ setInterval(() => {
13046
+ const now = process.cpuUsage() // cumulative { user, system } in µs
13047
+ logger.info('cpu', { cpuUserUs: now.user - last.user, cpuSystemUs: now.system - last.system })
13048
+ last = now
13049
+ }, 60_000)
13050
+ ```
13051
+
13052
+ If you build attribution yourself: credit an operation only the CPU of the
13053
+ synchronous segments it ran, never its waiting time; put everything outside those
13054
+ segments into `unattributed`; never credit a window to the one operation in
13055
+ flight — that operation is usually the one waiting.
13056
+
12845
13057
  ### Write the calls that changed something
12846
13058
 
12847
13059
  `auditChanges` is the filter most projects end up writing, shipped so they do not
@@ -12916,8 +13128,8 @@ needed:
12916
13128
  createServer({ services, logging, observability: observability.request })
12917
13129
  ```
12918
13130
 
12919
- `wrapInRequestContext` remains available for a custom fetch pipeline that does
12920
- not use `createHandler`; it is no longer part of built-in HTTP audit wiring:
13131
+ `wrapInRequestContext` serves a custom fetch pipeline that does not use
13132
+ `createHandler`; built-in HTTP audit wiring does not use it:
12921
13133
 
12922
13134
  ```ts
12923
13135
  Bun.serve({ fetch: wrapInRequestContext(customFetch) })
@@ -13254,9 +13466,9 @@ which the built-in **tool** row does not read: a tool event takes
13254
13466
  `errorCode` / `errorMessage` / `errorDetail` from the `ToolResult`, and only
13255
13467
  identity and `dimensions` from the context. Calling it in `onToolError` would
13256
13468
  leave the tool row exactly as scrubbed as before. It is right for the **HTTP**
13257
- path, where the request *is* the record. (Since ADR 0045 a tool call runs in its
13258
- own context, so it can no longer write into the enclosing `/mcp` request's row
13259
- either — the call is simply not where that helper belongs.)
13469
+ path, where the request *is* the record. (A tool call runs in its own context, ADR 0045,
13470
+ so it cannot write into the enclosing `/mcp` request's row either — the call is
13471
+ simply not where that helper belongs.)
13260
13472
 
13261
13473
  ### One row that names the cause
13262
13474
 
@@ -14107,15 +14319,6 @@ per-transport tenant plumbing.
14107
14319
  # Guide: Frontend integrations (docs/guide/frontend-integrations.md)
14108
14320
  ==============================================================================
14109
14321
 
14110
- ---
14111
- title: Frontend integrations
14112
- description: Compose Stitchkit with Next.js, React Router or a separate Vite development server
14113
- type: architecture
14114
- status: active
14115
- created: 2026-08-08
14116
- updated: 2026-08-08
14117
- ---
14118
-
14119
14322
  # Frontend integrations
14120
14323
 
14121
14324
  The official `bun create stitchkit` application uses Next.js with a separate
@@ -14177,15 +14380,6 @@ shared contract remains the only transport schema source.
14177
14380
  # Guide: React Query policy (docs/guide/react.md)
14178
14381
  ==============================================================================
14179
14382
 
14180
- ---
14181
- title: React Query policy
14182
- description: One request-local server client, one browser singleton and explicit retry rules for Stitchkit API errors
14183
- type: guide
14184
- status: active
14185
- created: 2026-09-06
14186
- updated: 2026-09-06
14187
- ---
14188
-
14189
14383
  # React Query policy
14190
14384
 
14191
14385
  `stitchkit/react` contains policy adapters for TanStack Query. It does not own
@@ -14240,15 +14434,6 @@ endpoint or cookie vocabulary.
14240
14434
  # Guide: Keyboard layers and list zones (docs/guide/keyboard.md)
14241
14435
  ==============================================================================
14242
14436
 
14243
- ---
14244
- title: Keyboard layers and list zones
14245
- description: One listener decides which part of a screen gets a key; lists move real focus with the arrows
14246
- type: guide
14247
- status: active
14248
- created: 2026-09-29
14249
- updated: 2026-09-29
14250
- ---
14251
-
14252
14437
  # Keyboard layers and list zones
14253
14438
 
14254
14439
  `stitchkit/react/keyboard` answers one question for a React screen: **who gets
@@ -15112,8 +15297,8 @@ the press was forged (a client can send any bytes) and goes to `onStale`.
15112
15297
 
15113
15298
  A press on a message the record does not know — an older menu higher up — is
15114
15299
  answered in place: a one-message view takes that message over, a longer view is
15115
- sent below it and the pressed message is deleted. Either way the previously
15116
- recorded view is removed: one live menu per chat.
15300
+ sent below it and the pressed message is deleted. Either way the recorded
15301
+ view is removed: one live menu per chat.
15117
15302
 
15118
15303
  For a message the screens do not own — a notification from the backend —
15119
15304
  `screens.button(link('🔑 Refresh token', refreshToken, { botId }))` makes a
@@ -15227,15 +15412,6 @@ an incoming message to HTML, and a history stack.
15227
15412
  # Guide: GeoIP (docs/guide/geo.md)
15228
15413
  ==============================================================================
15229
15414
 
15230
- ---
15231
- title: GeoIP
15232
- description: A server-only managed GeoIP reader with three observable states and last-known-good generation reloads.
15233
- type: guide
15234
- status: active
15235
- created: 2026-09-06
15236
- updated: 2026-09-06
15237
- ---
15238
-
15239
15415
  # GeoIP
15240
15416
 
15241
15417
  `stitchkit/geo` is a server-only, evolving entrypoint for reading local GeoIP
@@ -15319,6 +15495,413 @@ analytics policy belong to the application.
15319
15495
 
15320
15496
  # Upgrading stitchkit
15321
15497
 
15498
+ ## Released migration: 0.105.0
15499
+
15500
+ ### CLI publication has its own entrypoint
15501
+
15502
+ **Who must act:** code that imports `publishCli`, `CliPublicationOptions`, `CliPublicationPhase` or
15503
+ `CliPublicationResult` from `stitchkit/cli`. Everything else in `stitchkit/cli` — `createCli`, the
15504
+ manifest schemas, signatures, `renderCliInstaller` and the updater — is unchanged.
15505
+
15506
+ `publishCli` is release infrastructure, so it now ships from the evolving entrypoint
15507
+ `stitchkit/cli/publish` instead of the stable `stitchkit/cli`
15508
+ ([ADR 0245](../decisions/0245-cli-publication-is-an-evolving-leaf.md)). Its options, result and
15509
+ behaviour are the same.
15510
+
15511
+ 1. Move the publication names to the new specifier; keep every other CLI import where it is:
15512
+
15513
+ ```ts
15514
+ // before
15515
+ import { publishCli, type CliPublicationOptions, verifyCliManifest } from 'stitchkit/cli'
15516
+ // after
15517
+ import { verifyCliManifest } from 'stitchkit/cli'
15518
+ import { publishCli, type CliPublicationOptions } from 'stitchkit/cli/publish'
15519
+ ```
15520
+
15521
+ Search pattern: `\b(publishCli|CliPublication(Options|Phase|Result))\b` in files that import
15522
+ from `'stitchkit/cli'`.
15523
+ 2. Type-check: an import left on `stitchkit/cli` fails with "has no exported member".
15524
+ 3. `stitchkit/cli/publish` is evolving: read its breaking entries in each minor you upgrade across.
15525
+
15526
+ ### a command stops by its `stop` policy
15527
+
15528
+ **Who must act:** callers of `runNativeCommand` (`stitchkit/process`) that pass `killGraceMs`.
15529
+ Callers that omit it need no change: the default stop is the same TERM to the group, 100 ms, KILL.
15530
+
15531
+ 1. Replace `killGraceMs: N` with a group stop of the same grace:
15532
+
15533
+ ```ts
15534
+ // before
15535
+ await runNativeCommand({ executable, args, signal, killGraceMs: 500 })
15536
+ // after
15537
+ await runNativeCommand({ executable, args, signal, stop: { target: 'group', graceMs: 500 } })
15538
+ ```
15539
+
15540
+ Search pattern: `killGraceMs:\s*(\d[\d_]*)` → `stop: { target: 'group', graceMs: $1 }`.
15541
+ 2. If your own wrapper signals only the leader and waits a long grace (cooperative cancellation),
15542
+ replace it with `stop: { target: 'leader', signal: 'SIGINT', graceMs }` (up to one hour). Pass
15543
+ `killOn` with your shutdown signal so an application that stops sooner kills the group first.
15544
+ 3. The result of a stopped command rejects once the group is gone, up to `graceMs` plus
15545
+ `cleanupTimeoutMs` after the abort; size your caller deadlines on that sum.
15546
+
15547
+ ### a command stops what its leader left
15548
+
15549
+ **Who must act:** callers of `runNativeCommand` (`stitchkit/process`) whose command starts a helper
15550
+ in its own process group that must keep running after the command returns. A daemon that detaches
15551
+ with `setsid` (as `daemon(3)` does) is not in the group and needs
15552
+ no change.
15553
+
15554
+ `descendants` now defaults to `'terminate-after-leader'`
15555
+ ([ADR 0246](../decisions/0246-a-command-stops-its-descendants-by-default.md)): when the leader
15556
+ exits, the members it left in the group are stopped by the command's `stop` policy before the
15557
+ pipes drain.
15558
+
15559
+ 1. Find the commands that start a background helper on purpose (a `&` in a shell script, a spawned
15560
+ child the leader does not wait for) and declare `'leave'` on them:
15561
+
15562
+ ```ts
15563
+ // before
15564
+ await runNativeCommand({ executable, args, timeoutMs })
15565
+ // after
15566
+ await runNativeCommand({ executable, args, timeoutMs, descendants: 'leave' })
15567
+ ```
15568
+
15569
+ 2. Remove an explicit `descendants: 'terminate-after-leader'`, or keep it: it is now the default.
15570
+ 3. A command whose helper inherited stdout or stderr used to wait for that helper until its
15571
+ deadline. It now completes when the leader exits; drop a workaround that killed the helper from
15572
+ `onLeaderSettled`.
15573
+
15574
+ ### native packaging verifies published digests
15575
+
15576
+ **Who must act:** build scripts that call `createNativePackaging` (`stitchkit/files/packaging`) and
15577
+ read `asset.sourcePath`, hash the addon themselves, switch over the refusal `state`/`code`, pass a
15578
+ `platform` other than the literal `'darwin'`, or parse `node_modules/stitchkit/native-assets.json`.
15579
+
15580
+ The package now publishes the `size` and SHA256 of each Darwin addon, computed when Stitchkit is
15581
+ built, in `native-assets.json` at `formatVersion: 2`. `createNativePackaging` reads each selected
15582
+ addon once, compares it with that digest and returns the verified bytes. An installed addon that
15583
+ differs refuses with `NATIVE_ASSET_DIGEST_MISMATCH`.
15584
+
15585
+ 1. Replace every read of `asset.sourcePath` with `asset.bytes`, and delete your own re-hash of the
15586
+ addon: the result's `sha256` is the published digest and `bytes` already match it.
15587
+
15588
+ ```ts
15589
+ // before
15590
+ const bytes = readFileSync(asset.sourcePath)
15591
+ if (createHash('sha256').update(bytes).digest('hex') !== asset.sha256) throw …
15592
+ copyFileSync(asset.sourcePath, destination)
15593
+ // after
15594
+ writeFileSync(destination, asset.bytes)
15595
+ ```
15596
+
15597
+ 2. Handle the third refusal. A `switch` over `result.state` gains `'mismatch'`
15598
+ (`code: 'NATIVE_ASSET_DIGEST_MISMATCH'`); treat it as a failed build, never as a missing addon.
15599
+ It carries the differing `architecture` with `expected` (published) and `actual` (installed)
15600
+ `{ size, sha256 }`; log both pairs.
15601
+ 3. Pass the target platform as the literal `'darwin'`. `platform` is a closed enum, so a free
15602
+ string (a target name read from configuration, `process.platform`) no longer type-checks, and
15603
+ another platform throws instead of returning `unsupported`. Skip packaging for non-Darwin
15604
+ targets before the call:
15605
+
15606
+ ```ts
15607
+ // before
15608
+ const native = createNativePackaging({ platform: target.platform, architecture, … })
15609
+ if (native.state === 'unsupported') return
15610
+ // after
15611
+ if (target.platform !== 'darwin') return
15612
+ const native = createNativePackaging({ platform: target.platform, architecture, … })
15613
+ ```
15614
+
15615
+ `unsupported` now means only an architecture the package has no addon for.
15616
+ 4. Call `createNativePackaging` once per build and pass the result (its `assets` and `plugin`)
15617
+ where it is needed. Each call reads and hashes the selected addons again.
15618
+ 5. If you read `native-assets.json` directly, read `assets.<arch>.path`, `.size` and `.sha256`
15619
+ instead of a bare path per architecture; an architecture with no entry is not published.
15620
+
15621
+ ### exclusive locks publish their owner first
15622
+
15623
+ **Who must act:** callers of `withExclusiveLock` (`stitchkit/files`) that rely on a lock file with no
15624
+ readable owner being taken by age, and anything that puts lock files on a filesystem without hard
15625
+ links. Every other caller needs no change.
15626
+
15627
+ A lock is now written to a private `.lock-<id>.tmp` file in the lock's directory, then hard-linked to
15628
+ the lock name. The name never exists without a complete owner record, so `ownerlessGraceMs` defaults
15629
+ to `null` instead of 5 000 ms: a lock file with no readable owner is never taken because of its age.
15630
+
15631
+ 1. If you depended on age-based reclaim of an empty lock file left by an older writer, opt in:
15632
+
15633
+ ```ts
15634
+ // before: an empty lock older than 5 s was taken by default
15635
+ await withExclusiveLock(path, run)
15636
+ // after: say so; only legacy empty files need it
15637
+ await withExclusiveLock(path, run, { ownerlessGraceMs: 5_000 })
15638
+ ```
15639
+
15640
+ 2. Keep lock directories on a filesystem that supports hard links (local ext4, xfs, APFS and the
15641
+ like). `link` failing with `EPERM` or `ENOTSUP` fails the acquisition.
15642
+ 3. Expect a `.lock-*.tmp` sibling only after a holder was killed between writing its record and
15643
+ publishing it. It is not a lock; the next reclaim in that directory removes the ones whose owner
15644
+ is gone on this machine. Exclude the pattern from directory-size caps or backups if you have any.
15645
+
15646
+ ### one runtime-tool declaration
15647
+
15648
+ **Who must act:** code that names `RuntimeToolExecution`, `RuntimeToolExecutionWithOutput`,
15649
+ `RuntimeMcpToolDefinition`, `RuntimeMcpToolDefinitionWithOutput` or `ToolSurfaceDefinition`; code
15650
+ that annotates a definition carrying `present` as `RuntimeToolDefinitionWithOutput<I, O>` from
15651
+ `stitchkit/tools`; imports of `stitchkit/agent-runtime/harness-tools`; code that compares agent
15652
+ control error codes; and callers that send a JSON array as text to a string-or-array tool field.
15653
+ Definitions returned by `defineRuntimeTool` / `createRuntimeToolFactory`, native CLI `commands` and
15654
+ contract tools need no change.
15655
+
15656
+ A runtime tool now has one declaration, `RuntimeToolDefinitionWithOutput` /
15657
+ `RuntimeToolDefinitionWithoutOutput`, exported with the same name from `stitchkit/cli`,
15658
+ `stitchkit/tools` and `stitchkit/tools/mcp`. It carries no SDK type: the presenters are the adapter's
15659
+ extension, named as the fourth type argument. `RuntimeToolDefinition` is the one registration type.
15660
+
15661
+ 1. Replace each removed name.
15662
+
15663
+ | before | after |
15664
+ |---|---|
15665
+ | `RuntimeToolExecutionWithOutput<I, O>` (`stitchkit/cli`) | `RuntimeToolDefinitionWithOutput<I, O>` |
15666
+ | `RuntimeToolExecution` (`stitchkit/cli`) | `RuntimeToolDefinition` |
15667
+ | `RuntimeMcpToolDefinitionWithOutput<I, O>` (`stitchkit/tools/mcp`) | `RuntimeToolDefinitionWithOutput<I, O, undefined, RuntimeMcpToolPresenters<z.output<O>>>` |
15668
+ | `RuntimeMcpToolDefinition` (`stitchkit/tools/mcp`) | `RuntimeToolDefinition` |
15669
+ | `ToolSurfaceDefinition` (`stitchkit/tools`) | `ToolSurfaceProjection<RuntimeToolDefinition>` |
15670
+
15671
+ 2. In `stitchkit/tools`, a type that names a definition with presenters passes the presenter type:
15672
+
15673
+ ```ts
15674
+ // before
15675
+ const tool: RuntimeToolDefinitionWithOutput<typeof input, typeof output> = defineRuntimeTool({ …, present })
15676
+ // after: let defineRuntimeTool infer it, or name the presenters
15677
+ const tool = defineRuntimeTool({ …, present })
15678
+ const named: RuntimeToolDefinitionWithOutput<typeof input, typeof output, undefined, RuntimeToolPresenters<z.output<typeof output>>> = tool
15679
+ ```
15680
+
15681
+ 3. An inline handler in `runtimeTools: [...]` needs no wrapper: it is typed by the parsed object.
15682
+ To type `input` from the schema, construct with `defineRuntimeTool` (or
15683
+ `satisfies RuntimeToolDefinitionWithOutput<typeof input, typeof output>`) before registering.
15684
+
15685
+ 4. Replace `createAgentHarnessTools` (`stitchkit/agent-runtime/harness-tools`) with the call it made:
15686
+
15687
+ ```ts
15688
+ // before
15689
+ tools: createAgentHarnessTools(({ context }) => ({ services: [svc], context, lifecycle: auth }))
15690
+ // after
15691
+ import { composeToolLifecycle, mountAgent } from 'stitchkit/tools'
15692
+ tools: ({ context, toolFenceLifecycle }) =>
15693
+ mountAgent([svc], { context, lifecycle: composeToolLifecycle(auth, toolFenceLifecycle) })
15694
+ ```
15695
+
15696
+ 5. Agent control codes: `ACCESS_DENIED` is `FORBIDDEN`, and `CONNECTION_CLOSED` is returned for a closed
15697
+ connection. `AgentControlErrorCode` lists every code. `authorize` now runs once per request; after you
15698
+ withdraw someone's access, call `binding.revoke(conversationId, matches?)` so an open stream is
15699
+ re-authorized and ends with an `access-denied` delivery.
15700
+
15701
+ 6. A string-or-array field that receives `'["a","b"]'` now fails with `VALIDATION_ERROR` ("`paths` is a
15702
+ list written as text"). Send the array itself. A string that is not a list the array member accepts
15703
+ (`[preview].png`) is still one plain string.
15704
+
15705
+ ### flattened unions keep declaration order
15706
+
15707
+ **Who must act:** consumers that use `flattenUnionInput: true` and keep snapshot tests, golden
15708
+ files or provider prompt caches of flattened tool schemas.
15709
+
15710
+ A flattened union now lists its properties, enum values and branch labels in the order they first
15711
+ appear across the variants, not alphabetically. Declaration order often encodes priority, so the
15712
+ model now reads the order you wrote. Alternatives of different kinds (`anyOf`) keep their
15713
+ canonical order, independent of variant order.
15714
+
15715
+ 1. Regenerate every snapshot or golden file of a flattened tool listing (`buildToolManifest`,
15716
+ `listToolNames`, MCP `tools/list`, Agent tool schemas) and review the diff: only ordering moves.
15717
+ 2. Expect one provider prompt-cache miss per flattened tool after the upgrade.
15718
+ 3. To keep an order you prefer, declare the variants and fields in that order.
15719
+
15720
+ ### connection limits are validated
15721
+
15722
+ **Who must act:** callers that pass `timeoutMs` or `maxResponseBytes` to a connection and relied
15723
+ on an invalid value silently becoming the default.
15724
+
15725
+ `stitchkit/tools/connections` throws a `RangeError` when `timeoutMs` is not a positive integer
15726
+ within timer range (at most 2 147 483 647) or `maxResponseBytes` is not a positive safe integer.
15727
+ Before 0.104.0 a value of 0, a negative number, a fraction or an out-of-range number fell back to
15728
+ the default without a message.
15729
+
15730
+ ```ts
15731
+ // Before: { timeoutMs: 0 } meant "use the default" by accident.
15732
+ // After: omit the option to take the default, or pass a real limit.
15733
+ { timeoutMs: 30_000 }
15734
+ ```
15735
+
15736
+ ### one canonical JSON serializer
15737
+
15738
+ **Who must act:** tests that call `serializeSurfaceValue` from `stitchkit/testing`, and code that
15739
+ puts a value nested deeper than 100 levels into agent store events, archives or projections
15740
+ through `canonicalAgentJson`.
15741
+
15742
+ `canonicalJson` from `stitchkit/primitives` is the package's one canonical serializer. It
15743
+ admits dense arrays, plain objects, strings, booleans, `null` and finite numbers; an `undefined`
15744
+ object member is omitted. It refuses everything else with a `CanonicalJsonError`, a `TypeError`
15745
+ whose `reason` says why: `'depth'`, `'nodes'` and `'bytes'` are the limits you set, while
15746
+ `'cycle'`, `'negative-zero'` and `'not-json'` say the value is not JSON data.
15747
+
15748
+ `serializeSurfaceValue` is removed. It produced the same bytes without limits:
15749
+
15750
+ ```ts
15751
+ // Before
15752
+ import { serializeSurfaceValue } from 'stitchkit/testing'
15753
+ const text = serializeSurfaceValue(manifest)
15754
+ // After: the limits default to 100 levels, 100 000 nodes and 1 MiB; raise them for a large manifest
15755
+ import { canonicalJson } from 'stitchkit/primitives'
15756
+ const text = canonicalJson(manifest, { maxBytes: 16 * 1024 * 1024 })
15757
+ ```
15758
+
15759
+ `canonicalAgentJson` in `stitchkit/agent-runtime` is `canonicalJson` with the node and byte
15760
+ limits at their maximum. A value both accepted has the same bytes as before, so stored digests
15761
+ and archives still match: like `z.json()` before it, the store hashes `-0` as `0` and drops an
15762
+ own `__proto__` member (the public `canonicalJson` refuses `-0` and keeps `__proto__`). What
15763
+ changed is the refusal table: an `undefined` object member used to fail and is now omitted, and
15764
+ a value nested deeper than 100 levels is now refused.
15765
+
15766
+ ```ts
15767
+ // Before: canonicalAgentJson({ a: undefined }) threw a ZodError
15768
+ // After: canonicalAgentJson({ a: undefined }) === '{}'
15769
+ // canonicalAgentJson(nestedBeyond100Levels) throws CanonicalJsonError (reason 'depth')
15770
+ ```
15771
+
15772
+ ### directory inbox entries
15773
+
15774
+ **Who must act:** producers that drop hard links into an inbox directory, and callers that pass
15775
+ `accept` the output of a transforming schema.
15776
+
15777
+ A file dropped into the inbox directory with more than one link is set aside as `invalid`
15778
+ (detail `managed file has multiple links`). Another name can still rewrite such a file after it
15779
+ was validated, so it is never trusted. Write each entry as its own regular file: a temporary
15780
+ name in the same directory, then a rename onto `<name>.json`.
15781
+
15782
+ `accept` takes the schema's input type, stores that input as given and parses it with the
15783
+ schema on delivery. A schema without a transform has the same input and output type and needs
15784
+ no change. With a transform, pass the input:
15785
+
15786
+ ```ts
15787
+ const schema = z.object({ amount: z.string().transform(Number) })
15788
+ // Before: accept({ source, key, entry: { amount: 42 } }) // typed as the output; `z.string()` refused it
15789
+ // After: accept({ source, key, entry: { amount: '42' } }) // delivered as { amount: 42 }
15790
+ ```
15791
+
15792
+ A name that is already in `rejected/` no longer stops the pass: identical bytes are the entry
15793
+ already set aside, different bytes are kept as `rejected/<name>.<16 hex digits>`.
15794
+
15795
+ ### journal startup scan
15796
+
15797
+ **Who must act:** operators that read `status.recovery` right after `createDiagnosticJournal` to
15798
+ find damaged rows in the middle of a file.
15799
+
15800
+ Opening a journal reads only the last line of the active file and of every retained
15801
+ generation (`startupScan: 'tails'`, the default). A crash tears the end of a file, so a torn tail
15802
+ is still reported and still rotated away, and opening no longer parses and validates every
15803
+ line of every generation. It still reads each file once in large blocks to count lines, so the time
15804
+ grows with the bytes retained, at read speed. Pass `startupScan: 'full'` to read and validate every line as before:
15805
+
15806
+ ```ts
15807
+ // Before: every open parsed every line of every generation.
15808
+ // After: the same full scan, on request.
15809
+ const journal = await createDiagnosticJournal({ path, eventSchema, limits, startupScan: 'full' })
15810
+ ```
15811
+
15812
+ ### journal startup quarantine
15813
+
15814
+ **Who must act:** operators of a journal that is a source of truth and must not start over
15815
+ damaged retained files; code that constructs `DiagnosticJournalRecoveryError` or reads its
15816
+ `recovery`; protocols that embed `DiagnosticJournalRecoveryStatusSchema`, and their readers.
15817
+ A diagnostic journal needs no change.
15818
+
15819
+ Opening a journal no longer refuses to start over a torn active file with `maxFiles: 1`, a
15820
+ generation name that is not a regular file or a retained file it cannot read. It renames the file
15821
+ to `<file>.quarantined-<epoch>` beside the journal, starts, and lists the move in
15822
+ `getStatus().recovery.quarantined`. Quarantined files are outside retention and never deleted;
15823
+ every later open lists them again, without a `reason`, until you remove them.
15824
+
15825
+ 1. A journal whose damage must stop the process declares the old behaviour:
15826
+
15827
+ ```ts
15828
+ // Before: refusing was the only behaviour.
15829
+ await createDiagnosticJournal({ path, eventSchema, limits })
15830
+ // After:
15831
+ await createDiagnosticJournal({ path, eventSchema, limits, onStartupRefusal: 'fail' })
15832
+ ```
15833
+
15834
+ 2. Branch on the refusal's `reason` instead of its message or error class. Every startup refusal
15835
+ about retained evidence is a `DiagnosticJournalRecoveryError`, including the former plain
15836
+ `Error` for a non-file generation and the raw `EACCES` for an unreadable one (now in `cause`):
15837
+
15838
+ ```ts
15839
+ // Before: catch (error) { if (error instanceof DiagnosticJournalRecoveryError) use(error.recovery) }
15840
+ // After:
15841
+ catch (error) {
15842
+ if (error instanceof DiagnosticJournalRecoveryError && error.reason === 'torn-without-retention-slot') use(error.recovery)
15843
+ }
15844
+ ```
15845
+
15846
+ `recovery` is present only for `torn-without-retention-slot`. The constructor takes one
15847
+ options object: `new DiagnosticJournalRecoveryError({ reason, file, recovery?, cause? })`.
15848
+
15849
+ 3. A protocol that embeds `DiagnosticJournalRecoveryStatusSchema` carries the new optional
15850
+ `quarantined` array (at most 32 entries, `reason` optional) and `quarantinedUnlisted` count.
15851
+ The schema is `.strict()`: upgrade every reader that parses it with its own copy before a
15852
+ writer on this version sends a status with either field, or bump your protocol version.
15853
+
15854
+ ### error constructors take named options
15855
+
15856
+ **Who must act:** code that constructs an `AppError` or an `ApiError` with anything beyond its
15857
+ code, and classes that extend `AppError` and call `super(code, message, status, …)`. Code that
15858
+ only throws the `notFound`, `badRequest`, `unauthorized`, `forbidden`, `conflict` and
15859
+ `rateLimited` helpers, or only reads `code`, `status`, `details`, `hint`, `traceId`,
15860
+ `retryable` and `cause` from a caught error, needs no change.
15861
+
15862
+ `AppError` (`stitchkit/contract`, `stitchkit/server`, `stitchkit/node`) and `ApiError`
15863
+ (`stitchkit`) keep the code as the one positional argument. Every other value is a named
15864
+ option, so a call site reads without the signature open. `new AppError(code)` and
15865
+ `new ApiError(code)` are unchanged. The compiler finds every old call: a string or number
15866
+ in the second position is a type error.
15867
+
15868
+ 1. `AppError`: move `message`, `status`, `details`, `hint`, `traceId` and `retryable` into one
15869
+ object after the code. `status` defaults to 500 and `message` to the code.
15870
+
15871
+ ```ts
15872
+ // before
15873
+ throw new AppError('CONFLICT', 'Selection is stale', 409, { revision: 7 })
15874
+ throw new AppError('BUSY', 'Try later', 503, undefined, undefined, undefined, true)
15875
+ // after
15876
+ throw new AppError('CONFLICT', { message: 'Selection is stale', status: 409, details: { revision: 7 } })
15877
+ throw new AppError('BUSY', { message: 'Try later', status: 503, retryable: true })
15878
+ ```
15879
+
15880
+ 2. A subclass passes the same object to `super`:
15881
+
15882
+ ```ts
15883
+ // before
15884
+ super('FEATURE_LOCKED', 'locked', 403)
15885
+ // after
15886
+ super('FEATURE_LOCKED', { message: 'locked', status: 403 })
15887
+ ```
15888
+
15889
+ 3. `ApiError`: the old order was `code, status, details, message, hint, traceId, errorOptions,
15890
+ retryable`. Name each value; `status` defaults to 0 and the former `ErrorOptions` argument
15891
+ is now the `cause` option.
15892
+
15893
+ ```ts
15894
+ // before
15895
+ new ApiError('RATE_LIMITED', 429, undefined, 'slow down', 'wait', 'trace-1', { cause }, true)
15896
+ new ApiError('VALIDATION_ERROR', 0, { issues }, 'bad input')
15897
+ // after
15898
+ new ApiError('RATE_LIMITED', { status: 429, message: 'slow down', hint: 'wait', traceId: 'trace-1', retryable: true, cause })
15899
+ new ApiError('VALIDATION_ERROR', { status: 0, details: { issues }, message: 'bad input' })
15900
+ ```
15901
+
15902
+ 4. Both classes accept the standard `cause`, which `error.cause` returns; omit the option to
15903
+ leave the property absent.
15904
+
15322
15905
  ## Released migration: 0.104.0
15323
15906
 
15324
15907
  JS bundles carrying Darwin native capabilities also need the
@@ -15444,10 +16027,20 @@ that work actually completes.
15444
16027
 
15445
16028
  ### Darwin native assets in JS distributions
15446
16029
 
15447
- **Who must act:** Bun JS builds using `--outfile`, copying only `outputs[0]`, custom
15448
- single-file/archive packagers and installers delivering native process/files capabilities.
15449
- First affected publication: **0.103.13**. It shipped as a patch; its changed delivery graph
15450
- requires this migration despite unchanged API signatures.
16030
+ **Who must act:** Bun builds that import `stitchkit/server`, `stitchkit/files` or
16031
+ `stitchkit/process` (Linux-only server apps included) and use `--outfile`, copy only
16032
+ `outputs[0]`, or package a single file; custom single-file/archive packagers and installers
16033
+ delivering native process/files capabilities.
16034
+ First affected publication: **0.103.13**, through **0.104.2**. It shipped as a patch; its
16035
+ changed delivery graph requires this migration despite unchanged API signatures.
16036
+
16037
+ In those versions every Bun bundle that reaches the lock or process-identity code,
16038
+ `stitchkit/server` included, carries both Darwin `.node` addons as extra outputs on every
16039
+ operating system. `bun build --outfile` then fails with
16040
+ `cannot write multiple output files without an output directory`, and `--outdir` writes the
16041
+ addons next to a Linux artifact. Releases after 0.104.2 resolve the default loader at run time,
16042
+ so these bundles are one JS file with no `.node` output again. The static loader exists only
16043
+ in a build that uses the `stitchkit/files/packaging` plugin.
15451
16044
 
15452
16045
  ```sh
15453
16046
  # Before: one JS file was assumed to be the entire distribution.
@@ -15464,12 +16057,39 @@ Custom output layout uses the evolving build-only packaging contract in the
15464
16057
 
15465
16058
  Three deliveries have different rules: an ordinary npm install retains the package's
15466
16059
  native assets; a JS distribution must ship its complete output graph; a Bun compiled
15467
- executable embeds the matching addon and may still use `--outfile`.
16060
+ executable embeds the matching addon and may still use `--outfile`. In 0.103.13–0.104.2 a plain
16061
+ `bun build` carries the addon without a plugin; later releases carry it only when the build
16062
+ uses `createNativePackaging` (`delivery: 'companion'` for JS, `'embedded'` for a compiled
16063
+ executable).
15468
16064
  The [native IO guide](native-io.md#libraries-with-a-zod-only-runtime) is the canonical recipe.
15469
16065
  Check the resulting artifact offline outside the build tree and `node_modules`, including
15470
16066
  process identity, live-owner lock refusal, dead-owner recovery and contained IO.
15471
16067
  A missing or corrupt addon remains `unavailable`, never evidence that an owner died.
15472
16068
 
16069
+ ### CLI: repeated options are refused
16070
+
16071
+ **Who must act:** wrappers, aliases and scripts that append `--json`, `--profile X` or an
16072
+ application global to arguments the user may already have given, and anything that passes the
16073
+ same global twice.
16074
+ First affected publication: **0.103.13**. It shipped as a patch; the change in grammar needs
16075
+ this migration.
16076
+
16077
+ A repeated option that is not an array field is an argument error:
16078
+ `--json --json` and `--profile a --profile b` fail with `--json was passed 2 times`. Before
16079
+ 0.103.13 the last value won, for framework globals, application globals and repeated
16080
+ `kind: 'other'` fields alike. Array fields, and untyped passthrough fields that keep their
16081
+ raw-list grammar, still repeat.
16082
+
16083
+ ```sh
16084
+ # Before: a wrapper appended its default and the last value won.
16085
+ mytool deploy --profile staging "$@" --profile production
16086
+ # After: build the argument list once, so each option appears once.
16087
+ mytool deploy --profile "${PROFILE:-production}" "$@"
16088
+ ```
16089
+
16090
+ Dotted leaves such as `--filter.name` follow the same rule as their parent field. Declare a
16091
+ field as an array when repetition is meant.
16092
+
15473
16093
  ## Released migration: 0.103.0
15474
16094
 
15475
16095
  ### Diagnostic journal startup preserves torn evidence and inspects archives
@@ -19257,15 +19877,6 @@ end of this file.
19257
19877
  # Guide: Project declaration (docs/guide/declaration.md)
19258
19878
  ==============================================================================
19259
19879
 
19260
- ---
19261
- title: Project declaration
19262
- description: One machine-readable statement a repository makes about itself — identity, roles, build, requirements, release steps and the names of the values a deployment supplies.
19263
- type: architecture
19264
- status: active
19265
- created: 2026-08-25
19266
- updated: 2026-08-30
19267
- ---
19268
-
19269
19880
  # Project declaration
19270
19881
 
19271
19882
  > **Maturity: evolving.** This surface is still finding its shape and may be
@@ -19828,6 +20439,7 @@ schedules, transports and file generation remain application-owned.
19828
20439
  |---|---|---|
19829
20440
  | `canonicalJson` | function | bounded plain JSON, UTF-16 key sort, optional object members omitted |
19830
20441
  | `CanonicalJsonOptions` | type | inferred depth/node/UTF-8 bytes limits |
20442
+ | `CanonicalJsonError` / `CanonicalJsonRefusal` | class / _type_ | the `TypeError` of a refusal and its `reason`: `cycle`, `negative-zero`, `depth`, `nodes`, `bytes` or `not-json` |
19831
20443
 
19832
20444
 
19833
20445
  ## `stitchkit/server`
@@ -19981,7 +20593,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
19981
20593
  ### Primitives
19982
20594
 
19983
20595
  | `createFileStateStore` | function | cross-process locked atomic JSON state updates over one Zod schema |
19984
- | `FileStateStoreCorruption` / `FileStateStoreOptions` | _type_ | explicit corrupt-state report/policy and lock timing configuration |
20596
+ | `FileStateStoreCorruption` / `FileStateStoreOptions` | _type_ | explicit corrupt-state report/policy, lock timing and the state read cap `maxBytes`; a transition whose resulting state encodes to the bytes it started from writes nothing |
19985
20597
  | Export | Kind | Summary |
19986
20598
  |--------|------|---------|
19987
20599
  | `streamSSE` | function | an async generator → SSE `Response` — [guide](../guide/server.md#sse-streaming) |
@@ -20087,7 +20699,7 @@ cutovers are covered by the executable
20087
20699
  | `NotificationOutboxItem` / `NotificationOutboxReceipt` / `NotificationOutboxState` / `NotificationSend` / `NotificationFailureClassification` / `DroppedNotification` / `EnqueueNotification` | _type_ | versioned queue records, stable send identity and delivery decisions |
20088
20700
  | `notificationOutboxStateSchema` | function | strict persisted-state schema over an application payload schema |
20089
20701
  | `notificationOutboxResource` / `NotificationOutboxResource` / `NotificationOutboxResourceConfig` | function / _type_ | thin managed-resource scheduler over an outbox handle |
20090
- | `createDirectoryInbox` (from `stitchkit/application/directory-inbox`) / `DirectoryInbox` / `DirectoryInboxConfig` / `DirectoryInboxDelivery` / `DirectoryInboxResource` | function / _type_ | one managed directory inbox for files or durable `accept({ source, key, entry })`; first payload wins, bounded capacity/bytes/receipts and renewable fenced leases; delivery begins after readiness and effects remain at least once |
20702
+ | `createDirectoryInbox` (from `stitchkit/application/directory-inbox`) / `DirectoryInbox` / `DirectoryInboxConfig` / `DirectoryInboxDelivery` / `DirectoryInboxResource` | function / _type_ | one managed directory inbox for files or durable `accept({ source, key, entry })` (`entry` is the schema's input, stored as given); a hard-linked file is set aside as `invalid`; first payload wins, bounded capacity/bytes/receipts and renewable fenced leases; delivery begins after readiness and effects remain at least once |
20091
20703
  | `DirectoryInboxIdentitySchema` / `DirectoryInboxIdentity` / `DirectoryInboxAccept` / `DirectoryInboxAcceptResultSchema` / `DirectoryInboxAcceptResult` | schema / _type_ | validated source/key, typed entry and durable accepted/duplicate result with filename; duplicates do not replace the first payload |
20092
20704
  | `StateStoreUpdateContext` | _type_ | real active protected transaction: `assertHeld()` refuses after ownership loss or completion; custom adapters supply this context to their transition |
20093
20705
  | `DirectoryInboxStateSchema` / `DirectoryInboxState` / `DirectoryInboxRejection` / `DirectoryInboxRejectionReasonSchema` / `DirectoryInboxRejectionReason` | schema / _type_ | the inbox's claims, receipts and rejections (`invalid`, `too-large`, `attempt-limit`) |
@@ -20173,9 +20785,10 @@ cutovers are covered by the executable
20173
20785
  | Export | Kind | Summary |
20174
20786
  |--------|------|---------|
20175
20787
  | `createDiagnosticJournal` | function | create one schema-owned FIFO JSONL writer with bounded retained memory, exclusive local path ownership and finite rotation |
20176
- | `DiagnosticJournalConfig` / `DiagnosticJournal` | _type_ | owner schema/path/limits/failure observer and the synchronous `submit`, bounded-wait `flush`/`close`, status handle |
20788
+ | `DiagnosticJournalConfig` / `DiagnosticJournal` | _type_ | owner schema/path/limits/failure observer, `startupScan` (`tails` by default, or `full`), `onStartupRefusal` (`quarantine` by default, or `fail`) and the synchronous `submit`, bounded-wait `flush`/`close`, status handle |
20177
20789
  | `DiagnosticJournalLimitsSchema` / `DiagnosticJournalLimits` | schema / _type_ | positive event, pending-item, pending-byte, file-byte and retained-file limits |
20178
20790
  | `DiagnosticJournalLockPolicySchema` / `DiagnosticJournalLockPolicy` | schema / _type_ | `refuse` (default) or `reclaim-stale`, which reclaims only a lock whose recorded owner is provably gone |
20791
+ | `DiagnosticJournalStartupScanSchema` / `DiagnosticJournalStartupScan` | schema / _type_ | how much of the retained generations opening a journal reads: `tails` (default) checks only the end of each file for a torn line, `full` parses every line |
20179
20792
  | `readDiagnosticJournalLockDiagnosis` / `DiagnosticJournalLockDiagnosis` | function / _type_ | why a `reclaim-stale` acquisition refused — attribution, PID liveness and optional process `identity` evidence (`matched`, `different-boot`, `reused-pid`, `pid-gone`, `legacy`, `unavailable`) — read off the thrown `EEXIST` |
20180
20793
  | `DiagnosticJournalSubmitResultSchema` / `DiagnosticJournalSubmitResult` | schema / _type_ | accepted epoch/sequence or explicit invalid, oversized, capacity, closed or failed refusal |
20181
20794
  | `DiagnosticJournalStatusSchema` / `DiagnosticJournalStatus` | schema / _type_ | state, limits, exact admission/write/failure counters, pending ownership, rotations, partial tails and last safe sequences |
@@ -20188,8 +20801,10 @@ cutovers are covered by the executable
20188
20801
  | `DiagnosticJournalWaitOptions` / `DiagnosticJournalFailure` | _type_ | caller wait signal/timeout and isolated internal failure callback record |
20189
20802
  | `DiagnosticJournalAnomalySchema` / `DiagnosticJournalAnomaly` | schema / _type_ | corrupt-row reason, file, zero-based byte offset, one-based line, tail/interior position and skipped bytes |
20190
20803
  | `createDiagnosticJournalReadResultSchema` / `DiagnosticJournalReadResult` | function / _type_ | owner-schema frame output or explicit anomaly; retains transformed event output types |
20191
- | `DiagnosticJournalRecoveryStatusSchema` / `DiagnosticJournalRecoveryStatus` | schema / _type_ | bounded startup inspection counts and first/last anomaly locations |
20192
- | `DiagnosticJournalRecoveryError` | class | torn single-file startup refusal carrying recovery observations, preserving evidence and releasing its lock |
20804
+ | `DiagnosticJournalRecoveryStatusSchema` / `DiagnosticJournalRecoveryStatus` | schema / _type_ | bounded startup inspection counts, first/last anomaly locations and every quarantined file beside the journal (`quarantined`, this open's moves first with their `reason`, at most 32; `quarantinedUnlisted` counts the rest); evolving and `.strict()`, so every shape change is a breaking changelog item |
20805
+ | `DiagnosticJournalStartupRefusalPolicySchema` / `DiagnosticJournalStartupRefusalPolicy` | schema / _type_ | `quarantine` (default) renames a file opening cannot keep in place to `<file>.quarantined-<epoch>` and starts; `fail` throws and moves nothing |
20806
+ | `DiagnosticJournalStartupRefusalReasonSchema` / `DiagnosticJournalStartupRefusalReason` | schema / _type_ | `torn-without-retention-slot \| not-a-regular-file \| unreadable` |
20807
+ | `DiagnosticJournalRecoveryError` / `DiagnosticJournalRecoveryErrorOptions` | class / _type_ | typed startup refusal (`reason`, `file`, optional `recovery`, `quarantineFailed`) thrown under `fail` or when moving a file aside failed; preserves evidence and releases its lock |
20193
20808
 
20194
20809
  `accepted` is bounded in-memory admission and `written` is completed append, not `fsync` or durable
20195
20810
  delivery. Its filesystem reader is in the diagnostic-journal leaf; there is no managed replay/upload API. See the
@@ -20495,7 +21110,7 @@ Server-only optional application runtime. See the
20495
21110
 
20496
21111
  | Export | Kind | Summary |
20497
21112
  |--------|------|---------|
20498
- | `AgentConversationArchiveSchema` / `AgentConversationArchive` / `encodeAgentConversationArchive` / `decodeAgentConversationArchive` / `canonicalAgentJson` | schema / _type_ / functions | canonical validated conversation archive with exact ledger events, recovery projection and durable spill payloads |
21113
+ | `AgentConversationArchiveSchema` / `AgentConversationArchive` / `encodeAgentConversationArchive` / `decodeAgentConversationArchive` / `canonicalAgentJson` | schema / _type_ / functions | canonical validated conversation archive (`canonicalAgentJson` is `canonicalJson` with the node and byte limits at their maximum, reading `-0` as `0` like `z.json()`) with exact ledger events, recovery projection and durable spill payloads |
20499
21114
  | `AgentStoreEventEnvelopeSchema` / `AgentStoreEventEnvelope` / `AgentStoreEventKindSchema` / `AgentStoreEventKind` / `AgentStoreEventPageSchema` / `AgentStoreEventPage` | schema / _type_ | append-only event envelope, closed current vocabulary (including `provider/request` and `provider/response`) and bounded page |
20500
21115
  | `AppendAgentStoreEventSchema` / `AppendAgentStoreEvent` / `ReadAgentStoreEventsSchema` / `ReadAgentStoreEvents` | schema / _type_ | required append and bounded-read store contracts |
20501
21116
  | `AgentStoreTransitionSchema` / `AgentStoreTransition` / `AgentStoreEventDraft` / `AgentStoreEventDecodeAccepted` / `AgentStoreEventDecodeIgnored` / `AgentStoreEventDecodeResult` / `decodeAgentStoreEvent` | schema / _types_ / function | normalized runtime mutations, append drafts and strict current/ignorable future-event decoding outcomes |
@@ -20505,7 +21120,7 @@ Server-only optional application runtime. See the
20505
21120
  | `defineStateSlot` / `AgentStateSlotDefinition` / `AgentStateSlotStore` / `AgentStateSlotValue` / `AnyAgentStateSlot` / `createAgentStateSlotStore` / `renderAgentStateSlots` | functions / _types_ | typed durable state written as ledger events and injected on every provider request |
20506
21121
  | `agentGoalStateSlot` / `agentTodoStateSlot` / `createAgentStateTools` | constants / function | built-in goal/todo state and bound `goal_*` / `todo_write` Agent tools |
20507
21122
  | `AgentSandboxGradeSchema` / `AgentSandboxGrade` / `AgentSandboxRestrictionSchema` / `AgentSandboxRestriction` / `AgentProcessSandbox` | schema / _type_ | host-provided process sandbox capability and explicit restriction vocabulary |
20508
- | `probeAgentProcessSandbox` / `missingSandboxRestrictions` / `recordAgentSandboxProbe` | functions | process-cached probe, fail-closed required-gap calculation and durable probe record |
21123
+ | `probeAgentProcessSandbox` / `missingSandboxRestrictions` / `recordAgentSandboxProbe` | functions | probe cached per sandbox object (keep the sandbox long-lived), fail-closed required-gap calculation and durable probe record; a probe that throws or returns no valid grade resolves to `unavailable` with a fixed reason, its cause emitted as a process warning (the coding tools instead carry it as the refusal's `cause` to the mount's `onToolError` hook) |
20509
21124
  | `AgentSandboxProcess` / `AgentSandboxOutputStream` | _types_ | structural child/output interface for optional lifecycle-owned `AgentProcessSandbox.spawn`, without Node ambient type dependencies |
20510
21125
  | `AgentEventSearchResultSchema` / `AgentEventSearchResult` / `createSqliteAgentEventSearch` / `createAgentEventSearchTools` | schema / _type_ / functions | authorized FTS5 search with exact event addresses and `session_*` tools |
20511
21126
  | `createSqliteAgentSpillStore` | function | durable content-address-checked artifact storage, bounded read/search, retention facts and archive participation |
@@ -20800,7 +21415,10 @@ terminal and provider-envelope schemas/types listed under
20800
21415
  including `AgentAttemptResetEventSchema` for `attempt-reset`),
20801
21416
  `AgentRuntimeEventCursorSchema`, `advanceAgentRuntimeEventCursor`,
20802
21417
  `AgentControlRequestSchema` / `AgentControlRequest`, `AgentControlResponseSchema` /
20803
- `AgentControlResponse`, `AgentControlDeliverySchema` / `AgentControlDelivery`, `AgentMultiSessionCursorSchema` /
21418
+ `AgentControlResponse`, `AgentControlDeliverySchema` / `AgentControlDelivery`,
21419
+ `AgentControlErrorCodeSchema` / `AgentControlErrorCode` (the one vocabulary of every control
21420
+ error code: `FORBIDDEN`, `CONNECTION_CLOSED`, `REQUEST_CAPACITY`, `ATTACHMENT_CAPACITY`,
21421
+ `LEASE_CONFLICT`, `EVENT_OVERFLOW` and the rest), `AgentMultiSessionCursorSchema` /
20804
21422
  `AgentMultiSessionCursor`, `AgentConversationView`, `AgentControlView`,
20805
21423
  `advanceAgentMultiSessionCursor`, `createAgentControlView`, `reduceAgentControlSnapshot`,
20806
21424
  `reduceAgentControlEvent` and
@@ -20830,9 +21448,12 @@ attachment per conversation on that transport; share the controller between view
20830
21448
  `submit` requires a caller-owned idempotency key. Unknown-outcome mutations are never
20831
21449
  replayed automatically. `AgentControllerState` distinguishes connecting, ready,
20832
21450
  disconnected, error and closed and retains the canonical full `view` plus safe error
20833
- code/message. Errors in automatic attach/refresh appear there. Reconnect attaches again
20834
- and reads a snapshot; snapshot gaps coalesce, stale generations cannot overwrite state,
20835
- and buffers/queues are bounded. Overflow or failed refresh is visible and requires
21451
+ code/message. Errors in automatic attach/refresh appear there, and so does an
21452
+ `access-denied` delivery: the server detached the conversation, the state becomes
21453
+ `error` with code `FORBIDDEN`, and nothing arrives until the controller attaches again.
21454
+ Reconnect attaches again and reads a snapshot; requests of the previous connection are
21455
+ rejected at once and release their capacity, snapshot gaps coalesce, stale generations
21456
+ cannot overwrite state, and buffers/queues are bounded. Overflow or failed refresh is visible and requires
20836
21457
  explicit recovery. Closing detaches without closing the shared transport or remote run.
20837
21458
 
20838
21459
  See [agent composition](../guide/agent-composition.md) for a complete wiring example.
@@ -21061,10 +21682,10 @@ payload.
21061
21682
  | `McpMountConfig` | _type_ | config for `mountMcp` |
21062
21683
  | `McpSchemaValidationConfig` | _type_ | shared `{ policy, requireTypedProperties, allowUntyped, requirePortableFormats, allowFormats }` profile |
21063
21684
  | `ValidateMcpSchemasConfig` | _type_ | standalone validation profile plus `services`, `extend`, flattening and logger |
21064
- | `RuntimeToolDefinition` | _type_ | transport-neutral pathless operation with identity, schemas, handler and optional presenters |
21685
+ | `RuntimeToolDefinition` | _type_ | heterogeneous registration of pathless operations in `runtimeTools: [...]`; an inline handler is contextually typed by the parsed object, a definition constructed with its own schema registers unchanged, and the runner validates input before the handler |
21065
21686
  | `RuntimeToolDefinitionBase` | _type_ | common name, identity, input, exposure and MCP metadata fields |
21066
- | `RuntimeToolDefinitionWithOutput` | _type_ | runtime definition whose handler and presenters share a validated output type |
21067
- | `RuntimeToolDefinitionWithoutOutput` | _type_ | runtime definition with a void handler and no presentation callbacks |
21687
+ | `RuntimeToolDefinitionWithOutput` | _type_ | the one neutral construction: identity, schemas and a handler typed from them; the fourth type argument names the adapter's `present` extension (`RuntimeToolPresenters<…>` here, `RuntimeMcpToolPresenters<…>` on `stitchkit/tools/mcp`, none by default) |
21688
+ | `RuntimeToolDefinitionWithoutOutput` | _type_ | the same construction with a void handler and no presentation callbacks |
21068
21689
  | `RuntimeToolFactory` | _type_ | identity/context-bound runtime-tool definition factory |
21069
21690
  | `RuntimeToolFactoryConfig` | _type_ | factory service identity and context schema |
21070
21691
  | `RuntimeToolFactoryDefinitionWithOutput` | _type_ | factory-authored runtime tool with a validated output schema |
@@ -21273,10 +21894,9 @@ Advanced building blocks — the shared machinery the mounts are built on.
21273
21894
  | `ToolCallRecord` | _type_ | the structured record `createToolLogger` passes to `onRecord` |
21274
21895
  | `TransportSummary` | _type_ | `{ contractServices, runtimeTools, totals, sources }` from `summarizeTransports` |
21275
21896
  | `TransportCounts` | _type_ | per-transport counts (`{ HTTP, MCP, AGENT, CLI }`) |
21276
- | `ToolSurfaceDefinition` | _type_ | shared object-shaped `{ services?, runtimeTools? }` introspection surface |
21277
- | `ToolSurfaceProjection` | _type_ | one readonly services/runtime container shared by full and MCP-only surfaces; executable mounts specialize the runtime definition without pulling in another adapter's peers |
21897
+ | `ToolSurfaceProjection` | _type_ | the one readonly `{ services?, runtimeTools? }` container shared by full, MCP-only and introspection surfaces (`buildToolManifest`, `listToolNames`, `summarizeTransports` take `ToolSurfaceProjection<RuntimeToolDefinition>`) |
21278
21898
  | `ToolManifestConfig` | _type_ | mixed surface plus required model-facing `transport` and presentation options |
21279
- | `coerceJsonArgs` | function | coerce JSON-stringified array/object tool arguments |
21899
+ | `coerceJsonArgs` | function | coerce JSON-stringified array/object tool arguments; throws a `ZodError` naming the field when a union that keeps a string member receives a string that parses to an array its array member accepts |
21280
21900
  | `flattenToolJsonSchema` | function | project structurally identifiable discriminated unions into conservative object joins; scalar collisions retain provable types, object/array collisions retain structural alternatives, and field descriptions retain discriminator labels plus availability/requiredness hints; the projection never executes validation |
21281
21901
  | `ToolPresentationSchema` | _type_ | immutable model-facing JSON Schema document shared by tool transports |
21282
21902
  | `MountableTool` | _type_ | one operation with separate executable CLI argument schema and model-facing presentation schema |
@@ -21295,10 +21915,9 @@ behavior and names. See [MCP guide](../guide/mcp-and-agents.md#mcp--createmcphan
21295
21915
  |--------|------|---------|
21296
21916
  | `createMcpHandler` / `createMcpHttpRoute` / `buildMcpServer` / `mountMcp` | function | the shared authenticated HTTP handler, raw-route adapter, SDK server and contract mount |
21297
21917
  | `createStdioMcpServer` / `bindStdioProcessSignals` | function | the shared stdio server and bounded process cleanup |
21298
- | `RuntimeMcpToolDefinitionWithOutput` | _type_ | schema-aware MCP-only construction with concrete handler output and presenter input |
21299
- | `RuntimeMcpToolDefinition` | _type_ | heterogeneous MCP registration; the canonical runner validates input/output before invocation |
21918
+ | `RuntimeToolDefinitionWithOutput` / `RuntimeToolDefinitionWithoutOutput` | _type_ | the one neutral construction, shared with `stitchkit/cli` and `stitchkit/tools`; MCP-only presenters are its fourth type argument, `RuntimeMcpToolPresenters<z.output<typeof output>>` |
21919
+ | `RuntimeToolDefinition` | _type_ | heterogeneous registration; the canonical runner validates input/output before invocation |
21300
21920
  | `RuntimeMcpToolPresenters` / `RuntimeMcpPresentation` | _type_ | official SDK content without framework-owned `structuredContent` or `isError`; one presenter owner shared with the full SDK surface |
21301
- | `RuntimeToolDefinitionWithoutOutput` | _type_ | the shared neutral void definition |
21302
21921
 
21303
21922
  ## `stitchkit/tools/connections`
21304
21923
 
@@ -21398,7 +22017,6 @@ handler pipeline without opening a TCP port.
21398
22017
  | `McpSchemaValidationConfig` / `IncompatibleSchemaPolicy` | _type_ | canonical MCP schema-preparation policy used by real mounts and manifests |
21399
22018
  | `SurfaceRealtimeSchemaPairSchema` | schema | input/output digest pair for args or acknowledgements |
21400
22019
  | `SurfaceSchemaDigestsSchema` | schema | params/input/output/multipart digest object |
21401
- | `serializeSurfaceValue` | function | canonical versioned serialization used for deterministic digests |
21402
22020
 
21403
22021
  ---
21404
22022
 
@@ -21419,8 +22037,10 @@ available from `stitchkit/contract`.
21419
22037
  | `ManagedFileError` / `ManagedFileErrorCode` | class / _type_ | stable boundary failures; registered `FILE_*` mistakes are caller-safe while `FILE_IO_ERROR` remains internal |
21420
22038
  | `ManagedFileInspector` | _type_ | bounded-prefix read/write inspection callback with a finite cancellation signal that cannot own path or size |
21421
22039
  | `ManagedFileInspectionInput` / `ManagedFileInspection` | _type_ | inspector prefix/name/declared media/signal input and validated metadata-only result |
21422
- | `writeFileAtomic` / `writeFileAtomicSync` / `WriteFileAtomicOptions` | function / _type_ | replace a file atomically: a random staging name created exclusively (never through a planted link), the mode set on the descriptor before the file is visible (default `0o600`, not masked by the umask), `fsync`, rename; a failure leaves the target and no staging file. The asynchronous form keeps the event loop running |
21423
- | `withExclusiveLock` / `ExclusiveLockOptions` / `ExclusiveLock` / `ExclusiveLockOwner` | function / _type_ | run work under an exclusive lock between processes — a file recording its owner (pid, host, machine identity, time and nullable kernel process identity); waits up to `timeoutMs` (default 10 s) and stops on `signal`; a dead owner on this machine is taken over, a live, slow or foreign one never is; an ownerless lock only after `ownerlessGraceMs` (default 5000 ms, `null` disables age-based reclaim including guards); exact `mode` is independent of umask; shared readers need a common group and traversable directories |
22040
+ | `writeFileAtomic` / `writeFileAtomicSync` / `WriteFileAtomicOptions` | function / _type_ | replace a file atomically: a random staging name created exclusively (never through a planted link), the mode set on the descriptor before the file is visible (default `0o600`, not masked by the umask), `fsync`, rename; a failure leaves the target and no staging file. The asynchronous form keeps the event loop running. A process killed before publication leaves its staging file; sweeping it is the caller's job |
22041
+ | `isAtomicStagingName` | function | true for a staging file name of an atomic write of this library (`.stitchkit-<24 lowercase hex>.tmp`, also used by the managed writer and the chunk spool); the form is a stable contract, and changing it is a breaking change ([ADR 0242](../decisions/0242-atomic-staging-name-is-a-public-contract.md)) |
22042
+ | `sweepAtomicStaging` / `SweepAtomicStagingOptions` | function / _type_ | remove abandoned staging files from one `directory`: only regular files matching `isAtomicStagingName` and older than the required `olderThanMs`, never following a symlink or entering a subdirectory; stops on `signal`; returns the removed names |
22043
+ | `withExclusiveLock` / `ExclusiveLockOptions` / `ExclusiveLock` / `ExclusiveLockOwner` | function / _type_ | run work under an exclusive lock between processes — a file recording its owner (pid, host, machine identity, time and nullable kernel process identity); waits up to `timeoutMs` (default 10 s) and stops on `signal`; a dead owner on this machine is taken over, a live, slow or foreign one never is; the lock is published with its owner already recorded (temporary file plus hard link), so a stalled holder is never displaced; a legacy empty lock file is taken by age only when `ownerlessGraceMs` is a number (default `null`: never; an abandoned empty reclaim guard is taken after 5 000 ms unless a value is set); exact `mode` is independent of umask; shared readers need a common group and traversable directories |
21424
22044
  | `ExclusiveLockError` | class | `LOCK_TIMEOUT` naming the resource and its holder, or `LOCK_ABORTED` with the signal's reason |
21425
22045
  | `createChunkSpool` | function | the server half of a chunked upload: parts on disk under `owner` + client-minted `uploadId`, each with a size+sha256 receipt; idempotent `open` and `put`, the first writer of a part wins across processes, `assemble` in order once every part is in, `sweep` of untouched uploads — [guide](../guide/client.md#chunked-uploads) |
21426
22046
  | `ChunkSpool` / `ChunkSpoolConfig` / `ChunkSpoolKey` / `ChunkSpoolOpen` / `ChunkSpoolPart` / `ChunkSpoolAssembly` | _type_ | the spool, its directory, part size, file limit, meta schema and age; an upload's key; its declaration; one part; the assembled parts with a concatenating `stream()` |
@@ -21798,7 +22418,6 @@ SDK nor the `ai` peer.
21798
22418
  | `checkCliUpdate` | function | bounded, interval-limited, never-throwing check for a newer published build |
21799
22419
  | `applyCliUpdate` | function | download, verify the decompressed digest and replace the binary by rename |
21800
22420
  | `assertCliPublishable` | function | refuse republishing one version from a different commit — takes one manifest or every manifest already published, because the promise a version makes does not stop at a channel boundary |
21801
- | `publishCli` / `CliPublicationOptions` / `CliPublicationPhase` / `CliPublicationResult` | function / _type_ | optional bounded builder/admission over one immutable name/version/commit stamp; commit verified assets before the manifest pointer, recover exact completed versions, retain verified history; existing signatures/installer/updater own the wire contract |
21802
22421
  | `rollbackCliUpdate` / `CliRollbackConfig` / `RolledBackCliUpdate` | function / _types_ | put the kept previous build back, digest-checked first and written by the same atomic rename the update uses |
21803
22422
  | `signCliManifest` / `verifyCliManifest` / `cliManifestSigningPayload` | function | Ed25519 over `{name, version, commit, builtAt, assets[]}` — every asset's digest included, so the chain closes on the file that executes |
21804
22423
  | `cliSignatureAccepted` / `CliSignatureVerdict` | function / _type_ | `valid` \| `unenforced` \| `missing` \| `unknown-key` \| `invalid`; `unenforced` keeps an unpinned build updating while making the absence of a check visible |
@@ -21830,8 +22449,8 @@ SDK nor the `ai` peer.
21830
22449
  | `CliConfig` | _type_ | config for `createCli`; `defaultCommand`, `globalOptions`, `optionAliases` and `positionals` define the shared command presentation policy |
21831
22450
  | `CliPresentationPolicyConfig` | _type_ | shared command presentation-policy subset of `CliConfig` |
21832
22451
  | `CliSurfaceSource` | _type_ | static service/runtime array or identity-dependent factory |
21833
- | `RuntimeToolExecution` | _type_ | neutral heterogeneous runtime-tool registration; handlers execute through the schema-validating managed runner |
21834
- | `RuntimeToolExecutionWithOutput` | _type_ | strict neutral managed definition with schema-derived handler input/output; construct before adding to CLI `runtimeTools` |
22452
+ | `RuntimeToolDefinition` | _type_ | neutral heterogeneous runtime-tool registration; an inline handler is contextually typed, and handlers execute through the schema-validating managed runner |
22453
+ | `RuntimeToolDefinitionWithOutput` | _type_ | strict neutral managed definition with schema-derived handler input/output, the same declaration `stitchkit/tools` constructs with presenters |
21835
22454
  | `RuntimeToolDefinitionWithoutOutput` | _type_ | strict neutral managed void definition without an output contract or transport presenters |
21836
22455
  | `CliCommandDefinition` | _type_ | native command definition union |
21837
22456
  | `CliCommandDefinitionBase` | _type_ | native command name, description and input schema |
@@ -21848,6 +22467,22 @@ SDK nor the `ai` peer.
21848
22467
 
21849
22468
  ---
21850
22469
 
22470
+ ## `stitchkit/cli/publish`
22471
+
22472
+ Build and release tooling, evolving. Publishes a CLI distribution into a local
22473
+ application-owned directory; the manifest, signatures, installer and updater it
22474
+ writes for stay in `stitchkit/cli` — [guide](../guide/cli.md#publishing-a-complete-version),
22475
+ [ADR 0245](../decisions/0245-cli-publication-is-an-evolving-leaf.md).
22476
+
22477
+ | Export | Kind | Summary |
22478
+ |--------|------|---------|
22479
+ | `publishCli` | function | optional bounded builder/admission over one immutable name/version/commit stamp; commit verified assets before the manifest pointer, recover exact completed versions, retain verified history |
22480
+ | `CliPublicationOptions` | _type_ | identity, storage root, base URL, targets, limits, retention, the `admit` and `build` callbacks, optional signing and trust |
22481
+ | `CliPublicationPhase` | _type_ | the step `admit` is asked about — `prepare`, `build`, `commit` or `promote` |
22482
+ | `CliPublicationResult` | _type_ | `published` or `existing`, with the manifest ready to serve |
22483
+
22484
+ ---
22485
+
21851
22486
  ## `stitchkit/react`
21852
22487
 
21853
22488
  Browser- and server-render-safe React data-layer helpers. Needs the
@@ -21936,11 +22571,20 @@ network enforcement, gateway credentials and platform/resource boundaries.
21936
22571
 
21937
22572
  ## `stitchkit/agent-runtime/realtime`
21938
22573
 
21939
- Optional evolving Socket.IO server binding. `bindAgentHarnessRealtime` takes `(harness, socket, config)` and returns `close()` for its subscriptions and control connections, without closing
21940
- socket or harness. `AgentHarnessRealtimeConfig` requires `authorize({ identity, request,
21941
- signal })`: return `{ context }` after verifying the session principal, conversation,
21942
- operation, files and metadata, or `null`. Identity is `socket.data` from the application's
21943
- verified handshake. Every request and event delivery is authorized. Observe access does
22574
+ Optional evolving Socket.IO server binding, experimental: no committed consumer yet.
22575
+ `bindAgentHarnessRealtime` takes `(harness, socket, config)` and returns an
22576
+ `AgentHarnessRealtimeBinding`: `close()` for its subscriptions and control connections, without closing
22577
+ socket or harness, and `revoke(conversationId, matches?)`. `AgentHarnessRealtimeConfig` requires
22578
+ `authorize({ identity, request, source, signal })`: return `{ context }` after verifying the session
22579
+ principal, conversation, operation, files and metadata, or `null`. Identity is `socket.data` from the
22580
+ application's verified handshake. Every request is authorized on arrival. The events of an
22581
+ attached conversation are delivered under the grant the latest successful request created, so a
22582
+ token stream costs no authorization call per event. `revoke` drops the grant of every matching
22583
+ socket; the next event is re-authorized (`source: 'delivery'`, an `operation: 'snapshot'` request),
22584
+ and a refusal or failure detaches the conversation and sends an `access-denied` delivery, which
22585
+ turns the browser controller into an `error` state. Without a `revoke` call a grant lasts until the socket disconnects or the conversation
22586
+ is detached; this fail-open default is deliberate, so call `revoke` whenever access to a conversation may
22587
+ have been withdrawn. An authorization that began before a `revoke` never restores the grant. Observe access does
21944
22588
  not grant mutation; controller leases and approval policy still belong to the harness.
21945
22589
 
21946
22590
  `maxPendingEvents` and `maxPendingAttachments` forward to the canonical control server;
@@ -21952,23 +22596,11 @@ application transport. A transport's Engine.IO payload limit remains application
21952
22596
 
21953
22597
  ## `stitchkit/agent-runtime/react`
21954
22598
 
21955
- Optional evolving React peer leaf. `useAgent` takes a controller and subscribes through
22599
+ Optional evolving React peer leaf, experimental: no committed consumer yet. `useAgent` takes a controller and subscribes through
21956
22600
  `useSyncExternalStore`, returning the same `AgentControllerState` as `getSnapshot()`.
21957
22601
  Create and close the controller at application/session scope. Multiple components can
21958
22602
  observe it; StrictMode and unmount release only the component's own subscription.
21959
22603
 
21960
- ## `stitchkit/agent-runtime/harness-tools`
21961
-
21962
- Optional evolving composition of the full `mountAgent` configuration with the runtime
21963
- fence. Its runtime peer is `ai`; its full tool presenter declarations also require
21964
- `@modelcontextprotocol/server`, as `stitchkit/tools` does. The base harness retains its
21965
- AI-only peer boundary. Existing custom tool callbacks need no new import.
21966
-
21967
- | Export | Kind | Summary |
21968
- |--------|------|---------|
21969
- | `createAgentHarnessTools` / `AgentHarnessToolsConfig` | function / _type_ | async per-run full mount configuration (`services` plus all `AgentMountConfig` options); composes application lifecycle then runtime fence exactly once |
21970
-
21971
-
21972
22604
  ## `stitchkit/process`
21973
22605
 
21974
22606
  Native POSIX Bun/Node one-shot command execution. See [native IO](../guide/native-io.md).
@@ -21976,7 +22608,8 @@ Native POSIX Bun/Node one-shot command execution. See [native IO](../guide/nativ
21976
22608
  | Export | Kind | Purpose |
21977
22609
  |---|---|---|
21978
22610
  | `runNativeCommand` | function | caller-lifetime streaming or explicitly bounded binary capture |
21979
- | `NativeCommandOptions` | type | executable/args, env policy, signal/deadline, bytes sink and finite cleanup limits |
22611
+ | `NativeCommandOptions` | type | executable/args, env policy, signal/deadline, bytes sink, finite cleanup limits, the `stop` policy and `descendants` (`'terminate-after-leader'`, the default: stop the group after the leader exits, or `'leave'`) |
22612
+ | `NativeCommandStopPolicy` | type | how a cancelled command stops: `target` `'group'` (signal every member) or `'leader'` (signal only the leader; its exit ends the grace), `signal` (default `SIGTERM`), `graceMs` (at most one hour) before KILL to the group, and `killOn`, an `AbortSignal` that skips the grace; default `{ target: 'group', signal: 'SIGTERM', graceMs: 100 }` |
21980
22613
  | `NativeCommandResult` | type | observed nullable exit code/signal and captured stdout/stderr bytes |
21981
22614
  | `NativeCommandError` | class | COMMAND_LIMIT with machine reason `deadline` or `output-budget`; COMMAND_UNAVAILABLE / COMMAND_CLEANUP have no limit reason. Preserves internal cause and existing constructor; caller-created limits without supplied reason remain unclassified |
21982
22615
 
@@ -21989,12 +22622,27 @@ Native POSIX Bun/Node one-shot command execution. See [native IO](../guide/nativ
21989
22622
 
21990
22623
  `stitchkit/files/packaging` owns `createNativePackaging` and the types
21991
22624
  `NativePackagingOptions`, `NativePackagingAsset`, `NativePackagingPlugin`, `NativePackagingResult`.
21992
- It resolves the installed package's target/version and original addon SHA256.
21993
- `platform`, `architecture`, `delivery`, `entryPath` and `assetPath` are explicit public inputs.
21994
- The result is `ready` with assets and a structural Bun plugin, or `unsupported`/`missing` with a safe code.
21995
- Malformed options/metadata and other filesystem errors fail rather than masquerade as missing.
21996
- Companion mode places an external addon relative to the emitted entry; embedded mode lets
21997
- Bun compile retain the matching addon. Keep JS builds without splitting, preserve every output,
21998
- and copy each original asset to its `outputPath` after verifying `sha256`.
22625
+ It resolves the installed package's target/version, reads each selected addon once and checks it
22626
+ against the `size` and `sha256` published in the package's `native-assets.json` (`formatVersion: 2`,
22627
+ written when Stitchkit is built). Call it once per build and reuse the result.
22628
+ `platform`, `architecture`, `delivery`, `entryPath` and `assetPath` are explicit public inputs;
22629
+ `platform` is the closed set `'darwin'`, and another name is a type error and a schema refusal.
22630
+ The result is `ready` with verified assets and a structural Bun plugin, or `unsupported` /
22631
+ `missing` / `mismatch` with the safe code `NATIVE_TARGET_UNSUPPORTED`, `NATIVE_ASSET_MISSING` or
22632
+ `NATIVE_ASSET_DIGEST_MISMATCH`. A `mismatch` names the first differing `architecture` with
22633
+ `expected` (published) and `actual` (installed) `{ size, sha256 }`.
22634
+ Malformed options, a manifest of another `formatVersion` and other filesystem errors throw rather
22635
+ than masquerade as missing.
22636
+ Companion accepts a single `architecture` string with an `assetPath` string, or an architecture
22637
+ array with an exact per-architecture `assetPath` map. `NativePackagingOptions<true>` and
22638
+ `NativePackagingResult<true>` describe the array form; default types preserve single-target signatures.
22639
+ A ready array-form result carries the architecture array; `NativePackagingAsset<true>` identifies
22640
+ each asset architecture. A single-target asset has four fields: `outputPath`, the verified `bytes`,
22641
+ and the published `size` and `sha256`.
22642
+ Targets must be unique; entry and all asset paths must be disjoint. One lazy loader chooses only
22643
+ the actual runtime architecture, with no fallback.
22644
+ Companion mode places external addons relative to the emitted entry; embedded mode gives
22645
+ Bun compile the verified bytes of the matching addon. Keep JS builds without splitting, preserve
22646
+ every output, and write each asset's `bytes` to its `outputPath`.
21999
22647
  This evolving entrypoint is a build dependency; runtime imports do not reach it.
22000
22648
  The detailed recipe is in [native IO](../guide/native-io.md#public-native-packaging).