@intentic/sandbox-contract 1.176.3 → 1.209.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 (334) hide show
  1. package/README.md +65 -0
  2. package/dist/agent-catalog.d.ts +15 -2
  3. package/dist/agent-catalog.d.ts.map +1 -1
  4. package/dist/agent-catalog.js +50 -1
  5. package/dist/agent-catalog.js.map +1 -1
  6. package/dist/agent-run-model.d.ts +3 -0
  7. package/dist/agent-run-model.d.ts.map +1 -0
  8. package/dist/agent-run-model.js +10 -0
  9. package/dist/agent-run-model.js.map +1 -0
  10. package/dist/capability-env.d.ts +2 -0
  11. package/dist/capability-env.d.ts.map +1 -0
  12. package/dist/capability-env.js +2 -0
  13. package/dist/capability-env.js.map +1 -0
  14. package/dist/chores/chores.d.ts +9 -0
  15. package/dist/chores/chores.d.ts.map +1 -1
  16. package/dist/chores/chores.js +257 -18
  17. package/dist/chores/chores.js.map +1 -1
  18. package/dist/chores/digest.js +0 -0
  19. package/dist/chores/digest.js.map +1 -1
  20. package/dist/chores/extension-update.d.ts +9 -0
  21. package/dist/chores/extension-update.d.ts.map +1 -0
  22. package/dist/chores/extension-update.js +15 -0
  23. package/dist/chores/extension-update.js.map +1 -0
  24. package/dist/chores/fix-deps.d.ts +10 -0
  25. package/dist/chores/fix-deps.d.ts.map +1 -0
  26. package/dist/chores/fix-deps.js +18 -0
  27. package/dist/chores/fix-deps.js.map +1 -0
  28. package/dist/chores/index.d.ts +7 -2
  29. package/dist/chores/index.d.ts.map +1 -1
  30. package/dist/chores/index.js +4 -1
  31. package/dist/chores/index.js.map +1 -1
  32. package/dist/chores/probes.d.ts.map +1 -1
  33. package/dist/chores/probes.js +127 -3
  34. package/dist/chores/probes.js.map +1 -1
  35. package/dist/chores/stack.d.ts +27 -0
  36. package/dist/chores/stack.d.ts.map +1 -0
  37. package/dist/chores/stack.js +121 -0
  38. package/dist/chores/stack.js.map +1 -0
  39. package/dist/chores/verdict.d.ts +2 -1
  40. package/dist/chores/verdict.d.ts.map +1 -1
  41. package/dist/chores/verdict.js +53 -6
  42. package/dist/chores/verdict.js.map +1 -1
  43. package/dist/contract-lock.d.ts +3 -0
  44. package/dist/contract-lock.d.ts.map +1 -0
  45. package/dist/contract-lock.js +35 -0
  46. package/dist/contract-lock.js.map +1 -0
  47. package/dist/contracts/activity.contract.d.ts +10 -0
  48. package/dist/contracts/activity.contract.d.ts.map +1 -1
  49. package/dist/contracts/agent.contract.d.ts +230 -20
  50. package/dist/contracts/agent.contract.d.ts.map +1 -1
  51. package/dist/contracts/agent.contract.js +2 -1
  52. package/dist/contracts/agent.contract.js.map +1 -1
  53. package/dist/contracts/agents.contract.d.ts +847 -1
  54. package/dist/contracts/agents.contract.d.ts.map +1 -1
  55. package/dist/contracts/agents.contract.js +3 -1
  56. package/dist/contracts/agents.contract.js.map +1 -1
  57. package/dist/contracts/automations.contract.d.ts +89 -0
  58. package/dist/contracts/automations.contract.d.ts.map +1 -1
  59. package/dist/contracts/automations.contract.js +3 -1
  60. package/dist/contracts/automations.contract.js.map +1 -1
  61. package/dist/contracts/capabilities.contract.d.ts +108 -32
  62. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  63. package/dist/contracts/capabilities.contract.js +5 -1
  64. package/dist/contracts/capabilities.contract.js.map +1 -1
  65. package/dist/contracts/chores.contract.d.ts +43 -0
  66. package/dist/contracts/chores.contract.d.ts.map +1 -1
  67. package/dist/contracts/ci.contract.d.ts +5 -0
  68. package/dist/contracts/ci.contract.d.ts.map +1 -1
  69. package/dist/contracts/ci.contract.js +2 -2
  70. package/dist/contracts/ci.contract.js.map +1 -1
  71. package/dist/contracts/claude.contract.d.ts +3 -14
  72. package/dist/contracts/claude.contract.d.ts.map +1 -1
  73. package/dist/contracts/claude.contract.js +2 -3
  74. package/dist/contracts/claude.contract.js.map +1 -1
  75. package/dist/contracts/drafts.contract.d.ts +4 -0
  76. package/dist/contracts/drafts.contract.d.ts.map +1 -1
  77. package/dist/contracts/endpoints.contract.d.ts +28 -12
  78. package/dist/contracts/endpoints.contract.d.ts.map +1 -1
  79. package/dist/contracts/endpoints.contract.js +9 -0
  80. package/dist/contracts/endpoints.contract.js.map +1 -1
  81. package/dist/contracts/extensions.contract.d.ts +399 -69
  82. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  83. package/dist/contracts/extensions.contract.js +15 -1
  84. package/dist/contracts/extensions.contract.js.map +1 -1
  85. package/dist/contracts/git.contract.d.ts +209 -11
  86. package/dist/contracts/git.contract.d.ts.map +1 -1
  87. package/dist/contracts/git.contract.js +12 -2
  88. package/dist/contracts/git.contract.js.map +1 -1
  89. package/dist/contracts/grok.contract.d.ts +0 -13
  90. package/dist/contracts/grok.contract.d.ts.map +1 -1
  91. package/dist/contracts/grok.contract.js +1 -2
  92. package/dist/contracts/grok.contract.js.map +1 -1
  93. package/dist/contracts/host.contract.d.ts +40 -0
  94. package/dist/contracts/host.contract.d.ts.map +1 -1
  95. package/dist/contracts/host.contract.js +3 -2
  96. package/dist/contracts/host.contract.js.map +1 -1
  97. package/dist/contracts/loops.contract.d.ts +343 -0
  98. package/dist/contracts/loops.contract.d.ts.map +1 -0
  99. package/dist/contracts/loops.contract.js +11 -0
  100. package/dist/contracts/loops.contract.js.map +1 -0
  101. package/dist/contracts/panels.contract.d.ts +6 -0
  102. package/dist/contracts/panels.contract.d.ts.map +1 -1
  103. package/dist/contracts/personas.contract.d.ts +112 -0
  104. package/dist/contracts/personas.contract.d.ts.map +1 -0
  105. package/dist/contracts/personas.contract.js +13 -0
  106. package/dist/contracts/personas.contract.js.map +1 -0
  107. package/dist/contracts/ports.contract.d.ts +1 -0
  108. package/dist/contracts/ports.contract.d.ts.map +1 -1
  109. package/dist/contracts/prepush.contract.d.ts +1 -0
  110. package/dist/contracts/prepush.contract.d.ts.map +1 -1
  111. package/dist/contracts/prepush.contract.js.map +1 -1
  112. package/dist/contracts/{codex.contract.d.ts → providers.contract.d.ts} +11 -3
  113. package/dist/contracts/providers.contract.d.ts.map +1 -0
  114. package/dist/contracts/providers.contract.js +6 -0
  115. package/dist/contracts/providers.contract.js.map +1 -0
  116. package/dist/contracts/public.contract.d.ts +24 -0
  117. package/dist/contracts/public.contract.d.ts.map +1 -0
  118. package/dist/contracts/public.contract.js +8 -0
  119. package/dist/contracts/public.contract.js.map +1 -0
  120. package/dist/contracts/push.contract.d.ts +3 -3
  121. package/dist/contracts/secrets.contract.d.ts +9 -0
  122. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  123. package/dist/contracts/sessions.contract.d.ts +16 -1
  124. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  125. package/dist/contracts/sessions.contract.js +1 -1
  126. package/dist/contracts/sessions.contract.js.map +1 -1
  127. package/dist/contracts/settings.contract.d.ts +352 -58
  128. package/dist/contracts/settings.contract.d.ts.map +1 -1
  129. package/dist/contracts/settings.contract.js +2 -1
  130. package/dist/contracts/settings.contract.js.map +1 -1
  131. package/dist/contracts/share.contract.d.ts +55 -0
  132. package/dist/contracts/share.contract.d.ts.map +1 -0
  133. package/dist/contracts/share.contract.js +9 -0
  134. package/dist/contracts/share.contract.js.map +1 -0
  135. package/dist/contracts/skills.contract.d.ts +41 -0
  136. package/dist/contracts/skills.contract.d.ts.map +1 -0
  137. package/dist/contracts/skills.contract.js +9 -0
  138. package/dist/contracts/skills.contract.js.map +1 -0
  139. package/dist/contracts/system.contract.d.ts +205 -8
  140. package/dist/contracts/system.contract.d.ts.map +1 -1
  141. package/dist/contracts/system.contract.js +10 -2
  142. package/dist/contracts/system.contract.js.map +1 -1
  143. package/dist/contracts/workflows.contract.d.ts +575 -0
  144. package/dist/contracts/workflows.contract.d.ts.map +1 -0
  145. package/dist/contracts/workflows.contract.js +13 -0
  146. package/dist/contracts/workflows.contract.js.map +1 -0
  147. package/dist/contracts/workspace.contract.d.ts +18 -4
  148. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  149. package/dist/contracts/workspace.contract.js +6 -3
  150. package/dist/contracts/workspace.contract.js.map +1 -1
  151. package/dist/conversation-ids.d.ts +2 -0
  152. package/dist/conversation-ids.d.ts.map +1 -0
  153. package/dist/conversation-ids.js +123 -0
  154. package/dist/conversation-ids.js.map +1 -0
  155. package/dist/events.d.ts +523 -17
  156. package/dist/events.d.ts.map +1 -1
  157. package/dist/events.js +106 -13
  158. package/dist/events.js.map +1 -1
  159. package/dist/history-state.d.ts.map +1 -1
  160. package/dist/history-state.js +10 -0
  161. package/dist/history-state.js.map +1 -1
  162. package/dist/hostnames.d.ts +4 -0
  163. package/dist/hostnames.d.ts.map +1 -1
  164. package/dist/hostnames.js +7 -2
  165. package/dist/hostnames.js.map +1 -1
  166. package/dist/index.d.ts +3792 -478
  167. package/dist/index.d.ts.map +1 -1
  168. package/dist/index.js +39 -17
  169. package/dist/index.js.map +1 -1
  170. package/dist/listener-protocol.d.ts +77 -0
  171. package/dist/listener-protocol.d.ts.map +1 -0
  172. package/dist/listener-protocol.js +33 -0
  173. package/dist/listener-protocol.js.map +1 -0
  174. package/dist/model-order.d.ts +1 -0
  175. package/dist/model-order.d.ts.map +1 -1
  176. package/dist/model-order.js +16 -0
  177. package/dist/model-order.js.map +1 -1
  178. package/dist/output-fields.d.ts +30 -0
  179. package/dist/output-fields.d.ts.map +1 -0
  180. package/dist/output-fields.js +57 -0
  181. package/dist/output-fields.js.map +1 -0
  182. package/dist/path-refs.d.ts.map +1 -1
  183. package/dist/path-refs.js.map +1 -1
  184. package/dist/publish-drafts.d.ts +7 -0
  185. package/dist/publish-drafts.d.ts.map +1 -0
  186. package/dist/publish-drafts.js +20 -0
  187. package/dist/publish-drafts.js.map +1 -0
  188. package/dist/quick-model.d.ts +2 -1
  189. package/dist/quick-model.d.ts.map +1 -1
  190. package/dist/quick-model.js +18 -14
  191. package/dist/quick-model.js.map +1 -1
  192. package/dist/routes.d.ts +3 -0
  193. package/dist/routes.d.ts.map +1 -1
  194. package/dist/routes.js +63 -0
  195. package/dist/routes.js.map +1 -1
  196. package/dist/runtime-state.d.ts +33 -0
  197. package/dist/runtime-state.d.ts.map +1 -0
  198. package/dist/runtime-state.js +14 -0
  199. package/dist/runtime-state.js.map +1 -0
  200. package/dist/schemas.d.ts +4889 -851
  201. package/dist/schemas.d.ts.map +1 -1
  202. package/dist/schemas.js +993 -192
  203. package/dist/schemas.js.map +1 -1
  204. package/dist/search-globs.d.ts +6 -0
  205. package/dist/search-globs.d.ts.map +1 -0
  206. package/dist/search-globs.js +39 -0
  207. package/dist/search-globs.js.map +1 -0
  208. package/dist/share-paths.d.ts +8 -0
  209. package/dist/share-paths.d.ts.map +1 -0
  210. package/dist/share-paths.js +21 -0
  211. package/dist/share-paths.js.map +1 -0
  212. package/dist/title.d.ts.map +1 -1
  213. package/dist/title.js +1 -1
  214. package/dist/title.js.map +1 -1
  215. package/dist/tunnel-ids.d.ts +1 -0
  216. package/dist/tunnel-ids.d.ts.map +1 -1
  217. package/dist/tunnel-ids.js +1 -0
  218. package/dist/tunnel-ids.js.map +1 -1
  219. package/dist/versions.d.ts +4 -0
  220. package/dist/versions.d.ts.map +1 -0
  221. package/dist/versions.js +15 -0
  222. package/dist/versions.js.map +1 -0
  223. package/dist/workflow-faults.d.ts +4 -0
  224. package/dist/workflow-faults.d.ts.map +1 -0
  225. package/dist/workflow-faults.js +115 -0
  226. package/dist/workflow-faults.js.map +1 -0
  227. package/dist/workspace-state.d.ts +280 -1
  228. package/dist/workspace-state.d.ts.map +1 -1
  229. package/dist/workspace-state.js +163 -21
  230. package/dist/workspace-state.js.map +1 -1
  231. package/package.json +30 -6
  232. package/src/agent-catalog.test.ts +147 -7
  233. package/src/agent-catalog.ts +233 -21
  234. package/src/agent-run-model.test.ts +60 -0
  235. package/src/agent-run-model.ts +51 -0
  236. package/src/capability-env.ts +10 -0
  237. package/src/capability-ledger.test.ts +110 -0
  238. package/src/chores/chores.test.ts +32 -0
  239. package/src/chores/chores.ts +456 -32
  240. package/src/chores/digest.ts +0 -0
  241. package/src/chores/extension-update.ts +37 -0
  242. package/src/chores/fix-deps.ts +40 -0
  243. package/src/chores/index.ts +7 -2
  244. package/src/chores/probes.test.ts +155 -3
  245. package/src/chores/probes.ts +200 -4
  246. package/src/chores/stack.test.ts +126 -0
  247. package/src/chores/stack.ts +244 -0
  248. package/src/chores/verdict.test.ts +357 -19
  249. package/src/chores/verdict.ts +95 -21
  250. package/src/contract-lock.test.ts +17 -0
  251. package/src/contract-lock.ts +66 -0
  252. package/src/contracts/agent.contract.ts +6 -0
  253. package/src/contracts/agents.contract.ts +12 -0
  254. package/src/contracts/automations.contract.ts +17 -1
  255. package/src/contracts/capabilities.contract.ts +22 -0
  256. package/src/contracts/ci.contract.ts +10 -2
  257. package/src/contracts/claude.contract.ts +8 -4
  258. package/src/contracts/endpoints.contract.ts +21 -0
  259. package/src/contracts/extensions.contract.ts +42 -0
  260. package/src/contracts/git.contract.ts +33 -6
  261. package/src/contracts/grok.contract.ts +4 -3
  262. package/src/contracts/host.contract.ts +13 -2
  263. package/src/contracts/loops.contract.ts +62 -0
  264. package/src/contracts/personas.contract.ts +56 -0
  265. package/src/contracts/prepush.contract.ts +4 -3
  266. package/src/contracts/providers.contract.ts +23 -0
  267. package/src/contracts/public.contract.ts +14 -0
  268. package/src/contracts/sessions.contract.ts +5 -1
  269. package/src/contracts/settings.contract.ts +5 -0
  270. package/src/contracts/share.contract.ts +20 -0
  271. package/src/contracts/skills.contract.ts +24 -0
  272. package/src/contracts/system.contract.ts +27 -3
  273. package/src/contracts/workflows.contract.ts +78 -0
  274. package/src/contracts/workspace.contract.ts +10 -2
  275. package/src/conversation-ids.test.ts +61 -0
  276. package/src/conversation-ids.ts +173 -0
  277. package/src/events.test.ts +20 -1
  278. package/src/events.ts +404 -43
  279. package/src/history-state.ts +30 -1
  280. package/src/hostnames.ts +20 -6
  281. package/src/index.ts +58 -17
  282. package/src/listener-protocol.ts +96 -0
  283. package/src/model-order.test.ts +32 -1
  284. package/src/model-order.ts +56 -3
  285. package/src/output-fields.ts +111 -0
  286. package/src/path-refs.test.ts +4 -4
  287. package/src/path-refs.ts +5 -5
  288. package/src/publish-drafts.ts +59 -0
  289. package/src/quick-model.test.ts +70 -27
  290. package/src/quick-model.ts +76 -40
  291. package/src/routes.test.ts +91 -3
  292. package/src/routes.ts +135 -0
  293. package/src/runtime-state.test.ts +45 -0
  294. package/src/runtime-state.ts +124 -0
  295. package/src/schemas.test.ts +16 -29
  296. package/src/schemas.ts +3480 -552
  297. package/src/search-globs.test.ts +46 -0
  298. package/src/search-globs.ts +76 -0
  299. package/src/share-paths.test.ts +48 -0
  300. package/src/share-paths.ts +68 -0
  301. package/src/state-portability.ts +8 -8
  302. package/src/title.test.ts +20 -1
  303. package/src/title.ts +12 -3
  304. package/src/tunnel-ids.ts +15 -1
  305. package/src/versions.test.ts +39 -0
  306. package/src/versions.ts +48 -0
  307. package/src/workflow-faults.test.ts +127 -0
  308. package/src/workflow-faults.ts +188 -0
  309. package/src/workspace-state.test.ts +169 -15
  310. package/src/workspace-state.ts +427 -35
  311. package/dist/contracts/codex.contract.d.ts.map +0 -1
  312. package/dist/contracts/codex.contract.js +0 -6
  313. package/dist/contracts/codex.contract.js.map +0 -1
  314. package/dist/contracts/gemini.contract.d.ts +0 -16
  315. package/dist/contracts/gemini.contract.d.ts.map +0 -1
  316. package/dist/contracts/gemini.contract.js +0 -6
  317. package/dist/contracts/gemini.contract.js.map +0 -1
  318. package/dist/contracts/kimi.contract.d.ts +0 -16
  319. package/dist/contracts/kimi.contract.d.ts.map +0 -1
  320. package/dist/contracts/kimi.contract.js +0 -6
  321. package/dist/contracts/kimi.contract.js.map +0 -1
  322. package/dist/contracts/komodo.contract.d.ts +0 -125
  323. package/dist/contracts/komodo.contract.d.ts.map +0 -1
  324. package/dist/contracts/komodo.contract.js +0 -14
  325. package/dist/contracts/komodo.contract.js.map +0 -1
  326. package/dist/contracts/memory.contract.d.ts +0 -34
  327. package/dist/contracts/memory.contract.d.ts.map +0 -1
  328. package/dist/contracts/memory.contract.js +0 -9
  329. package/dist/contracts/memory.contract.js.map +0 -1
  330. package/src/contracts/codex.contract.ts +0 -10
  331. package/src/contracts/gemini.contract.ts +0 -11
  332. package/src/contracts/kimi.contract.ts +0 -9
  333. package/src/contracts/komodo.contract.ts +0 -36
  334. package/src/contracts/memory.contract.ts +0 -13
