stitchkit 0.104.2 → 0.105.1

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 (430) hide show
  1. package/CHANGELOG.md +401 -13
  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 +161 -66
  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 +78 -17
  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-269zqj8m.js → index-1ptj98e0.js} +28 -12
  160. package/dist/{index-kemm0ej5.js → index-1pwwan01.js} +65 -22
  161. package/dist/{index-vgkbcnkc.js → index-299sm5tr.js} +1 -1
  162. package/dist/{index-2vnh05w4.js → index-2k91mewf.js} +31 -74
  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-f330qqyg.js → index-5c26631f.js} +244 -67
  168. package/dist/{index-adhgd4pe.js → index-5p55xreg.js} +2 -2
  169. package/dist/index-5w6t91ze.js +47 -0
  170. package/dist/{index-9pw0a51a.js → index-6yxhe58g.js} +40 -7
  171. package/dist/{index-6d9xaxw1.js → index-8ks9b85e.js} +33 -5
  172. package/dist/{index-bhb950qs.js → index-916yh4e2.js} +25 -12
  173. package/dist/index-9tqbxv5p.js +44 -0
  174. package/dist/{index-5csmzesf.js → index-azx12jgv.js} +4 -3
  175. package/dist/{index-mbnnw3f2.js → index-b0kddvzw.js} +15 -5
  176. package/dist/{index-n5yqxd3d.js → index-b0w7rbpt.js} +23 -2
  177. package/dist/index-b56nnscm.js +132 -0
  178. package/dist/{index-0ndk4555.js → index-c0hffcmj.js} +2 -2
  179. package/dist/{index-pptyv13k.js → index-c17xcx4k.js} +42 -6
  180. package/dist/index-cazts87s.js +105 -0
  181. package/dist/{index-g2j7pnak.js → index-cmzvhhtd.js} +5 -2
  182. package/dist/{index-ssmd41kh.js → index-d2h99n7d.js} +1 -1
  183. package/dist/{index-z94wht4y.js → index-dky0dcmf.js} +34 -5
  184. package/dist/index-drs41arz.js +455 -0
  185. package/dist/{index-za6j7nwy.js → index-dsnjzb62.js} +104 -51
  186. package/dist/index-eemvahkc.js +21 -0
  187. package/dist/{index-fa4wg312.js → index-f199nfq5.js} +10 -6
  188. package/dist/{index-ck71tbcf.js → index-fsw1ybf3.js} +11 -15
  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-j4zh647a.js +59 -0
  192. package/dist/{index-x928baz2.js → index-j9gxbztg.js} +18 -14
  193. package/dist/index-kfjkb396.js +37 -0
  194. package/dist/{index-6drst9sg.js → index-ksfagmyz.js} +45 -345
  195. package/dist/index-m616f0yz.js +120 -0
  196. package/dist/{index-fm1c1a08.js → index-m6w6jtda.js} +40 -21
  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-hbd32c6a.js → index-w8etpm8j.js} +38 -89
  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-hp9d77wk.js → index-x7w1z6pf.js} +15 -4
  211. package/dist/{index-ptn0g0gj.js → index-y7k2cdgv.js} +21 -20
  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 +47 -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 +7 -2
  255. package/dist/process/cleanup.d.ts.map +1 -1
  256. package/dist/process/command-lifetime.d.ts +6 -1
  257. package/dist/process/command-lifetime.d.ts.map +1 -1
  258. package/dist/process/command-owner.d.ts +1 -0
  259. package/dist/process/command-owner.d.ts.map +1 -1
  260. package/dist/process/command.d.ts +4 -0
  261. package/dist/process/command.d.ts.map +1 -1
  262. package/dist/process/contract.d.ts +68 -2
  263. package/dist/process/contract.d.ts.map +1 -1
  264. package/dist/process/group.d.ts +42 -3
  265. package/dist/process/group.d.ts.map +1 -1
  266. package/dist/process/launch.d.ts +12 -1
  267. package/dist/process/launch.d.ts.map +1 -1
  268. package/dist/process/terminate.d.ts +40 -0
  269. package/dist/process/terminate.d.ts.map +1 -0
  270. package/dist/react/query-client.d.ts.map +1 -1
  271. package/dist/realtime/rejection.d.ts.map +1 -1
  272. package/dist/server/contract-stream.d.ts.map +1 -1
  273. package/dist/server/handler-state.d.ts.map +1 -1
  274. package/dist/server/implement.d.ts.map +1 -1
  275. package/dist/server/request-body.d.ts +0 -1
  276. package/dist/server/request-body.d.ts.map +1 -1
  277. package/dist/server/unix-client.d.ts.map +1 -1
  278. package/dist/telegram/broadcast-delivery.d.ts.map +1 -1
  279. package/dist/telegram/broadcast-failure.d.ts +10 -2
  280. package/dist/telegram/broadcast-failure.d.ts.map +1 -1
  281. package/dist/telegram/operator-channel.d.ts.map +1 -1
  282. package/dist/telegram/screens/outcome.d.ts.map +1 -1
  283. package/dist/testing/surface-conformance.d.ts +1 -1
  284. package/dist/testing/surface-conformance.d.ts.map +1 -1
  285. package/dist/testing/surface-manifest.d.ts +0 -6
  286. package/dist/testing/surface-manifest.d.ts.map +1 -1
  287. package/dist/tools/agent.d.ts +1 -1
  288. package/dist/tools/agent.d.ts.map +1 -1
  289. package/dist/tools/cli/args.d.ts +1 -1
  290. package/dist/tools/cli/args.d.ts.map +1 -1
  291. package/dist/tools/cli/help.d.ts.map +1 -1
  292. package/dist/tools/cli/invoke.d.ts +2 -2
  293. package/dist/tools/cli/invoke.d.ts.map +1 -1
  294. package/dist/tools/cli/profile.d.ts.map +1 -1
  295. package/dist/tools/cli/publication-assets.d.ts.map +1 -1
  296. package/dist/tools/cli/publication-contract.d.ts +8 -0
  297. package/dist/tools/cli/publication-contract.d.ts.map +1 -1
  298. package/dist/tools/cli/publication-control.d.ts.map +1 -1
  299. package/dist/tools/cli/publication-storage.d.ts +23 -2
  300. package/dist/tools/cli/publication-storage.d.ts.map +1 -1
  301. package/dist/tools/cli/publication.d.ts +8 -1
  302. package/dist/tools/cli/publication.d.ts.map +1 -1
  303. package/dist/tools/connections/errors.d.ts +18 -6
  304. package/dist/tools/connections/errors.d.ts.map +1 -1
  305. package/dist/tools/connections/limits.d.ts +8 -2
  306. package/dist/tools/connections/limits.d.ts.map +1 -1
  307. package/dist/tools/connections/mcp-client.d.ts.map +1 -1
  308. package/dist/tools/connections/mcp-envelope.d.ts.map +1 -1
  309. package/dist/tools/connections/openapi.d.ts.map +1 -1
  310. package/dist/tools/connections/schema-budget.d.ts +1 -1
  311. package/dist/tools/connections/schema-budget.d.ts.map +1 -1
  312. package/dist/tools/connections/sse-session.d.ts.map +1 -1
  313. package/dist/tools/execute-args.d.ts.map +1 -1
  314. package/dist/tools/execute-result.d.ts.map +1 -1
  315. package/dist/tools/internal/surface-projector.d.ts +17 -3
  316. package/dist/tools/internal/surface-projector.d.ts.map +1 -1
  317. package/dist/tools/internal/tool-error-projection.d.ts +16 -0
  318. package/dist/tools/internal/tool-error-projection.d.ts.map +1 -0
  319. package/dist/tools/internal/tool-view.d.ts.map +1 -1
  320. package/dist/tools/invoker.d.ts.map +1 -1
  321. package/dist/tools/list-names.d.ts +3 -2
  322. package/dist/tools/list-names.d.ts.map +1 -1
  323. package/dist/tools/manifest.d.ts +3 -2
  324. package/dist/tools/manifest.d.ts.map +1 -1
  325. package/dist/tools/mcp/handler.d.ts +7 -0
  326. package/dist/tools/mcp/handler.d.ts.map +1 -1
  327. package/dist/tools/mcp/mount.d.ts +18 -2
  328. package/dist/tools/mcp/mount.d.ts.map +1 -1
  329. package/dist/tools/mcp/prepare.d.ts +3 -3
  330. package/dist/tools/mcp/prepare.d.ts.map +1 -1
  331. package/dist/tools/mcp/register.d.ts.map +1 -1
  332. package/dist/tools/mcp/runtime-tools.d.ts.map +1 -1
  333. package/dist/tools/mcp/stdio-signals.d.ts +14 -0
  334. package/dist/tools/mcp/stdio-signals.d.ts.map +1 -1
  335. package/dist/tools/mcp/stdio.d.ts +7 -0
  336. package/dist/tools/mcp/stdio.d.ts.map +1 -1
  337. package/dist/tools/operations/async-operation-runtime-tools.d.ts.map +1 -1
  338. package/dist/tools/operations/async-operation-runtime-types.d.ts +3 -3
  339. package/dist/tools/operations/async-operation-runtime-types.d.ts.map +1 -1
  340. package/dist/tools/operations/define-wait-tool.d.ts +1 -1
  341. package/dist/tools/operations/define-wait-tool.d.ts.map +1 -1
  342. package/dist/tools/operations/wait-core.d.ts +0 -6
  343. package/dist/tools/operations/wait-core.d.ts.map +1 -1
  344. package/dist/tools/registry.d.ts +1 -1
  345. package/dist/tools/registry.d.ts.map +1 -1
  346. package/dist/tools/remote.d.ts +1 -1
  347. package/dist/tools/remote.d.ts.map +1 -1
  348. package/dist/tools/{runtime-tool-execution.d.ts → runtime-tool-declaration.d.ts} +32 -9
  349. package/dist/tools/runtime-tool-declaration.d.ts.map +1 -0
  350. package/dist/tools/runtime-tool-mcp.d.ts +6 -15
  351. package/dist/tools/runtime-tool-mcp.d.ts.map +1 -1
  352. package/dist/tools/runtime-tool.d.ts +14 -12
  353. package/dist/tools/runtime-tool.d.ts.map +1 -1
  354. package/dist/tools/schema/coerce.d.ts +4 -0
  355. package/dist/tools/schema/coerce.d.ts.map +1 -1
  356. package/dist/tools/schema/flatten-join.d.ts.map +1 -1
  357. package/dist/tools/surface.d.ts +7 -11
  358. package/dist/tools/surface.d.ts.map +1 -1
  359. package/dist/tools/transfer/define-download-tool.d.ts +1 -1
  360. package/dist/tools/transfer/define-download-tool.d.ts.map +1 -1
  361. package/dist/tools/transfer/define-upload-tool.d.ts +1 -1
  362. package/dist/tools/transfer/define-upload-tool.d.ts.map +1 -1
  363. package/dist/tools/transfer/define-view-file-tool.d.ts +1 -1
  364. package/dist/tools/transfer/define-view-file-tool.d.ts.map +1 -1
  365. package/dist/tools/transfer/managed-file-error.d.ts.map +1 -1
  366. package/dist/tools/transfer/view-file-contract.d.ts.map +1 -1
  367. package/dist/tools/transfer/view-file.d.ts +1 -1
  368. package/dist/tools/transfer/view-file.d.ts.map +1 -1
  369. package/dist/tools/transports.d.ts +3 -2
  370. package/dist/tools/transports.d.ts.map +1 -1
  371. package/llms/agent-runtime-browser.txt +10 -4
  372. package/llms/agent-runtime-harness.txt +32 -31
  373. package/llms/agent-runtime-react.txt +1 -1
  374. package/llms/agent-runtime-realtime.txt +14 -5
  375. package/llms/agent-runtime-sandbox.txt +3 -11
  376. package/llms/agent-runtime.3.txt +2 -2
  377. package/llms/agent-runtime.txt +0 -9
  378. package/llms/application.2.txt +3 -12
  379. package/llms/application.3.txt +7 -4
  380. package/llms/application.txt +44 -15
  381. package/llms/cli-publish.txt +29 -0
  382. package/llms/cli.2.txt +9 -11
  383. package/llms/cli.txt +26 -14
  384. package/llms/declaration.txt +0 -9
  385. package/llms/files-packaging.txt +18 -8
  386. package/llms/files.txt +4 -2
  387. package/llms/geo.txt +0 -9
  388. package/llms/live.txt +4 -13
  389. package/llms/observability.txt +31 -5
  390. package/llms/primitives.txt +38 -20
  391. package/llms/process.txt +324 -160
  392. package/llms/react-keyboard.txt +0 -9
  393. package/llms/react.txt +0 -18
  394. package/llms/server.4.txt +1 -1
  395. package/llms/server.5.txt +1 -1
  396. package/llms/server.txt +1 -1
  397. package/llms/stitchkit.txt +6 -7
  398. package/llms/telegram-screens.txt +2 -2
  399. package/llms/testing.txt +0 -1
  400. package/llms/tools-mcp.txt +2 -3
  401. package/llms/tools.2.txt +12 -0
  402. package/llms/tools.3.txt +5 -6
  403. package/llms/tools.txt +6 -4
  404. package/llms/upgrading-0.37.0-0.50.0.txt +947 -0
  405. package/llms/{upgrading-0.44.0-0.61.0.txt → upgrading-0.53.0-0.67.0.txt} +576 -506
  406. package/llms/{upgrading-0.62.0-0.80.0.txt → upgrading-0.68.0-0.94.0.txt} +562 -576
  407. package/llms/{upgrading-0.81.0-0.104.0.txt → upgrading-0.95.0-0.105.0.txt} +452 -567
  408. package/llms/upgrading.txt +4 -4
  409. package/llms-full.txt +1068 -412
  410. package/llms.txt +24 -24
  411. package/native-assets.json +11 -3
  412. package/package.json +6 -6
  413. package/dist/agent-runtime/harness-tools.d.ts +0 -11
  414. package/dist/agent-runtime/harness-tools.d.ts.map +0 -1
  415. package/dist/entrypoints/agent-runtime/harness-tools.d.ts +0 -2
  416. package/dist/entrypoints/agent-runtime/harness-tools.d.ts.map +0 -1
  417. package/dist/entrypoints/agent-runtime/harness-tools.js +0 -32
  418. package/dist/index-1a6jtnd9.js +0 -88
  419. package/dist/index-8a9ksm91.js +0 -95
  420. package/dist/index-a3wma1hk.js +0 -48
  421. package/dist/index-jd470k6q.js +0 -109
  422. package/dist/index-n1dqr7gf.js +0 -20
  423. package/dist/index-nggfa2yn.js +0 -34
  424. package/dist/index-s4evrmp5.js +0 -98
  425. package/dist/index-ze1eph1k.js +0 -20
  426. package/dist/tools/connections/error-projection.d.ts +0 -4
  427. package/dist/tools/connections/error-projection.d.ts.map +0 -1
  428. package/dist/tools/runtime-tool-execution.d.ts.map +0 -1
  429. package/llms/agent-runtime-harness-tools.txt +0 -25
  430. 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,51 @@ 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. A directory that does not exist yet has nothing to
