@intentic/sandbox-contract 1.176.2 → 1.209.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) 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 +1 -0
  33. package/dist/chores/probes.d.ts.map +1 -1
  34. package/dist/chores/probes.js +131 -3
  35. package/dist/chores/probes.js.map +1 -1
  36. package/dist/chores/stack.d.ts +27 -0
  37. package/dist/chores/stack.d.ts.map +1 -0
  38. package/dist/chores/stack.js +121 -0
  39. package/dist/chores/stack.js.map +1 -0
  40. package/dist/chores/verdict.d.ts +2 -1
  41. package/dist/chores/verdict.d.ts.map +1 -1
  42. package/dist/chores/verdict.js +57 -7
  43. package/dist/chores/verdict.js.map +1 -1
  44. package/dist/contract-lock.d.ts +3 -0
  45. package/dist/contract-lock.d.ts.map +1 -0
  46. package/dist/contract-lock.js +35 -0
  47. package/dist/contract-lock.js.map +1 -0
  48. package/dist/contracts/activity.contract.d.ts +10 -0
  49. package/dist/contracts/activity.contract.d.ts.map +1 -1
  50. package/dist/contracts/agent.contract.d.ts +230 -20
  51. package/dist/contracts/agent.contract.d.ts.map +1 -1
  52. package/dist/contracts/agent.contract.js +2 -1
  53. package/dist/contracts/agent.contract.js.map +1 -1
  54. package/dist/contracts/agents.contract.d.ts +847 -1
  55. package/dist/contracts/agents.contract.d.ts.map +1 -1
  56. package/dist/contracts/agents.contract.js +3 -1
  57. package/dist/contracts/agents.contract.js.map +1 -1
  58. package/dist/contracts/automations.contract.d.ts +89 -0
  59. package/dist/contracts/automations.contract.d.ts.map +1 -1
  60. package/dist/contracts/automations.contract.js +3 -1
  61. package/dist/contracts/automations.contract.js.map +1 -1
  62. package/dist/contracts/capabilities.contract.d.ts +108 -32
  63. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  64. package/dist/contracts/capabilities.contract.js +5 -1
  65. package/dist/contracts/capabilities.contract.js.map +1 -1
  66. package/dist/contracts/chores.contract.d.ts +43 -0
  67. package/dist/contracts/chores.contract.d.ts.map +1 -1
  68. package/dist/contracts/ci.contract.d.ts +5 -0
  69. package/dist/contracts/ci.contract.d.ts.map +1 -1
  70. package/dist/contracts/ci.contract.js +2 -2
  71. package/dist/contracts/ci.contract.js.map +1 -1
  72. package/dist/contracts/claude.contract.d.ts +3 -14
  73. package/dist/contracts/claude.contract.d.ts.map +1 -1
  74. package/dist/contracts/claude.contract.js +2 -3
  75. package/dist/contracts/claude.contract.js.map +1 -1
  76. package/dist/contracts/drafts.contract.d.ts +4 -0
  77. package/dist/contracts/drafts.contract.d.ts.map +1 -1
  78. package/dist/contracts/endpoints.contract.d.ts +28 -12
  79. package/dist/contracts/endpoints.contract.d.ts.map +1 -1
  80. package/dist/contracts/endpoints.contract.js +9 -0
  81. package/dist/contracts/endpoints.contract.js.map +1 -1
  82. package/dist/contracts/extensions.contract.d.ts +399 -69
  83. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  84. package/dist/contracts/extensions.contract.js +15 -1
  85. package/dist/contracts/extensions.contract.js.map +1 -1
  86. package/dist/contracts/git.contract.d.ts +209 -11
  87. package/dist/contracts/git.contract.d.ts.map +1 -1
  88. package/dist/contracts/git.contract.js +12 -2
  89. package/dist/contracts/git.contract.js.map +1 -1
  90. package/dist/contracts/grok.contract.d.ts +0 -13
  91. package/dist/contracts/grok.contract.d.ts.map +1 -1
  92. package/dist/contracts/grok.contract.js +1 -2
  93. package/dist/contracts/grok.contract.js.map +1 -1
  94. package/dist/contracts/host.contract.d.ts +39 -0
  95. package/dist/contracts/host.contract.d.ts.map +1 -1
  96. package/dist/contracts/host.contract.js +3 -2
  97. package/dist/contracts/host.contract.js.map +1 -1
  98. package/dist/contracts/loops.contract.d.ts +343 -0
  99. package/dist/contracts/loops.contract.d.ts.map +1 -0
  100. package/dist/contracts/loops.contract.js +11 -0
  101. package/dist/contracts/loops.contract.js.map +1 -0
  102. package/dist/contracts/panels.contract.d.ts +6 -0
  103. package/dist/contracts/panels.contract.d.ts.map +1 -1
  104. package/dist/contracts/personas.contract.d.ts +112 -0
  105. package/dist/contracts/personas.contract.d.ts.map +1 -0
  106. package/dist/contracts/personas.contract.js +13 -0
  107. package/dist/contracts/personas.contract.js.map +1 -0
  108. package/dist/contracts/ports.contract.d.ts +1 -0
  109. package/dist/contracts/ports.contract.d.ts.map +1 -1
  110. package/dist/contracts/prepush.contract.d.ts +1 -0
  111. package/dist/contracts/prepush.contract.d.ts.map +1 -1
  112. package/dist/contracts/prepush.contract.js.map +1 -1
  113. package/dist/contracts/{codex.contract.d.ts → providers.contract.d.ts} +11 -3
  114. package/dist/contracts/providers.contract.d.ts.map +1 -0
  115. package/dist/contracts/providers.contract.js +6 -0
  116. package/dist/contracts/providers.contract.js.map +1 -0
  117. package/dist/contracts/public.contract.d.ts +24 -0
  118. package/dist/contracts/public.contract.d.ts.map +1 -0
  119. package/dist/contracts/public.contract.js +8 -0
  120. package/dist/contracts/public.contract.js.map +1 -0
  121. package/dist/contracts/push.contract.d.ts +3 -3
  122. package/dist/contracts/secrets.contract.d.ts +9 -0
  123. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  124. package/dist/contracts/sessions.contract.d.ts +16 -1
  125. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  126. package/dist/contracts/sessions.contract.js +1 -1
  127. package/dist/contracts/sessions.contract.js.map +1 -1
  128. package/dist/contracts/settings.contract.d.ts +352 -58
  129. package/dist/contracts/settings.contract.d.ts.map +1 -1
  130. package/dist/contracts/settings.contract.js +2 -1
  131. package/dist/contracts/settings.contract.js.map +1 -1
  132. package/dist/contracts/share.contract.d.ts +55 -0
  133. package/dist/contracts/share.contract.d.ts.map +1 -0
  134. package/dist/contracts/share.contract.js +9 -0
  135. package/dist/contracts/share.contract.js.map +1 -0
  136. package/dist/contracts/skills.contract.d.ts +41 -0
  137. package/dist/contracts/skills.contract.d.ts.map +1 -0
  138. package/dist/contracts/skills.contract.js +9 -0
  139. package/dist/contracts/skills.contract.js.map +1 -0
  140. package/dist/contracts/system.contract.d.ts +218 -22
  141. package/dist/contracts/system.contract.d.ts.map +1 -1
  142. package/dist/contracts/system.contract.js +10 -2
  143. package/dist/contracts/system.contract.js.map +1 -1
  144. package/dist/contracts/workflows.contract.d.ts +575 -0
  145. package/dist/contracts/workflows.contract.d.ts.map +1 -0
  146. package/dist/contracts/workflows.contract.js +13 -0
  147. package/dist/contracts/workflows.contract.js.map +1 -0
  148. package/dist/contracts/workspace.contract.d.ts +18 -4
  149. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  150. package/dist/contracts/workspace.contract.js +6 -3
  151. package/dist/contracts/workspace.contract.js.map +1 -1
  152. package/dist/conversation-ids.d.ts +2 -0
  153. package/dist/conversation-ids.d.ts.map +1 -0
  154. package/dist/conversation-ids.js +113 -0
  155. package/dist/conversation-ids.js.map +1 -0
  156. package/dist/events.d.ts +523 -17
  157. package/dist/events.d.ts.map +1 -1
  158. package/dist/events.js +106 -13
  159. package/dist/events.js.map +1 -1
  160. package/dist/history-state.d.ts +3 -0
  161. package/dist/history-state.d.ts.map +1 -0
  162. package/dist/history-state.js +53 -0
  163. package/dist/history-state.js.map +1 -0
  164. package/dist/hostnames.d.ts +4 -0
  165. package/dist/hostnames.d.ts.map +1 -1
  166. package/dist/hostnames.js +7 -2
  167. package/dist/hostnames.js.map +1 -1
  168. package/dist/index.d.ts +3793 -478
  169. package/dist/index.d.ts.map +1 -1
  170. package/dist/index.js +41 -17
  171. package/dist/index.js.map +1 -1
  172. package/dist/listener-protocol.d.ts +77 -0
  173. package/dist/listener-protocol.d.ts.map +1 -0
  174. package/dist/listener-protocol.js +33 -0
  175. package/dist/listener-protocol.js.map +1 -0
  176. package/dist/model-order.d.ts +1 -0
  177. package/dist/model-order.d.ts.map +1 -1
  178. package/dist/model-order.js +16 -0
  179. package/dist/model-order.js.map +1 -1
  180. package/dist/output-fields.d.ts +30 -0
  181. package/dist/output-fields.d.ts.map +1 -0
  182. package/dist/output-fields.js +57 -0
  183. package/dist/output-fields.js.map +1 -0
  184. package/dist/path-refs.d.ts.map +1 -1
  185. package/dist/path-refs.js.map +1 -1
  186. package/dist/publish-drafts.d.ts +7 -0
  187. package/dist/publish-drafts.d.ts.map +1 -0
  188. package/dist/publish-drafts.js +20 -0
  189. package/dist/publish-drafts.js.map +1 -0
  190. package/dist/quick-model.d.ts +2 -1
  191. package/dist/quick-model.d.ts.map +1 -1
  192. package/dist/quick-model.js +18 -14
  193. package/dist/quick-model.js.map +1 -1
  194. package/dist/routes.d.ts +3 -0
  195. package/dist/routes.d.ts.map +1 -1
  196. package/dist/routes.js +63 -0
  197. package/dist/routes.js.map +1 -1
  198. package/dist/runtime-state.d.ts +33 -0
  199. package/dist/runtime-state.d.ts.map +1 -0
  200. package/dist/runtime-state.js +14 -0
  201. package/dist/runtime-state.js.map +1 -0
  202. package/dist/schemas.d.ts +4938 -866
  203. package/dist/schemas.d.ts.map +1 -1
  204. package/dist/schemas.js +1011 -192
  205. package/dist/schemas.js.map +1 -1
  206. package/dist/search-globs.d.ts +6 -0
  207. package/dist/search-globs.d.ts.map +1 -0
  208. package/dist/search-globs.js +39 -0
  209. package/dist/search-globs.js.map +1 -0
  210. package/dist/share-paths.d.ts +8 -0
  211. package/dist/share-paths.d.ts.map +1 -0
  212. package/dist/share-paths.js +21 -0
  213. package/dist/share-paths.js.map +1 -0
  214. package/dist/state-portability.d.ts +8 -0
  215. package/dist/state-portability.d.ts.map +1 -0
  216. package/dist/state-portability.js +4 -0
  217. package/dist/state-portability.js.map +1 -0
  218. package/dist/title.d.ts.map +1 -1
  219. package/dist/title.js +1 -1
  220. package/dist/title.js.map +1 -1
  221. package/dist/tunnel-ids.d.ts +1 -0
  222. package/dist/tunnel-ids.d.ts.map +1 -1
  223. package/dist/tunnel-ids.js +1 -0
  224. package/dist/tunnel-ids.js.map +1 -1
  225. package/dist/versions.d.ts +4 -0
  226. package/dist/versions.d.ts.map +1 -0
  227. package/dist/versions.js +15 -0
  228. package/dist/versions.js.map +1 -0
  229. package/dist/workflow-faults.d.ts +4 -0
  230. package/dist/workflow-faults.d.ts.map +1 -0
  231. package/dist/workflow-faults.js +115 -0
  232. package/dist/workflow-faults.js.map +1 -0
  233. package/dist/workspace-state.d.ts +282 -3
  234. package/dist/workspace-state.d.ts.map +1 -1
  235. package/dist/workspace-state.js +198 -12
  236. package/dist/workspace-state.js.map +1 -1
  237. package/package.json +30 -6
  238. package/src/agent-catalog.test.ts +147 -7
  239. package/src/agent-catalog.ts +233 -21
  240. package/src/agent-run-model.test.ts +60 -0
  241. package/src/agent-run-model.ts +51 -0
  242. package/src/capability-env.ts +10 -0
  243. package/src/capability-ledger.test.ts +110 -0
  244. package/src/chores/chores.test.ts +32 -0
  245. package/src/chores/chores.ts +456 -32
  246. package/src/chores/digest.ts +0 -0
  247. package/src/chores/extension-update.ts +37 -0
  248. package/src/chores/fix-deps.ts +40 -0
  249. package/src/chores/index.ts +7 -2
  250. package/src/chores/probes.test.ts +155 -3
  251. package/src/chores/probes.ts +209 -4
  252. package/src/chores/stack.test.ts +126 -0
  253. package/src/chores/stack.ts +244 -0
  254. package/src/chores/verdict.test.ts +357 -19
  255. package/src/chores/verdict.ts +102 -22
  256. package/src/contract-lock.test.ts +17 -0
  257. package/src/contract-lock.ts +66 -0
  258. package/src/contracts/agent.contract.ts +6 -0
  259. package/src/contracts/agents.contract.ts +12 -0
  260. package/src/contracts/automations.contract.ts +17 -1
  261. package/src/contracts/capabilities.contract.ts +22 -0
  262. package/src/contracts/ci.contract.ts +10 -2
  263. package/src/contracts/claude.contract.ts +8 -4
  264. package/src/contracts/endpoints.contract.ts +21 -0
  265. package/src/contracts/extensions.contract.ts +42 -0
  266. package/src/contracts/git.contract.ts +33 -6
  267. package/src/contracts/grok.contract.ts +4 -3
  268. package/src/contracts/host.contract.ts +13 -2
  269. package/src/contracts/loops.contract.ts +62 -0
  270. package/src/contracts/personas.contract.ts +56 -0
  271. package/src/contracts/prepush.contract.ts +4 -3
  272. package/src/contracts/providers.contract.ts +23 -0
  273. package/src/contracts/public.contract.ts +14 -0
  274. package/src/contracts/sessions.contract.ts +5 -1
  275. package/src/contracts/settings.contract.ts +5 -0
  276. package/src/contracts/share.contract.ts +20 -0
  277. package/src/contracts/skills.contract.ts +24 -0
  278. package/src/contracts/system.contract.ts +27 -3
  279. package/src/contracts/workflows.contract.ts +78 -0
  280. package/src/contracts/workspace.contract.ts +10 -2
  281. package/src/conversation-ids.test.ts +25 -0
  282. package/src/conversation-ids.ts +160 -0
  283. package/src/events.test.ts +20 -1
  284. package/src/events.ts +404 -43
  285. package/src/history-state.ts +129 -0
  286. package/src/hostnames.ts +20 -6
  287. package/src/index.ts +60 -17
  288. package/src/listener-protocol.ts +96 -0
  289. package/src/model-order.test.ts +32 -1
  290. package/src/model-order.ts +56 -3
  291. package/src/output-fields.ts +111 -0
  292. package/src/path-refs.test.ts +4 -4
  293. package/src/path-refs.ts +5 -5
  294. package/src/publish-drafts.ts +59 -0
  295. package/src/quick-model.test.ts +70 -27
  296. package/src/quick-model.ts +76 -40
  297. package/src/routes.test.ts +91 -3
  298. package/src/routes.ts +135 -0
  299. package/src/runtime-state.test.ts +45 -0
  300. package/src/runtime-state.ts +124 -0
  301. package/src/schemas.test.ts +16 -29
  302. package/src/schemas.ts +3526 -553
  303. package/src/search-globs.test.ts +46 -0
  304. package/src/search-globs.ts +76 -0
  305. package/src/share-paths.test.ts +48 -0
  306. package/src/share-paths.ts +68 -0
  307. package/src/state-portability.ts +56 -0
  308. package/src/title.test.ts +20 -1
  309. package/src/title.ts +12 -3
  310. package/src/tunnel-ids.ts +15 -1
  311. package/src/versions.test.ts +39 -0
  312. package/src/versions.ts +48 -0
  313. package/src/workflow-faults.test.ts +127 -0
  314. package/src/workflow-faults.ts +188 -0
  315. package/src/workspace-state.test.ts +189 -17
  316. package/src/workspace-state.ts +500 -30
  317. package/dist/contracts/codex.contract.d.ts.map +0 -1
  318. package/dist/contracts/codex.contract.js +0 -6
  319. package/dist/contracts/codex.contract.js.map +0 -1
  320. package/dist/contracts/gemini.contract.d.ts +0 -16
  321. package/dist/contracts/gemini.contract.d.ts.map +0 -1
  322. package/dist/contracts/gemini.contract.js +0 -6
  323. package/dist/contracts/gemini.contract.js.map +0 -1
  324. package/dist/contracts/kimi.contract.d.ts +0 -16
  325. package/dist/contracts/kimi.contract.d.ts.map +0 -1
  326. package/dist/contracts/kimi.contract.js +0 -6
  327. package/dist/contracts/kimi.contract.js.map +0 -1
  328. package/dist/contracts/komodo.contract.d.ts +0 -125
  329. package/dist/contracts/komodo.contract.d.ts.map +0 -1
  330. package/dist/contracts/komodo.contract.js +0 -14
  331. package/dist/contracts/komodo.contract.js.map +0 -1
  332. package/dist/contracts/memory.contract.d.ts +0 -34
  333. package/dist/contracts/memory.contract.d.ts.map +0 -1
  334. package/dist/contracts/memory.contract.js +0 -9
  335. package/dist/contracts/memory.contract.js.map +0 -1
  336. package/src/contracts/codex.contract.ts +0 -10
  337. package/src/contracts/gemini.contract.ts +0 -11
  338. package/src/contracts/kimi.contract.ts +0 -9
  339. package/src/contracts/komodo.contract.ts +0 -36
  340. package/src/contracts/memory.contract.ts +0 -13