@@ -16,7 +16,7 @@ import type { StateFile } from "./state-portability.js";
16
16
  * these entries would be a column of empty arrays. What they share is the portability class, and that is
17
17
  * imported rather than duplicated.
18
18
  *
19
- * `history-state.test.ts` fails when a daemon store builds a `/history` path this list doesn't carry, in both
19
+ * `history-state-coverage.test.ts` fails when a daemon store builds a `/history` path this list doesn't carry, in both
20
20
  * directions — the same shape-recognizing guard that covers the workspace table.
21
21
  */
22
22
 
@@ -38,10 +38,26 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
38
38
  { path: "agents.json", portability: "carry" },
39
39
  { path: "turns/", portability: "carry" },
40
40
  { path: "transcripts/", portability: "carry" },
41
+ // What each message can be put back to — a workspace checkpoint, or an isolated conversation's own commits.
42
+ // Carried WITH the transcripts and the scopes above, because it is the join between them: without it a
43
+ // restored conversation reads back whole and offers no way back into it, even though both the messages and
44
+ // the states they name travelled.
45
+ { path: "turn-anchors.json", portability: "carry" },
46
+ /* WHICH CONVERSATIONS ARE PUBLISHED AS PAGES ANYONE WITH THE LINK CAN READ.
47
+ *
48
+ * Carried, and it is the entry with the most to say for itself: the PAGES live in the workspace's outbox
49
+ * (`public/`), so they travel with `/work` whatever this says. Leaving the index behind would restore a
50
+ * sandbox that is still serving somebody's conversation on the internet with nothing in the app that knows
51
+ * it — no row, no link, and no way to stop sharing short of deleting files by hand. The index is what makes
52
+ * a published page withdrawable, so it goes wherever the pages go. */
53
+ { path: "shares.json", portability: "carry" },
41
54
  { path: "activity.jsonl", portability: "carry" },