8214
+ sweep and yields `[]`; a file standing where the directory should be, or a denied listing, throws:
8215
+
8216
+ ```ts
8217
+ import { isAtomicStagingName, sweepAtomicStaging } from 'stitchkit/files'
8218
+
8219
+ // At startup, or before trusting that a directory holds only your own records:
8220
+ const removed = await sweepAtomicStaging({ directory: '/srv/app/registry', olderThanMs: 60 * 60_000 })
8186
8221
 
8187
- `AtomicFilePublicationError.published === true` отличает видимость от неподтверждённого
8188
- acknowledgement. `phase` — `cleanup`, `directory-sync` или `directory-close`; первичная причина
8189
- сохранена в `cause`. Ошибка до publish оставляет прежний target. Cleanup вторичен к первичной ошибке.
8190
- При create кратко существует второй hardlink; strict single-link reader вправе отказать в этом окне.
8222
+ // Skip staging files when listing a directory of your records:
8223
+ const records = (await readdir('/srv/app/registry')).filter((name) => !isAtomicStagingName(name))
8224
+ ```
8191
8225
 
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.
8226
+ `olderThanMs` is required and must exceed the longest write in flight; a younger staging file is
8227
+ left alone. The sweep removes only regular files, never follows a symlink and never enters a
8228
+ subdirectory. The name form is a stable public contract: changing it is a breaking change of
8229
+ `stitchkit/files`. An exclusive lock's `.lock-*.tmp` file is not an atomic-write staging file and
8230
+ does not match.
8197
8231
 
8198
- ## Строгое чтение
8232
+ Directory fsync is a native POSIX filesystem capability. Windows refuses it explicitly, and another
8233
+ filesystem may refuse at open or sync. A check on Linux does not confirm macOS behavior, network
8234
+ filesystems or survival of a power loss; the disk state is whatever the filesystem and OS guarantee.
8235
+ `createManagedFileBoundary.write(..., { durable: true })` uses the same publish owner and parent sync;
8236
+ its create stays a create, and `replace: true` allows replacement explicitly.
8237
+
8238
+ ## Strict reads
8199
8239
 
8200
8240
  ```ts
8201
8241
  import { createManagedFileBoundary } from 'stitchkit/files'
@@ -8206,25 +8246,27 @@ const source = await files.read('receipt.json', {
8206
8246
  })
8207
8247
  console.log(source.observation) // dev, ino, size, nlink, mtimeMs, ctimeMs
8208
8248
  const receipt = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(source.bytes))
8209
- // schema.parse(receipt) и проверка digest принадлежат приложению.
8249
+ // Schema parsing and digest checks belong to the application.
8210
8250
  ```
8211
8251
 
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.
8252
+ Without options the result is `{ ref, bytes }` and a trusted in-root symlink or hardlink is
8253
+ allowed. `rejectSymlinks` refuses a symlink at the requested leaf, `singleLink` checks `nlink` of
8254
+ the opened descriptor, `stable` compares dev/ino/size/mtime/ctime/nlink around the byte read, and
8255
+ `observe` adds the metadata. These refusals are distinct: `FILE_NOT_FOUND`, `FILE_NOT_REGULAR`,
8256
+ `FILE_UNSAFE_LINK`, `FILE_TOO_LARGE`, `FILE_CHANGED`, `FILE_UNSUPPORTED` and `FILE_IO_ERROR`. Abort
8257
+ keeps the caller's reason. A nonblocking open lets a FIFO be refused without waiting for a writer.
8258
+ Bytes are bounded while reading, including a file that grows, not only by the initial stat. Chunk
8259
+ allocation is at most `maxBytes + 1 + 64 KiB` and the flattened result at most `maxBytes`. The
8260
+ descriptor closes on success, refusal, abort and IO error. A close error keeps the primary read error;
8261
+ after a successful read a close error is returned to the caller.
8221
8262
 
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.
8263
+ The root and its ancestors belong to a trusted actor. These options do not give
8264
+ descriptor-relative containment against a hostile concurrent ancestor replacement, and there is no
8265
+ switch to treat ancestors as safe: the portable owner has no such capability. Metadata stability does
8266
+ not prove an immutable snapshot against an arbitrary hostile writer; use an application lock or
8267
+ immutable publication.
8226
8268
 
8227
- ## Команды
8269
+ ## Commands
8228
8270
 
8229
8271
  ```ts
8230
8272
  import { runNativeCommand } from 'stitchkit/process'
@@ -8246,21 +8288,23 @@ await runNativeCommand({
8246
8288
  })
8247
8289
  ```
8248
8290
 
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.
8291
+ A command needs a caller `signal` or a finite `timeoutMs` (1 to 2147483647 ms, the native timer
8292
+ range; a larger value is refused, never turned into 1 ms). Streaming runs for the caller's lifetime,
8293
+ with no hidden 60-second timeout or 1 MiB output limit. `capture` defaults to false; `true` requires
8294
+ `maxOutputBytes` for the combined stdout and stderr. The limit counts source bytes; the API returns
8295
+ `Uint8Array` and does not decode invalid UTF-8. The result holds the observed `exitCode: number | null`
8296
+ and `signal: string | null`; a signal termination is never turned into an invented exit code.
8297
+ `maxOutputBytes` may bound streaming too. `stdin` is bytes bounded by `maxStdinBytes` (default 1 MiB).
8298
+ `envPolicy` defaults to `declared-only`: no `env` means an empty environment, and `ambient` inherits
8299
+ explicitly. The application owns cwd and executable policy; the primitive launches a native
8300
+ executable and never creates a shell.
8257
8301
 
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.
8302
+ `NativeCommandError.reason` classifies a limit error by machine: `deadline` when the command's own
8303
+ `timeoutMs` expires, `output-budget` when the combined stdout and stderr bytes exceed the limit, in
8304
+ capture or streaming. Both keep `code: 'COMMAND_LIMIT'`; `message` is for diagnosis, not for
8305
+ protocols. `COMMAND_UNAVAILABLE` and `COMMAND_CLEANUP` have no reason. A caller abort and a sink
8306
+ failure keep the original error. When cleanup fails, the original limit and its reason stay inside the
8307
+ `AggregateError` cause.
8264
8308
 
8265
8309
  ```ts
8266
8310
  import { NativeCommandError } from 'stitchkit/process'
@@ -8276,38 +8320,137 @@ try {
8276
8320
  }
8277
8321
  ```