@@ -0,0 +1,129 @@
1
+ import type { StateFile } from "./state-portability.js";
2
+
3
+ /* WHAT LIVES ON /history — the second half of the daemon's state, and the half nothing declared until an
4
+ * export had to reason about it.
5
+ *
6
+ * `WORKSPACE_STATE_FILES` covers `<workspace>/.intentic/`, which is where the manifests live. It is not where
7
+ * the machinery lives. Every repo's REAL git dir is here (a repo's in-tree `.git` is a pointer file — see
8
+ * git/repo-git-dirs.ts for the invariant that forces it), and so are the fleet registry, the turn journal, the
9
+ * ledgers, the checkpoint scopes and the isolated agents' checkouts. A "workspace export" that took `/work`
10
+ * alone would carry a tree of repos with dangling gitdir pointers — every git command in the restored sandbox
11
+ * answering `fatal: not a git repository` — and an empty agent board.
12
+ *
13
+ * The two tables stay separate rather than becoming one keyed by volume, because they answer different
14
+ * questions. A `.intentic` entry also declares which browser QUERY it makes stale, since the file watcher
15
+ * reports it; nothing here is watched at all (that is the point of the volume), so an `invalidates` field on
16
+ * these entries would be a column of empty arrays. What they share is the portability class, and that is
17
+ * imported rather than duplicated.
18
+ *
19
+ * `history-state-coverage.test.ts` fails when a daemon store builds a `/history` path this list doesn't carry, in both
20
+ * directions — the same shape-recognizing guard that covers the workspace table.
21
+ */
22
+
23
+ // Paths are historyRoot-relative, forward-slash, matched by PREFIX; a directory entry keeps its trailing slash
24
+ // so it cannot prefix-match a sibling file. See stateFileFor for how nesting resolves.
25
+ export const HISTORY_STATE_FILES: readonly StateFile[] = [
26
+ /* ---- the machinery a restored workspace is inert without ---- */
27
+
28
+ /* THE ONE THAT MAKES A BUNDLE A WORKSPACE. Every repo's real git dir, including the /work root's own
29
+ * ("root"), keyed by URI-encoded repo id. Carrying the working tree without this hands the target files
30
+ * whose `.git` points at a path that does not exist there — which is not a degraded repo but a broken one,
31
+ * and it takes the Changes review, the diff, land and every agent branch with it. The agent BRANCHES live
32
+ * in here too, which is what lets the checkouts below be left out. */
33
+ { path: "gits/", portability: "carry" },
34
+ // The checkpoint timeline (one bare repo per scope, snapshots on refs/snapshots/head). Restoring it is what
35
+ // makes "restore to before that turn" still reach back past the move.
36
+ { path: "scopes/", portability: "carry" },
37
+ // The fleet: every conversation card, its branch, its session ids, its standing.
38
+ { path: "agents.json", portability: "carry" },
39
+ { path: "turns/", portability: "carry" },
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" },
54
+ { path: "activity.jsonl", portability: "carry" },
55
+ { path: "usage.jsonl", portability: "carry" },
56
+ { path: "account-usage.json", portability: "carry" },
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" },
61
+ // The deploy engine's own ledgers — a run's events and the check results the Pipelines view reads back.
62
+ { path: "apply-events.ndjson", portability: "carry" },
63
+ { path: "check-events/", portability: "carry" },
64
+
65
+ /* ---- regenerated by the target ---- */
66
+
67
+ /* THE DELIBERATE OMISSION, and the difference between a bundle of gigabytes and one of hundreds.
68
+ *
69
+ * A conversation's worktree is a full checkout of the monorepo per agent (plus its overlay upper dir), and
70
+ * there can be a hundred of them. None of it is unique: the branch it holds is in `gits/` above, and the
71
+ * registry entry naming it travels in agents.json — so an imported conversation arrives in exactly the
72
+ * shape the system already has a name for. `attached()` reports its checkout as absent, the board renders
73
+ * it, and the next turn's `ensure()` re-creates it from the recorded composition, which is the same path an
74
+ * archived agent takes when it runs again. The boot sweep's `git worktree prune` clears the stale admin
75
+ * entries the restored git dirs still carry. */
76
+ {
77
+ path: "worktrees/",
78
+ portability: "derived",
79
+ note: "Each conversation re-attaches its checkout from its branch on its next turn.",
80
+ },
81
+ { path: "overlays/", portability: "derived" },
82
+ { path: "logs/", portability: "derived" },
83
+ { path: "trash/", portability: "derived" },
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" },
90
+
91
+ /* ---- credentials ---- */
92
+
93
+ // The ssh alias dir ~/.ssh/intentic-hosts symlinks to: per-host config, private keys and passphrases for
94
+ // every host capability and git remote the sandbox reaches.
95
+ {
96
+ path: "ssh-hosts/",
97
+ portability: "secret",
98
+ note: "Re-add each ssh host on the Capabilities view — its key does not travel.",
99
+ },
100
+ // The cli-proxy's config, which holds the routed subscriptions' provider tokens.
101
+ {
102
+ path: "translator/",
103
+ portability: "secret",
104
+ note: "Sign the routed AI subscriptions in again on the Agent tab.",
105
+ },
106
+
107
+ /* ---- identity: what binds this sandbox to its owner, its browsers and its host ---- */
108
+
109
+ /* Signs every browser session cookie. Carrying it would let a bundle's holder mint sessions against the
110
+ * target — an export becomes a credential — and the target minting its own costs exactly one sign-in. */
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
+ },
117
+ {
118
+ path: "push.json",
119
+ portability: "identity",
120
+ note: "Re-enable notifications in the browsers you use — a push subscription is bound to the sandbox that minted it.",
121
+ },
122
+ { path: "sync-enrollments.json", portability: "identity", note: "Re-pair desktop sync from the Sync tab." },
123
+ { path: "sync-pair-consumed.json", portability: "identity" },
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" },
128
+ { path: "local-cert/", portability: "identity" },
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,35 +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";
95
+ export * from "./state-portability.js";
96
+ export * from "./history-state.js";
79
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";
80
101
  export * from "./host-protocol.js";