42
55
  { path: "usage.jsonl", portability: "carry" },
43
56
  { path: "account-usage.json", portability: "carry" },
44
57
  { path: "provider-refusals.json", portability: "carry" },
58
+ // Explicit first-time dependency setup requests. Carrying the worklist preserves the owner's decision when
59
+ // an export interrupts the queue before its terminal starts; fulfilled entries remove themselves.
60
+ { path: "dependency-requests.json", portability: "carry" },
45
61
  // The deploy engine's own ledgers — a run's events and the check results the Pipelines view reads back.
46
62
  { path: "apply-events.ndjson", portability: "carry" },
47
63
  { path: "check-events/", portability: "carry" },
@@ -66,6 +82,11 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
66
82
  { path: "logs/", portability: "derived" },
67
83
  { path: "trash/", portability: "derived" },
68
84
  { path: ".isolation-probe", portability: "derived" },
85
+ /* The finished bundles themselves. `derived` is doing real work here rather than describing leftovers: an
86
+ * export that carried the export directory would pack every previous bundle into the new one, and the next
87
+ * export would pack THAT — each one a multiple of the last. Living on this volume is the other half of the
88
+ * same guard; under `/work` the file would also be watched, indexed by iq, and snapshotted into history. */
89
+ { path: "exports/", portability: "derived" },
69
90
 
70
91
  /* ---- credentials ---- */
71
92
 
@@ -88,6 +109,11 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
88
109
  /* Signs every browser session cookie. Carrying it would let a bundle's holder mint sessions against the
89
110
  * target — an export becomes a credential — and the target minting its own costs exactly one sign-in. */