8278
8322
 
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.
8323
+ The constructor is `new NativeCommandError(code, message, { cause })`; it accepts
8324
+ `{ cause, reason: 'deadline' | 'output-budget' }` only for `COMMAND_LIMIT`. The owner always sets the
8325
+ reason for its own limits. An error a caller constructs without a reason gets none inferred from the
8326
+ text; its `reason` is `undefined`.
8327
+
8328
+ Output is read with backpressure, stdout and stderr separately: one running sink per channel plus a
8329
+ bounded native Readable buffer, with no accumulation of the whole history while streaming. The
8330
+ callback receives the lifetime signal. Abort releases the wait even for a callback that ignores the
8331
+ signal, but it cannot cancel third-party work the callback continues after the abort. A sink must
8332
+ cancel its own IO, must not keep an unbounded history and must not change shared descriptor flags.
8333
+ After an abort the owner starts no new callbacks. Normal completion waits for all bytes and sinks.
8334
+
8335
+ Cleanup uses the POSIX process group under the `stop` policy, then KILL; `cleanupTimeoutMs`
8336
+ (default 2000, maximum 30 000) bounds the wait for close after the signals. On
8337
+ Darwin a transient `EPERM` when signalling a group can mean zombies the OS reaper has not collected
8338
+ yet. The retry is bounded by `cleanupTimeoutMs`; success needs a delivered signal or `ESRCH`, and a
8339
+ permanent refusal is kept in the cause. The close of every owned pipe and the leader's exit are
8340
+ observed separately, including the absence of a shared `child.close` during teardown. The caller or
8341
+ sink cause is kept; a cleanup failure is a `NativeCommandError` with `COMMAND_CLEANUP` and an
8342
+ `AggregateError` cause. Even an exited parent does not exclude a helper that holds a pipe. Windows
8343
+ refuses with `COMMAND_UNAVAILABLE`; a descendant that has left the group on its own with `setsid` is
8344
+ outside this capability. This is one-shot execution, not a PTY, a supervisor or a daemon host.
8345
+
8346
+ ### Stopping a command: the `stop` policy
8347
+
8348
+ A caller abort, the `timeoutMs` deadline, an exceeded output budget and a failing sink all stop the
8349
+ command the same way, by its `stop` policy:
8350
+
8351
+ | field | meaning |
8352
+ |---|---|
8353
+ | `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. |
8354
+ | `signal` | `SIGTERM` (default), `SIGINT`, `SIGHUP`, `SIGQUIT`, `SIGUSR1` or `SIGUSR2`. KILL is not a stop signal: it is what the grace ends in. |
8355
+ | `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. |
8356
+ | `killOn` | Optional `AbortSignal`. Its abort stops the command with KILL to the whole group at once, also in the middle of a grace. |
8357
+
8358
+ Omitting `stop` means `{ target: 'group', signal: 'SIGTERM', graceMs: 100 }`.
8359
+
8360
+ ```ts
8361
+ // Cooperative cancellation: the leader rolls its work back, its helpers are not asked.
8362
+ await runNativeCommand({
8363
+ executable: '/usr/local/bin/migrate', args: ['apply'], signal: request.signal,
8364
+ stop: { target: 'leader', signal: 'SIGINT', graceMs: 15 * 60_000, killOn: shutdown.signal },
8365
+ })
8366
+ ```
8367
+
8368
+ With `target: 'leader'` the rest of the group never receives the cooperative signal: they are
8369
+ processes the leader started, and the leader decides what happens to them. Whatever is left in the
8370
+ group when the leader exits, or when the grace ends, gets KILL, including a helper with its stdio
8371
+ closed that no pipe would have revealed. A leader that has already exited is never signalled again.
8372
+
8373
+ The output pipes stay open until the stop sequence ends. Output a stopping command writes is read and
8374
+ dropped, never passed to `onOutput` and never captured, so a leader that writes while it shuts down
8375
+ does not meet a broken pipe. The result rejects with the abort reason once the group is gone and the
8376
+ pipes are closed, so it can take `graceMs` plus `cleanupTimeoutMs` after the abort.
8377
+
8378
+ The grace is a timer inside the process that runs the command. If that process exits first, the
8379
+ timer is gone and the group, which is detached into its own session, keeps running: nothing in this
8380
+ package can bound it from outside. Abort `killOn` in your shutdown path, so the group is killed
8381
+ before the process exits, and let the supervisor own the case where the process dies without a
8382
+ shutdown. Under systemd, keep `KillMode=control-group` (the default) so the stop of the unit
8383
+ reaches every process in its cgroup, whatever its process group; launchd only kills the job's own
8384
+ process group, which a command's group is not.
8385
+
8386
+ ### Stopping a group and its descendants
8387
+
8388
+ A group is signalled only while the kernel still reports members. `ESRCH` from the first signal,
8389
+ or from the existence probe that follows it during the grace, means no member is left: no KILL
8390
+ is sent afterwards, because a numeric group id that has vanished can already belong to an
8391
+ unrelated group. A group that is still visible after the grace period receives KILL.
8392
+
8393
+ `descendants` declares what happens to processes the leader left running once it has exited
8394
+ successfully:
8395
+
8396
+ | `descendants` | After the leader exits |
8397
+ |---|---|
8398
+ | `'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. A descendant that had already left the group with `setsid` when the leader exited stays out of reach. |
8399
+ | `'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. |
8400
+
8401
+ Omitting the option means `'terminate-after-leader'`: a command does not leak the helpers it
8402
+ started. The result's `descendantsStopped` is `true` when the group still had members after the
8403
+ leader exited and they were stopped, so a helper that was ended this way is visible to the caller
8404
+ rather than only missing afterwards; under `'leave'` it is always `false`.
8405
+
8406
+ A real daemon detaches into its own session (`setsid`, as `daemon(3)` does), is no member of the
8407
+ group and is unaffected, provided it has left the group **before the leader exits**. Leaving is
8408
+ the daemon's own system call, and the group is stopped as soon as the leader's exit is observed,
8409
+ so a helper started in the background races the leader:
8410
+
8411
+ | launched by the leader as | after the leader exits |
8412
+ |---|---|
8413
+ | `setsid -f helper` (returns once the new session exists) | survives |
8414
+ | `setsid helper &`, `(setsid helper &)`, `nohup helper &` | stopped: the leader usually exits before the background job has left the group |
8415
+ | any of these under `descendants: 'leave'` | survives |
8416
+
8417
+ Start a daemon so that the session exists before the leader returns (`setsid -f`, or the
8418
+ daemon's own double fork followed by the leader waiting for it), or declare `'leave'`. Declare
8419
+ `'leave'` only for a helper that has to outlive the command while staying in its group:
8420
+
8421
+ ```ts
8422
+ await runNativeCommand({
8423
+ executable: '/usr/local/bin/start-helper', timeoutMs: 60_000,
8424
+ // the helper started here outlives this command on purpose
8425
+ descendants: 'leave',
8426
+ })
8427
+ ```
8428
+
8429
+ A command whose group this package did not create (a structural
8430
+ launcher supplied by a host) has no group to stop, and `descendants` has nothing to act on there.
8431
+
8432
+
8433
+ ## Replacing hand-written wrappers
8434
+
8435
+ An atomic-file wrapper becomes one `writeFileAtomic` call with the chosen options. A bounded
8436
+ regular-file read becomes `files.read`; JSON and schema parsing, receipt conflicts, journal rotation
8437
+ and application locks stay with the application. A spawn, drain, capture and cancellation wrapper
8438
+ becomes `runNativeCommand`; shell policy, command admission, logging, redaction, the bytes-to-text
8439
+ decoder and the operator sink stay with the application. Long signal-only commands stay streaming;
8440
+ bounded tar or capture commands get their own explicit limits.
8441
+
8442
+ A child the caller holds until it exits is the same call. A guardian that forwards its own SIGTERM
8443
+ passes an abort signal and `stop: { target: 'leader', signal: 'SIGTERM', graceMs }`; it reads the
8444
+ exit code in `onLeaderSettled`, before the pipes drain, and stderr through `onOutput` or `capture`
8445
+ with `maxOutputBytes`, and `cleanupTimeoutMs` bounds the drain (`COMMAND_CLEANUP` when a holder
8446
+ keeps the pipe past it). A worker whose whole group must stop on abort is the default
8447
+ `target: 'group'`. A child that inherits the caller's terminal is outside this capability: every
8448
+ command runs in a group of its own, and a background group that reads the terminal is stopped
8449
+ by the kernel (`SIGTTIN`), so an interactive re-execution stays a plain spawn in the caller's
8450
+ group. Importing one leaf does not make
8451
+ installing the whole npm package smaller; choose a dependency after measuring packed size and its
8452
+ dependency closure.
8453
+
8311
8454
  ## Resource-scoped launchers and shared owners
8312
8455
 
8313
8456
  `onLeaderSettled(event, signal)` observes leader exit before inherited stdout/stderr pipes
@@ -8349,8 +8492,23 @@ syscall interruptible or provide a snapshot against a hostile writer.
8349
8492
  An exclusive lock applies exact requested permissions through its creating descriptor,
8350
8493
  independent of umask. Default `0600` stays private. Shared `0640` requires a common group and
8351
8494
  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.
8495
+ proven-dead owner's file.
8496
+
8497
+ The lock is published with its owner already recorded: the owner record is written to a
8498
+ private temporary file in the lock's directory, then hard-linked to the lock name. `link`
8499
+ fails with `EEXIST` for every caller but one, and the name never exists without a complete
8500
+ record, so a holder that stalls or dies before publishing leaves no lock and cannot be
8501
+ displaced. The directory must therefore be on a filesystem that supports hard links. A crash
8502
+ between the temporary write and the link can leave a `.lock-*.tmp` file behind; it is not a
8503
+ lock and nothing waits on it.
8504
+
8505
+ `ownerlessGraceMs` defaults to `null`: a lock file with no readable owner is never taken
8506
+ because of its age. This library does not produce such a file; only a writer that creates the name
8507
+ before its owner record can leave one (for example an empty file from a crashed process).
8508
+ `ownerlessGraceMs: <ms>` opts in to reclaiming an empty lock file older than that, and also governs
8509
+ abandoned empty reclaim guards.
8510
+ When unset, an empty reclaim guard left by an older writer is taken after 5 000 ms so recovery
8511
+ is never disabled for good; `null` refuses that too.
8354
8512
  Stale reclaim guards use the same lock owner and a child guard for the inode check and
8355
8513
  unlink. Recovery depth is capped at 16; a deeper stale chain refuses recovery with a depth diagnosis retained as the cause of
8356
8514
  the bounded `LOCK_TIMEOUT`. Live owners, unknown process-lifetime evidence and unsafe records are never
@@ -8376,27 +8534,26 @@ version: carry their reachable relative `.d.ts` closure, rather than emitting re
8376
8534
  instead emit declarations for only that DTO contract. Keep the library's manifest at runtime
8377
8535
  `dependencies: {}` and `peerDependencies: { zod: ... }` when that is its declared promise.
8378
8536
 
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.
8537
+ The packaged Darwin loader names its addons by a path computed at run time, so a bundler
8538
+ never follows it: bundling `stitchkit/server`, `stitchkit/files` or `stitchkit/process` yields
8539
+ the same single JS file on every operating system, with no `.node` output, and
8540
+ `bun build --outfile` works. Such a bundle looks for the addon beside the original package
8541
+ files, never inside itself. Where that file is absent (an artifact moved to another machine)
8542
+ the Darwin backend is `unavailable`: process identity cannot be recorded, so a lock held by a
8543
+ crashed owner is not reclaimed. An artifact that must carry the Darwin addon uses the packaging
8544
+ plugin of the next section; it is the only way a static loader enters a bundle.
8384
8545
 
8385
8546
  ```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
8547
+ bun build src/native.ts --target=bun --minify --outfile=dist/native.js
8388
8548
  ```
8389
8549
 
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
8550
  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.
8551
+ identity, live-owner refusal, dead-owner recovery and contained file operations in artifacts built
8552
+ with the packaging plugin, and check that an unpackaged build reports the backend `unavailable`
8553
+ rather than certifying a dead owner. Bun and Node package imports retain the same
8554
+ lazy loader; importing a portable leaf does not load a Darwin addon on another OS. Test
8555
+ declarations too, and check that importing an unbundled Stitchkit leaf fails in a deliberately
8556
+ isolated library distribution. A separate schema-only entry must not import the native entry.
8400
8557
 
8401
8558
  If the Darwin backend cannot load, `observeProcessInstance` still returns `unavailable`.
8402
8559
  Its backend `Error` preserves the original `cause`; JSON serialization carries only the stable
@@ -8412,17 +8569,21 @@ Stitchkit installation or create an independent lightweight npm package.
8412
8569
  ## Public native packaging
8413
8570
 
8414
8571
  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.
8572
+ `createNativePackaging` resolves the installed version and target addon, reads the addon once and
8573
+ checks it against the size and SHA256 that Stitchkit published in its `native-assets.json` when the
8574
+ package was built. A ready asset carries those verified `bytes` with the published `size` and
8575
+ `sha256`; its Bun-compatible plugin integrates the same bytes with the same lazy native loader.
8576
+ Each call reads and hashes the selected addons, so call it once per build and reuse the result.
8417
8577
  The leaf is evolving. It requires no optional peer or Bun ambient declarations for Node imports.
8418
- 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.
8578
+ Calling it must happen in a process that runs it from its installed package: it locates the
8579
+ package's `native-assets.json` beside its own file and throws `Native packaging must run from its
8580
+ installed Stitchkit package` anywhere else, such as inside an application's runtime artifact.
8581
+ Importing the leaf is side-effect free (it loads no addon and reads no file), so a module that
8582
+ both builds and is bundled may import it; only the call has to stay in the build step.
8421
8583
 
8422
8584
  ```ts
8423
8585
  import { createNativePackaging } from 'stitchkit/files/packaging'
8424
- import { createHash } from 'node:crypto'
8425
- import { copyFileSync, mkdirSync, readFileSync } from 'node:fs'
8586
+ import { mkdirSync, writeFileSync } from 'node:fs'
8426
8587
  import { dirname, join } from 'node:path'
8427
8588
 
8428
8589
  const native = createNativePackaging({
@@ -8436,28 +8597,33 @@ const result = await Bun.build({
8436
8597
  plugins: [native.plugin],
8437
8598
  })
8438
8599
  if (!result.success) throw new AggregateError(result.logs, 'Build failed')
8439
- // Preserve every result.outputs file under dist, then add the selected companion.
8600
+ // Preserve every result.outputs file under dist, then write the verified companion bytes.
8440
8601
  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
8602
  const destination = join('dist', asset.outputPath)
8445
8603
  mkdirSync(dirname(destination), { recursive: true })
8446
- copyFileSync(asset.sourcePath, destination)
8604
+ writeFileSync(destination, asset.bytes)
8447
8605
  }
8448
8606
  ```
8449
8607
 
8450
8608
  `entryPath` and `assetPath` are relative paths inside the application's output root, without
8451
- absolute paths, traversal or overlapping file/directory paths. `entryPath` is fixed, without
8609
+ absolute paths, traversal or overlapping file/directory paths. `assetPath` may contain `[hash]`,
8610
+ which becomes the first 16 hex digits of that addon's published SHA256: the digest is known before
8611
+ the build, so `addons/darwin-arm64-[hash].node` gives two installed versions of the package
8612
+ different companion names, and the loader requires exactly that name. It is the only template an
8613
+ addon path accepts. `entryPath` is fixed, without
8452
8614
  Bun naming templates such as `[dir]` or `[name]`. Companion JS builds require one entry, no splitting,
8453
8615
  and `naming.entry` exactly matching `entryPath`; the plugin rejects a mismatching layout.
8454
8616
  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.
8617
+ Keep every bundler output and write `asset.bytes`; never read the addon from `node_modules` a
8618
+ second time. An installed addon whose size or SHA256 differs from the published manifest refuses
8619
+ with `mismatch` / `NATIVE_ASSET_DIGEST_MISMATCH`, so a file substituted after installation never
8620
+ reaches an artifact. Archive the complete output directory with an integrity manifest that records
8621
+ `asset.sha256`, unpack to a clean directory, verify the same hashes there, and run offline without
8622
+ the build tree or `node_modules`. The published digest ties the bytes to the package Stitchkit
8623
+ built; the package itself is only as trusted as its tarball (the lockfile integrity of the install).
8624
+ Signature and trust policy remain application-owned.
8459
8625
 
8460
- From 0.104.2, one JS artifact can run on both Darwin architectures; change only the packaging inputs:
8626
+ One JS artifact can run on both Darwin architectures; change only the packaging inputs:
8461
8627
 
8462
8628
  ```ts
8463
8629
  const native = createNativePackaging({
@@ -8467,10 +8633,10 @@ const native = createNativePackaging({
8467
8633
  })
8468
8634
  ```
8469
8635
 
8470
- Use the same single plugin and copy loop above. A single-target loader deliberately refuses the
8636
+ Use the same single plugin and write loop above. A single-target loader deliberately refuses the
8471
8637
  other architecture; two competing plugins cannot produce a universal loader. The array form
8472
8638
  requires exactly one distinct output path per declared target. A ready result carries the target
8473
- array and both original asset hashes; `NativePackagingOptions<true>` /
8639
+ array and both verified assets with their published digests; `NativePackagingOptions<true>` /
8474
8640
  `NativePackagingResult<true>` describe this form when explicitly annotating variables.
8475
8641
  The loader selects only `process.arch`: a missing or wrong-architecture selected addon refuses,
8476
8642
  even when the other valid addon is present. Linux runtime imports remain lazy and use Linux
@@ -8479,17 +8645,36 @@ Darwin arm64 and x64 machines, with both companions preserved through archive de
8479
8645
  Build-machine architecture and cross-build success cannot establish universal native support.
8480
8646
 
8481
8647
  For a standalone executable use `delivery: 'embedded'` and the same plugin in `Bun.build`
8482
- with `compile: { outfile: ... }`. Bun embeds the selected addon; `assets` identifies its
8483
- original bytes for qualification, not a companion that must be installed beside the executable.
8648
+ with `compile: { outfile: ... }`. Bun embeds the selected addon from the verified bytes the
8649
+ plugin holds, not from a second read of the file; `assets` identifies those bytes and their
8650
+ published digest for qualification, not a companion that must be installed beside the executable.
8484
8651
  Cross-builds must select the requested architecture explicitly; the artifact must run on that target.
8485
8652
  The installed package must contain that target's addon. No automatic fallback/downgrade occurs.
8486
8653
 
8487
- The API returns `unsupported` / `NATIVE_TARGET_UNSUPPORTED` for non-Darwin or unsupported
8488
- architectures, and `missing` / `NATIVE_ASSET_MISSING` for a missing addon. Portable Linux libraries
8489
- use their normal build, without a Darwin plugin; merely importing the build leaf loads no addon.
8490
- Malformed inputs, unsupported metadata versions and other IO errors throw before packaging.
8491
- Runtime missing/corrupt companions preserve the native `unavailable` result and safe stage/code
8492
- from the existing observer; they never certify a dead owner. Preserve the raw cause internally.
8654
+ `platform` is the closed set of platforms with native addons, today `'darwin'`: another name is a
8655
+ type error and a schema refusal that throws, so pass the literal (or narrow `process.platform`
8656
+ with `=== 'darwin'`) rather than a free string. The API returns `unsupported` /
8657
+ `NATIVE_TARGET_UNSUPPORTED` for an unsupported architecture, `missing` / `NATIVE_ASSET_MISSING` for
8658
+ an addon the package does not publish or whose file is gone, and `mismatch` /
8659
+ `NATIVE_ASSET_DIGEST_MISMATCH` for an addon whose bytes differ from the published size or SHA256.
8660
+ A `mismatch` names the first differing `architecture` and carries `expected` (the published
8661
+ `{ size, sha256 }`) and `actual` (the installed file's size and the SHA256 of the bytes read), so
8662
+ a build log can tell a truncated copy from a substituted one:
8663
+
8664
+ ```ts
8665
+ if (native.state === 'mismatch')
8666
+ throw new Error(
8667
+ `${native.architecture} addon: expected ${native.expected.size} bytes ` +
8668
+ `${native.expected.sha256}, found ${native.actual.size} bytes ${native.actual.sha256}`,
8669
+ )
8670
+ ```
8671
+
8672
+ Portable libraries
8673
+ and Linux builds use their normal build, without a Darwin plugin; merely importing the build leaf
8674
+ loads no addon.
8675
+ Malformed inputs, a manifest whose `formatVersion` is not 2 and other IO errors throw before packaging.
8676
+ A missing or corrupt companion at runtime yields the native `unavailable` result with the safe
8677
+ stage and code from the observer; it never certifies a dead owner. Preserve the raw cause internally.
8493
8678
  The plugin does not parse consumer JS or establish a second runtime native implementation.
8494
8679
 
8495
8680
  ## Qualification boundaries
@@ -8508,24 +8693,19 @@ the qualification for native Darwin behavior.
8508
8693
 
8509
8694
  Local filesystem tests do not attest delayed NFS visibility, shared-volume microVM or gVisor
8510
8695
  ownership, actual PGID reuse, power-loss durability or macOS full-sync behavior. Those
8511
- require their actual topology or failure environment. The default ownerless grace remains
8512
- 5000 ms; stricter callers can retain `ownerlessGraceMs: null`. No age-only observation
8513
- proves a partially published owner's process dead.
8696
+ require their actual topology or failure environment. No age-only observation proves a
8697
+ process dead; the lock's owner record exists before its name does.
8698
+
8699
+ A sandbox admission slot is released once a direct command's pipes have closed and its owner has settled.
8700
+ A `COMMAND_CLEANUP` outcome means the death of its group was not proven, so the slot stays
8701
+ occupied, `stop` keeps rejecting with the cleanup error, and the sandbox session is recreated to
8702
+ recover.
8514
8703
 
8515
8704
 
8516
8705
  ==============================================================================
8517
8706
  # Guide: Generic application primitives (docs/guide/primitives.md)
8518
8707
  ==============================================================================
8519
8708
 
8520
- ---
8521
- title: Generic application primitives
8522
- description: Declare lifecycle, access, exact values, audit, delivery and exports without moving application infrastructure into Stitchkit.
8523
- type: guide
8524
- status: active
8525
- created: 2026-09-01
8526
- updated: 2026-10-01 21:03 +07:00
8527
- ---
8528
-
8529
8709
  # Generic application primitives
8530
8710
 
8531
8711
  `stitchkit/primitives` is an optional browser-safe leaf. It declares values and policies; it does
@@ -8628,33 +8808,50 @@ const bytes = canonicalJson({ '2': 'two', '10': 'ten', optional: undefined, nest
8628
8808
  // Native crypto or browser WebCrypto hashes these same UTF-8 bytes.
8629
8809
  ```
8630
8810
 
8631
- `canonicalJson` использует единый serializer исторических digest: object keys в UTF-16 code-unit
8632
- order, включая integer-like keys; array order сохраняется. Optional object members с undefined
8633
- опускаются. Top-level/array undefined, sparse arrays, functions/symbols/BigInt, non-finite numbers,
8634
- negative zero, cycles, user prototypes, accessors, non-enumerable/symbol members отказаны до
8635
- serialization. Приложение валидирует свою schema первым; canonical boundary не заменяет её.
8811
+ `canonicalJson` is the one serializer behind the digests: object keys sort by UTF-16 code unit,
8812
+ integer-like keys included; array order is kept; object members that are `undefined` are omitted.
8813
+ It refuses, before serializing, a top-level or array `undefined`, sparse arrays, functions, symbols,
8814
+ BigInt, non-finite numbers, negative zero, cycles, user prototypes, accessors and non-enumerable or
8815
+ symbol members. Validate your own schema first; the canonical boundary does not replace it.
8816
+
8817
+ Defaults: depth 100 (the maximum), nodes 100 000, UTF-8 bytes 1 MiB. Invalid options throw a Zod
8818
+ validation error. A refused value throws `CanonicalJsonError`, a `TypeError` whose `reason` is a
8819
+ `CanonicalJsonRefusal`; branch on the reason, never on the message:
8636
8820
 
8637
- Defaults: depth 100 (maximum 100), nodes 100 000, UTF-8 bytes 1 MiB. Превышение или не-JSON data —
8638
- TypeError; неверные options — Zod validation error. Это browser-safe leaf без Node/Bun/crypto,
8639
- agent-runtime/testing или optional peers. Native и browser используют один algorithm; hashing
8640
- остаётся platform adapter. Переносите только serializer recursion, сохраняя proof schemas и
8641
- verification boundary. Существующие внутренние historical normalization/digest fixtures не меняются;
8642
- новый public boundary сознательно отказывает неоднозначным values вместо фиктивной совместимости.
8821
+ | `reason` | Meaning |
8822
+ |---|---|
8823
+ | `depth` | The value is nested deeper than `maxDepth`. |
8824
+ | `nodes` | The value has more nodes than `maxNodes`. |
8825
+ | `bytes` | The output is longer than `maxBytes` in UTF-8. |
8826
+ | `cycle` | The value contains a cycle. |
8827
+ | `negative-zero` | The value contains `-0`, which JSON cannot carry. |
8828
+ | `not-json` | The value is not bounded plain JSON: sparse arrays, non-finite numbers, non-plain objects, accessors. |
8829
+
8830
+ `depth`, `nodes` and `bytes` are the caller's limits being exceeded; the other three say the value is
8831
+ not JSON data at all.
8832
+
8833
+ ```ts
8834
+ import { CanonicalJsonError, canonicalJson } from 'stitchkit/primitives'
8835
+
8836
+ try {
8837
+ canonicalJson(payload, { maxBytes: 4096 })
8838
+ } catch (error) {
8839
+ if (error instanceof CanonicalJsonError && error.reason === 'bytes') {
8840
+ // too large: reject the request
8841
+ } else throw error
8842
+ }
8843
+ ```
8844
+
8845
+ The leaf is browser-safe: no Node, Bun or crypto, no `agent-runtime/testing`, no optional peers. Native
8846
+ and browser code run the same algorithm; hashing stays a platform adapter. A consumer that needs the
8847
+ same bytes moves only the serializer recursion and keeps its own proof schemas and verification
8848
+ boundary.
8643
8849
 
8644
8850
 
8645
8851
  ==============================================================================
8646
8852
  # Guide: Application migration recipes (docs/guide/application-migration-recipes.md)
8647
8853
  ==============================================================================
8648
8854
 
8649
- ---
8650
- title: Application migration recipes
8651
- description: Executable patterns for moving database, poller, queue and operational publishing lifecycle into the managed application kernel.
8652
- type: architecture
8653
- status: active
8654
- created: 2026-08-24
8655
- updated: 2026-10-04 13:28 +07:00
8656
- ---
8657
-
8658
8855
  # Application migration recipes
8659
8856
 
8660
8857
  These recipes are the migration companion to the
@@ -8806,10 +9003,10 @@ guard must refuse. Filesystem mutations made inside inbox transactions call
8806
9003
  `assertHeld` immediately before publication or cleanup.
8807
9004
 
8808
9005
  For `createFileStateStore`, remove `staleLockMs` and `retryMs` from configuration.
8809
- `lockTimeoutMs` remains the acquisition budget. Timestamp age no longer revokes
9006
+ `lockTimeoutMs` is the acquisition budget. Timestamp age never revokes
8810
9007
  a live or unknown writer. Stop and verify old writers before recovering a legacy
8811
- pid-only lock; do not invent owner metadata. Historical temporary files are no
8812
- longer deleted by age. See the [durable intake contract](application-kernel.md#durable-process-facts-and-owner-notifications).
9008
+ pid-only lock; do not invent owner metadata. Temporary files are never
9009
+ deleted by age. See the [durable intake contract](application-kernel.md#durable-process-facts-and-owner-notifications).
8813
9010
 
8814
9011
  ## Long-running poller
8815
9012
 
@@ -9057,9 +9254,10 @@ declarations do not require `ai`, the MCP SDK or Bun globals. Node consumers can
9057
9254
  use strict `NodeNext` resolution with `skipLibCheck: false` and ordinary Node
9058
9255
  types; the package keeps its required dependencies separate from optional SDKs.
9059
9256
 
9060
- CLI runtime tools use the shared executable contract: identity, input and output
9061
- schemas, handler, and explicit CLI exposure. SDK presenters belong to the MCP/AI
9062
- definitions in `stitchkit/tools`, which require the corresponding peers. A typed
9257
+ CLI runtime tools use the one runtime-tool declaration: identity, input and output
9258
+ schemas, handler, and explicit CLI exposure. SDK presenters are the adapter's extension
9259
+ of that declaration (the fourth type argument of `RuntimeToolDefinitionWithOutput`, or
9260
+ `defineRuntimeTool` in `stitchkit/tools`), which requires the corresponding peers. A typed
9063
9261
  definition returned by `defineRuntimeTool` or `createRuntimeToolFactory` can be
9064
9262
  passed directly to both CLI and SDK mounts.
9065
9263
 
@@ -9073,7 +9271,7 @@ For a CLI-only managed tool, use the neutral generic construction contract to
9073
9271
  infer the handler from its schemas:
9074
9272
 
9075
9273
  ```ts
9076
- import type { RuntimeToolExecutionWithOutput } from 'stitchkit/cli'
9274
+ import type { RuntimeToolDefinitionWithOutput } from 'stitchkit/cli'
9077
9275
  import { z } from 'zod'
9078
9276
 
9079
9277
  const input = z.object({ text: z.string() })
@@ -9083,12 +9281,14 @@ const measure = {
9083
9281
  identity: { serviceName: 'text', action: 'measure', method: 'POST' },
9084
9282
  transports: ['CLI'], input, output,
9085
9283
  handler: ({ input }) => ({ size: input.text.length }),
9086
- } satisfies RuntimeToolExecutionWithOutput<typeof input, typeof output>
9284
+ } satisfies RuntimeToolDefinitionWithOutput<typeof input, typeof output>
9087
9285
  ```
9088
9286
 
9089
- Pass `measure` to `runtimeTools`. The heterogeneous registration type preserves
9090
- the definition without inventing a common handler input; execute it through
9091
- `createCli` or `createCliInvoker` so validation remains on the canonical path.
9287
+ Pass `measure` to `runtimeTools`. `RuntimeToolDefinition`, the heterogeneous
9288
+ registration type, accepts it unchanged, and types an inline handler written directly
9289
+ in `runtimeTools` by the parsed object (use the construction type above to type the
9290
+ input from the schema). Execute it through `createCli` or `createCliInvoker` so
9291
+ validation remains on the canonical path.
9092
9292
 
9093
9293
  ## Exposure is opt-in
9094
9294
 
@@ -9278,7 +9478,9 @@ the values agree. Duplicates are refused before command execution. Array
9278
9478
  options accumulate across long and short forms; unknown non-dotted passthrough
9279
9479
  options retain their raw-list grammar. Tokens after `--` are literal data.
9280
9480
  Command `--help` and `-h` keep their diagnostic precedence over malformed
9281
- operation arguments; application globals are validated before routing.
9481
+ operation arguments; application globals are validated before routing. Help is a
9482
+ request, not a setting, so repeating it in any spelling (`-h -h`, `-h --help`) asks once
9483
+ and never errors.
9282
9484
 
9283
9485
  Without presentation configuration, positional arguments fill non-boolean
9284
9486
  fields in declaration order, so `myapp generate "a fox"` is
@@ -9505,8 +9707,10 @@ Declared aliases render beside their canonical options, for example
9505
9707
  The argument table and `Application options:` show bounds from their JSON Schema,
9506
9708
  for example `<integer> [>=1, <=50]`, `<string> [length >=1, length <=12]`, and
9507
9709
  `<value…> [items >=2, items <=4]`. Numeric `>` and `<` are exclusive, while `>=`
9508
- and `<=` include the endpoint; a zero limit is displayed too. Unbounded fields
9509
- keep just their type and description. Bounds on an array's items are not bounds
9710
+ and `<=` include the endpoint; a zero limit is displayed too. Only limits the
9711
+ schema declares are shown: the safe-integer range every `z.int()` carries is the
9712
+ type, not a bound, so a bare integer prints `<integer>`, `.min(1)` prints `>=1` and
9713
+ `.positive()` prints `>0`. Unbounded fields keep just their type and description. Bounds on an array's items are not bounds
9510
9714
  on the array itself.
9511
9715
 
9512
9716
  Composed schemas retain their meaning: `any of:` and `one of:` separate
@@ -9734,11 +9938,16 @@ exists to distrust.
9734
9938
 
9735
9939
  `publishCli` is an opt-in publisher for a local application-owned distribution
9736
9940
  directory. It commits every target and its version manifest before atomically
9737
- replacing the public `manifest.json`. Your application selects the CLI source
9738
- identity and admits that snapshot at each phase:
9941
+ replacing the public `manifest.json`. It ships from its own evolving entrypoint,
9942
+ `stitchkit/cli/publish`: release infrastructure follows how distributions are
9943
+ published, so it is not held to the stable promise of `stitchkit/cli`
9944
+ ([ADR 0245](../decisions/0245-cli-publication-is-an-evolving-leaf.md)). The
9945
+ manifest, signatures, installer and updater it writes for stay in
9946
+ `stitchkit/cli`. Your application selects the CLI source identity and admits
9947
+ that snapshot at each phase:
9739
9948
 
9740
9949
  ```ts
9741
- import { publishCli } from 'stitchkit/cli'
9950
+ import { publishCli } from 'stitchkit/cli/publish'
9742
9951
 
9743
9952
  const result = await publishCli({
9744
9953
  storageRoot: '/srv/myapp/cli',
@@ -10063,7 +10272,7 @@ await createCli({ name: 'myapp', version, runtimeTools: discovered, commands: [.
10063
10272
  commands, and a connection without it still contributes nothing to the CLI —
10064
10273
  exposure stays explicit, as it is everywhere else in the framework.
10065
10274
 
10066
- One unconvertible schema no longer takes the connection down with it. The tool
10275
+ One unconvertible schema does not take the connection down. The tool
10067
10276
  is skipped and **named** (`onSkippedTool`, or a stderr line by default), so a
10068
10277
  surface of two hundred tools is not lost to one.
10069
10278
 
@@ -10096,13 +10305,12 @@ credential, body, identity and stack stay with the original in-process cause in
10096
10305
  Unknown throws remain `INTERNAL_SERVER_ERROR`. A timeout is not proof that a
10097
10306
  write did not happen: verify the remote outcome before repeating an operation.
10098
10307
 
10099
- **A remote refusal keeps its code.** A failed `tools/call` used to become a
10100
- one-sentence `Error` with the result discarded, which cost three things at once:
10101
- the code (so `exitCodes` had nothing to map and every remote failure exited `1`),
10102
- the message the operator needed, and the error's own class — a plain `Error` is
10103
- an *unexpected* error to the runner, so it printed a code frame of the framework
10104
- bundle before the JSON failure and was then scrubbed to `INTERNAL_SERVER_ERROR`.
10105
- A structured `{ error, details }` body is now relayed as the contract error it
10308
+ **A remote refusal keeps its code.** A failed `tools/call` must keep three things:
10309
+ the code (so `exitCodes` can map it instead of every remote failure exiting `1`),
10310
+ the message the operator needs, and the error's own class — a plain `Error` is
10311
+ an *unexpected* error to the runner, which would print a code frame of the framework
10312
+ bundle before the JSON failure and scrub it to `INTERNAL_SERVER_ERROR`.
10313
+ A structured `{ error, details }` body is relayed as the contract error it
10106
10314
  is, on every transport; anything else fails as `UPSTREAM_TOOL_ERROR` carrying
10107
10315
  what the server did send. Not `INTERNAL_SERVER_ERROR`, because nothing of ours
10108
10316
  broke.
@@ -11251,15 +11459,6 @@ watch client; silent lost notifications are repaired by hub reconciliation.
11251
11459
  # Guide: Live data (docs/guide/live.md)
11252
11460
  ==============================================================================
11253
11461
 
11254
- ---
11255
- title: Live data — announcements, watched reads, keyspaces and the trust fence
11256
- 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.
11257
- type: guide
11258
- status: active
11259
- created: 2026-09-02
11260
- updated: 2026-09-02
11261
- ---
11262
-
11263
11462
  # Live data
11264
11463
 
11265
11464
  Four things an application builds by hand the second time it needs them.
@@ -11671,11 +11870,11 @@ Every refusal answers the same bare 403. A response that said *which* rule
11671
11870
  refused would let a caller learn your trusted list one guess at a time; the
11672
11871
  reason goes to `onRefused` and the log, where you are.
11673
11872
 
11674
- ### One thing that changed
11873
+ ### Group hooks
11675
11874
 
11676
- A route group can no longer declare `onRequest`. It never worked — the framework
11677
- dispatched only the server-level hook — and it could not be made to work without
11678
- matching group prefixes a second time, ahead of the real router. Use the
11875
+ A route group cannot declare `onRequest`: the framework dispatches only the
11876
+ server-level hook, and a group hook would need group prefixes matched a second
11877
+ time, ahead of the real router. Use the
11679
11878
  server-level `hooks.onRequest` to refuse before dispatch, or the group's
11680
11879
  `authorize` to gate once the endpoint is known.
11681
11880
 
@@ -12350,7 +12549,7 @@ whether the code declares `details`:
12350
12549
 
12351
12550
  The model-facing envelope is `{ error, retryable, details?, _hint? }`; for a code with no
12352
12551
  details schema the framework fills `details` with `{ message }`, so declaring a
12353
- message changes what the model reads there — it used to be the code itself. Put
12552
+ message changes what the model reads there; without one it is the code itself. Put
12354
12553
  anything the model must reliably read in `details` or `hint`, not in `message`.
12355
12554
 
12356
12555
  With no `details` schema, the options object forbids `details`. A required
@@ -12863,6 +13062,32 @@ Without an `observability` the work still runs inside a context, so trace ids,
12863
13062
  decision from having a context. `traceparent` continues the trace of whatever
12864
13063
  scheduled the work, instead of starting an unrelated one.
12865
13064
 
13065
+ ### CPU is not attributed to work
13066
+
13067
+ Request rows, `runUnitOfWork` rows and schedule status carry durations and
13068
+ counts, never CPU. One JavaScript thread interleaves every operation, so a
13069
+ `process.cpuUsage()` delta across an operation's start and end measures
13070
+ everything the process ran in that time — a long wait reads as the CPU of the
13071
+ timers that fired during it. The correct basis is the synchronous segment, and
13072
+ Bun gives no callback when a continuation is entered or left
13073
+ ([ADR 0244](../decisions/0244-cpu-is-not-attributed-to-operations.md)).
13074
+
13075
+ Process CPU per window is the number you can observe:
13076
+
13077
+ ```ts
13078
+ let last = process.cpuUsage()
13079
+ setInterval(() => {
13080
+ const now = process.cpuUsage() // cumulative { user, system } in µs
13081
+ logger.info('cpu', { cpuUserUs: now.user - last.user, cpuSystemUs: now.system - last.system })
13082
+ last = now
13083
+ }, 60_000)
13084
+ ```
13085
+
13086
+ If you build attribution yourself: credit an operation only the CPU of the
13087
+ synchronous segments it ran, never its waiting time; put everything outside those
13088
+ segments into `unattributed`; never credit a window to the one operation in
13089
+ flight — that operation is usually the one waiting.
13090
+
12866
13091
  ### Write the calls that changed something
12867
13092
 
12868
13093
  `auditChanges` is the filter most projects end up writing, shipped so they do not
@@ -12937,8 +13162,8 @@ needed:
12937
13162
  createServer({ services, logging, observability: observability.request })
12938
13163
  ```
12939
13164
 
12940
- `wrapInRequestContext` remains available for a custom fetch pipeline that does
12941
- not use `createHandler`; it is no longer part of built-in HTTP audit wiring:
13165
+ `wrapInRequestContext` serves a custom fetch pipeline that does not use
13166
+ `createHandler`; built-in HTTP audit wiring does not use it:
12942
13167
 
12943
13168
  ```ts
12944
13169
  Bun.serve({ fetch: wrapInRequestContext(customFetch) })
@@ -13275,9 +13500,9 @@ which the built-in **tool** row does not read: a tool event takes
13275
13500
  `errorCode` / `errorMessage` / `errorDetail` from the `ToolResult`, and only
13276
13501
  identity and `dimensions` from the context. Calling it in `onToolError` would
13277
13502
  leave the tool row exactly as scrubbed as before. It is right for the **HTTP**
13278
- path, where the request *is* the record. (Since ADR 0045 a tool call runs in its
13279
- own context, so it can no longer write into the enclosing `/mcp` request's row
13280
- either — the call is simply not where that helper belongs.)
13503
+ path, where the request *is* the record. (A tool call runs in its own context, ADR 0045,
13504
+ so it cannot write into the enclosing `/mcp` request's row either — the call is
13505
+ simply not where that helper belongs.)
13281
13506
 
13282
13507
  ### One row that names the cause
13283
13508
 
@@ -14128,15 +14353,6 @@ per-transport tenant plumbing.
14128
14353
  # Guide: Frontend integrations (docs/guide/frontend-integrations.md)
14129
14354
  ==============================================================================
14130
14355
 
14131
- ---
14132
- title: Frontend integrations
14133
- description: Compose Stitchkit with Next.js, React Router or a separate Vite development server
14134
- type: architecture
14135
- status: active
14136
- created: 2026-08-08
14137
- updated: 2026-08-08
14138
- ---
14139
-
14140
14356
  # Frontend integrations
14141
14357
 
14142
14358
  The official `bun create stitchkit` application uses Next.js with a separate
@@ -14198,15 +14414,6 @@ shared contract remains the only transport schema source.
14198
14414
  # Guide: React Query policy (docs/guide/react.md)
14199
14415
  ==============================================================================
14200
14416
 
14201
- ---
14202
- title: React Query policy
14203
- description: One request-local server client, one browser singleton and explicit retry rules for Stitchkit API errors
14204
- type: guide
14205
- status: active
14206
- created: 2026-09-06
14207
- updated: 2026-09-06
14208
- ---
14209
-
14210
14417
  # React Query policy
14211
14418
 
14212
14419
  `stitchkit/react` contains policy adapters for TanStack Query. It does not own
@@ -14261,15 +14468,6 @@ endpoint or cookie vocabulary.
14261
14468
  # Guide: Keyboard layers and list zones (docs/guide/keyboard.md)
14262
14469
  ==============================================================================
14263
14470
 
14264
- ---
14265
- title: Keyboard layers and list zones
14266
- description: One listener decides which part of a screen gets a key; lists move real focus with the arrows
14267
- type: guide
14268
- status: active
14269
- created: 2026-09-29
14270
- updated: 2026-09-29
14271
- ---
14272
-
14273
14471
  # Keyboard layers and list zones
14274
14472
 
14275
14473
  `stitchkit/react/keyboard` answers one question for a React screen: **who gets
@@ -15133,8 +15331,8 @@ the press was forged (a client can send any bytes) and goes to `onStale`.
15133
15331
 
15134
15332
  A press on a message the record does not know — an older menu higher up — is
15135
15333
  answered in place: a one-message view takes that message over, a longer view is
15136
- sent below it and the pressed message is deleted. Either way the previously
15137
- recorded view is removed: one live menu per chat.
15334
+ sent below it and the pressed message is deleted. Either way the recorded
15335
+ view is removed: one live menu per chat.
15138
15336
 
15139
15337
  For a message the screens do not own — a notification from the backend —
15140
15338
  `screens.button(link('🔑 Refresh token', refreshToken, { botId }))` makes a
@@ -15248,15 +15446,6 @@ an incoming message to HTML, and a history stack.
15248
15446
  # Guide: GeoIP (docs/guide/geo.md)
15249
15447
  ==============================================================================
15250
15448
 
15251
- ---
15252
- title: GeoIP
15253
- description: A server-only managed GeoIP reader with three observable states and last-known-good generation reloads.
15254
- type: guide
15255
- status: active
15256
- created: 2026-09-06
15257
- updated: 2026-09-06
15258
- ---
15259
-
15260
15449
  # GeoIP
15261
15450
 
15262
15451
  `stitchkit/geo` is a server-only, evolving entrypoint for reading local GeoIP
@@ -15340,6 +15529,413 @@ analytics policy belong to the application.
15340
15529
 
15341
15530
  # Upgrading stitchkit
15342
15531
 
15532
+ ## Released migration: 0.105.0
15533
+
15534
+ ### CLI publication has its own entrypoint
15535
+
15536
+ **Who must act:** code that imports `publishCli`, `CliPublicationOptions`, `CliPublicationPhase` or
15537
+ `CliPublicationResult` from `stitchkit/cli`. Everything else in `stitchkit/cli` — `createCli`, the
15538
+ manifest schemas, signatures, `renderCliInstaller` and the updater — is unchanged.
15539
+
15540
+ `publishCli` is release infrastructure, so it now ships from the evolving entrypoint
15541
+ `stitchkit/cli/publish` instead of the stable `stitchkit/cli`
15542
+ ([ADR 0245](../decisions/0245-cli-publication-is-an-evolving-leaf.md)). Its options, result and
15543
+ behaviour are the same.
15544
+
15545
+ 1. Move the publication names to the new specifier; keep every other CLI import where it is:
15546
+
15547
+ ```ts
15548
+ // before
15549
+ import { publishCli, type CliPublicationOptions, verifyCliManifest } from 'stitchkit/cli'
15550
+ // after
15551
+ import { verifyCliManifest } from 'stitchkit/cli'
15552
+ import { publishCli, type CliPublicationOptions } from 'stitchkit/cli/publish'
15553
+ ```
15554
+
15555
+ Search pattern: `\b(publishCli|CliPublication(Options|Phase|Result))\b` in files that import
15556
+ from `'stitchkit/cli'`.
15557
+ 2. Type-check: an import left on `stitchkit/cli` fails with "has no exported member".
15558
+ 3. `stitchkit/cli/publish` is evolving: read its breaking entries in each minor you upgrade across.
15559
+
15560
+ ### a command stops by its `stop` policy
15561
+
15562
+ **Who must act:** callers of `runNativeCommand` (`stitchkit/process`) that pass `killGraceMs`.
15563
+ Callers that omit it need no change: the default stop is the same TERM to the group, 100 ms, KILL.
15564
+
15565
+ 1. Replace `killGraceMs: N` with a group stop of the same grace:
15566
+
15567
+ ```ts
15568
+ // before
15569
+ await runNativeCommand({ executable, args, signal, killGraceMs: 500 })
15570
+ // after
15571
+ await runNativeCommand({ executable, args, signal, stop: { target: 'group', graceMs: 500 } })
15572
+ ```
15573
+
15574
+ Search pattern: `killGraceMs:\s*(\d[\d_]*)` → `stop: { target: 'group', graceMs: $1 }`.
15575
+ 2. If your own wrapper signals only the leader and waits a long grace (cooperative cancellation),
15576
+ replace it with `stop: { target: 'leader', signal: 'SIGINT', graceMs }` (up to one hour). Pass
15577
+ `killOn` with your shutdown signal so an application that stops sooner kills the group first.
15578
+ 3. The result of a stopped command rejects once the group is gone, up to `graceMs` plus
15579
+ `cleanupTimeoutMs` after the abort; size your caller deadlines on that sum.
15580
+
15581
+ ### a command stops what its leader left
15582
+
15583
+ **Who must act:** callers of `runNativeCommand` (`stitchkit/process`) whose command starts a helper
15584
+ in its own process group that must keep running after the command returns. A daemon that detaches
15585
+ with `setsid` (as `daemon(3)` does) is not in the group and needs
15586
+ no change.
15587
+
15588
+ `descendants` now defaults to `'terminate-after-leader'`
15589
+ ([ADR 0246](../decisions/0246-a-command-stops-its-descendants-by-default.md)): when the leader
15590
+ exits, the members it left in the group are stopped by the command's `stop` policy before the
15591
+ pipes drain.
15592
+
15593
+ 1. Find the commands that start a background helper on purpose (a `&` in a shell script, a spawned
15594
+ child the leader does not wait for) and declare `'leave'` on them:
15595
+
15596
+ ```ts
15597
+ // before
15598
+ await runNativeCommand({ executable, args, timeoutMs })
15599
+ // after
15600
+ await runNativeCommand({ executable, args, timeoutMs, descendants: 'leave' })
15601
+ ```
15602
+
15603
+ 2. Remove an explicit `descendants: 'terminate-after-leader'`, or keep it: it is now the default.
15604
+ 3. A command whose helper inherited stdout or stderr used to wait for that helper until its
15605
+ deadline. It now completes when the leader exits; drop a workaround that killed the helper from
15606
+ `onLeaderSettled`.
15607
+
15608
+ ### native packaging verifies published digests
15609
+
15610
+ **Who must act:** build scripts that call `createNativePackaging` (`stitchkit/files/packaging`) and
15611
+ read `asset.sourcePath`, hash the addon themselves, switch over the refusal `state`/`code`, pass a
15612
+ `platform` other than the literal `'darwin'`, or parse `node_modules/stitchkit/native-assets.json`.
15613
+
15614
+ The package now publishes the `size` and SHA256 of each Darwin addon, computed when Stitchkit is
15615
+ built, in `native-assets.json` at `formatVersion: 2`. `createNativePackaging` reads each selected
15616
+ addon once, compares it with that digest and returns the verified bytes. An installed addon that
15617
+ differs refuses with `NATIVE_ASSET_DIGEST_MISMATCH`.
15618
+
15619
+ 1. Replace every read of `asset.sourcePath` with `asset.bytes`, and delete your own re-hash of the
15620
+ addon: the result's `sha256` is the published digest and `bytes` already match it.
15621
+
15622
+ ```ts
15623
+ // before
15624
+ const bytes = readFileSync(asset.sourcePath)
15625
+ if (createHash('sha256').update(bytes).digest('hex') !== asset.sha256) throw …
15626
+ copyFileSync(asset.sourcePath, destination)
15627
+ // after
15628
+ writeFileSync(destination, asset.bytes)
15629
+ ```
15630
+
15631
+ 2. Handle the third refusal. A `switch` over `result.state` gains `'mismatch'`
15632
+ (`code: 'NATIVE_ASSET_DIGEST_MISMATCH'`); treat it as a failed build, never as a missing addon.
15633
+ It carries the differing `architecture` with `expected` (published) and `actual` (installed)
15634
+ `{ size, sha256 }`; log both pairs.
15635
+ 3. Pass the target platform as the literal `'darwin'`. `platform` is a closed enum, so a free
15636
+ string (a target name read from configuration, `process.platform`) no longer type-checks, and
15637
+ another platform throws instead of returning `unsupported`. Skip packaging for non-Darwin
15638
+ targets before the call:
15639
+
15640
+ ```ts
15641
+ // before
15642
+ const native = createNativePackaging({ platform: target.platform, architecture, … })
15643
+ if (native.state === 'unsupported') return
15644
+ // after
15645
+ if (target.platform !== 'darwin') return
15646
+ const native = createNativePackaging({ platform: target.platform, architecture, … })
15647
+ ```
15648
+
15649
+ `unsupported` now means only an architecture the package has no addon for.
15650
+ 4. Call `createNativePackaging` once per build and pass the result (its `assets` and `plugin`)
15651
+ where it is needed. Each call reads and hashes the selected addons again.
15652
+ 5. If you read `native-assets.json` directly, read `assets.<arch>.path`, `.size` and `.sha256`
15653
+ instead of a bare path per architecture; an architecture with no entry is not published.
15654
+
15655
+ ### exclusive locks publish their owner first
15656
+
15657
+ **Who must act:** callers of `withExclusiveLock` (`stitchkit/files`) that rely on a lock file with no
15658
+ readable owner being taken by age, and anything that puts lock files on a filesystem without hard
15659
+ links. Every other caller needs no change.
15660
+
15661
+ A lock is now written to a private `.lock-<id>.tmp` file in the lock's directory, then hard-linked to
15662
+ the lock name. The name never exists without a complete owner record, so `ownerlessGraceMs` defaults
15663
+ to `null` instead of 5 000 ms: a lock file with no readable owner is never taken because of its age.
15664
+
15665
+ 1. If you depended on age-based reclaim of an empty lock file left by an older writer, opt in:
15666
+
15667
+ ```ts
15668
+ // before: an empty lock older than 5 s was taken by default
15669
+ await withExclusiveLock(path, run)
15670
+ // after: say so; only legacy empty files need it
15671
+ await withExclusiveLock(path, run, { ownerlessGraceMs: 5_000 })
15672
+ ```
15673
+
15674
+ 2. Keep lock directories on a filesystem that supports hard links (local ext4, xfs, APFS and the
15675
+ like). `link` failing with `EPERM` or `ENOTSUP` fails the acquisition.
15676
+ 3. Expect a `.lock-*.tmp` sibling only after a holder was killed between writing its record and
15677
+ publishing it. It is not a lock; the next reclaim in that directory removes the ones whose owner
15678
+ is gone on this machine. Exclude the pattern from directory-size caps or backups if you have any.
15679
+
15680
+ ### one runtime-tool declaration
15681
+
15682
+ **Who must act:** code that names `RuntimeToolExecution`, `RuntimeToolExecutionWithOutput`,
15683
+ `RuntimeMcpToolDefinition`, `RuntimeMcpToolDefinitionWithOutput` or `ToolSurfaceDefinition`; code
15684
+ that annotates a definition carrying `present` as `RuntimeToolDefinitionWithOutput<I, O>` from
15685
+ `stitchkit/tools`; imports of `stitchkit/agent-runtime/harness-tools`; code that compares agent
15686
+ control error codes; and callers that send a JSON array as text to a string-or-array tool field.
15687
+ Definitions returned by `defineRuntimeTool` / `createRuntimeToolFactory`, native CLI `commands` and
15688
+ contract tools need no change.
15689
+
15690
+ A runtime tool now has one declaration, `RuntimeToolDefinitionWithOutput` /
15691
+ `RuntimeToolDefinitionWithoutOutput`, exported with the same name from `stitchkit/cli`,
15692
+ `stitchkit/tools` and `stitchkit/tools/mcp`. It carries no SDK type: the presenters are the adapter's
15693
+ extension, named as the fourth type argument. `RuntimeToolDefinition` is the one registration type.
15694
+
15695
+ 1. Replace each removed name.
15696
+
15697
+ | before | after |
15698
+ |---|---|
15699
+ | `RuntimeToolExecutionWithOutput<I, O>` (`stitchkit/cli`) | `RuntimeToolDefinitionWithOutput<I, O>` |
15700
+ | `RuntimeToolExecution` (`stitchkit/cli`) | `RuntimeToolDefinition` |
15701
+ | `RuntimeMcpToolDefinitionWithOutput<I, O>` (`stitchkit/tools/mcp`) | `RuntimeToolDefinitionWithOutput<I, O, undefined, RuntimeMcpToolPresenters<z.output<O>>>` |
15702
+ | `RuntimeMcpToolDefinition` (`stitchkit/tools/mcp`) | `RuntimeToolDefinition` |
15703
+ | `ToolSurfaceDefinition` (`stitchkit/tools`) | `ToolSurfaceProjection<RuntimeToolDefinition>` |
15704
+
15705
+ 2. In `stitchkit/tools`, a type that names a definition with presenters passes the presenter type:
15706
+
15707
+ ```ts
15708
+ // before
15709
+ const tool: RuntimeToolDefinitionWithOutput<typeof input, typeof output> = defineRuntimeTool({ …, present })
15710
+ // after: let defineRuntimeTool infer it, or name the presenters
15711
+ const tool = defineRuntimeTool({ …, present })
15712
+ const named: RuntimeToolDefinitionWithOutput<typeof input, typeof output, undefined, RuntimeToolPresenters<z.output<typeof output>>> = tool
15713
+ ```
15714
+
15715
+ 3. An inline handler in `runtimeTools: [...]` needs no wrapper: it is typed by the parsed object.
15716
+ To type `input` from the schema, construct with `defineRuntimeTool` (or
15717
+ `satisfies RuntimeToolDefinitionWithOutput<typeof input, typeof output>`) before registering.
15718
+
15719
+ 4. Replace `createAgentHarnessTools` (`stitchkit/agent-runtime/harness-tools`) with the call it made:
15720
+
15721
+ ```ts
15722
+ // before
15723
+ tools: createAgentHarnessTools(({ context }) => ({ services: [svc], context, lifecycle: auth }))
15724
+ // after
15725
+ import { composeToolLifecycle, mountAgent } from 'stitchkit/tools'
15726
+ tools: ({ context, toolFenceLifecycle }) =>
15727
+ mountAgent([svc], { context, lifecycle: composeToolLifecycle(auth, toolFenceLifecycle) })
15728
+ ```
15729
+
15730
+ 5. Agent control codes: `ACCESS_DENIED` is `FORBIDDEN`, and `CONNECTION_CLOSED` is returned for a closed
15731
+ connection. `AgentControlErrorCode` lists every code. `authorize` now runs once per request; after you
15732
+ withdraw someone's access, call `binding.revoke(conversationId, matches?)` so an open stream is
15733
+ re-authorized and ends with an `access-denied` delivery.
15734
+
15735
+ 6. A string-or-array field that receives `'["a","b"]'` now fails with `VALIDATION_ERROR` ("`paths` is a
15736
+ list written as text"). Send the array itself. A string that is not a list the array member accepts
15737
+ (`[preview].png`) is still one plain string.
15738
+
15739
+ ### flattened unions keep declaration order
15740
+
15741
+ **Who must act:** consumers that use `flattenUnionInput: true` and keep snapshot tests, golden
15742
+ files or provider prompt caches of flattened tool schemas.
15743
+
15744
+ A flattened union now lists its properties, enum values and branch labels in the order they first
15745
+ appear across the variants, not alphabetically. Declaration order often encodes priority, so the
15746
+ model now reads the order you wrote. Alternatives of different kinds (`anyOf`) keep their
15747
+ canonical order, independent of variant order.
15748
+
15749
+ 1. Regenerate every snapshot or golden file of a flattened tool listing (`buildToolManifest`,
15750
+ `listToolNames`, MCP `tools/list`, Agent tool schemas) and review the diff: only ordering moves.
15751
+ 2. Expect one provider prompt-cache miss per flattened tool after the upgrade.
15752
+ 3. To keep an order you prefer, declare the variants and fields in that order.
15753
+
15754
+ ### connection limits are validated
15755
+
15756
+ **Who must act:** callers that pass `timeoutMs` or `maxResponseBytes` to a connection and relied
15757
+ on an invalid value silently becoming the default.
15758
+
15759
+ `stitchkit/tools/connections` throws a `RangeError` when `timeoutMs` is not a positive integer
15760
+ within timer range (at most 2 147 483 647) or `maxResponseBytes` is not a positive safe integer.
15761
+ Before 0.104.0 a value of 0, a negative number, a fraction or an out-of-range number fell back to
15762
+ the default without a message.
15763
+
15764
+ ```ts
15765
+ // Before: { timeoutMs: 0 } meant "use the default" by accident.
15766
+ // After: omit the option to take the default, or pass a real limit.
15767
+ { timeoutMs: 30_000 }
15768
+ ```
15769
+
15770
+ ### one canonical JSON serializer
15771
+
15772
+ **Who must act:** tests that call `serializeSurfaceValue` from `stitchkit/testing`, and code that
15773
+ puts a value nested deeper than 100 levels into agent store events, archives or projections
15774
+ through `canonicalAgentJson`.
15775
+
15776
+ `canonicalJson` from `stitchkit/primitives` is the package's one canonical serializer. It
15777
+ admits dense arrays, plain objects, strings, booleans, `null` and finite numbers; an `undefined`
15778
+ object member is omitted. It refuses everything else with a `CanonicalJsonError`, a `TypeError`
15779
+ whose `reason` says why: `'depth'`, `'nodes'` and `'bytes'` are the limits you set, while
15780
+ `'cycle'`, `'negative-zero'` and `'not-json'` say the value is not JSON data.
15781
+
15782
+ `serializeSurfaceValue` is removed. It produced the same bytes without limits:
15783
+
15784
+ ```ts
15785
+ // Before
15786
+ import { serializeSurfaceValue } from 'stitchkit/testing'
15787
+ const text = serializeSurfaceValue(manifest)
15788
+ // After: the limits default to 100 levels, 100 000 nodes and 1 MiB; raise them for a large manifest
15789
+ import { canonicalJson } from 'stitchkit/primitives'
15790
+ const text = canonicalJson(manifest, { maxBytes: 16 * 1024 * 1024 })
15791
+ ```
15792
+
15793
+ `canonicalAgentJson` in `stitchkit/agent-runtime` is `canonicalJson` with the node and byte
15794
+ limits at their maximum. A value both accepted has the same bytes as before, so stored digests
15795
+ and archives still match: like `z.json()` before it, the store hashes `-0` as `0` and drops an
15796
+ own `__proto__` member (the public `canonicalJson` refuses `-0` and keeps `__proto__`). What
15797
+ changed is the refusal table: an `undefined` object member used to fail and is now omitted, and
15798
+ a value nested deeper than 100 levels is now refused.
15799
+
15800
+ ```ts
15801
+ // Before: canonicalAgentJson({ a: undefined }) threw a ZodError
15802
+ // After: canonicalAgentJson({ a: undefined }) === '{}'
15803
+ // canonicalAgentJson(nestedBeyond100Levels) throws CanonicalJsonError (reason 'depth')
15804
+ ```
15805
+
15806
+ ### directory inbox entries
15807
+
15808
+ **Who must act:** producers that drop hard links into an inbox directory, and callers that pass
15809
+ `accept` the output of a transforming schema.
15810
+
15811
+ A file dropped into the inbox directory with more than one link is set aside as `invalid`
15812
+ (detail `managed file has multiple links`). Another name can still rewrite such a file after it
15813
+ was validated, so it is never trusted. Write each entry as its own regular file: a temporary
15814
+ name in the same directory, then a rename onto `<name>.json`.
15815
+
15816
+ `accept` takes the schema's input type, stores that input as given and parses it with the
15817
+ schema on delivery. A schema without a transform has the same input and output type and needs
15818
+ no change. With a transform, pass the input:
15819
+
15820
+ ```ts
15821
+ const schema = z.object({ amount: z.string().transform(Number) })
15822
+ // Before: accept({ source, key, entry: { amount: 42 } }) // typed as the output; `z.string()` refused it
15823
+ // After: accept({ source, key, entry: { amount: '42' } }) // delivered as { amount: 42 }
15824
+ ```
15825
+
15826
+ A name that is already in `rejected/` no longer stops the pass: identical bytes are the entry
15827
+ already set aside, different bytes are kept as `rejected/<name>.<16 hex digits>`.
15828
+
15829
+ ### journal startup scan
15830
+
15831
+ **Who must act:** operators that read `status.recovery` right after `createDiagnosticJournal` to
15832
+ find damaged rows in the middle of a file.
15833
+
15834
+ Opening a journal reads only the last line of the active file and of every retained
15835
+ generation (`startupScan: 'tails'`, the default). A crash tears the end of a file, so a torn tail
15836
+ is still reported and still rotated away, and opening no longer parses and validates every
15837
+ line of every generation. It still reads each file once in large blocks to count lines, so the time
15838
+ grows with the bytes retained, at read speed. Pass `startupScan: 'full'` to read and validate every line as before:
15839
+
15840
+ ```ts
15841
+ // Before: every open parsed every line of every generation.
15842
+ // After: the same full scan, on request.
15843
+ const journal = await createDiagnosticJournal({ path, eventSchema, limits, startupScan: 'full' })
15844
+ ```
15845
+
15846
+ ### journal startup quarantine
15847
+
15848
+ **Who must act:** operators of a journal that is a source of truth and must not start over
15849
+ damaged retained files; code that constructs `DiagnosticJournalRecoveryError` or reads its
15850
+ `recovery`; protocols that embed `DiagnosticJournalRecoveryStatusSchema`, and their readers.
15851
+ A diagnostic journal needs no change.
15852
+
15853
+ Opening a journal no longer refuses to start over a torn active file with `maxFiles: 1`, a
15854
+ generation name that is not a regular file or a retained file it cannot read. It renames the file
15855
+ to `<file>.quarantined-<epoch>` beside the journal, starts, and lists the move in
15856
+ `getStatus().recovery.quarantined`. Quarantined files are outside retention and never deleted;
15857
+ every later open lists them again, without a `reason`, until you remove them.
15858
+
15859
+ 1. A journal whose damage must stop the process declares the old behaviour:
15860
+
15861
+ ```ts
15862
+ // Before: refusing was the only behaviour.
15863
+ await createDiagnosticJournal({ path, eventSchema, limits })
15864
+ // After:
15865
+ await createDiagnosticJournal({ path, eventSchema, limits, onStartupRefusal: 'fail' })
15866
+ ```
15867
+
15868
+ 2. Branch on the refusal's `reason` instead of its message or error class. Every startup refusal
15869
+ about retained evidence is a `DiagnosticJournalRecoveryError`, including the former plain
15870
+ `Error` for a non-file generation and the raw `EACCES` for an unreadable one (now in `cause`):
15871
+
15872
+ ```ts
15873
+ // Before: catch (error) { if (error instanceof DiagnosticJournalRecoveryError) use(error.recovery) }
15874
+ // After:
15875
+ catch (error) {
15876
+ if (error instanceof DiagnosticJournalRecoveryError && error.reason === 'torn-without-retention-slot') use(error.recovery)
15877
+ }
15878
+ ```
15879
+
15880
+ `recovery` is present only for `torn-without-retention-slot`. The constructor takes one
15881
+ options object: `new DiagnosticJournalRecoveryError({ reason, file, recovery?, cause? })`.
15882
+
15883
+ 3. A protocol that embeds `DiagnosticJournalRecoveryStatusSchema` carries the new optional
15884
+ `quarantined` array (at most 32 entries, `reason` optional) and `quarantinedUnlisted` count.
15885
+ The schema is `.strict()`: upgrade every reader that parses it with its own copy before a
15886
+ writer on this version sends a status with either field, or bump your protocol version.
15887
+
15888
+ ### error constructors take named options
15889
+
15890
+ **Who must act:** code that constructs an `AppError` or an `ApiError` with anything beyond its
15891
+ code, and classes that extend `AppError` and call `super(code, message, status, …)`. Code that
15892
+ only throws the `notFound`, `badRequest`, `unauthorized`, `forbidden`, `conflict` and
15893
+ `rateLimited` helpers, or only reads `code`, `status`, `details`, `hint`, `traceId`,
15894
+ `retryable` and `cause` from a caught error, needs no change.
15895
+
15896
+ `AppError` (`stitchkit/contract`, `stitchkit/server`, `stitchkit/node`) and `ApiError`
15897
+ (`stitchkit`) keep the code as the one positional argument. Every other value is a named
15898
+ option, so a call site reads without the signature open. `new AppError(code)` and
15899
+ `new ApiError(code)` are unchanged. The compiler finds every old call: a string or number
15900
+ in the second position is a type error.
15901
+
15902
+ 1. `AppError`: move `message`, `status`, `details`, `hint`, `traceId` and `retryable` into one
15903
+ object after the code. `status` defaults to 500 and `message` to the code.
15904
+
15905
+ ```ts
15906
+ // before
15907
+ throw new AppError('CONFLICT', 'Selection is stale', 409, { revision: 7 })
15908
+ throw new AppError('BUSY', 'Try later', 503, undefined, undefined, undefined, true)
15909
+ // after
15910
+ throw new AppError('CONFLICT', { message: 'Selection is stale', status: 409, details: { revision: 7 } })
15911
+ throw new AppError('BUSY', { message: 'Try later', status: 503, retryable: true })
15912
+ ```
15913
+
15914
+ 2. A subclass passes the same object to `super`:
15915
+
15916
+ ```ts
15917
+ // before
15918
+ super('FEATURE_LOCKED', 'locked', 403)
15919
+ // after
15920
+ super('FEATURE_LOCKED', { message: 'locked', status: 403 })
15921
+ ```
15922
+
15923
+ 3. `ApiError`: the old order was `code, status, details, message, hint, traceId, errorOptions,
15924
+ retryable`. Name each value; `status` defaults to 0 and the former `ErrorOptions` argument
15925
+ is now the `cause` option.
15926
+
15927
+ ```ts
15928
+ // before
15929
+ new ApiError('RATE_LIMITED', 429, undefined, 'slow down', 'wait', 'trace-1', { cause }, true)
15930
+ new ApiError('VALIDATION_ERROR', 0, { issues }, 'bad input')
15931
+ // after
15932
+ new ApiError('RATE_LIMITED', { status: 429, message: 'slow down', hint: 'wait', traceId: 'trace-1', retryable: true, cause })
15933
+ new ApiError('VALIDATION_ERROR', { status: 0, details: { issues }, message: 'bad input' })
15934
+ ```
15935
+
15936
+ 4. Both classes accept the standard `cause`, which `error.cause` returns; omit the option to
15937
+ leave the property absent.
15938
+
15343
15939
  ## Released migration: 0.104.0
15344
15940
 
15345
15941
  JS bundles carrying Darwin native capabilities also need the
@@ -15465,10 +16061,20 @@ that work actually completes.
15465
16061
 
15466
16062
  ### Darwin native assets in JS distributions
15467
16063
 
15468
- **Who must act:** Bun JS builds using `--outfile`, copying only `outputs[0]`, custom
15469
- single-file/archive packagers and installers delivering native process/files capabilities.
15470
- First affected publication: **0.103.13**. It shipped as a patch; its changed delivery graph
15471
- requires this migration despite unchanged API signatures.
16064
+ **Who must act:** Bun builds that import `stitchkit/server`, `stitchkit/files` or
16065
+ `stitchkit/process` (Linux-only server apps included) and use `--outfile`, copy only
16066
+ `outputs[0]`, or package a single file; custom single-file/archive packagers and installers
16067
+ delivering native process/files capabilities.
16068
+ First affected publication: **0.103.13**, through **0.104.2**. It shipped as a patch; its
16069
+ changed delivery graph requires this migration despite unchanged API signatures.
16070
+
16071
+ In those versions every Bun bundle that reaches the lock or process-identity code,
16072
+ `stitchkit/server` included, carries both Darwin `.node` addons as extra outputs on every
16073
+ operating system. `bun build --outfile` then fails with
16074
+ `cannot write multiple output files without an output directory`, and `--outdir` writes the
16075
+ addons next to a Linux artifact. Releases after 0.104.2 resolve the default loader at run time,
16076
+ so these bundles are one JS file with no `.node` output again. The static loader exists only
16077
+ in a build that uses the `stitchkit/files/packaging` plugin.
15472
16078
 
15473
16079
  ```sh
15474
16080
  # Before: one JS file was assumed to be the entire distribution.
@@ -15485,12 +16091,39 @@ Custom output layout uses the evolving build-only packaging contract in the
15485
16091
 
15486
16092
  Three deliveries have different rules: an ordinary npm install retains the package's
15487
16093
  native assets; a JS distribution must ship its complete output graph; a Bun compiled
15488
- executable embeds the matching addon and may still use `--outfile`.
16094
+ executable embeds the matching addon and may still use `--outfile`. In 0.103.13–0.104.2 a plain
16095
+ `bun build` carries the addon without a plugin; later releases carry it only when the build
16096
+ uses `createNativePackaging` (`delivery: 'companion'` for JS, `'embedded'` for a compiled
16097
+ executable).
15489
16098
  The [native IO guide](native-io.md#libraries-with-a-zod-only-runtime) is the canonical recipe.
15490
16099
  Check the resulting artifact offline outside the build tree and `node_modules`, including
15491
16100
  process identity, live-owner lock refusal, dead-owner recovery and contained IO.
15492
16101
  A missing or corrupt addon remains `unavailable`, never evidence that an owner died.
15493
16102
 
16103
+ ### CLI: repeated options are refused
16104
+
16105
+ **Who must act:** wrappers, aliases and scripts that append `--json`, `--profile X` or an
16106
+ application global to arguments the user may already have given, and anything that passes the
16107
+ same global twice.
16108
+ First affected publication: **0.103.13**. It shipped as a patch; the change in grammar needs
16109
+ this migration.
16110
+
16111
+ A repeated option that is not an array field is an argument error:
16112
+ `--json --json` and `--profile a --profile b` fail with `--json was passed 2 times`. Before
16113
+ 0.103.13 the last value won, for framework globals, application globals and repeated
16114
+ `kind: 'other'` fields alike. Array fields, and untyped passthrough fields that keep their
16115
+ raw-list grammar, still repeat.
16116
+
16117
+ ```sh
16118
+ # Before: a wrapper appended its default and the last value won.
16119
+ mytool deploy --profile staging "$@" --profile production
16120
+ # After: build the argument list once, so each option appears once.
16121
+ mytool deploy --profile "${PROFILE:-production}" "$@"
16122
+ ```
16123
+
16124
+ Dotted leaves such as `--filter.name` follow the same rule as their parent field. Declare a
16125
+ field as an array when repetition is meant.
16126
+
15494
16127
  ## Released migration: 0.103.0
15495
16128
 
15496
16129
  ### Diagnostic journal startup preserves torn evidence and inspects archives
@@ -19278,15 +19911,6 @@ end of this file.
19278
19911
  # Guide: Project declaration (docs/guide/declaration.md)
19279
19912
  ==============================================================================
19280
19913
 
19281
- ---
19282
- title: Project declaration
19283
- 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.
19284
- type: architecture
19285
- status: active
19286
- created: 2026-08-25
19287
- updated: 2026-08-30
19288
- ---
19289
-
19290
19914
  # Project declaration
19291
19915
 
19292
19916
  > **Maturity: evolving.** This surface is still finding its shape and may be
@@ -19849,6 +20473,7 @@ schedules, transports and file generation remain application-owned.
19849
20473
  |---|---|---|
19850
20474
  | `canonicalJson` | function | bounded plain JSON, UTF-16 key sort, optional object members omitted |
19851
20475
  | `CanonicalJsonOptions` | type | inferred depth/node/UTF-8 bytes limits |
20476
+ | `CanonicalJsonError` / `CanonicalJsonRefusal` | class / _type_ | the `TypeError` of a refusal and its `reason`: `cycle`, `negative-zero`, `depth`, `nodes`, `bytes` or `not-json` |
19852
20477
 
19853
20478
 
19854
20479
  ## `stitchkit/server`
@@ -20002,7 +20627,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
20002
20627
  ### Primitives
20003
20628
 
20004
20629
  | `createFileStateStore` | function | cross-process locked atomic JSON state updates over one Zod schema |
20005
- | `FileStateStoreCorruption` / `FileStateStoreOptions` | _type_ | explicit corrupt-state report/policy and lock timing configuration |
20630
+ | `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 |
20006
20631
  | Export | Kind | Summary |
20007
20632
  |--------|------|---------|
20008
20633
  | `streamSSE` | function | an async generator → SSE `Response` — [guide](../guide/server.md#sse-streaming) |
@@ -20108,7 +20733,7 @@ cutovers are covered by the executable
20108
20733
  | `NotificationOutboxItem` / `NotificationOutboxReceipt` / `NotificationOutboxState` / `NotificationSend` / `NotificationFailureClassification` / `DroppedNotification` / `EnqueueNotification` | _type_ | versioned queue records, stable send identity and delivery decisions |
20109
20734
  | `notificationOutboxStateSchema` | function | strict persisted-state schema over an application payload schema |
20110
20735
  | `notificationOutboxResource` / `NotificationOutboxResource` / `NotificationOutboxResourceConfig` | function / _type_ | thin managed-resource scheduler over an outbox handle |
20111
- | `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 |
20736
+ | `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 |
20112
20737
  | `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 |
20113
20738
  | `StateStoreUpdateContext` | _type_ | real active protected transaction: `assertHeld()` refuses after ownership loss or completion; custom adapters supply this context to their transition |
20114
20739
  | `DirectoryInboxStateSchema` / `DirectoryInboxState` / `DirectoryInboxRejection` / `DirectoryInboxRejectionReasonSchema` / `DirectoryInboxRejectionReason` | schema / _type_ | the inbox's claims, receipts and rejections (`invalid`, `too-large`, `attempt-limit`) |
@@ -20194,9 +20819,10 @@ cutovers are covered by the executable
20194
20819
  | Export | Kind | Summary |
20195
20820
  |--------|------|---------|
20196
20821
  | `createDiagnosticJournal` | function | create one schema-owned FIFO JSONL writer with bounded retained memory, exclusive local path ownership and finite rotation |
20197
- | `DiagnosticJournalConfig` / `DiagnosticJournal` | _type_ | owner schema/path/limits/failure observer and the synchronous `submit`, bounded-wait `flush`/`close`, status handle |
20822
+ | `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 |
20198
20823
  | `DiagnosticJournalLimitsSchema` / `DiagnosticJournalLimits` | schema / _type_ | positive event, pending-item, pending-byte, file-byte and retained-file limits |
20199
20824
  | `DiagnosticJournalLockPolicySchema` / `DiagnosticJournalLockPolicy` | schema / _type_ | `refuse` (default) or `reclaim-stale`, which reclaims only a lock whose recorded owner is provably gone |
20825
+ | `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 |
20200
20826
  | `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` |
20201
20827
  | `DiagnosticJournalSubmitResultSchema` / `DiagnosticJournalSubmitResult` | schema / _type_ | accepted epoch/sequence or explicit invalid, oversized, capacity, closed or failed refusal |
20202
20828
  | `DiagnosticJournalStatusSchema` / `DiagnosticJournalStatus` | schema / _type_ | state, limits, exact admission/write/failure counters, pending ownership, rotations, partial tails and last safe sequences |
@@ -20209,8 +20835,10 @@ cutovers are covered by the executable
20209
20835
  | `DiagnosticJournalWaitOptions` / `DiagnosticJournalFailure` | _type_ | caller wait signal/timeout and isolated internal failure callback record |
20210
20836
  | `DiagnosticJournalAnomalySchema` / `DiagnosticJournalAnomaly` | schema / _type_ | corrupt-row reason, file, zero-based byte offset, one-based line, tail/interior position and skipped bytes |
20211
20837
  | `createDiagnosticJournalReadResultSchema` / `DiagnosticJournalReadResult` | function / _type_ | owner-schema frame output or explicit anomaly; retains transformed event output types |
20212
- | `DiagnosticJournalRecoveryStatusSchema` / `DiagnosticJournalRecoveryStatus` | schema / _type_ | bounded startup inspection counts and first/last anomaly locations |
20213
- | `DiagnosticJournalRecoveryError` | class | torn single-file startup refusal carrying recovery observations, preserving evidence and releasing its lock |
20838
+ | `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 |
20839
+ | `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 |
20840
+ | `DiagnosticJournalStartupRefusalReasonSchema` / `DiagnosticJournalStartupRefusalReason` | schema / _type_ | `torn-without-retention-slot \| not-a-regular-file \| unreadable` |
20841
+ | `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 |
20214
20842
 
20215
20843
  `accepted` is bounded in-memory admission and `written` is completed append, not `fsync` or durable
20216
20844
  delivery. Its filesystem reader is in the diagnostic-journal leaf; there is no managed replay/upload API. See the
@@ -20516,7 +21144,7 @@ Server-only optional application runtime. See the
20516
21144
 
20517
21145
  | Export | Kind | Summary |
20518
21146
  |--------|------|---------|
20519
- | `AgentConversationArchiveSchema` / `AgentConversationArchive` / `encodeAgentConversationArchive` / `decodeAgentConversationArchive` / `canonicalAgentJson` | schema / _type_ / functions | canonical validated conversation archive with exact ledger events, recovery projection and durable spill payloads |
21147
+ | `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 |
20520
21148
  | `AgentStoreEventEnvelopeSchema` / `AgentStoreEventEnvelope` / `AgentStoreEventKindSchema` / `AgentStoreEventKind` / `AgentStoreEventPageSchema` / `AgentStoreEventPage` | schema / _type_ | append-only event envelope, closed current vocabulary (including `provider/request` and `provider/response`) and bounded page |
20521
21149
  | `AppendAgentStoreEventSchema` / `AppendAgentStoreEvent` / `ReadAgentStoreEventsSchema` / `ReadAgentStoreEvents` | schema / _type_ | required append and bounded-read store contracts |
20522
21150
  | `AgentStoreTransitionSchema` / `AgentStoreTransition` / `AgentStoreEventDraft` / `AgentStoreEventDecodeAccepted` / `AgentStoreEventDecodeIgnored` / `AgentStoreEventDecodeResult` / `decodeAgentStoreEvent` | schema / _types_ / function | normalized runtime mutations, append drafts and strict current/ignorable future-event decoding outcomes |
@@ -20526,7 +21154,7 @@ Server-only optional application runtime. See the
20526
21154
  | `defineStateSlot` / `AgentStateSlotDefinition` / `AgentStateSlotStore` / `AgentStateSlotValue` / `AnyAgentStateSlot` / `createAgentStateSlotStore` / `renderAgentStateSlots` | functions / _types_ | typed durable state written as ledger events and injected on every provider request |
20527
21155
  | `agentGoalStateSlot` / `agentTodoStateSlot` / `createAgentStateTools` | constants / function | built-in goal/todo state and bound `goal_*` / `todo_write` Agent tools |
20528
21156
  | `AgentSandboxGradeSchema` / `AgentSandboxGrade` / `AgentSandboxRestrictionSchema` / `AgentSandboxRestriction` / `AgentProcessSandbox` | schema / _type_ | host-provided process sandbox capability and explicit restriction vocabulary |
20529
- | `probeAgentProcessSandbox` / `missingSandboxRestrictions` / `recordAgentSandboxProbe` | functions | process-cached probe, fail-closed required-gap calculation and durable probe record |
21157
+ | `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) |
20530
21158
  | `AgentSandboxProcess` / `AgentSandboxOutputStream` | _types_ | structural child/output interface for optional lifecycle-owned `AgentProcessSandbox.spawn`, without Node ambient type dependencies |
20531
21159
  | `AgentEventSearchResultSchema` / `AgentEventSearchResult` / `createSqliteAgentEventSearch` / `createAgentEventSearchTools` | schema / _type_ / functions | authorized FTS5 search with exact event addresses and `session_*` tools |
20532
21160
  | `createSqliteAgentSpillStore` | function | durable content-address-checked artifact storage, bounded read/search, retention facts and archive participation |
@@ -20821,7 +21449,10 @@ terminal and provider-envelope schemas/types listed under
20821
21449
  including `AgentAttemptResetEventSchema` for `attempt-reset`),
20822
21450
  `AgentRuntimeEventCursorSchema`, `advanceAgentRuntimeEventCursor`,
20823
21451
  `AgentControlRequestSchema` / `AgentControlRequest`, `AgentControlResponseSchema` /
20824
- `AgentControlResponse`, `AgentControlDeliverySchema` / `AgentControlDelivery`, `AgentMultiSessionCursorSchema` /
21452
+ `AgentControlResponse`, `AgentControlDeliverySchema` / `AgentControlDelivery`,
21453
+ `AgentControlErrorCodeSchema` / `AgentControlErrorCode` (the one vocabulary of every control
21454
+ error code: `FORBIDDEN`, `CONNECTION_CLOSED`, `REQUEST_CAPACITY`, `ATTACHMENT_CAPACITY`,
21455
+ `LEASE_CONFLICT`, `EVENT_OVERFLOW` and the rest), `AgentMultiSessionCursorSchema` /
20825
21456
  `AgentMultiSessionCursor`, `AgentConversationView`, `AgentControlView`,
20826
21457
  `advanceAgentMultiSessionCursor`, `createAgentControlView`, `reduceAgentControlSnapshot`,
20827
21458
  `reduceAgentControlEvent` and
@@ -20851,9 +21482,12 @@ attachment per conversation on that transport; share the controller between view
20851
21482
  `submit` requires a caller-owned idempotency key. Unknown-outcome mutations are never
20852
21483
  replayed automatically. `AgentControllerState` distinguishes connecting, ready,
20853
21484
  disconnected, error and closed and retains the canonical full `view` plus safe error
20854
- code/message. Errors in automatic attach/refresh appear there. Reconnect attaches again
20855
- and reads a snapshot; snapshot gaps coalesce, stale generations cannot overwrite state,
20856
- and buffers/queues are bounded. Overflow or failed refresh is visible and requires
21485
+ code/message. Errors in automatic attach/refresh appear there, and so does an
21486
+ `access-denied` delivery: the server detached the conversation, the state becomes
21487
+ `error` with code `FORBIDDEN`, and nothing arrives until the controller attaches again.
21488
+ Reconnect attaches again and reads a snapshot; requests of the previous connection are
21489
+ rejected at once and release their capacity, snapshot gaps coalesce, stale generations
21490
+ cannot overwrite state, and buffers/queues are bounded. Overflow or failed refresh is visible and requires
20857
21491
  explicit recovery. Closing detaches without closing the shared transport or remote run.
20858
21492
 
20859
21493
  See [agent composition](../guide/agent-composition.md) for a complete wiring example.
@@ -21082,10 +21716,10 @@ payload.
21082
21716
  | `McpMountConfig` | _type_ | config for `mountMcp` |
21083
21717
  | `McpSchemaValidationConfig` | _type_ | shared `{ policy, requireTypedProperties, allowUntyped, requirePortableFormats, allowFormats }` profile |
21084
21718
  | `ValidateMcpSchemasConfig` | _type_ | standalone validation profile plus `services`, `extend`, flattening and logger |
21085
- | `RuntimeToolDefinition` | _type_ | transport-neutral pathless operation with identity, schemas, handler and optional presenters |
21719
+ | `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 |
21086
21720
  | `RuntimeToolDefinitionBase` | _type_ | common name, identity, input, exposure and MCP metadata fields |
21087
- | `RuntimeToolDefinitionWithOutput` | _type_ | runtime definition whose handler and presenters share a validated output type |
21088
- | `RuntimeToolDefinitionWithoutOutput` | _type_ | runtime definition with a void handler and no presentation callbacks |
21721
+ | `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) |
21722
+ | `RuntimeToolDefinitionWithoutOutput` | _type_ | the same construction with a void handler and no presentation callbacks |
21089
21723
  | `RuntimeToolFactory` | _type_ | identity/context-bound runtime-tool definition factory |
21090
21724
  | `RuntimeToolFactoryConfig` | _type_ | factory service identity and context schema |
21091
21725
  | `RuntimeToolFactoryDefinitionWithOutput` | _type_ | factory-authored runtime tool with a validated output schema |
@@ -21294,10 +21928,9 @@ Advanced building blocks — the shared machinery the mounts are built on.
21294
21928
  | `ToolCallRecord` | _type_ | the structured record `createToolLogger` passes to `onRecord` |
21295
21929
  | `TransportSummary` | _type_ | `{ contractServices, runtimeTools, totals, sources }` from `summarizeTransports` |
21296
21930
  | `TransportCounts` | _type_ | per-transport counts (`{ HTTP, MCP, AGENT, CLI }`) |
21297
- | `ToolSurfaceDefinition` | _type_ | shared object-shaped `{ services?, runtimeTools? }` introspection surface |
21298
- | `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 |
21931
+ | `ToolSurfaceProjection` | _type_ | the one readonly `{ services?, runtimeTools? }` container shared by full, MCP-only and introspection surfaces (`buildToolManifest`, `listToolNames`, `summarizeTransports` take `ToolSurfaceProjection<RuntimeToolDefinition>`) |
21299
21932
  | `ToolManifestConfig` | _type_ | mixed surface plus required model-facing `transport` and presentation options |
21300
- | `coerceJsonArgs` | function | coerce JSON-stringified array/object tool arguments |
21933
+ | `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 |
21301
21934
  | `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 |
21302
21935
  | `ToolPresentationSchema` | _type_ | immutable model-facing JSON Schema document shared by tool transports |
21303
21936
  | `MountableTool` | _type_ | one operation with separate executable CLI argument schema and model-facing presentation schema |
@@ -21316,10 +21949,9 @@ behavior and names. See [MCP guide](../guide/mcp-and-agents.md#mcp--createmcphan
21316
21949
  |--------|------|---------|
21317
21950
  | `createMcpHandler` / `createMcpHttpRoute` / `buildMcpServer` / `mountMcp` | function | the shared authenticated HTTP handler, raw-route adapter, SDK server and contract mount |
21318
21951
  | `createStdioMcpServer` / `bindStdioProcessSignals` | function | the shared stdio server and bounded process cleanup |
21319
- | `RuntimeMcpToolDefinitionWithOutput` | _type_ | schema-aware MCP-only construction with concrete handler output and presenter input |
21320
- | `RuntimeMcpToolDefinition` | _type_ | heterogeneous MCP registration; the canonical runner validates input/output before invocation |
21952
+ | `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>>` |
21953
+ | `RuntimeToolDefinition` | _type_ | heterogeneous registration; the canonical runner validates input/output before invocation |
21321
21954
  | `RuntimeMcpToolPresenters` / `RuntimeMcpPresentation` | _type_ | official SDK content without framework-owned `structuredContent` or `isError`; one presenter owner shared with the full SDK surface |
21322
- | `RuntimeToolDefinitionWithoutOutput` | _type_ | the shared neutral void definition |
21323
21955
 
21324
21956
  ## `stitchkit/tools/connections`
21325
21957
 
@@ -21419,7 +22051,6 @@ handler pipeline without opening a TCP port.
21419
22051
  | `McpSchemaValidationConfig` / `IncompatibleSchemaPolicy` | _type_ | canonical MCP schema-preparation policy used by real mounts and manifests |
21420
22052
  | `SurfaceRealtimeSchemaPairSchema` | schema | input/output digest pair for args or acknowledgements |
21421
22053
  | `SurfaceSchemaDigestsSchema` | schema | params/input/output/multipart digest object |
21422
- | `serializeSurfaceValue` | function | canonical versioned serialization used for deterministic digests |
21423
22054
 
21424
22055
  ---
21425
22056
 
@@ -21440,8 +22071,10 @@ available from `stitchkit/contract`.
21440
22071
  | `ManagedFileError` / `ManagedFileErrorCode` | class / _type_ | stable boundary failures; registered `FILE_*` mistakes are caller-safe while `FILE_IO_ERROR` remains internal |
21441
22072
  | `ManagedFileInspector` | _type_ | bounded-prefix read/write inspection callback with a finite cancellation signal that cannot own path or size |
21442
22073
  | `ManagedFileInspectionInput` / `ManagedFileInspection` | _type_ | inspector prefix/name/declared media/signal input and validated metadata-only result |
21443
- | `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 |
21444
- | `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 |
22074
+ | `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 |
22075
+ | `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)) |
22076
+ | `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, `[]` for a `directory` that does not exist |
22077
+ | `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 |
21445
22078
  | `ExclusiveLockError` | class | `LOCK_TIMEOUT` naming the resource and its holder, or `LOCK_ABORTED` with the signal's reason |
21446
22079
  | `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) |
21447
22080
  | `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()` |
@@ -21819,7 +22452,6 @@ SDK nor the `ai` peer.
21819
22452
  | `checkCliUpdate` | function | bounded, interval-limited, never-throwing check for a newer published build |
21820
22453
  | `applyCliUpdate` | function | download, verify the decompressed digest and replace the binary by rename |
21821
22454
  | `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 |
21822
- | `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 |
21823
22455
  | `rollbackCliUpdate` / `CliRollbackConfig` / `RolledBackCliUpdate` | function / _types_ | put the kept previous build back, digest-checked first and written by the same atomic rename the update uses |
21824
22456
  | `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 |
21825
22457
  | `cliSignatureAccepted` / `CliSignatureVerdict` | function / _type_ | `valid` \| `unenforced` \| `missing` \| `unknown-key` \| `invalid`; `unenforced` keeps an unpinned build updating while making the absence of a check visible |
@@ -21851,8 +22483,8 @@ SDK nor the `ai` peer.
21851
22483
  | `CliConfig` | _type_ | config for `createCli`; `defaultCommand`, `globalOptions`, `optionAliases` and `positionals` define the shared command presentation policy |
21852
22484
  | `CliPresentationPolicyConfig` | _type_ | shared command presentation-policy subset of `CliConfig` |
21853
22485
  | `CliSurfaceSource` | _type_ | static service/runtime array or identity-dependent factory |
21854
- | `RuntimeToolExecution` | _type_ | neutral heterogeneous runtime-tool registration; handlers execute through the schema-validating managed runner |
21855
- | `RuntimeToolExecutionWithOutput` | _type_ | strict neutral managed definition with schema-derived handler input/output; construct before adding to CLI `runtimeTools` |
22486
+ | `RuntimeToolDefinition` | _type_ | neutral heterogeneous runtime-tool registration; an inline handler is contextually typed, and handlers execute through the schema-validating managed runner |
22487
+ | `RuntimeToolDefinitionWithOutput` | _type_ | strict neutral managed definition with schema-derived handler input/output, the same declaration `stitchkit/tools` constructs with presenters |
21856
22488
  | `RuntimeToolDefinitionWithoutOutput` | _type_ | strict neutral managed void definition without an output contract or transport presenters |
21857
22489
  | `CliCommandDefinition` | _type_ | native command definition union |
21858
22490
  | `CliCommandDefinitionBase` | _type_ | native command name, description and input schema |
@@ -21869,6 +22501,22 @@ SDK nor the `ai` peer.
21869
22501
 
21870
22502
  ---
21871
22503
 
22504
+ ## `stitchkit/cli/publish`
22505
+
22506
+ Build and release tooling, evolving. Publishes a CLI distribution into a local
22507
+ application-owned directory; the manifest, signatures, installer and updater it
22508
+ writes for stay in `stitchkit/cli` — [guide](../guide/cli.md#publishing-a-complete-version),
22509
+ [ADR 0245](../decisions/0245-cli-publication-is-an-evolving-leaf.md).
22510
+
22511
+ | Export | Kind | Summary |
22512
+ |--------|------|---------|
22513
+ | `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 |
22514
+ | `CliPublicationOptions` | _type_ | identity, storage root, base URL, targets, limits, retention, the `admit` and `build` callbacks, optional signing and trust |
22515
+ | `CliPublicationPhase` | _type_ | the step `admit` is asked about — `prepare`, `build`, `commit` or `promote` |
22516
+ | `CliPublicationResult` | _type_ | `published` or `existing`, with the manifest ready to serve |
22517
+
22518
+ ---
22519
+
21872
22520
  ## `stitchkit/react`
21873
22521
 
21874
22522
  Browser- and server-render-safe React data-layer helpers. Needs the
@@ -21957,11 +22605,20 @@ network enforcement, gateway credentials and platform/resource boundaries.
21957
22605
 
21958
22606
  ## `stitchkit/agent-runtime/realtime`
21959
22607
 
21960
- Optional evolving Socket.IO server binding. `bindAgentHarnessRealtime` takes `(harness, socket, config)` and returns `close()` for its subscriptions and control connections, without closing
21961
- socket or harness. `AgentHarnessRealtimeConfig` requires `authorize({ identity, request,
21962
- signal })`: return `{ context }` after verifying the session principal, conversation,
21963
- operation, files and metadata, or `null`. Identity is `socket.data` from the application's
21964
- verified handshake. Every request and event delivery is authorized. Observe access does
22608
+ Optional evolving Socket.IO server binding, experimental: no committed consumer yet.
22609
+ `bindAgentHarnessRealtime` takes `(harness, socket, config)` and returns an
22610
+ `AgentHarnessRealtimeBinding`: `close()` for its subscriptions and control connections, without closing
22611
+ socket or harness, and `revoke(conversationId, matches?)`. `AgentHarnessRealtimeConfig` requires
22612
+ `authorize({ identity, request, source, signal })`: return `{ context }` after verifying the session
22613
+ principal, conversation, operation, files and metadata, or `null`. Identity is `socket.data` from the
22614
+ application's verified handshake. Every request is authorized on arrival. The events of an
22615
+ attached conversation are delivered under the grant the latest successful request created, so a
22616
+ token stream costs no authorization call per event. `revoke` drops the grant of every matching
22617
+ socket; the next event is re-authorized (`source: 'delivery'`, an `operation: 'snapshot'` request),
22618
+ and a refusal or failure detaches the conversation and sends an `access-denied` delivery, which
22619
+ turns the browser controller into an `error` state. Without a `revoke` call a grant lasts until the socket disconnects or the conversation
22620
+ is detached; this fail-open default is deliberate, so call `revoke` whenever access to a conversation may
22621
+ have been withdrawn. An authorization that began before a `revoke` never restores the grant. Observe access does
21965
22622
  not grant mutation; controller leases and approval policy still belong to the harness.
21966
22623
 
21967
22624
  `maxPendingEvents` and `maxPendingAttachments` forward to the canonical control server;
@@ -21973,23 +22630,11 @@ application transport. A transport's Engine.IO payload limit remains application
21973
22630
 
21974
22631
  ## `stitchkit/agent-runtime/react`
21975
22632
 
21976
- Optional evolving React peer leaf. `useAgent` takes a controller and subscribes through
22633
+ Optional evolving React peer leaf, experimental: no committed consumer yet. `useAgent` takes a controller and subscribes through
21977
22634
  `useSyncExternalStore`, returning the same `AgentControllerState` as `getSnapshot()`.
21978
22635
  Create and close the controller at application/session scope. Multiple components can
21979
22636
  observe it; StrictMode and unmount release only the component's own subscription.
21980
22637
 
21981
- ## `stitchkit/agent-runtime/harness-tools`
21982
-
21983
- Optional evolving composition of the full `mountAgent` configuration with the runtime
21984
- fence. Its runtime peer is `ai`; its full tool presenter declarations also require
21985
- `@modelcontextprotocol/server`, as `stitchkit/tools` does. The base harness retains its
21986
- AI-only peer boundary. Existing custom tool callbacks need no new import.
21987
-
21988
- | Export | Kind | Summary |
21989
- |--------|------|---------|
21990
- | `createAgentHarnessTools` / `AgentHarnessToolsConfig` | function / _type_ | async per-run full mount configuration (`services` plus all `AgentMountConfig` options); composes application lifecycle then runtime fence exactly once |
21991
-
21992
-
21993
22638
  ## `stitchkit/process`
21994
22639
 
21995
22640
  Native POSIX Bun/Node one-shot command execution. See [native IO](../guide/native-io.md).
@@ -21997,8 +22642,9 @@ Native POSIX Bun/Node one-shot command execution. See [native IO](../guide/nativ
21997
22642
  | Export | Kind | Purpose |
21998
22643
  |---|---|---|
21999
22644
  | `runNativeCommand` | function | caller-lifetime streaming or explicitly bounded binary capture |
22000
- | `NativeCommandOptions` | type | executable/args, env policy, signal/deadline, bytes sink and finite cleanup limits |
22001
- | `NativeCommandResult` | type | observed nullable exit code/signal and captured stdout/stderr bytes |
22645
+ | `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'`); an unknown key is refused by name, a removed one (`killGraceMs`) with its replacement |
22646
+ | `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 }` |
22647
+ | `NativeCommandResult` | type | observed nullable exit code/signal, `descendantsStopped` (members left in the group after the leader exited were stopped) and captured stdout/stderr bytes |
22002
22648
  | `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 |
22003
22649
 
22004
22650
  | `NativeCommandSettlement` | type | observed leader `exit` or terminal `error`, including synchronous native launch failure; `onLeaderSettled` runs once before drain, bounded by cleanupTimeoutMs, with a cancellable settlement signal; schema/pre-abort refusal creates no settlement obligation |
@@ -22010,19 +22656,29 @@ Native POSIX Bun/Node one-shot command execution. See [native IO](../guide/nativ
22010
22656
 
22011
22657
  `stitchkit/files/packaging` owns `createNativePackaging` and the types
22012
22658
  `NativePackagingOptions`, `NativePackagingAsset`, `NativePackagingPlugin`, `NativePackagingResult`.
22013
- It resolves the installed package's target/version and original addon SHA256.
22014
- `platform`, `architecture`, `delivery`, `entryPath` and `assetPath` are explicit public inputs.
22015
- The result is `ready` with assets and a structural Bun plugin, or `unsupported`/`missing` with a safe code.
22016
- Malformed options/metadata and other filesystem errors fail rather than masquerade as missing.
22659
+ It resolves the installed package's target/version, reads each selected addon once and checks it
22660
+ against the `size` and `sha256` published in the package's `native-assets.json` (`formatVersion: 2`,
22661
+ written when Stitchkit is built). Call it once per build and reuse the result.
22662
+ `platform`, `architecture`, `delivery`, `entryPath` and `assetPath` are explicit public inputs;
22663
+ `assetPath` accepts one template, `[hash]`, replaced by the first 16 hex digits of the addon's
22664
+ published SHA256;
22665
+ `platform` is the closed set `'darwin'`, and another name is a type error and a schema refusal.
22666
+ The result is `ready` with verified assets and a structural Bun plugin, or `unsupported` /
22667
+ `missing` / `mismatch` with the safe code `NATIVE_TARGET_UNSUPPORTED`, `NATIVE_ASSET_MISSING` or
22668
+ `NATIVE_ASSET_DIGEST_MISMATCH`. A `mismatch` names the first differing `architecture` with
22669
+ `expected` (published) and `actual` (installed) `{ size, sha256 }`.
22670
+ Malformed options, a manifest of another `formatVersion` and other filesystem errors throw rather
22671
+ than masquerade as missing.
22017
22672
  Companion accepts a single `architecture` string with an `assetPath` string, or an architecture
22018
22673
  array with an exact per-architecture `assetPath` map. `NativePackagingOptions<true>` and
22019
22674
  `NativePackagingResult<true>` describe the array form; default types preserve single-target signatures.
22020
22675
  A ready array-form result carries the architecture array; `NativePackagingAsset<true>` identifies
22021
- each asset architecture. Default asset types and single-target runtime objects retain their three fields.
22676
+ each asset architecture. A single-target asset has four fields: `outputPath`, the verified `bytes`,
22677
+ and the published `size` and `sha256`.
22022
22678
  Targets must be unique; entry and all asset paths must be disjoint. One lazy loader chooses only
22023
22679
  the actual runtime architecture, with no fallback.
22024
- Companion mode places external addons relative to the emitted entry; embedded mode lets
22025
- Bun compile retain the matching addon. Keep JS builds without splitting, preserve every output,
22026
- and copy each original asset to its `outputPath` after verifying `sha256`.
22680
+ Companion mode places external addons relative to the emitted entry; embedded mode gives
22681
+ Bun compile the verified bytes of the matching addon. Keep JS builds without splitting, preserve
22682
+ every output, and write each asset's `bytes` to its `outputPath`.
22027
22683
  This evolving entrypoint is a build dependency; runtime imports do not reach it.
22028
22684
  The detailed recipe is in [native IO](../guide/native-io.md#public-native-packaging).