@intentic/sandbox-contract 1.176.3 → 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 (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 +39 -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 +204 -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 +113 -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 +3791 -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 +4886 -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 +25 -0
  276. package/src/conversation-ids.ts +160 -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 +3475 -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
@@ -1,7 +1,5 @@
1
1
  import { oc } from "@orpc/contract";
2
2
  import {
3
- CommitMessageDraftSchema,
4
- CommitMessageSchema,
5
3
  CommitResultSchema,
6
4
  CommitSchema,
7
5
  DiscardSchema,
@@ -24,15 +22,25 @@ import {
24
22
  GitFileWriteSchema,
25
23
  GitLogQuerySchema,
26
24
  GitLogSchema,
25
+ GitOperationStateSchema,
26
+ GitUndoSchema,
27
+ GitUndoStateSchema,
27
28
  GitRemoteStateSchema,
28
29
  GitReposSchema,
29
30
  GitResetSchema,
30
31
  GitStageSchema,
31
32
  GitStatusSchema,
32
33
  GitTagCreateSchema,
34
+ GitTagDeleteSchema,
35
+ GitTagPushSchema,
33
36
  OkSchema,
34
37
  PushSchema,
35
38
  RepoParamSchema,
39
+ StashApplySchema,
40
+ StashDiffQuerySchema,
41
+ StashListSchema,
42
+ StashPushSchema,
43
+ StashRefParamSchema,
36
44
  } from "../schemas.js";
37
45
 
38
46
  // Per-repo git ops over the workspace repos: "root" (the /work repo) plus every discovered repo under /work
@@ -41,10 +49,6 @@ import {
41
49
  // Changes panel renders; commit/discard take optional `paths` for per-file actions.
42
50
  export const gitContract = {
43
51
  changes: oc.route({ method: "GET", path: "/git/changes" }).output(GitChangesSchema),
44
- // Drafts a commit message for what the commit box is about to record, on the sandbox's quick model (the
45
- // cheap rung — see quick-model.ts). Workspace-wide like `changes` and for the same reason: one commit box,
46
- // one message, every staged repo. POST because it spends a model call, not because it writes anything.
47
- commitMessage: oc.route({ method: "POST", path: "/git/commit-message" }).input(CommitMessageDraftSchema).output(CommitMessageSchema),
48
52
  // The git-history graph over one repo's real commits: the repo list (for the tree affordance + switcher),
49
53
  // one repo's commit log, and lazy per-commit detail (changed files, then a file's before/after AT the
50
54
  // commit). Read-only — commit/discard on the working tree stay the write path (above).
@@ -55,8 +59,31 @@ export const gitContract = {
55
59
  // Write actions from the graph's commit context menu (VSCode "Git Graph" parity). Non-destructive refs
56
60
  // (branch/tag) return Ok and let git's errors propagate; the sequence + HEAD-moving ops return a
57
61
  // GitActionResult so a conflict/clean-apply failure is a value, not a 500. Read routes above.
62
+ /* The halted-operation pair. `operation` is a READ every git surface can use to explain a worktree it cannot
63
+ * otherwise act on; `abort` is the single way out, and it is git's own `--abort` rather than anything
64
+ * clever. Neither is reachable from the daemon's own verbs — those abort themselves — so this exists purely
65
+ * for what a terminal left behind. */
66
+ operation: oc.route({ method: "GET", path: "/git/{repo}/operation" }).input(RepoParamSchema).output(GitOperationStateSchema),
67
+ abort: oc.route({ method: "POST", path: "/git/{repo}/abort" }).input(RepoParamSchema).output(GitActionResultSchema),
68
+ /* Walk the current branch back to where it was before its last action, off the branch's own reflog. The
69
+ * complement to the Checkpoints timeline, not a duplicate of it: a checkpoint restores the working tree,
70
+ * this moves the ref. The read carries `previousSha`, which the write sends back as a concurrency token —
71
+ * an undo prepared against a stale view is refused rather than landing somewhere unlooked-at. */
72
+ undoable: oc.route({ method: "GET", path: "/git/{repo}/undo" }).input(RepoParamSchema).output(GitUndoStateSchema),
73
+ undo: oc.route({ method: "POST", path: "/git/{repo}/undo" }).input(GitUndoSchema).output(GitActionResultSchema),
74
+ /* The stash. Read as a list plus a per-entry diff, mirroring the commit log and commit-diff pair above,
75
+ * because a stash entry is a commit and the graph renders it as one. The writes are git's own four verbs;
76
+ * only `drop` is unrecoverable, and the route checkpoints before it. */
77
+ stashes: oc.route({ method: "GET", path: "/git/{repo}/stashes" }).input(RepoParamSchema).output(StashListSchema),
78
+ stashDiff: oc.route({ method: "GET", path: "/git/{repo}/stash-diff" }).input(StashDiffQuerySchema).output(GitCommitDiffSchema),
79
+ stashPush: oc.route({ method: "POST", path: "/git/{repo}/stash" }).input(StashPushSchema).output(GitActionResultSchema),
80
+ stashApply: oc.route({ method: "POST", path: "/git/{repo}/stash/apply" }).input(StashApplySchema).output(GitActionResultSchema),
81
+ stashDrop: oc.route({ method: "POST", path: "/git/{repo}/stash/drop" }).input(StashRefParamSchema).output(OkSchema),
58
82
  createBranch: oc.route({ method: "POST", path: "/git/{repo}/branch" }).input(GitBranchCreateSchema).output(OkSchema),
59
83
  createTag: oc.route({ method: "POST", path: "/git/{repo}/tag" }).input(GitTagCreateSchema).output(OkSchema),
84
+ // The other two things one does with a tag, so the graph's tag pills are not a create-only affordance.
85
+ deleteTag: oc.route({ method: "POST", path: "/git/{repo}/tag/delete" }).input(GitTagDeleteSchema).output(OkSchema),
86
+ pushTag: oc.route({ method: "POST", path: "/git/{repo}/tag/push" }).input(GitTagPushSchema).output(GitActionResultSchema),
60
87
  checkout: oc.route({ method: "POST", path: "/git/{repo}/checkout" }).input(GitCheckoutSchema).output(GitActionResultSchema),
61
88
  cherryPick: oc.route({ method: "POST", path: "/git/{repo}/cherry-pick" }).input(GitCommitActionSchema).output(GitActionResultSchema),
62
89
  revert: oc.route({ method: "POST", path: "/git/{repo}/revert" }).input(GitCommitActionSchema).output(GitActionResultSchema),
@@ -1,5 +1,5 @@
1
1
  import { oc } from "@orpc/contract";
2
- import { AccountIdSchema, DeviceStartSchema, ModelsSchema, OauthAccountListSchema, OkSchema } from "../schemas.js";
2
+ import { AccountIdSchema, DeviceStartSchema, OauthAccountListSchema, OkSchema } from "../schemas.js";
3
3
 
4
4
  // xAI Grok (via OpenCode) uses subscription OAuth — the sandbox owns the credential (OpenCode persists the
5
5
  // tokens and refreshes them). `start` authorizes xAI's headless device-code method and returns the verification
@@ -7,10 +7,11 @@ import { AccountIdSchema, DeviceStartSchema, ModelsSchema, OauthAccountListSchem
7
7
  // — there is no paste-back — and the UI polls `accounts` until connected. `disconnect` clears the tokens.
8
8
  // ponytail: OpenCode holds one xAI auth per data dir, so `accounts` is 0 or 1 — the list shape matches the
9
9
  // other providers without paying for per-account OpenCode servers yet.
10
+ //
11
+ // Like Claude's, this contract is now the account handshake alone — the model catalog answers on the shared
12
+ // /providers/{provider}/models route (providers.contract.ts).
10
13
  export const grokContract = {
11
14
  start: oc.route({ method: "POST", path: "/grok/oauth/start" }).output(DeviceStartSchema),
12
- // xAI's live models (via OpenCode) for the model picker — the source of truth for valid model ids.
13
- models: oc.route({ method: "GET", path: "/grok/models" }).output(ModelsSchema),
14
15
  accounts: oc.route({ method: "GET", path: "/grok/accounts" }).output(OauthAccountListSchema),
15
16
  disconnect: oc.route({ method: "POST", path: "/grok/account/disconnect" }).input(AccountIdSchema).output(OkSchema),
16
17
  };
@@ -1,6 +1,6 @@
1
- import { oc } from "@orpc/contract";
1
+ import { eventIterator, oc } from "@orpc/contract";
2
2
  import { z } from "zod";
3
- import { HostFactsSchema, HostScopesSchema, OkSchema } from "../schemas.js";
3
+ import { HostFactsSchema, HostScopesSchema, MachineFlowLineSchema, MachineSandboxFlowSchema, OkSchema } from "../schemas.js";
4
4
 
5
5
  /* What a connected computer can be ASKED, over the socket it opened to this sandbox.
6
6
  *
@@ -30,4 +30,15 @@ export const hostContract = {
30
30
  ping: oc.output(OkSchema),
31
31
  // One MCP JSON-RPC message in, its answer out — forwarded verbatim in both directions. See above.
32
32
  mcp: oc.input(z.unknown()).output(z.unknown()),
33
+ /* One operation on one of this machine's sandboxes, narrated as it happens.
34
+ *
35
+ * TYPED, unlike `mcp`, and the difference is who is on the other end. `mcp`'s reader is a model, which has
36
+ * nothing to do with a line as it arrives and everything to gain from the machine learning tools without a
37
+ * daemon release. This reader is a PERSON watching a progress log: an update pulls an image and recreates a
38
+ * container, which is minutes of silence unless the lines travel while they are produced. A stream is what
39
+ * the browser needs, and a stream is the one thing an MCP tool result cannot be.
40
+ *
41
+ * The scope is still checked here, on the machine, by the same functions the MCP tools call — this adds a
42
+ * way of WATCHING an operation, never a way of skipping the switch that permits it. */
43
+ runSandboxFlow: oc.input(MachineSandboxFlowSchema).output(eventIterator(MachineFlowLineSchema)),
33
44
  };
@@ -0,0 +1,62 @@
1
+ import { oc } from "@orpc/contract";
2
+ import {
3
+ LoopDesignIdParamSchema,
4
+ LoopDesignSaveSchema,
5
+ LoopDesignSchema,
6
+ LoopDesignsListSchema,
7
+ LoopIdParamSchema,
8
+ LoopRecordSchema,
9
+ LoopSchema,
10
+ LoopsListSchema,
11
+ OkSchema,
12
+ } from "../schemas.js";
13
+
14
+ /* The loop routes — "run this conversation again until the goal is met".
15
+ *
16
+ * TWO HALVES THAT LOOK LIKE ONE FEATURE AND ARE NOT, which is why they share a file and share nothing else.
17
+ *
18
+ * A RUNNING LOOP has no editor. It is started against a conversation, it converges or it gives up, and then it
19
+ * is history: `start`, `stop`, and a `list` of what has run. No upsert, no enabled toggle, no id of its own —
20
+ * the conversation IS the id.
21
+ *
22
+ * A SAVED LOOP is a manifest entry like a workflow, and gets the manifest treatment: list, save, remove. It is
23
+ * the loop's MACHINERY without its goal (LoopDesignSchema says why at length), so it is authored once and
24
+ * pointed at a different job every time. It has no `run` route of its own on purpose — running one is `start`
25
+ * with the design's fields and the composer's sentence, so there is exactly one way a loop begins and exactly
26
+ * one place that can refuse it.
27
+ *
28
+ * `start` acks immediately with the loop as recorded and runs detached, the same contract POST /agent keeps: the
29
+ * first iteration alone can take minutes, and every surface that would render progress is already attached to
30
+ * the conversation. What comes back is the record, not an outcome — the outcome arrives on the fleet card.
31
+ */
32
+ export const loopsContract = {
33
+ // Every loop this workspace has run, newest first — the record is kept after the loop ends, because "why did
34
+ // it stop at iteration 4" is the question a loop is read for, and the answer is its iteration history.
35
+ list: oc.route({ method: "GET", path: "/loops" }).output(LoopsListSchema),
36
+ /* Start looping a conversation. Rejects when that conversation is already looping — a second loop on one
37
+ * agent would have two pumps racing the same worktree and the same turn mutex, and the loser would spend a
38
+ * turn to discover it.
39
+ *
40
+ * The conversation need not exist yet: a loop against a fresh id opens it, exactly as a first chat turn
41
+ * does, which is what lets "run this until it's green" be the FIRST thing said to a new agent. */
42
+ start: oc.route({ method: "POST", path: "/loops" }).input(LoopSchema).output(LoopRecordSchema),
43
+ /* Stop the loop, leaving the turn in flight alone.
44
+ *
45
+ * The split is deliberate and it is the one thing about this route that has to be right: stopping a LOOP
46
+ * means "do not start another iteration", not "kill what is running". A user watching iteration 6 do good
47
+ * work should be able to say "this is the last one" without throwing that work away. Killing the turn is
48
+ * what /agent/stop is for, and pressing both is the ordinary way to abandon a loop outright. */
49
+ stop: oc.route({ method: "POST", path: "/loops/{conversationId}/stop" }).input(LoopIdParamSchema).output(OkSchema),
50
+
51
+ // Every saved loop. A literal path segment under /loops rather than a surface of its own, because a saved
52
+ // loop is not a different feature — it is the same loop with its goal left blank until somebody types one.
53
+ designs: oc.route({ method: "GET", path: "/loops/designs" }).output(LoopDesignsListSchema),
54
+ /* Create or replace a saved loop, with the operation explicit so a name collision cannot turn a create into
55
+ * a replacement. Refuses a design that could never finish — nothing to produce and nothing to check — with
56
+ * the same sentence `start` refuses an ad-hoc loop for, because it is the same mistake made earlier and
57
+ * catching it at save time is the whole advantage of saving. */
58
+ saveDesign: oc.route({ method: "POST", path: "/loops/designs" }).input(LoopDesignSaveSchema).output(LoopDesignSchema),
59
+ // Deleting a saved loop does NOT stop a loop running from it: a running loop copied the fields it needed
60
+ // when it started, so it converges or gives up on its own terms, and its record stays readable.
61
+ removeDesign: oc.route({ method: "DELETE", path: "/loops/designs/{id}" }).input(LoopDesignIdParamSchema).output(OkSchema),
62
+ };
@@ -0,0 +1,56 @@
1
+ import { oc } from "@orpc/contract";
2
+ import {
3
+ OkSchema,
4
+ PersonaIdParamSchema,
5
+ PersonaKitSchema,
6
+ PersonaPromptSchema,
7
+ PersonaSchema,
8
+ PersonasListSchema,
9
+ PersonaSkillBodySchema,
10
+ PersonaSkillNameSchema,
11
+ PersonaSkillSchema,
12
+ } from "../schemas.js";
13
+
14
+ /* The sandbox's named personas (PersonaSchema): which connected accounts each one speaks for, what a session
15
+ * wearing it may do, where it works, and what it is told. The card half is a plain three-verb CRUD with no apply
16
+ * step, because a card provisions NOTHING — unlike a capability, saving one connects nothing, installs nothing
17
+ * and spends nothing. It records a decision about accounts that already exist.
18
+ *
19
+ * The file behind it is committed workspace config (personas/personas-store.ts), so these routes are one of
20
+ * two equally supported ways to edit them: this, and opening the file in the editor like any other project
21
+ * config. Neither is the "real" one — which is why `save` is a whole-card upsert rather than a field patch, so
22
+ * a round trip through the UI leaves a diff a reviewer would recognise.
23
+ *
24
+ * THE KIT ROUTES BELOW EDIT FILES, not the card, and they are here rather than on the skills contract because
25
+ * what they write belongs to one persona: its prompt, and the skills only its turns can reach
26
+ * (personas/persona-kit.ts). The sandbox's `skills` domain answers "what does the agent know" for every chat;
27
+ * this answers "what does this card carry", and the two lists are different questions with different answers.
28
+ *
29
+ * Per-verb routes rather than one whole-kit PUT: a kit is a directory of files somebody edits one at a time, and
30
+ * a save that shipped the whole folder would make an edit to one skill capable of deleting another. */
31
+ export const personasContract = {
32
+ list: oc.route({ method: "GET", path: "/personas" }).output(PersonasListSchema),
33
+ // Upsert by id — re-saving the same id edits that card.
34
+ save: oc.route({ method: "POST", path: "/personas" }).input(PersonaSchema).output(OkSchema),
35
+ /* Removing a card takes away a persona, never an account: the login it named stays connected and reachable from
36
+ * every other surface. What it CAN do is orphan a reference — an automation pinned to this id now names a
37
+ * card that no longer exists — and the resolver reads that as "no accounts at all" rather than "all of
38
+ * them", so the automation goes quiet instead of posting as somebody unintended. */
39
+ /* Removing a card takes away a persona, never an account: the login it named stays connected and reachable from
40
+ * every other surface. It DOES take the card's kit with it — a folder no card can reach is a folder no list
41
+ * shows, and leaving the owner's prompt and skills orphaned on disk is worse than deleting what they just
42
+ * asked to delete. */
43
+ remove: oc.route({ method: "DELETE", path: "/personas/{id}" }).input(PersonaIdParamSchema).output(OkSchema),
44
+
45
+ // ---- the kit: what this card is told, and the skills only it reaches ----
46
+
47
+ kit: oc.route({ method: "GET", path: "/personas/{id}/kit" }).input(PersonaIdParamSchema).output(PersonaKitSchema),
48
+ // An empty prompt DELETES the file rather than storing a blank one, so "custom with nothing written" is one
49
+ // state instead of two — the resolver falls back to the sandbox's prompt for it (personas.ts personaPrompt).
50
+ savePrompt: oc.route({ method: "POST", path: "/personas/{id}/prompt" }).input(PersonaPromptSchema).output(OkSchema),
51
+ readSkill: oc.route({ method: "GET", path: "/personas/{id}/skills/read" }).input(PersonaSkillNameSchema).output(PersonaSkillBodySchema),
52
+ // Upsert by name, like the sandbox's own skills — and with no enabled list to write, because a kit skill is
53
+ // on exactly when its persona is worn. That is what "specific to that persona" has to mean.
54
+ saveSkill: oc.route({ method: "POST", path: "/personas/{id}/skills" }).input(PersonaSkillSchema).output(OkSchema),
55
+ removeSkill: oc.route({ method: "POST", path: "/personas/{id}/skills/remove" }).input(PersonaSkillNameSchema).output(OkSchema),
56
+ };
@@ -6,9 +6,10 @@ import { OkSchema, PrepushRunSchema } from "../schemas.js";
6
6
  // main working tree, of which there is exactly one, so nothing here is addressed by id.
7
7
  //
8
8
  // `run` starts the check and returns immediately — a suite takes minutes, and an oRPC call held open for one
9
- // would die on the first proxy timeout with the work still going. The dialog that started it polls `state`,
10
- // which carries the live output tail while `status` is `running`. `cancel` kills the child's whole process
11
- // group; the run settles as `cancelled` and the push it was gating does not go.
9
+ // would die on the first proxy timeout with the work still going. The dialog that started it polls `state` for
10
+ // the verdict, and opens the terminals panel on the `session` the first answer names: the suite runs in a real
11
+ // tmux window, so watching it is the terminal's job. `cancel` kills that window; the run settles as `cancelled`
12
+ // and the push it was gating does not go.
12
13
  export const prepushContract = {
13
14
  state: oc.route({ method: "GET", path: "/prepush/state" }).output(PrepushRunSchema),
14
15
  run: oc.route({ method: "POST", path: "/prepush/run" }).output(OkSchema),
@@ -0,0 +1,23 @@
1
+ import { oc } from "@orpc/contract";
2
+ import { ModelsSchema, NativeProviderParamSchema } from "../schemas.js";
3
+
4
+ // EVERY NATIVE PROVIDER'S PICKER CATALOG, ON ONE ROUTE.
5
+ //
6
+ // There were five of these — /claude/models, /codex/models, /grok/models, /kimi/models, /gemini/models — one per
7
+ // provider, each a contract entry, a route factory and a service field wired separately. They differed in
8
+ // nothing: same method, same output schema, same "the daemon's catalog for this provider, never empty". Three of
9
+ // the five route files were the same fifteen lines with a name swapped, and the daemon paid for that shape twice
10
+ // more in the branch chains that had to fan back out over the providers to ask them all the same question.
11
+ //
12
+ // So the provider is a PARAMETER, not five routes. Adding one is a row in the daemon's catalog registry — the
13
+ // same discipline the adapter registry already applies to serving a turn — rather than a vertical slice through
14
+ // the contract, the router, the service container and every test double.
15
+ //
16
+ // Endpoints keep their own route (endpoints.contract.ts) and should: they are user-created and unbounded, their
17
+ // id names a capability that may not exist, and a missing one is a NOT_FOUND rather than an empty catalog. That
18
+ // is a different question with a different failure, not a sixth provider.
19
+ export const providersContract = {
20
+ // One provider's models (+ its default id), never empty — live discovery with a persisted/seed floor behind
21
+ // it. Order is the provider's own preference order and is not re-ranked here; see ModelsSchema.
22
+ models: oc.route({ method: "GET", path: "/providers/{provider}/models" }).input(NativeProviderParamSchema).output(ModelsSchema),
23
+ };
@@ -0,0 +1,14 @@
1
+ import { oc } from "@orpc/contract";
2
+ import { OkSchema, PublicListSchema, PublishResultSchema, PublishSchema, UnpublishSchema } from "../schemas.js";
3
+
4
+ // The workspace outbox — what `public/` currently holds and its address (see the public section in schemas.ts).
5
+ // `publish` copies a workspace file or directory in, creating the outbox if this is the first one; `unpublish`
6
+ // withdraws one and removes the outbox behind the last, so the directory's presence always means exactly "there
7
+ // is something published". There is no route to READ a published file: that is the whole point of the
8
+ // unauthenticated `public-<slot>` hostname, and a second, authenticated way in would just be the workspace file
9
+ // API with extra steps.
10
+ export const publicContract = {
11
+ list: oc.route({ method: "GET", path: "/public" }).output(PublicListSchema),
12
+ publish: oc.route({ method: "POST", path: "/public/publish" }).input(PublishSchema).output(PublishResultSchema),
13
+ unpublish: oc.route({ method: "POST", path: "/public/unpublish" }).input(UnpublishSchema).output(OkSchema),
14
+ };
@@ -7,10 +7,14 @@ import { SessionIdParamSchema, SessionsListSchema } from "../schemas.js";
7
7
  // repo covers its linked worktrees too, so an isolated conversation's transcript is reachable from the
8
8
  // workspace root). `list` returns summaries for the history menu (filtered by `query` when the search box is
9
9
  // used); `get` restores one transcript for display.
10
+ //
11
+ // `caseSensitive` is the filter's Aa switch, on the same terms as the fleet search's — off means the letters do
12
+ // not matter, and the two routes answer one query together (the board lists these rows under its own cards), so
13
+ // a switch either of them ignored would show as one field returning two different match sets.
10
14
  export const sessionsContract = {
11
15
  list: oc
12
16
  .route({ method: "GET", path: "/sessions" })
13
- .input(z.object({ query: z.string().optional() }))
17
+ .input(z.object({ query: z.string().optional(), caseSensitive: z.stringbool().optional() }))
14
18
  .output(SessionsListSchema),
15
19
  get: oc.route({ method: "GET", path: "/sessions/{id}" }).input(SessionIdParamSchema).output(SessionTranscriptSchema),
16
20
  };
@@ -4,6 +4,7 @@ import {
4
4
  BuiltinPromptTextSchema,
5
5
  DayWindowQuerySchema,
6
6
  OkSchema,
7
+ RuleFiringsSchema,
7
8
  SandboxSettingsSchema,
8
9
  SavingsReportSchema,
9
10
  } from "../schemas.js";
@@ -20,4 +21,8 @@ export const settingsContract = {
20
21
  set: oc.route({ method: "POST", path: "/settings" }).input(SandboxSettingsSchema).output(OkSchema),
21
22
  savings: oc.route({ method: "GET", path: "/settings/savings" }).input(DayWindowQuerySchema).output(SavingsReportSchema),
22
23
  builtinPrompt: oc.route({ method: "GET", path: "/settings/system-prompt/{base}" }).input(BuiltinPromptSchema).output(BuiltinPromptTextSchema),
24
+ // When each rule last did something, keyed by rule id. Its own route rather than a field on the settings
25
+ // object because a firing is not an edit: folding it in would make every push a settings write, and would
26
+ // put a value that changes on its own inside the object the screen optimistically patches.
27
+ firings: oc.route({ method: "GET", path: "/settings/rule-firings" }).output(RuleFiringsSchema),
23
28
  };
@@ -0,0 +1,20 @@
1
+ import { oc } from "@orpc/contract";
2
+ import { OkSchema, ShareCreateSchema, ShareListSchema, ShareRemoveSchema, ShareUpdateSchema, SharedConversationSchema } from "../schemas.js";
3
+
4
+ /* Conversations published as read-only pages (see the share section in schemas.ts).
5
+ *
6
+ * Its own group rather than two more routes on `public`, because the two speak different path spaces and mean
7
+ * different things by "publish". The outbox's routes take a path in the workspace and copy the bytes at it; a
8
+ * share takes a CONVERSATION ID and renders something that did not exist as a file until it was asked for.
9
+ * Folding them together would have `publish` accept two unrelated kinds of input distinguished by a flag.
10
+ *
11
+ * There is no route to read a share back. The page is the read — it answers on the unauthenticated
12
+ * `public-<slot>` hostname like every other published file, which is the point of having made one. */
13
+ export const shareContract = {
14
+ list: oc.route({ method: "GET", path: "/share" }).output(ShareListSchema),
15
+ // Answers with the row it just wrote, link included, so the dialog can show the address without re-listing.
16
+ create: oc.route({ method: "POST", path: "/share" }).input(ShareCreateSchema).output(SharedConversationSchema),
17
+ // Re-render an existing share from the conversation as it stands now. Same id, same link, new snapshot.
18
+ update: oc.route({ method: "POST", path: "/share/update" }).input(ShareUpdateSchema).output(SharedConversationSchema),
19
+ remove: oc.route({ method: "POST", path: "/share/remove" }).input(ShareRemoveSchema).output(OkSchema),
20
+ };
@@ -0,0 +1,24 @@
1
+ import { oc } from "@orpc/contract";
2
+ import { OkSchema, SkillBodySchema, SkillDraftSchema, SkillIdSchema, SkillRemoveSchema, SkillsListSchema } from "../schemas.js";
3
+
4
+ /* WHAT THE AGENT KNOWS RIGHT NOW, and which half of it the owner controls.
5
+ *
6
+ * Its own domain rather than three more routes on `settings`, because only one of the four sources it reads is
7
+ * the settings file: the rest are the owner's own skill store, the plugin checkouts a capability cloned, and the
8
+ * skills folders inside installed extensions. `list` is the join of all four (skill-inventory.ts).
9
+ *
10
+ * The ENABLED half still rides the settings object's `skills` array — that array is what the reconciler
11
+ * converges and what the boot path already reads, so a switch on this list is an ordinary settings write and
12
+ * there is exactly one place that decides which skills exist on disk. `save` and `remove` write the text AND
13
+ * that array together, which is why they are here rather than being two calls a screen has to sequence.
14
+ *
15
+ * `read` is a GET with the id in the query rather than in the path: an id can name an owner
16
+ * (`extension:intentic.knowledge:knowledge`), and a path template cannot carry those segments. */
17
+ export const skillsContract = {
18
+ list: oc.route({ method: "GET", path: "/skills" }).output(SkillsListSchema),
19
+ read: oc.route({ method: "GET", path: "/skills/read" }).input(SkillIdSchema).output(SkillBodySchema),
20
+ // Upsert by name: saving over an existing skill rewrites it, which is also how one is renamed (the old name
21
+ // is a different skill and is deleted on its own). A saved skill is switched ON — you wrote it to use it.
22
+ save: oc.route({ method: "POST", path: "/skills" }).input(SkillDraftSchema).output(OkSchema),
23
+ remove: oc.route({ method: "POST", path: "/skills/remove" }).input(SkillRemoveSchema).output(OkSchema),
24
+ };
@@ -5,14 +5,17 @@ import {
5
5
  BrowserNameParamSchema,
6
6
  BrowsersListSchema,
7
7
  DaemonSessionSchema,
8
- HostTunnelInputSchema,
9
- HostTunnelSchema,
10
8
  InfoSchema,
9
+ ManifestProblemsSchema,
10
+ MachineFlowLineSchema,
11
+ MachineSandboxFlowInputSchema,
11
12
  OkSchema,
12
13
  PresenceReportSchema,
13
14
  SubagentIdParamSchema,
14
15
  SubagentsListSchema,
15
16
  TerminalNameParamSchema,
17
+ TerminalScrollbackQuerySchema,
18
+ TerminalScrollbackSchema,
16
19
  TerminalsListSchema,
17
20
  UsageSummarySchema,
18
21
  } from "../schemas.js";
@@ -24,6 +27,10 @@ import {
24
27
  // connection simply never joins the roster.
25
28
  export const systemContract = {
26
29
  info: oc.route({ method: "GET", path: "/info" }).output(InfoSchema),
30
+ // What the daemon could not read in its own `.intentic/` manifests — a file it fell back on, a key it did
31
+ // not recognise, an entry it skipped. Its own route rather than a field on /info because it is invalidated
32
+ // by a different thing: a manifest changing on disk, which the workspace-state table already broadcasts.
33
+ manifestProblems: oc.route({ method: "GET", path: "/system/manifest-problems" }).output(ManifestProblemsSchema),
27
34
  // Exchange the request's verified bearer (a Google ID token — or a still-valid session, which makes this
28
35
  // route sliding renewal) for a daemon-minted session, the credential every steady-state call presents.
29
36
  session: oc.route({ method: "POST", path: "/system/session" }).output(DaemonSessionSchema),
@@ -33,7 +40,6 @@ export const systemContract = {
33
40
  .output(eventIterator(SystemEventSchema)),
34
41
  // A tab's activity self-report (view/session/file/idle), fanned back out to every member on /events.
35
42
  presence: oc.route({ method: "POST", path: "/system/presence" }).input(PresenceReportSchema).output(OkSchema),
36
- hostTunnel: oc.route({ method: "POST", path: "/system/host-tunnel" }).input(HostTunnelInputSchema).output(HostTunnelSchema),
37
43
  // Per-account token/cost totals, aggregated from the activity log's turn.completed events.
38
44
  usage: oc.route({ method: "GET", path: "/system/usage" }).output(UsageSummarySchema),
39
45
  // The web-owned tmux sessions behind the terminal tabs. `terminals` enumerates them (the panel rebuilds a tab
@@ -42,6 +48,13 @@ export const systemContract = {
42
48
  // (browser fetch sends the header), unlike the header-less WS route which app.ts exempts.
43
49
  terminals: oc.route({ method: "GET", path: "/system/terminals" }).output(TerminalsListSchema),
44
50
  killTerminal: oc.route({ method: "DELETE", path: "/system/terminals/{name}" }).input(TerminalNameParamSchema).output(OkSchema),
51
+ // One session's pane history as selectable text — the answer to "scroll back and copy that" in a surface
52
+ // whose live view is a tmux client on the alternate screen, where the scrollback is on the far side of the
53
+ // socket and the page has nothing to select. See TerminalScrollbackSchema.
54
+ terminalScrollback: oc
55
+ .route({ method: "GET", path: "/system/terminals/{name}/scrollback" })
56
+ .input(TerminalScrollbackQuerySchema)
57
+ .output(TerminalScrollbackSchema),
45
58
  // The agent's live Chromiums and the pages each has open — the Browsers view's roster, polled while it is on
46
59
  // screen and by the rail so its tile can appear the moment a turn starts browsing. The frames are the
47
60
  // separate /system/browser-view WebSocket; this is the control plane, exactly as `terminals` is for tmux.
@@ -61,4 +74,15 @@ export const systemContract = {
61
74
  .route({ method: "GET", path: "/system/subagents/{id}/transcript" })
62
75
  .input(SubagentIdParamSchema)
63
76
  .output(SessionTranscriptSchema),
77
+ /* Start, stop, restart, update, rebuild, roll back or remove a sandbox on one of the user's own computers —
78
+ * the Computers view's buttons, relayed to the machine over the socket it holds open to us.
79
+ *
80
+ * Streamed because the slowest of these takes minutes, and it is the same stream whatever the op: one door
81
+ * for one decision, so the view has one shape to render rather than one per duration. The daemon adds no
82
+ * judgement — the machine enforces its own switches and its refusal arrives as the terminal `error` line,
83
+ * in its own words, naming the control to flip. */
84
+ manageMachineSandbox: oc
85
+ .route({ method: "POST", path: "/system/computers/{id}/sandboxes/{slug}" })
86
+ .input(MachineSandboxFlowInputSchema)
87
+ .output(eventIterator(MachineFlowLineSchema)),
64
88
  };
@@ -0,0 +1,78 @@
1
+ import { oc } from "@orpc/contract";
2
+ import {
3
+ OkSchema,
4
+ WorkflowIdParamSchema,
5
+ WorkflowRunIdParamSchema,
6
+ WorkflowRunSchema,
7
+ WorkflowRunsListSchema,
8
+ WorkflowRunStartSchema,
9
+ WorkflowSaveSchema,
10
+ WorkflowSchema,
11
+ WorkflowsListSchema,
12
+ } from "../schemas.js";
13
+
14
+ /* The workflow routes — "run these sessions, in this order, each handing its result to the next".
15
+ *
16
+ * SPLIT LIKE AUTOMATIONS, NOT LIKE LOOPS, and the split says what a workflow is. A loop has no editor because
17
+ * it is started against a conversation and then it is history; a workflow is a DESIGN — a thing the user
18
+ * authors once, keeps, edits, and runs repeatedly — so it gets the manifest treatment: create/update, delete,
19
+ * list. What it does not get is an `enabled` toggle, because nothing fires it on its own: a workflow runs when
20
+ * somebody (or an automation's prompt) says run it.
21
+ *
22
+ * `run` acks with the run as recorded and executes detached, the contract every turn-starting route here
23
+ * keeps. The first step alone can take minutes and the run may take hours; the run view is where it is watched,
24
+ * and the fleet board shows its steps as the ordinary agents they are.
25
+ */
26
+ export const workflowsContract = {
27
+ // Every saved workflow with its run history, newest run first. One route rather than two because the list
28
+ // page shows both and a workflow with no runs is the interesting case, not an error.
29
+ list: oc.route({ method: "GET", path: "/workflows" }).output(WorkflowsListSchema),
30
+ /* Create or replace a workflow, with the operation made explicit so an accidental id collision cannot turn
31
+ * a create into replacement. Refuses a graph that cannot run — a cycle, a `needs` naming a step
32
+ * that is not there, a step with no way to know it is finished — with the same sentences the designer shows
33
+ * while you type (see workflowFaults). Validation lives in the contract precisely so those two can never
34
+ * disagree about what is legal. */
35
+ save: oc.route({ method: "POST", path: "/workflows" }).input(WorkflowSaveSchema).output(WorkflowSchema),
36
+ // Deleting a workflow does NOT stop a run of it that is in flight, and does not delete its history: the run
37
+ // snapshotted its definition when it started, so it stays readable and stays stoppable.
38
+ remove: oc.route({ method: "DELETE", path: "/workflows/{id}" }).input(WorkflowIdParamSchema).output(OkSchema),
39
+ /* Start a run, optionally pointed at a request — the sentence the user typed in the composer, which every
40
+ * step is handed on top of its own prompt. Every step is recorded `pending` up front, so the graph is
41
+ * complete from the first frame and a missing node never has to mean two things. Several runs of one
42
+ * workflow may be in flight at once — they derive different conversation ids, so nothing is shared and
43
+ * nothing can collide, which is what makes "run this design again on a different question" free. */
44
+ run: oc.route({ method: "POST", path: "/workflows/{id}/run" }).input(WorkflowRunStartSchema).output(WorkflowRunSchema),
45
+ // Every run across every workflow, newest first — the history the run view opens onto, and the only place a
46
+ // deleted workflow's runs are still reachable.
47
+ runs: oc.route({ method: "GET", path: "/workflows/runs" }).output(WorkflowRunsListSchema),
48
+ /* Stop a run: nothing that has not started will start, and the steps in flight are CUT OFF where they
49
+ * are — their turns aborted exactly as /agent/stop aborts one, so whatever they had written stays on
50
+ * their branches and the step settles as stopped.
51
+ *
52
+ * Not the graceful "finish the iteration you are on" a LOOP's stop performs, and the difference is the
53
+ * unit: a loop's iteration is a round somebody is watching, a workflow step's is an entire agent turn.
54
+ * Waiting for one meant a stopped run kept working, kept spending and kept asking questions for minutes
55
+ * after the press, which is indistinguishable from a button that does nothing.
56
+ *
57
+ * IT ALWAYS ENDS THE RUN, including one no scheduler is behind — a record left `running` by a daemon that
58
+ * was replaced mid-flight. That case used to be refused, which made the stuck run permanent: a Stop that
59
+ * could not work, a step count that would never move, and no way off the board.
60
+ */
61
+ stopRun: oc.route({ method: "POST", path: "/workflows/runs/{runId}/stop" }).input(WorkflowRunIdParamSchema).output(OkSchema),
62
+ /* Take an ENDED run off the board — the run's half of `agents.archive`, and the same bargain: nothing is
63
+ * lost, the checkouts are reclaimed, and `unarchiveRun` puts it all back. It needs an exit of its own
64
+ * because nothing about a run transitions once it is over, so a failed one would sit in Attention until
65
+ * fifty more had rolled it off the ledger. Refused while the run is going.
66
+ *
67
+ * IT ARCHIVES THE STEPS WITH THE RUN, which is what makes it an archive rather than a dismissal. A step
68
+ * has no card of its own — the run's row stands for it — so merely dropping the record released the run's
69
+ * conversations onto the board as loose cards at the exact moment the user said they were done with the
70
+ * job. Every step that ran is archived, on the same terms as pointing `agents.archive` at those ids: that
71
+ * route archives what the user named without re-litigating whether each one was ready to go, and a run the
72
+ * user has archived is exactly that gesture made once for the whole graph.
73
+ */
74
+ archiveRun: oc.route({ method: "POST", path: "/workflows/runs/{runId}/archive" }).input(WorkflowRunIdParamSchema).output(OkSchema),
75
+ // Put an archived run and its sessions back on the board, the inverse of the above and the run's half of
76
+ // `agents.unarchive`.
77
+ unarchiveRun: oc.route({ method: "POST", path: "/workflows/runs/{runId}/unarchive" }).input(WorkflowRunIdParamSchema).output(OkSchema),
78
+ };
@@ -24,11 +24,13 @@ import {
24
24
  WorkspaceHealthSchema,
25
25
  WorkspaceInstallResultSchema,
26
26
  WorkspaceInstallSchema,
27
+ WorkspaceMediaTicketQuerySchema,
27
28
  WorkspaceMediaTicketSchema,
28
29
  WorkspaceModulesSchema,
29
30
  WorkspaceMoveSchema,
30
31
  WorkspaceResolveQuerySchema,
31
32
  WorkspaceResolveSchema,
33
+ WorkspaceScopeSchema,
32
34
  WorkspaceSetupSchema,
33
35
  WorkspaceSyncSchema,
34
36
  WorkspaceTreeSchema,
@@ -38,7 +40,10 @@ import {
38
40
  // stays a plain Hono route serving raw bytes with a Content-Type header (oRPC's request/response shape doesn't
39
41
  // fit a streamed binary body). External MCP tools moved to the unified capabilities manifest (mcp kind).
40
42
  export const workspaceContract = {
41
- tree: oc.route({ method: "GET", path: "/workspace/tree" }).output(WorkspaceTreeSchema),
43
+ // `agent` names whose copy of the workspace to read (WorkspaceScopeSchema); omitted is the shared /work
44
+ // tree. Every read route below takes it, so a link into a conversation's own checkout browses as one tree
45
+ // rather than as one openable file surrounded by the shared one.
46
+ tree: oc.route({ method: "GET", path: "/workspace/tree" }).input(WorkspaceScopeSchema).output(WorkspaceTreeSchema),
42
47
  // Lazy-load one directory's children — the tree returns ignored dirs (node_modules, .git, …) without children,
43
48
  // and the client fetches them here on expand so a giant node_modules can't blow the tree walk's entry budget.
44
49
  children: oc.route({ method: "GET", path: "/workspace/children" }).input(WorkspaceChildrenQuerySchema).output(WorkspaceChildrenSchema),
@@ -49,7 +54,10 @@ export const workspaceContract = {
49
54
  * it answers a streamed byte RANGE, which oRPC has no shape for). Minting is here rather than beside it so
50
55
  * it rides the bearer middleware and the contract's route advertisement: a browser can tell whether the
51
56
  * sandbox in front of it can stream video at all, instead of learning it from a 404 mid-playback. */
52
- mediaTicket: oc.route({ method: "POST", path: "/workspace/media-ticket" }).input(WorkspaceFileQuerySchema).output(WorkspaceMediaTicketSchema),
57
+ mediaTicket: oc
58
+ .route({ method: "POST", path: "/workspace/media-ticket" })
59
+ .input(WorkspaceMediaTicketQuerySchema)
60
+ .output(WorkspaceMediaTicketSchema),
53
61
  // Which file a NAMED reference means — the lookup behind every clickable path in the UI. A path an agent
54
62
  // wrote in prose is often only a suffix of the real one, so it is matched against the workspace tree rather
55
63
  // than trusted as root-relative.
@@ -0,0 +1,25 @@
1
+ import { expect, test } from "vitest";
2
+ import { newConversationId } from "./conversation-ids.js";
3
+ import { ConversationIdSchema } from "./schemas.js";
4
+
5
+ // The one property that is not a matter of taste: this string becomes a git branch and a filesystem path, and
6
+ // the id guard is what stands between those and an injection. Held over a large sample rather than one draw,
7
+ // because the generator picks from three independent spaces and any of them could produce the bad character.
8
+ test("every generated id passes the conversation-id guard", () => {
9
+ for (let index = 0; index < 2_000; index += 1) {
10
+ expect(ConversationIdSchema.safeParse(newConversationId()).success).toBe(true);
11
+ }
12
+ });
13
+
14
+ test("an id reads as a word pair with a short tail, and stays short", () => {
15
+ const id = newConversationId();
16
+ expect(id).toMatch(/^[a-z]+-[a-z]+-[0-9a-z]{4}$/);
17
+ // Comfortably under a UUID's 36, which is the whole reason this exists.
18
+ expect(id.length).toBeLessThan(24);
19
+ });
20
+
21
+ // The tail is what makes the readable half safe to repeat: names may rhyme, ids may not.
22
+ test("ids are unique across a burst", () => {
23
+ const ids = new Set(Array.from({ length: 5_000 }, newConversationId));
24
+ expect(ids.size).toBe(5_000);
25
+ });