90
111
  { path: "session-secret", portability: "identity", note: "Sign in again — the target signs its own sessions." },
112
+ {
113
+ path: "browser-access-disabled",
114
+ portability: "identity",
115
+ note: "Account-deletion retirement belongs to the source sandbox; the imported copy starts with fresh browser access.",
116
+ },
91
117
  {
92
118
  path: "push.json",
93
119
  portability: "identity",
@@ -96,5 +122,8 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
96
122
  { path: "sync-enrollments.json", portability: "identity", note: "Re-pair desktop sync from the Sync tab." },
97
123
  { path: "sync-pair-consumed.json", portability: "identity" },
98
124
  { path: "host-enrollments.json", portability: "identity" },
125
+ // The burn list for setup-time computer pairings. Identity, like sync's beside it, and for a sharper reason:
126
+ // carrying it into another sandbox would mark that sandbox's own fresh pairing as already spent.
127
+ { path: "host-pair-consumed.json", portability: "identity" },
99
128
  { path: "local-cert/", portability: "identity" },
100
129
  ];
package/src/hostnames.ts CHANGED
@@ -48,22 +48,34 @@ export const CATCH_ALL = { service: "http_status:404" } as const;
48
48
  // ports. The slot labels are salted with the connect token rather than being the letters a…h, so a forwarded
49
49
  // port's hostname is not derivable from the (public) sandbox id alone — see tunnel-ids for why that matters.
50
50
  //
51
- // A *label* is the first-DNS-label prefix before `-<sandboxId>` (`preview-<panel>` / `port-<slot>`) — the unit
52
- // the platform's /sandbox/preview-route mints, so one endpoint serves both schemes.
51
+ // Outbox scheme: `public-<slot>-<sandboxId>.<zone>` the same shape again, serving the workspace's `public/`
52
+ // directory as static files. <slot> is publicSlotFromToken (./tunnel-ids), salted for the same reason the port
53
+ // slots are, and one record per sandbox rather than a pool: there is one outbox, and its link has to stay good
54
+ // for as long as the file does.
55
+ //
56
+ // A *label* is the first-DNS-label prefix before `-<sandboxId>` (`preview-<panel>` / `port-<slot>` /
57
+ // `public-<slot>`) — the unit the platform's /sandbox/preview-route mints, so one endpoint serves all three.
53
58
  export const previewLabel = (panel: string): string => `preview-${panel}`;
54
59
  export const portLabel = (slot: string): string => `port-${slot}`;
60
+ export const publicLabel = (slot: string): string => `public-${slot}`;
55
61
 
56
62
  // The hostname a label resolves to — what the platform's /sandbox/preview-route mints from the label alone.
57
63
  export const labelHostname = (label: string, id: string, zone: string): string => `${label}-${id}.${zone}`;
58
64
  export const previewHostname = (panel: string, id: string, zone: string): string => labelHostname(previewLabel(panel), id, zone);
59
65
  export const portHostname = (slot: string, id: string, zone: string): string => labelHostname(portLabel(slot), id, zone);
66
+ export const publicHostname = (slot: string, id: string, zone: string): string => labelHostname(publicLabel(slot), id, zone);
60
67
 
61
- // A panel's / forwarded port's preview URL — undefined unless the sandbox has both a zone and an id
62
- // (headless/loopback sandboxes have neither and advertise no preview).
68
+ // A label's public URL — undefined unless the sandbox has both a zone and an id (headless/loopback sandboxes
69
+ // have neither and advertise nothing). One builder, three vocabularies: a panel's preview, a forwarded port's,
70
+ // and the outbox's.
71
+ const labelUrl = (label: string, zone: string | undefined, sandboxId: string | undefined): string | undefined =>
72
+ zone !== undefined && zone !== "" && sandboxId !== undefined ? `https://${labelHostname(label, sandboxId, zone)}` : undefined;
63
73
  export const previewUrl = (panel: string, zone: string | undefined, sandboxId: string | undefined): string | undefined =>
64
- zone !== undefined && zone !== "" && sandboxId !== undefined ? `https://${previewHostname(panel, sandboxId, zone)}` : undefined;
74
+ labelUrl(previewLabel(panel), zone, sandboxId);
65
75
  export const portUrl = (slot: string, zone: string | undefined, sandboxId: string | undefined): string | undefined =>
66
- zone !== undefined && zone !== "" && sandboxId !== undefined ? `https://${portHostname(slot, sandboxId, zone)}` : undefined;
76
+ labelUrl(portLabel(slot), zone, sandboxId);
77
+ export const publicUrl = (slot: string, zone: string | undefined, sandboxId: string | undefined): string | undefined =>
78
+ labelUrl(publicLabel(slot), zone, sandboxId);
67
79
 
68
80
  // The key after `<prefix>` from a request's Host header. The first DNS label must carry the prefix (the
69
81
  // own-Cloudflare wildcard also catches stray subdomains → undefined → the caller's 404) and, when the sandbox
@@ -87,6 +99,8 @@ export const panelFromHost = (hostHeader: string | undefined, sandboxId: string
87
99
  keyFromHost("preview-", hostHeader, sandboxId);
88
100
  export const portSlotFromHost = (hostHeader: string | undefined, sandboxId: string | undefined): string | undefined =>
89
101
  keyFromHost("port-", hostHeader, sandboxId);
102
+ export const publicSlotFromHost = (hostHeader: string | undefined, sandboxId: string | undefined): string | undefined =>
103
+ keyFromHost("public-", hostHeader, sandboxId);
90
104
 
91
105
  // The sandbox's identity AS THE USER SEES IT: the leading DNS label of its public URL, minus the `sandbox-`
92
106
  // prefix — `https://sandbox-0f310c3c4db4.intentic.dev` → `0f310c3c4db4`, i.e. sandboxIdFromToken's digest read
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ContractRoute } from "./routes.js";
2
- import { contractRoutes, routeNameForRequest } from "./routes.js";
2
+ import { contractRoutes, requestPathFor, routeForProcedure, routeNameForRequest, routeShapes } from "./routes.js";
3
3
  import { activityContract } from "./contracts/activity.contract.js";
4
4
  import { agentContract } from "./contracts/agent.contract.js";
5
5
  import { agentsContract } from "./contracts/agents.contract.js";
@@ -8,31 +8,33 @@ import { capabilitiesContract } from "./contracts/capabilities.contract.js";
8
8
  import { choresContract } from "./contracts/chores.contract.js";
9
9
  import { ciContract } from "./contracts/ci.contract.js";
10
10
  import { claudeContract } from "./contracts/claude.contract.js";
11
- import { codexContract } from "./contracts/codex.contract.js";
12
11
  import { draftsContract } from "./contracts/drafts.contract.js";
13
12
  import { endpointsContract } from "./contracts/endpoints.contract.js";
14
13
  import { extensionsContract } from "./contracts/extensions.contract.js";
15
- import { geminiContract } from "./contracts/gemini.contract.js";
14
+ import { personasContract } from "./contracts/personas.contract.js";
16
15
  import { gitContract } from "./contracts/git.contract.js";
17
16
  import { grokContract } from "./contracts/grok.contract.js";
18
17
  import { historyContract } from "./contracts/history.contract.js";
19
18
  import { intenticContract } from "./contracts/intentic.contract.js";
20
19
  import { inventoryContract } from "./contracts/inventory.contract.js";
21
- import { kimiContract } from "./contracts/kimi.contract.js";
22
- import { komodoContract } from "./contracts/komodo.contract.js";
23
20
  import { logsContract } from "./contracts/logs.contract.js";
24
- import { memoryContract } from "./contracts/memory.contract.js";
21
+ import { loopsContract } from "./contracts/loops.contract.js";
25
22
  import { panelsContract } from "./contracts/panels.contract.js";
26
23
  import { portsContract } from "./contracts/ports.contract.js";