102
+ export * from "./listener-protocol.js";
81
103
  export * from "./hostnames.js";
82
104
  export * from "./model-order.js";
83
105
  export * from "./path-refs.js";
84
106
  export * from "./quick-model.js";
107
+ export * from "./output-fields.js";
108
+ export * from "./publish-drafts.js";
85
109
  export * from "./schemas.js";
110
+ export * from "./search-globs.js";
86
111
  export * from "./terminal-protocol.js";
87
112
  export * from "./title.js";
113
+ export * from "./versions.js";
114
+ export * from "./workflow-faults.js";
88
115
 
89
116
  // The aggregated contract — implemented on the server by the per-domain route factories and consumed by the
90
117
  // browser's typed oRPC client (ContractRouterClient<typeof sandboxContract>). The wire paths it declares are
@@ -98,32 +125,34 @@ export const sandboxContract = {
98
125
  chores: choresContract,
99
126
  ci: ciContract,
100
127
  claude: claudeContract,
101
- codex: codexContract,
102
128
  drafts: draftsContract,
103
129
  endpoints: endpointsContract,
104
130
  extensions: extensionsContract,
131
+ personas: personasContract,
105
132
  sessions: sessionsContract,
106
133
  settings: settingsContract,
134
+ share: shareContract,
135
+ skills: skillsContract,
107
136
  intentic: intenticContract,
108
- gemini: geminiContract,
109
137
  git: gitContract,
110
138
  grok: grokContract,
111
- kimi: kimiContract,
112
- komodo: komodoContract,
113
139
  history: historyContract,
114
140
  workspace: workspaceContract,
115
141
  inventory: inventoryContract,
116
142
  logs: logsContract,
117
- memory: memoryContract,
143
+ loops: loopsContract,
118
144
  panels: panelsContract,
119
145
  ports: portsContract,
146
+ public: publicContract,
120
147
  prepush: prepushContract,
148
+ providers: providersContract,
121
149
  push: pushContract,
122
150
  secrets: secretsContract,
123
151
  system: systemContract,
124
152
  translator: translatorContract,
125
153
  usage: usageContract,
126
154
  vpn: vpnContract,
155
+ workflows: workflowsContract,
127
156
  };
128
157
 
129
158
  // Every route in THIS build of the contract, and the names the daemon advertises on its hello frame. Bound here
@@ -133,6 +162,20 @@ export const sandboxContract = {
133
162
  export const SANDBOX_ROUTES: readonly ContractRoute[] = contractRoutes(sandboxContract);
134
163
  export const SANDBOX_ROUTE_NAMES: readonly string[] = SANDBOX_ROUTES.map((route) => route.name);
135
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
+
136
171
  // The contract route a concrete browser request belongs to, bound to this build's route table.
137
172
  export const sandboxRouteName = (method: string, pathWithQuery: string): string | undefined =>
138
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;