24
+ import { publicContract } from "./contracts/public.contract.js";
27
25
  import { prepushContract } from "./contracts/prepush.contract.js";
26
+ import { providersContract } from "./contracts/providers.contract.js";
28
27
  import { pushContract } from "./contracts/push.contract.js";
29
28
  import { secretsContract } from "./contracts/secrets.contract.js";
30
29
  import { sessionsContract } from "./contracts/sessions.contract.js";
31
30
  import { settingsContract } from "./contracts/settings.contract.js";
31
+ import { shareContract } from "./contracts/share.contract.js";
32
+ import { skillsContract } from "./contracts/skills.contract.js";
32
33
  import { systemContract } from "./contracts/system.contract.js";
33
34
  import { translatorContract } from "./contracts/translator.contract.js";
34
35
  import { usageContract } from "./contracts/usage.contract.js";
35
36
  import { vpnContract } from "./contracts/vpn.contract.js";
37
+ import { workflowsContract } from "./contracts/workflows.contract.js";
36
38
  import { workspaceContract } from "./contracts/workspace.contract.js";
37
39
 
38
40
  export { activityContract } from "./contracts/activity.contract.js";
@@ -43,11 +45,10 @@ export { capabilitiesContract } from "./contracts/capabilities.contract.js";
43
45
  export { choresContract } from "./contracts/chores.contract.js";
44
46
  export { ciContract } from "./contracts/ci.contract.js";
45
47
  export { claudeContract } from "./contracts/claude.contract.js";
46
- export { codexContract } from "./contracts/codex.contract.js";
47
48
  export { draftsContract } from "./contracts/drafts.contract.js";
48
- export { endpointsContract } from "./contracts/endpoints.contract.js";
49
+ export { endpointsContract, TrialStatusSchema, type TrialStatusResponse } from "./contracts/endpoints.contract.js";
49
50
  export { extensionsContract } from "./contracts/extensions.contract.js";
50
- export { geminiContract } from "./contracts/gemini.contract.js";
51
+ export { personasContract } from "./contracts/personas.contract.js";
51
52
  export { gitContract } from "./contracts/git.contract.js";
52
53
  export { grokContract } from "./contracts/grok.contract.js";
53
54
  export { historyContract } from "./contracts/history.contract.js";
@@ -56,37 +57,61 @@ export { historyContract } from "./contracts/history.contract.js";
56
57
  export { hostContract } from "./contracts/host.contract.js";
57
58
  export { intenticContract } from "./contracts/intentic.contract.js";
58
59
  export { inventoryContract } from "./contracts/inventory.contract.js";
59
- export { kimiContract } from "./contracts/kimi.contract.js";
60
- export { komodoContract } from "./contracts/komodo.contract.js";
61
60
  export { logsContract } from "./contracts/logs.contract.js";
62
- export { memoryContract } from "./contracts/memory.contract.js";
61
+ export { loopsContract } from "./contracts/loops.contract.js";
63
62
  export { panelsContract } from "./contracts/panels.contract.js";
64
63
  export { portsContract } from "./contracts/ports.contract.js";
64
+ export { publicContract } from "./contracts/public.contract.js";
65
65
  export { prepushContract } from "./contracts/prepush.contract.js";
66
+ export { providersContract } from "./contracts/providers.contract.js";
66
67
  export { pushContract } from "./contracts/push.contract.js";
67
68
  export { secretsContract } from "./contracts/secrets.contract.js";
68
69
  export { sessionsContract } from "./contracts/sessions.contract.js";
69
70
  export { settingsContract } from "./contracts/settings.contract.js";
71
+ export { shareContract } from "./contracts/share.contract.js";
72
+ export { skillsContract } from "./contracts/skills.contract.js";
70
73
  export { systemContract } from "./contracts/system.contract.js";
71
74
  export { translatorContract } from "./contracts/translator.contract.js";
72
75
  export { usageContract } from "./contracts/usage.contract.js";
73
76
  export { vpnContract } from "./contracts/vpn.contract.js";
77
+ export { workflowsContract } from "./contracts/workflows.contract.js";
74
78
  export { workspaceContract } from "./contracts/workspace.contract.js";
75
79
  export * from "./events.js";
76
80
  export * from "./sse.js";
77
81
  export * from "./routes.js";
82
+ /* THE CONTAINER'S FIXED DIRECTORY LAYOUT, re-exported so extensions can reach it.
83
+ *
84
+ * The names are defined once in @intentic/constants, which sits at the bottom of the dependency graph. An
85
+ * EXTENSION may not import that package — the boundary rule (.oxlintrc.json, _extensions/README.md) allows
86
+ * only the SDK halves and this contract, so that an extension cannot couple itself to app or engine internals.
87
+ * That rule is right, and it left extensions with no way to name the workspace root except by spelling it.
88
+ *
89
+ * Re-exporting here is what closes that gap without widening the boundary: the layout is exactly the kind of
90
+ * thing this package already carries — shared vocabulary both sides of the wire must agree on, alongside the
91
+ * state-file table below — and there is still one definition, in one place, that everything resolves to. */
92
+ export { HISTORY_ROOT, HOST_STATE_ROOT, STATE_DIR, WORKSPACE_ROOT } from "@intentic/constants";
78
93
  export * from "./workspace-state.js";
94
+ export * from "./runtime-state.js";
79
95
  export * from "./state-portability.js";
80
96
  export * from "./history-state.js";
81
97
  export * from "./agent-catalog.js";
98
+ export * from "./agent-run-model.js";
99
+ export * from "./capability-env.js";
100
+ export * from "./conversation-ids.js";
82
101
  export * from "./host-protocol.js";
102
+ export * from "./listener-protocol.js";
83
103
  export * from "./hostnames.js";
84
104
  export * from "./model-order.js";
85
105
  export * from "./path-refs.js";
86
106
  export * from "./quick-model.js";
107
+ export * from "./output-fields.js";
108
+ export * from "./publish-drafts.js";
87
109
  export * from "./schemas.js";
110
+ export * from "./search-globs.js";
88
111
  export * from "./terminal-protocol.js";
89
112
  export * from "./title.js";
113
+ export * from "./versions.js";
114
+ export * from "./workflow-faults.js";
90
115
 
91
116
  // The aggregated contract — implemented on the server by the per-domain route factories and consumed by the
92
117
  // browser's typed oRPC client (ContractRouterClient<typeof sandboxContract>). The wire paths it declares are
@@ -100,32 +125,34 @@ export const sandboxContract = {
100
125
  chores: choresContract,
101
126
  ci: ciContract,
102
127
  claude: claudeContract,
103
- codex: codexContract,
104
128
  drafts: draftsContract,
105
129
  endpoints: endpointsContract,
106
130
  extensions: extensionsContract,
131
+ personas: personasContract,
107
132
  sessions: sessionsContract,
108
133
  settings: settingsContract,
134
+ share: shareContract,
135
+ skills: skillsContract,
109
136
  intentic: intenticContract,
110
- gemini: geminiContract,
111
137
  git: gitContract,
112
138
  grok: grokContract,
113
- kimi: kimiContract,
114
- komodo: komodoContract,
115
139
  history: historyContract,
116
140
  workspace: workspaceContract,
117
141
  inventory: inventoryContract,
118
142
  logs: logsContract,
119
- memory: memoryContract,
143
+ loops: loopsContract,
120
144
  panels: panelsContract,
121
145
  ports: portsContract,
146
+ public: publicContract,
122
147
  prepush: prepushContract,
148
+ providers: providersContract,
123
149
  push: pushContract,
124
150
  secrets: secretsContract,
125
151
  system: systemContract,
126
152
  translator: translatorContract,
127
153
  usage: usageContract,
128
154
  vpn: vpnContract,
155
+ workflows: workflowsContract,
129
156
  };
130
157
 
131
158
  // Every route in THIS build of the contract, and the names the daemon advertises on its hello frame. Bound here
@@ -135,6 +162,20 @@ export const sandboxContract = {
135
162
  export const SANDBOX_ROUTES: readonly ContractRoute[] = contractRoutes(sandboxContract);
136
163
  export const SANDBOX_ROUTE_NAMES: readonly string[] = SANDBOX_ROUTES.map((route) => route.name);
137
164
 
165
+ /* And the SHAPE of each of those routes, advertised beside the names for the failure the names cannot describe:
166
+ * a route both builds have, answering a payload only one of them expects. Computed once at module load — it
167
+ * walks every contract schema through `z.toJSONSchema`, which is far too much work to repeat per connection and
168
+ * exactly the kind of thing that never changes for the life of a process. See routes.ts. */
169
+ export const SANDBOX_ROUTE_SHAPES: Readonly<Record<string, string>> = routeShapes(sandboxContract);
170
+
138
171
  // The contract route a concrete browser request belongs to, bound to this build's route table.
139
172
  export const sandboxRouteName = (method: string, pathWithQuery: string): string | undefined =>
140
173
  routeNameForRequest(SANDBOX_ROUTES, method, pathWithQuery);
174
+
175
+ // The method and concrete path a TYPED call is about to put on the wire, bound to this build's route table.
176
+ // Undefined when the procedure is not one this contract declares, which a typed caller cannot reach — the host
177
+ // gate treats it as a refusal rather than assuming it is harmless.
178
+ export const sandboxRequestFor = (procedure: readonly string[], input: unknown): { method: string; path: string } | undefined => {
179
+ const route = routeForProcedure(SANDBOX_ROUTES, procedure);
180
+ return route === undefined ? undefined : { method: route.method, path: requestPathFor(route, input) };
181
+ };
@@ -0,0 +1,96 @@
1
+ import { z } from "zod";
2
+ import { ActivityStatusSchema } from "./schemas.js";
3
+
4
+ /* The wire between the daemon and an extension's realtime-listener GATEWAY process (ext-discord, ext-slack,
5
+ * ext-telegram, ext-whatsapp, ext-imap): the four provider-scoped routes app.ts mounts under
6
+ * /listeners/:provider — state, dispatch, failure, status. These shapes used to live daemon-side only, with
7
+ * every gateway hand-writing its own copy of the payloads as untyped literals; a field rename broke five
8
+ * producers silently. They live in the contract now so BOTH ends compile against one declaration — the daemon
9
+ * parses with the schemas, the gateways (via @intentic/connector-runtime) type against the inferred types. */
10
+
11
+ // One normalized inbound event — serialized as a JSON line in the automation's payload, and the JSON body a
12
+ // realtime source POSTs to /listeners/<provider>/dispatch. A zod schema (not a bare interface) because it's
13
+ // parsed from an extension gateway's request; `provider` and `type` are open strings — the source is
14
+ // extension-declared (contributes.listener), not a core enum.
15
+ export const ListenerMessageSchema = z.object({
16
+ provider: z.string().min(1),
17
+ type: z.string().min(1),
18
+ id: z.string(),
19
+ channelId: z.string(),
20
+ author: z.object({ id: z.string(), name: z.string() }),
21
+ content: z.string(),
22
+ // Discord message: it @mentions one of our bots or replies to a bot's message. Voice events never set it.
23
+ mentioned: z.boolean().optional(),
24
+ // CI pipeline event: the ref it ran on. Top-level rather than inside `extra` for the same reason
25
+ // `mentioned` is — the dispatcher MATCHES on it, and a narrowing axis the trigger can name has to be
26
+ // a field of the message rather than a key in a provider's opaque bag.
27
+ branch: z.string().optional(),
28
+ // Prior channel messages (chronological) fetched when a bot is tagged, so the agent can reason about why.
29
+ // Kept a top-level field (not in `extra`) so it reaches the model's payload but stays out of the activity
30
+ // feed, which logs only content/extra.
31
+ history: z
32
+ .array(
33
+ z.object({
34
+ author: z.object({ id: z.string(), name: z.string() }),
35
+ content: z.string(),
36
+ timestamp: z.string(),
37
+ self: z.boolean().optional(),
38
+ }),
39
+ )
40
+ .optional(),
41
+ timestamp: z.string(),
42
+ // Provider-specific fields (discord message: guildId, attachments; voice_utterance: path;
43
+ // voice_transcript: path, participants, durationSeconds).
44
+ extra: z.record(z.string(), z.unknown()).optional(),
45
+ });
46
+ export type ListenerMessage = z.infer<typeof ListenerMessageSchema>;
47
+
48
+ // One ndjson frame of a /listeners/<provider>/dispatch?stream=1 response — a text delta for one automation's
49
+ // reply, the provider's own failure sentence, or that automation's terminal marker. A type, not a schema: the
50
+ // DAEMON produces these (listener.routes.ts), so nothing parses them from untrusted input.
51
+ export interface ListenerDispatchFrame {
52
+ readonly automationId: string;
53
+ readonly delta?: string;
54
+ // The turn refused or broke, in the provider's own words — forwarded verbatim because a gateway delivers
55
+ // into the owner's own channel, where the actual sentence is the useful thing.
56
+ readonly failed?: string;
57
+ readonly end?: boolean;
58
+ }
59
+
60
+ /* WHERE ONE CAPABILITY'S LINK-A-DEVICE CEREMONY STANDS (whatsapp), reported every status tick.
61
+ *
62
+ * A CODE IS NOT THE ONLY THING WORTH SAYING, and publishing only codes is what made a phone that had never
63
+ * linked read as connected: the seconds before the first code, a gateway that just restarted, and a number
64
+ * WhatsApp refused all looked identical from the daemon's side — an absent code — so the card fell through to
65
+ * "ready" and the owner was sent away from the one screen that could have shown them the step. Each of those is
66
+ * its own state here, and every one of them means NOT PAIRED YET.
67
+ *
68
+ * `since` stamps the CURRENT code. WhatsApp closes an unpaired socket after a minute or so and each reopen mints
69
+ * a fresh code, so a code is a thing with an age — the card says how old the one on screen is rather than
70
+ * leaving the owner to type a dead one twice. */
71
+ export const ListenerPairingSchema = z.object({
72
+ // waiting: a socket is up and the code hasn't arrived (or the last one died with its socket).
73
+ // code: `code` is live — type it on the phone. failed: `detail` says what WhatsApp refused.
74
+ state: z.enum(["waiting", "code", "failed"]),
75
+ code: z.string().optional(),
76
+ detail: z.string().optional(),
77
+ since: z.number().optional(),
78
+ });
79
+ export type ListenerPairing = z.infer<typeof ListenerPairingSchema>;
80
+
81
+ // Push-based listener status: a gateway process POSTs its live connection/voice snapshot to
82
+ // /listeners/<provider>/status, and the activity route reads it from there — the daemon holds no provider
83
+ // connection of its own to probe. The body IS the ActivityStatus the /activity/status probe used to build from
84
+ // in-process discord singletons, plus the per-gateway extras that ride the same channel: whether whisper is
85
+ // present (discord's voice-pending signal) and each unpaired capability's ceremony by id (whatsapp's
86
+ // link-a-device flow — the capability card renders it as the step the owner is standing in front of).
87
+ export const ListenerStatusSchema = ActivityStatusSchema.extend({
88
+ whisperReady: z.boolean().optional(),
89
+ pairing: z.record(z.string(), ListenerPairingSchema).optional(),
90
+ });
91
+ export type ListenerStatus = z.infer<typeof ListenerStatusSchema>;
92
+
93
+ // A connection's place in the reconcile lifecycle, as the status snapshot reports it: `idle` = up but holding
94
+ // nothing on purpose (no enabled listener automation to connect for), `pairing` = the socket is up but the
95
+ // credential is a ceremony nobody has completed yet (whatsapp), the rest are the connect loop.
96
+ export type ListenerGatewayPhase = "idle" | "ready" | "pairing" | "connecting" | "disconnected";
@@ -1,5 +1,5 @@
1
1
  import { expect, test } from "vitest";
2
- import { compareCheapestFirst, compareModelIds, compareUnrankedModelIds, familyOf, releaseOf, tierRankOf } from "./model-order.js";
2
+ import { compareCheapestFirst, compareModelIds, compareUnrankedModelIds, familyOf, namesThinking, releaseOf, tierRankOf } from "./model-order.js";
3
3
 
4
4
  /* The order every provider's catalog is served and browsed in. The rule exists because only Anthropic publishes
5
5
  * a ranking: the OpenAI-compatible endpoints behind Codex, Gemini, Kimi and Grok hand back a SET, and taking
@@ -186,6 +186,37 @@ test("reads a release-local tier ladder from the cheap end too", () => {
186
186
  expect(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"].toSorted(compareCheapestFirst)[0]).toBe("gpt-5.6-luna");
187
187
  });
188
188
 
189
+ test("refuses the thinking variant of a model, however new it is", () => {
190
+ /* The bug this rule exists for, in the shape the live catalog actually publishes it: a routed channel vends
191
+ * one row per thinking LEVEL, and the newest row of the cheapest model was the high one — so the ladder
192
+ * whose whole job is to be the cheap rung reached for the most expensive reading of it, and a commit
193
+ * message that takes 2s took closer to 30. */
194
+ expect(["gemini-3.6-flash-high", "gemini-3.5-flash-extra-low"].toSorted(compareCheapestFirst)[0]).toBe("gemini-3.5-flash-extra-low");
195
+ // …and it is the LEVEL that decides, not the release: same model, quieter row wins.
196
+ expect(["gemini-3.5-flash-high", "gemini-3.5-flash-minimal"].toSorted(compareCheapestFirst)[0]).toBe("gemini-3.5-flash-minimal");
197
+ // Tier still outranks it: a thinking cheap model beats a silent expensive one, which is the order that
198
+ // keeps this from quietly promoting a frontier row for being unannotated.
199
+ expect(["gemini-3-pro", "gemini-3.6-flash-high"].toSorted(compareCheapestFirst)[0]).toBe("gemini-3.6-flash-high");
200
+ // An id nobody annotated is not accused of thinking, and is not credited with silence either: it sits
201
+ // between the stated ends.
202
+ expect(["gemini-3-flash", "gemini-3.5-flash-low"].toSorted(compareCheapestFirst)[0]).toBe("gemini-3.5-flash-low");
203
+ expect(["gemini-3-flash", "gemini-3.6-flash-high"].toSorted(compareCheapestFirst)[0]).toBe("gemini-3-flash");
204
+ });
205
+
206
+ test("names the thinking rows, and only those", () => {
207
+ // What a settings row shows beside a pin, so that choosing one is a choice rather than an accident.
208
+ expect(namesThinking("gemini-3.6-flash-high")).toBe(true);
209
+ expect(namesThinking("gemini-3.1-pro-low")).toBe(false);
210
+ expect(namesThinking("gemini-3.5-flash-extra-low")).toBe(false);
211
+ expect(namesThinking("claude-haiku-4-5-20251001")).toBe(false);
212
+ expect(namesThinking("gpt-5.6-luna")).toBe(false);
213
+ // An effort word that is not `high` still names one; so does the on/off form a channel vends beside its
214
+ // quiet row.
215
+ expect(namesThinking("gpt-oss-120b-medium")).toBe(true);
216
+ expect(namesThinking("kimi-k2-thinking")).toBe(true);
217
+ expect(namesThinking("kimi-k2")).toBe(false);
218
+ });
219
+
189
220
  test("falls back on the newest release for a catalog that publishes no cheap tier at all", () => {
190
221
  // Kimi names no tier word anywhere, so every row is UNRANKED and the tier term cancels. Serving the newest
191
222
  // of what it does publish is the honest answer — there is no cheaper rung to find.
@@ -135,6 +135,46 @@ const lastRankOf = (family: string, ranks: Readonly<Record<string, number>>): nu
135
135
 
136
136
  const releaseTierRankOf = (family: string): number => lastRankOf(family, RELEASE_TIER_RANK);
137
137
 
138
+ /* HOW HARD AN ID SAYS IT WILL THINK. A routed catalog does not publish one row per model — it publishes one row
139
+ * per model PER THINKING LEVEL, spelling the level into the id: `gemini-3.6-flash-high` and
140
+ * `gemini-3.5-flash-extra-low` are the same Flash at opposite ends of its reasoning budget.
141
+ *
142
+ * Which the ranking above cannot see at all, and that blind spot has a direction: `high` and `low` are both
143
+ * unrecognized words, so two variants of one model tie on tier and the RELEASE tiebreak settles them — and the
144
+ * newest variant a channel publishes is routinely the high one. The quick model, whose entire job is to be the
145
+ * cheap rung, would therefore reach for the most expensive reading of the cheapest model it can find.
146
+ *
147
+ * That is not a small mis-sort. Thinking is the difference between a commit message that is in the box before
148
+ * the user has finished reading the file list and one that arrives half a minute later — measured at ~2s versus
149
+ * ~27s on the same model and the same diff (agent/one-shot.ts, which disables thinking for the rungs where a
150
+ * request parameter can). For a routed rung there is no such parameter: the id IS the setting, so this ranking
151
+ * is the only place the choice can be made.
152
+ *
153
+ * Read ONLY by the cheap-end order. A picker orders a catalog by what a person reaches for, and a person
154
+ * pinning `-high` on purpose means it — see compareCheapestFirst for the seam this belongs to. */
155
+ const THINKING_RANK: Readonly<Record<string, number>> = {
156
+ minimal: 0,
157
+ none: 0,
158
+ low: 1,
159
+ medium: 3,
160
+ high: 4,
161
+ max: 4,
162
+ // Not a level but a switch, and the loudest statement an id can make about this: a channel that vends
163
+ // `kimi-k2` beside `kimi-k2-thinking` is naming the same model with its reasoning turned on.
164
+ thinking: 4,
165
+ };
166
+
167
+ /* An id naming NO level sits between the two ends rather than at either — the provider's own default, which for
168
+ * a model that can think is usually some thinking. Ranking it cheapest would seat a silent default ahead of an
169
+ * id that explicitly says `minimal`, and ranking it dearest would bury every model whose channel publishes no
170
+ * variants at all (Anthropic's, Kimi's) beneath one that does. Neither is what the id claims. */
171
+ const UNSTATED_THINKING = 2;
172
+
173
+ const thinkingRankOf = (family: string): number => {
174
+ const rank = lastRankOf(family, THINKING_RANK);
175
+ return rank === UNRANKED ? UNSTATED_THINKING : rank;
176
+ };
177
+
138
178
  // The LAST recognized word wins, because tier words compose and the rightmost is the most specific one:
139
179
  // gemini-flash-lite is the cheap end of Flash, gpt-codex-max the frontier end of Codex.
140
180
  export const tierRankOf = (family: string): number => lastRankOf(family, TIER_RANK);
@@ -165,9 +205,9 @@ export const compareModelIds = (left: string, right: string): number => {
165
205
  export const compareUnrankedModelIds = (left: string, right: string): number => compareModelIds(left, right) || left.localeCompare(right);
166
206
 
167
207
  /* THE SAME TIER SCALE READ FROM THE OTHER END, for the one caller that wants the WEAKEST model rather than the
168
- * strongest: the quick model behind a one-click helper (the commit box's autofill). A picker orders a catalog by
169
- * what a user reaches for; this orders it by what a helper should spend, and the two are exact opposites — so
170
- * they share TIER_RANK rather than each naming its own list of cheap ids.
208
+ * strongest: the quick model behind an automatic helper (the commit message written at land time). A picker
209
+ * orders a catalog by what a user reaches for; this orders it by what a helper should spend, and the two are
210
+ * exact opposites — so they share TIER_RANK rather than each naming its own list of cheap ids.
171
211
  *
172
212
  * The direction of UNRANKED is the reason this can't just be compareModelIds reversed. There, an unrecognized
173
213
  * family LEADS, because an id carrying no tier word is the provider's base line and a family nobody here has
@@ -181,7 +221,20 @@ export const compareCheapestFirst = (left: string, right: string): number => {
181
221
  const rightFamily = familyOf(right);
182
222
  return (
183
223
  tierRankOf(rightFamily) - tierRankOf(leftFamily) ||
224
+ // BEFORE release, and that placement is the point: two rows of one model differing only in thinking
225
+ // level are the same model, so recency has nothing to say between them, and letting it speak is what
226
+ // seated the high variant. Tier still wins over both — a thinking Haiku is cheaper than a silent Opus.
227
+ thinkingRankOf(leftFamily) - thinkingRankOf(rightFamily) ||
184
228
  compareRelease(releaseOf(left), releaseOf(right)) ||
185
229
  releaseTierRankOf(rightFamily) - releaseTierRankOf(leftFamily)
186
230
  );
187
231
  };
232
+
233
+ /* WOULD RUNNING THIS ID MAKE THE MODEL THINK — as far as its name admits, which for a routed catalog is as far
234
+ * as anyone can tell without running it. True only for an id that spells out a level ABOVE the quiet end, so an
235
+ * ordinary id nobody has annotated (claude-haiku-4-5, kimi-k2) is never accused of it.
236
+ *
237
+ * Exported for one job: a settings row that lets someone pin the quick model has to be able to say that the row
238
+ * they picked is the thinking one. The ordering above keeps Auto off these by construction; a PIN is a
239
+ * deliberate choice and is honoured as written, which only works if the choice is legible when it is made. */
240
+ export const namesThinking = (id: string): boolean => thinkingRankOf(familyOf(id)) > UNSTATED_THINKING;
@@ -0,0 +1,111 @@
1
+ import { z } from "zod";
2
+
3
+ /* A DECLARED OUTPUT SHAPE — the answer to "what does this session produce?", written once and used three ways.
4
+ *
5
+ * An agentic session's natural output is prose, and prose is unusable as an input to the next session: a step
6
+ * that must hand "the three files worth changing" to the step after it cannot hand over a paragraph that
7
+ * mentions them. So a session that feeds another one declares its shape here, and that one declaration
8
+ * becomes: the sentence in the prompt that tells the model what to write, the validator that decides whether
9
+ * it complied, and the table the run view renders. Written once because the three drift apart the moment they
10
+ * are written twice — a prompt asking for `files` and a validator wanting `paths` fails on every iteration and
11
+ * says nothing useful about why.
12
+ *
13
+ * WHY A FIELD LIST AND NOT JSON SCHEMA. JSON Schema is strictly more expressive and completely unauthorable in
14
+ * a form: nobody designing a workflow is going to hand-write `{"type":"object","properties":{...}}`, and a UI
15
+ * that generates it becomes a schema editor, which is a product of its own. Four scalar types plus a string
16
+ * list covers what one session actually hands another — a verdict, a count, a list of paths, a summary — and
17
+ * anything past that is better carried as a file the next step reads.
18
+ *
19
+ * `description` IS REQUIRED, and that is the field that decides whether this works at all. `{name: "risk"}`
20
+ * gets you the model's guess at what risk means; `{name: "risk", description: "high | medium | low — how
21
+ * likely this change is to break something at runtime"}` gets you the answer to the question you asked.
22
+ */
23
+
24
+ // Restricted to a JS-identifier-ish shape: these become object keys the prompt spells out literally, and a
25
+ // name with a quote or a newline in it produces a prompt that cannot be complied with.
26
+ const FIELD_NAME = /^[a-zA-Z_][a-zA-Z0-9_]{0,39}$/;
27
+
28
+ export const OutputFieldSchema = z.object({
29
+ name: z.string().regex(FIELD_NAME),
30
+ type: z.enum(["string", "number", "boolean", "string[]"]),
31
+ // What the field means, in the words the model is given. Not optional — see the note above.
32
+ description: z.string().min(1),
33
+ // An absent optional field validates; an absent required one does not, and the iteration is told which.
34
+ required: z.boolean(),
35
+ });
36
+ export type OutputField = z.infer<typeof OutputFieldSchema>;
37
+
38
+ // How many fields one output may declare. A shape past this is not a handoff, it is a report — and a report is
39
+ // what the prose half of the document is for.
40
+ export const OUTPUT_FIELDS_MAX = 16;
41
+
42
+ /* Repeated names make the declaration internally contradictory: object validation can only keep one rule for
43
+ * a key, while a consumer looking the field up sees whichever copy it happens to ask for first. Reject them at
44
+ * the declaration boundary, before either interpretation gets a chance to exist. Exported because graph-level
45
+ * validation uses the same fact to explain the fault in the designer before a save is attempted. */
46
+ export const duplicateOutputFieldNames = (fields: readonly Pick<OutputField, "name">[]): string[] => {
47
+ const seen = new Set<string>();
48
+ const repeated = new Set<string>();
49
+ for (const field of fields) {
50
+ if (seen.has(field.name)) {
51
+ repeated.add(field.name);
52
+ }
53
+ seen.add(field.name);
54
+ }
55
+ return [...repeated];
56
+ };
57
+
58
+ export const OutputFieldsSchema = z
59
+ .array(OutputFieldSchema)
60
+ .min(1)
61
+ .max(OUTPUT_FIELDS_MAX)
62
+ .superRefine((fields, context) => {
63
+ for (const name of duplicateOutputFieldNames(fields)) {
64
+ context.addIssue({ code: "custom", message: `Output field names must be unique; "${name}" is repeated.` });
65
+ }
66
+ });
67
+
68
+ const validatorFor = (field: OutputField): z.ZodType => {
69
+ if (field.type === "number") {
70
+ return z.number();
71
+ }
72
+ if (field.type === "boolean") {
73
+ return z.boolean();
74
+ }
75
+ if (field.type === "string[]") {
76
+ return z.array(z.string());
77
+ }
78
+ return z.string();
79
+ };
80
+
81
+ /* The declared shape as a validator. Unknown keys are ALLOWED THROUGH: a model that answered everything asked
82
+ * of it and then added a `notes` key has complied, and failing it there would burn an iteration teaching it to
83
+ * write less. What is enforced is that every required field is present and every present field has the
84
+ * declared type — the two things the reader downstream is entitled to assume.
85
+ */
86
+ export const fieldsValidator = (fields: readonly OutputField[]): z.ZodType =>
87
+ z.looseObject(Object.fromEntries(fields.map((field) => [field.name, field.required ? validatorFor(field) : validatorFor(field).optional()])));
88
+
89
+ // A worked example of the declared shape, so the prompt can show rather than describe. Values are the field's
90
+ // own description — a model copying the example's structure has the description in front of it as it fills each
91
+ // slot, which is where it is needed rather than in a legend three lines up.
92
+ export const fieldsExample = (fields: readonly OutputField[]): Record<string, unknown> =>
93
+ Object.fromEntries(
94
+ fields.map((field) => {
95
+ const hint = `${field.description}${field.required ? "" : " (optional — omit if it does not apply)"}`;
96
+ if (field.type === "number") {
97
+ return [field.name, 0];
98
+ }
99
+ if (field.type === "boolean") {
100
+ return [field.name, false];
101
+ }
102
+ if (field.type === "string[]") {
103
+ return [field.name, [hint]];
104
+ }
105
+ return [field.name, hint];
106
+ }),
107
+ );
108
+
109
+ // One line per field, for surfaces with no room to render an example: "risk (string, required) — how likely …".
110
+ export const describeFields = (fields: readonly OutputField[]): string =>
111
+ fields.map((field) => `- \`${field.name}\` (${field.type}${field.required ? ", required" : ", optional"}) — ${field.description}`).join(`\n`);