@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
@@ -6,35 +6,43 @@ import { probeSpec } from "./probes.js";
6
6
  * allowed to. Both the Maintenance panel and its rail badge run this function over the same report, so the number
7
7
  * on the tile and the reason in the panel are the same computation and cannot drift apart.
8
8
  *
9
- * Five states, and the distinctions between them are the whole design:
9
+ * Six states, and the distinctions between them are the whole design:
10
10
  *
11
11
  * not-applicable this chore is not a QUESTION worth asking of this repository — there is no Dockerfile to
12
12
  * slim, no pipeline to tighten, no documentation to re-read. Dropped from the panel entirely
13
13
  * rather than shown as clear, because "clear" claims we checked, and there was nothing to check.
14
- * The reason survives as a footer, so "why is there no Docker chore here?" has an answer.
14
+ * The reason survives in the scope strip, so "why is there no Docker chore here?" has an answer.
15
15
  * unavailable we have not measured this. knip is not a devDependency; there is no lockfile to audit. Rendered
16
16
  * greyed, never badged, and never collapsed into `clear` — a maintenance surface reporting a green
17
17
  * repository it has never actually measured is worse than one that says nothing.
18
18
  * clear we measured, and there is nothing to do. This is the common state, and it has to be visibly
19
19
  * reachable or the panel is just a list of complaints.
20
20
  * snoozed the owner said "not now". Still listed, still showing its evidence, silent until it lapses.
21
+ * stale we measured, then work landed, and we have not measured since. The evidence is still shown; the
22
+ * CLAIM comes off it, because it describes a tree that no longer exists.
21
23
  * due there is something to do.
22
24
  *
23
25
  * The first three are all ways of saying "no", and keeping them apart is what makes the surface trustworthy: they
24
26
  * mean we cannot ask, we did not measure, and we measured and found nothing — three different claims, and only
25
27
  * the last one is reassurance.
26
28
  *
27
- * And one flag that is not a state: `settled`. A due chore whose evidence is UNCHANGED since a turn was already
28
- * spent on it stays due — because it is — but must never light the rail again. This is what stops the surface
29
- * repeating itself while a fix sits in review, and it is why the ledger stores a digest rather than a timestamp:
30
- * "ran 3 days ago" cannot tell you whether it ran against THIS.
29
+ * And one flag that is not a state: `settled`. A due chore that has been RE-MEASURED since a turn was spent on
30
+ * it, and whose evidence did not move, stays due — because it is — but must never light the rail again. This is
31
+ * what stops the surface repeating itself while a fix sits in review, and it is why the ledger stores a digest
32
+ * rather than a timestamp: "ran 3 days ago" cannot tell you whether it ran against THIS.
33
+ *
34
+ * `stale` is the other half of that sentence, and it exists because the digest alone cannot tell the two apart.
35
+ * A probe that never re-ran produces an unchanged digest for free, so "the fix did not move the numbers" and "we
36
+ * have not looked since the fix" arrived at this function looking identical — and the panel showed the second as
37
+ * the first, quoting a week-old count an hour after the work that invalidated it. Comparing the run's time to the
38
+ * MEASUREMENT's time is what separates them, and it is a comparison of two numbers the report already carries.
31
39
  *
32
40
  * Nothing here can hide a problem. Snoozing and settling change whether the rail SPEAKS; the panel still shows
33
41
  * the chore, its evidence and its state. The one thing that removes a row entirely is `not-applicable`, and that
34
- * is not hiding — it is the absence of a subject, recorded in the panel's footer with its reason. A maintenance
35
- * surface you can quietly bury findings in is a maintenance surface nobody trusts. */
42
+ * is not hiding — it is the absence of a subject, counted in the panel's scope strip and expandable to the reason.
43
+ * A maintenance surface you can quietly bury findings in is a maintenance surface nobody trusts. */
36
44
 
37
- export type ChoreState = "due" | "clear" | "snoozed" | "unavailable" | "not-applicable";
45
+ export type ChoreState = "due" | "clear" | "snoozed" | "stale" | "unavailable" | "not-applicable";
38
46
 
39
47
  export interface ChoreVerdict {
40
48
  readonly chore: Chore;
@@ -46,15 +54,30 @@ export interface ChoreVerdict {
46
54
  readonly detail: readonly string[];
47
55
  // The evidence identity. Empty for `unavailable`, where there is no evidence to identify.
48
56
  readonly digest: string;
57
+ /* WHEN THE EVIDENCE WAS TAKEN — the fact every row shows beside its numbers, and the one whose absence let a
58
+ * measurement from last Tuesday read as this morning's. Undefined when the verdict rests on no measurement at
59
+ * all: a survey is decided by the calendar, and an unavailable chore has nothing to be out of date. */
60
+ readonly measuredAt: number | undefined;
49
61
  // The turn. Present only when there is something to do — a "start an agent" button on a clear chore is an
50
62
  // invitation to spend money proving that nothing is wrong.
51
63
  readonly prompt: string | undefined;
52
64
  readonly lastRun: ChoreLedgerEntry | undefined;
53
- // A turn has already been spent on exactly this evidence, and the chore's cadence has not lapsed since. Still
54
- // due, still shown, never badged.
65
+ // A turn has been spent on this chore, the evidence has been re-measured since and did not move, and the
66
+ // chore's cadence has not lapsed. Still due, still shown, never badged.
55
67
  readonly settled: boolean;
56
68
  }
57
69
 
70
+ /* HOW OLD THE EVIDENCE IS: the OLDEST of the measurements a verdict rests on, because a claim is only as current
71
+ * as the least current thing it was computed from. Undefined when it rests on none — a survey has no measurement,
72
+ * and an unavailable chore's probe did not produce one. */
73
+ const measurementAge = (needs: readonly ProbeId[], probes: ReadonlyMap<ProbeId, ProbeResult>): number | undefined => {
74
+ const taken = needs.flatMap((id) => {
75
+ const probe = probes.get(id);
76
+ return probe?.state === `ok` ? [probe.ranAt] : [];
77
+ });
78
+ return taken.length === 0 ? undefined : Math.min(...taken);
79
+ };
80
+
58
81
  // A survey that is clear is clear because it was READ recently, and saying so is the only way its row means
59
82
  // anything — "nothing to do" under a chore that has no measurement would be a claim about the code rather than
60
83
  // about the calendar.
@@ -87,21 +110,32 @@ export const assessChore = (chore: Chore, context: ChoreContext, ledger: ChoreLe
87
110
 
88
111
  /* APPLICABILITY FIRST, before anything is measured or any evidence is read. A chore that does not apply is
89
112
  * not "clear" and not "unmeasured" — the question does not arise here, and every subsequent branch of this
90
- * function would be answering it anyway. The reason is carried as the headline, because the panel's footer
91
- * is the only place it will ever be read. */
113
+ * function would be answering it anyway. The cause is carried as the headline, because the scope strip — which
114
+ * groups these rows BY it — is the only place it will ever be read. */
92
115
  const inapplicable = chore.applies?.(context.signals);
93
116
  if (inapplicable !== undefined) {
94
- return { ...base, state: `not-applicable`, severity: `info`, headline: inapplicable, detail: [], digest: `` };
117
+ return { ...base, state: `not-applicable`, severity: `info`, headline: inapplicable, detail: [], digest: ``, measuredAt: undefined };
95
118
  }
96
119
 
97
120
  const unmeasured = unmeasuredDetail(chore.needs, context.probes);
98
121
  if (unmeasured.length > 0) {
99
- return { ...base, state: `unavailable`, severity: `info`, headline: `Not measured`, detail: unmeasured, digest: `` };
122
+ return { ...base, state: `unavailable`, severity: `info`, headline: `Not measured`, detail: unmeasured, digest: ``, measuredAt: undefined };
100
123
  }
101
124
 
125
+ // Every state below this line rests on a measurement that ran, so all of them carry when it was taken.
126
+ const measuredAt = measurementAge(chore.needs, context.probes);
127
+
102
128
  const finding = chore.assess(context);
103
129
  if (finding === undefined) {
104
- return { ...base, state: `clear`, severity: `info`, headline: clearHeadline(chore, ledger, context.nowMs), detail: [], digest: `` };
130
+ return {
131
+ ...base,
132
+ state: `clear`,
133
+ severity: `info`,
134
+ headline: clearHeadline(chore, ledger, context.nowMs),
135
+ detail: [],
136
+ digest: ``,
137
+ measuredAt,
138
+ };
105
139
  }
106
140
 
107
141
  /* Has the last run's settlement lapsed? A cadence of 0 means "this is decided by evidence alone" — an advisory
@@ -116,13 +150,30 @@ export const assessChore = (chore: Chore, context: ChoreContext, ledger: ChoreLe
116
150
  * rather than its digest, because a survey run three days into a quarter and one three days before its end
117
151
  * are the same period but very different answers to "when was this last read?". */
118
152
  if (chore.survey === true && ledger !== undefined && context.nowMs - ledger.ranAt < chore.cadenceMs) {
119
- return { ...base, state: `clear`, severity: `info`, headline: clearHeadline(chore, ledger, context.nowMs), detail: finding.detail, digest: finding.digest };
153
+ return {
154
+ ...base,
155
+ state: `clear`,
156
+ severity: `info`,
157
+ headline: clearHeadline(chore, ledger, context.nowMs),
158
+ detail: finding.detail,
159
+ digest: finding.digest,
160
+ measuredAt,
161
+ };
120
162
  }
121
163
 
122
164
  const prompt = chorePrompt(chore, finding, context.repo);
123
165
 
124
166
  if (ledger?.snoozedUntil !== undefined && ledger.snoozedUntil > context.nowMs) {
125
- return { ...base, state: `snoozed`, severity: `info`, headline: finding.headline, detail: finding.detail, digest: finding.digest, prompt };
167
+ return {
168
+ ...base,
169
+ state: `snoozed`,
170
+ severity: `info`,
171
+ headline: finding.headline,
172
+ detail: finding.detail,
173
+ digest: finding.digest,
174
+ measuredAt,
175
+ prompt,
176
+ };
126
177
  }
127
178
 
128
179
  /* The agent looked at exactly this evidence and reported that there was nothing in it — knip's findings were
@@ -137,10 +188,32 @@ export const assessChore = (chore: Chore, context: ChoreContext, ledger: ChoreLe
137
188
  headline: `Checked — the findings did not hold up`,
138
189
  detail: finding.detail,
139
190
  digest: finding.digest,
191
+ measuredAt,
140
192
  };
141
193
  }
142
194
 
143
- return { ...base, state: `due`, severity: finding.severity, headline: finding.headline, detail: finding.detail, digest: finding.digest, prompt, settled: sameEvidence };
195
+ /* THE MEASUREMENT IS OLDER THAN THE WORK. A turn landed after the last time we looked, so the evidence below
196
+ * describes a tree that no longer exists — an hour after a run deleted the dead code, the row was still
197
+ * quoting the count from six days before it. `sameEvidence` cannot catch this: an unchanged digest is exactly
198
+ * what a probe that never re-ran produces, so the flag says "settled" at its most confident when it knows
199
+ * least. The chore steps down instead — evidence stays on the row, the CLAIM comes off it — and carries no
200
+ * prompt, because the honest next move is to measure again rather than to spend a second turn on a finding
201
+ * nobody has re-checked. It cannot badge either, which is what stops the tile lighting for work already done. */
202
+ if (ledger !== undefined && measuredAt !== undefined && ledger.ranAt > measuredAt) {
203
+ return { ...base, state: `stale`, severity: `info`, headline: finding.headline, detail: finding.detail, digest: finding.digest, measuredAt };
204
+ }
205
+
206
+ return {
207
+ ...base,
208
+ state: `due`,
209
+ severity: finding.severity,
210
+ headline: finding.headline,
211
+ detail: finding.detail,
212
+ digest: finding.digest,
213
+ measuredAt,
214
+ prompt,
215
+ settled: sameEvidence,
216
+ };
144
217
  };
145
218
 
146
219
  // The ledger is keyed by repo + chore, which is the grain a verdict is decided at: the same chore in two repos is
@@ -162,8 +235,9 @@ export const assessReport = (report: ChoresReport, nowMs: number): ChoreVerdict[
162
235
  * never "here is a statistic" — the extension API states that bar and this is the function that holds this
163
236
  * surface to it. Three filters, and every one of them removes a case that would otherwise light the tile forever:
164
237
  *
165
- * state === due the obvious one.
166
- * !settled a turn has already been spent on this exact evidence.
238
+ * state === due the obvious one — and it is also what keeps `stale` silent, since a measurement taken before
239
+ * the last turn is not a fact anyone should be interrupted about.
240
+ * !settled a turn has been spent on this chore and the re-measured evidence did not move.
167
241
  * unseen digest the owner has already LOOKED at this evidence in the panel. Acknowledgement is per digest
168
242
  * rather than per chore, so acknowledging today's finding does not also swallow tomorrow's.
169
243
  *
@@ -0,0 +1,17 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { expect, test } from "vitest";
3
+ import { currentLock } from "./contract-lock.js";
4
+
5
+ /* The committed lock and the code must say the same thing — see contract-lock.ts for what the pair buys.
6
+ *
7
+ * This is the HALF that runs everywhere the tests run; the other half (a shrunk lock needs a declared break)
8
+ * lives in prepass.mjs, which has git and this suite does not. */
9
+ test("contract.lock.json matches the schemas this package exports", () => {
10
+ const committed: unknown = JSON.parse(readFileSync(new URL("../contract.lock.json", import.meta.url), "utf8"));
11
+ expect(
12
+ currentLock(),
13
+ "the wire contract moved — run `pnpm --filter @intentic/sandbox-contract lock` and commit contract.lock.json with this change. " +
14
+ "If a schema or field was removed or changed (not added), land it as a `type!:` commit with a `Breaking-Note:` trailer " +
15
+ "saying, in the user's words, what stops working and what to do instead.",
16
+ ).toEqual(committed);
17
+ });
@@ -0,0 +1,66 @@
1
+ /* THE WIRE SURFACE, AS ONE COMPARABLE DOCUMENT — every zod schema this package exports, serialized to JSON
2
+ * Schema and sorted into a canonical shape. `contract.lock.json` at the package root is this function's
3
+ * committed output, and the pair is what makes "did the contract change?" a question git can answer.
4
+ *
5
+ * Two readers depend on that answer. contract-lock.test.ts fails whenever the committed file is out of step
6
+ * with the code, so a contract change always lands WITH its lock diff — visible in review, attributable to a
7
+ * commit. And prepass.mjs (invariant 6) compares the committed lock against its merge-base to insist that a
8
+ * SHRUNK surface — a schema or field removed, a type changed — arrives as a `!` commit carrying a
9
+ * `Breaking-Note:` trailer, which is what feeds the release's "Breaking changes" section and the update card's
10
+ * warning. Additions pass freely; every persisted-manifest reader parses loosely, so growth breaks nobody.
11
+ *
12
+ * Serialized from the package's EXPORTS rather than a hand-kept list, on the repo's own rule (AGENTS.md:
13
+ * "guard invariants by discovery, not enumeration"): a schema added tomorrow is in the lock tomorrow, and a
14
+ * schema that stops being exported is a removal the lock shows. `unrepresentable: "any"` keeps the rare
15
+ * function-valued corner from throwing — it serializes as `{}`, which still diffs when it moves. */
16
+
17
+ import { z } from "zod";
18
+ import * as contract from "./index.js";
19
+
20
+ // Canonical ordering, so two runs of the same code are byte-identical and a lock diff is a contract diff
21
+ // rather than a key-order shuffle. Arrays keep their order — for `required` and `enum` lists the order zod
22
+ // emits is stable, and sorting them would hide a reorder that is genuinely no change at all anyway.
23
+ const sorted = (value: unknown): unknown => {
24
+ if (Array.isArray(value)) {
25
+ return value.map(sorted);
26
+ }
27
+ if (typeof value === "object" && value !== null) {
28
+ return Object.fromEntries(
29
+ Object.keys(value)
30
+ .sort()
31
+ .map((key) => [key, sorted((value as Record<string, unknown>)[key])]),
32
+ );
33
+ }
34
+ return value;
35
+ };
36
+
37
+ export const currentLock = (): Record<string, unknown> => {
38
+ const lock: Record<string, unknown> = {};
39
+ for (const name of Object.keys(contract).sort()) {
40
+ const value = (contract as Record<string, unknown>)[name];
41
+ if (!(value instanceof z.ZodType)) {
42
+ continue;
43
+ }
44
+ try {
45
+ const schema = sorted(z.toJSONSchema(value, { unrepresentable: "any" })) as Record<string, unknown>;
46
+ // The dialect banner is identical on all ~500 entries — pure weight, no surface.
47
+ delete schema["$schema"];
48
+ lock[name] = schema;
49
+ } catch {
50
+ // A schema JSON Schema cannot express at all still holds a place in the lock: its appearance,
51
+ // disappearance, or transition to serializable are each a surface change worth a diff.
52
+ lock[name] = "<unserializable>";
53
+ }
54
+ }
55
+ return lock;
56
+ };
57
+
58
+ /* ONE LINE PER SCHEMA, on purpose — not JSON.stringify(lock, null, 4). Pretty-printed, the lock is a 35k-line
59
+ * wall nobody scrolls; a line per export keeps it ~500 lines and makes `git diff` read as the list of WHICH
60
+ * surfaces moved, which is the level a reviewer reviews at. The path-level detail lives in the tooling: the
61
+ * lock test diffs parsed objects and prepass invariant 6 names the exact removed paths, so nothing is lost by
62
+ * not laying the structure out vertically. */
63
+ export const serializeLock = (lock: Record<string, unknown>): string =>
64
+ `{\n${Object.entries(lock)
65
+ .map(([name, schema]) => `${JSON.stringify(name)}: ${JSON.stringify(schema)}`)
66
+ .join(",\n")}\n}\n`;
@@ -6,6 +6,8 @@ import {
6
6
  AttachTurnSchema,
7
7
  OkSchema,
8
8
  ProviderRefusalsSchema,
9
+ RewindResultSchema,
10
+ RewindTurnSchema,
9
11
  StartedTurnSchema,
10
12
  SteerSchema,
11
13
  StopTurnSchema,
@@ -22,6 +24,10 @@ export const agentContract = {
22
24
  reply: oc.route({ method: "POST", path: "/agent/reply" }).input(AgentReplySchema).output(OkSchema),
23
25
  steer: oc.route({ method: "POST", path: "/agent/steer" }).input(SteerSchema).output(OkSchema),
24
26
  stop: oc.route({ method: "POST", path: "/agent/stop" }).input(StopTurnSchema).output(OkSchema),
27
+ // Go back to a message: restore the workspace to that turn's checkpoint, drop the messages after it, and
28
+ // forget the provider session. CONFLICT while a turn is running — a restore cannot overwrite files an
29
+ // agent is editing. NOT_FOUND when that message has no checkpoint to go back to.
30
+ rewind: oc.route({ method: "POST", path: "/agent/rewind" }).input(RewindTurnSchema).output(RewindResultSchema),
25
31
  // The provider's slash commands as last published by one of its turns, so a conversation's `/` popover is
26
32
  // populated before it has run one. The live `commands` frame stays authoritative for a running turn.
27
33
  commands: oc.route({ method: "GET", path: "/agent/commands" }).input(AgentCommandsQuerySchema).output(AgentCommandsSchema),
@@ -8,6 +8,7 @@ import {
8
8
  AgentIdSchema,
9
9
  AgentIdsSchema,
10
10
  AgentLandSchema,
11
+ AgentPlaceSchema,
11
12
  AgentRenameSchema,
12
13
  AgentSearchQuerySchema,
13
14
  AgentSearchResultSchema,
@@ -54,6 +55,13 @@ export const agentsContract = {
54
55
  search: oc.route({ method: "GET", path: "/agents/search" }).input(AgentSearchQuerySchema).output(AgentSearchResultSchema),
55
56
  get: oc.route({ method: "GET", path: "/agents/{id}" }).input(AgentIdSchema).output(AgentSummarySchema),
56
57
  transcript: oc.route({ method: "GET", path: "/agents/{id}/transcript" }).input(AgentIdSchema).output(AgentTranscriptSchema),
58
+ /* SPEAK AS THE AGENT — append the user's words to the conversation's record as an assistant row, with no
59
+ * turn behind them and no reply. The row is marked `placed` for human readers (RestoredMessageSchema); the
60
+ * provider session is FORGOTTEN in the same breath, rewind-style, so the next real turn opens a fresh
61
+ * runtime session seeded from the record — where the placed line reads as the agent's own words, because
62
+ * the handoff renders every assistant row identically. A running turn is CONFLICT: the illusion can only be
63
+ * established between turns, and a concurrent turn would resume the very session this exists to retire. */
64
+ place: oc.route({ method: "POST", path: "/agents/{id}/place" }).input(AgentPlaceSchema).output(OkSchema),
57
65
  rename: oc.route({ method: "POST", path: "/agents/{id}/rename" }).input(AgentRenameSchema).output(AgentSummarySchema),
58
66
  // This agent's own land-at-completion posture — an override of the sandbox-wide `autoLand` setting; null
59
67
  // clears it back to "inherit". Legal mid-turn on purpose: the setting is read at turn COMPLETION, so
@@ -64,6 +72,10 @@ export const agentsContract = {
64
72
  diff: oc.route({ method: "GET", path: "/agents/{id}/diff" }).input(AgentIdSchema).output(AgentChangesSchema),
65
73
  fileDiff: oc.route({ method: "GET", path: "/agents/{id}/{repo}/file-diff" }).input(AgentFileDiffQuerySchema).output(FileDiffSchema),
66
74
  land: oc.route({ method: "POST", path: "/agents/{id}/land" }).input(AgentLandSchema).output(LandResultSchema),
75
+ // A collaborator's ask for the land they may not perform themselves (role floors put `land`/`discard` at
76
+ // maintainer). Stamps AgentSummarySchema.landRequested with the caller's identity and re-frames the fleet,
77
+ // so every maintainer's board carries the request; the land or discard that answers it clears the stamp.
78
+ requestLand: oc.route({ method: "POST", path: "/agents/{id}/request-land" }).input(AgentIdSchema).output(AgentSummarySchema),
67
79
  discard: oc.route({ method: "POST", path: "/agents/{id}/discard" }).input(AgentIdSchema).output(OkSchema),
68
80
  archive: oc.route({ method: "POST", path: "/agents/archive" }).input(AgentArchiveSchema).output(AgentsMovedSchema),
69
81
  unarchive: oc.route({ method: "POST", path: "/agents/unarchive" }).input(AgentIdsSchema).output(AgentsMovedSchema),
@@ -2,6 +2,8 @@ import { oc } from "@orpc/contract";
2
2
  import {
3
3
  AutomationApprovalIdParamSchema,
4
4
  AutomationApprovalsListSchema,
5
+ AutomationCatalogSchema,
6
+ AutomationEnabledInputSchema,
5
7
  AutomationIdParamSchema,
6
8
  AutomationSchema,
7
9
  AutomationsListSchema,
@@ -10,12 +12,19 @@ import {
10
12
 
11
13
  // The sandbox's automations manifest (scheduled agent wake-ups). `list` returns each automation with its recent
12
14
  // runs + next fire time. `upsert` adds or edits by id (nothing to provision — the scheduler picks it up on its
13
- // next poll), so the enabled toggle is a plain re-post. `remove` deletes.
15
+ // next poll); `setEnabled` changes only the switch, so a list-row action never has to reconstruct the record.
16
+ // `remove` deletes.
14
17
  // The `pending*` routes are the owner's approval queue: a `requireApproval` automation holds each fire here
15
18
  // instead of waking; `approve` runs the held wake, `reject` drops it.
16
19
  export const automationsContract = {
17
20
  list: oc.route({ method: "GET", path: "/automations" }).output(AutomationsListSchema),
21
+ /* WHAT CAN WAKE AN AGENT HERE, and what to start from — the daemon's own sources and templates merged with
22
+ * every installed extension's. The composer's entire vocabulary, so that adding a trigger to an area is a
23
+ * change to that area and to nothing else. `upsert` below validates against the same merge, which is what
24
+ * keeps the surface and the daemon from disagreeing about what is allowed. */
25
+ catalog: oc.route({ method: "GET", path: "/automations/catalog" }).output(AutomationCatalogSchema),
18
26
  upsert: oc.route({ method: "POST", path: "/automations" }).input(AutomationSchema).output(OkSchema),
27
+ setEnabled: oc.route({ method: "POST", path: "/automations/{id}/enabled" }).input(AutomationEnabledInputSchema).output(OkSchema),
19
28
  remove: oc.route({ method: "DELETE", path: "/automations/{id}" }).input(AutomationIdParamSchema).output(OkSchema),
20
29
  /* Fire one automation NOW, by hand — the answer to "I wrote a 3 a.m. cron and I have no way to try it".
21
30
  * It runs the SAME path the real trigger runs: a schedule stays a headless main-tree wake, because a
@@ -27,6 +36,13 @@ export const automationsContract = {
27
36
  * main reason to press this, and unlike the /automations/{id}/fire webhook there is no outside sender here to
28
37
  * fail closed against.
29
38
  *
39
+ * NOT FOR A LISTENER, which is the one trigger whose fire is nothing without the thing that fired it. A
40
+ * listener's prompt is a brief about handling the events riding with it, and by hand there are none — so the
41
+ * button could only ever produce an agent told to handle events, handed none, asking where they went. Worse,
42
+ * that pointless run took the automation's turn: a real mention arriving while it ran had to wait behind it.
43
+ * Refused here rather than hidden in the UI alone, because the honest answer to "how do I test this" is to
44
+ * send the bot a message, which costs nothing and tests the whole path.
45
+ *
30
46
  * Acks immediately with the turn detached, like /fire and `approve`: the guard alone may take a minute, and
31
47
  * the run history (with the session that makes it openable) is where the outcome lands. */
32
48
  run: oc.route({ method: "POST", path: "/automations/{id}/run" }).input(AutomationIdParamSchema).output(OkSchema),
@@ -2,8 +2,12 @@ import { eventIterator, oc } from "@orpc/contract";
2
2
  import { IntenticLineSchema } from "../events.js";
3
3
  import {
4
4
  CapabilitiesListSchema,
5
+ CapabilityCardParamSchema,
6
+ CapabilityConnectionSchema,
5
7
  CapabilityIdParamSchema,
6
8
  CapabilityLoginSchema,
9
+ CapabilityOtpSchema,
10
+ CapabilityRenameSchema,
7
11
  CapabilitySchema,
8
12
  CapabilitySecretInputSchema,
9
13
  CapabilityStatusSchema,
@@ -21,11 +25,29 @@ export const capabilitiesContract = {
21
25
  list: oc.route({ method: "GET", path: "/capabilities" }).output(CapabilitiesListSchema),
22
26
  add: oc.route({ method: "POST", path: "/capabilities" }).input(CapabilitySchema).output(eventIterator(IntenticLineSchema)),
23
27
  remove: oc.route({ method: "DELETE", path: "/capabilities/{id}" }).input(CapabilityIdParamSchema).output(OkSchema),
28
+ /* Give a connection a different name, carrying what the old one keyed: a browser profile with its logins,
29
+ * a connected machine's enrollment, an extension's checkout. The name is the agent's handle for the thing,
30
+ * so this is a migration — add + remove would lose exactly the state that makes the connection worth
31
+ * keeping. A kind whose name is part of what it IS (the scaffolders, the one-per-sandbox cards) refuses. */
32
+ rename: oc.route({ method: "POST", path: "/capabilities/{id}/rename" }).input(CapabilityRenameSchema).output(OkSchema),
24
33
  // Replace just the secret in a capability's config (the /secrets page's edit) and re-run its apply.
25
34
  setSecret: oc.route({ method: "POST", path: "/capabilities/{id}/secret" }).input(CapabilitySecretInputSchema).output(OkSchema),
26
35
  status: oc.route({ method: "GET", path: "/capabilities/{id}/status" }).input(CapabilityIdParamSchema).output(CapabilityStatusSchema),
36
+ /* One capability's stored config, secrets included — how an extension BACKEND dials the service behind a
37
+ * connected capability (ext-deployments reads its Komodo's key pair through this). Never a browser's: the
38
+ * handler refuses any caller with a member identity, so only the daemon's header grants reach it, and an
39
+ * extension's grant reaches it only when its manifest declares the route in `permissions.daemon` — which
40
+ * is the install dialog saying, in one line, "this extension can read connected credentials". */
41
+ connection: oc.route({ method: "GET", path: "/capabilities/{id}/connection" }).input(CapabilityIdParamSchema).output(CapabilityConnectionSchema),
27
42
  marketplace: oc.route({ method: "POST", path: "/capabilities/marketplace" }).input(MarketplaceRequestSchema).output(MarketplaceSchema),
43
+ // "Not needed": stop offering this card until the workspace evidence behind it changes. Nothing is torn
44
+ // down and nothing is remembered about the card itself — only the evidence it was declined against.
45
+ dismiss: oc.route({ method: "DELETE", path: "/capabilities/recommendations/{card}" }).input(CapabilityCardParamSchema).output(OkSchema),
28
46
  // Start an agent-kind capability's interactive login (its declared loginCommand) in a visible terminal
29
47
  // session the user types into — device-code sign-in flows. Returns the session the panel attaches to.
30
48
  login: oc.route({ method: "POST", path: "/capabilities/{id}/login" }).input(CapabilityIdParamSchema).output(CapabilityLoginSchema),
49
+ // Mint one TOTP code from the capability's stored seed (a field its card marks `totp`). The one capability
50
+ // read the agent token is admitted to (see auth/grants): a code expires within its period and never reveals
51
+ // the seed, so the in-sandbox `otp` command can answer a 2FA prompt without the agent holding the factor.
52
+ otp: oc.route({ method: "GET", path: "/capabilities/{id}/otp" }).input(CapabilityIdParamSchema).output(CapabilityOtpSchema),
31
53
  };
@@ -1,5 +1,13 @@
1
1
  import { oc } from "@orpc/contract";
2
- import { CiFixResponseSchema, CiJobsResponseSchema, CiRunParamSchema, CiRunsResponseSchema, CiSeenResponseSchema, OkSchema } from "../schemas.js";
2
+ import {
3
+ CiFixParamSchema,
4
+ CiFixResponseSchema,
5
+ CiJobsResponseSchema,
6
+ CiRunParamSchema,
7
+ CiRunsResponseSchema,
8
+ CiSeenResponseSchema,
9
+ OkSchema,
10
+ } from "../schemas.js";
3
11
 
4
12
  // Pipelines on the workspace repos' github/gitlab remotes. `runs` serves the Pipelines rail view (cache +
5
13
  // on-demand backfill; per-repo webhook warnings ride along); `rerun`/`cancel` proxy to the vendor; `fix`
@@ -11,7 +19,7 @@ export const ciContract = {
11
19
  rerun: oc.route({ method: "POST", path: "/ci/runs/rerun" }).input(CiRunParamSchema).output(OkSchema),
12
20
  cancel: oc.route({ method: "POST", path: "/ci/runs/cancel" }).input(CiRunParamSchema).output(OkSchema),
13
21
  jobs: oc.route({ method: "POST", path: "/ci/runs/jobs" }).input(CiRunParamSchema).output(CiJobsResponseSchema),
14
- fix: oc.route({ method: "POST", path: "/ci/fix" }).input(CiRunParamSchema).output(CiFixResponseSchema),
22
+ fix: oc.route({ method: "POST", path: "/ci/fix" }).input(CiFixParamSchema).output(CiFixResponseSchema),
15
23
  // "I have looked at the pipelines" — what silences the rail badge for breakages already read. No input:
16
24
  // the surface is read as a whole, and the daemon stamps its own clock so a skewed browser can't mark
17
25
  // future failures as already seen.
@@ -1,9 +1,9 @@
1
1
  import { oc } from "@orpc/contract";
2
2
  import {
3
3
  AccountIdSchema,
4
+ AccountListQuerySchema,
4
5
  AccountRenameSchema,
5
6
  AuthorizeChallengeSchema,
6
- ModelsSchema,
7
7
  OauthAccountListSchema,
8
8
  OauthAccountSchema,
9
9
  OauthExchangeSchema,
@@ -14,12 +14,16 @@ import {
14
14
  // PKCE material; `exchange` stores the resulting tokens as a new account and returns it; `accounts` lists the
15
15
  // connected accounts; `rename` renames one; `disconnect` clears the one named by id. A sandbox can hold several
16
16
  // Claude accounts.
17
+ //
18
+ // The model catalog is NOT here: it is the one thing every provider answers identically, so it lives on the
19
+ // shared /providers/{provider}/models route (providers.contract.ts). What is left is what is genuinely Claude's
20
+ // — an account handshake no other provider has.
17
21
  export const claudeContract = {
18
22
  start: oc.route({ method: "POST", path: "/claude/oauth/start" }).output(AuthorizeChallengeSchema),
19
23
  exchange: oc.route({ method: "POST", path: "/claude/oauth/exchange" }).input(OauthExchangeSchema).output(OauthAccountSchema),
20
- // Claude's available models for the picker, from the Agent SDK's supportedModels() (see claude-models.ts).
21
- models: oc.route({ method: "GET", path: "/claude/models" }).output(ModelsSchema),
22
- accounts: oc.route({ method: "GET", path: "/claude/accounts" }).output(OauthAccountListSchema),
24
+ // Each account carries its plan-limit reading. `force` re-measures before answering see
25
+ // AccountListQuerySchema, and USAGE_WAIT_MS in claude.routes.ts for what an ordinary read waits.
26
+ accounts: oc.route({ method: "GET", path: "/claude/accounts" }).input(AccountListQuerySchema).output(OauthAccountListSchema),
23
27
  rename: oc.route({ method: "POST", path: "/claude/account/rename" }).input(AccountRenameSchema).output(OauthAccountSchema),
24
28
  disconnect: oc.route({ method: "POST", path: "/claude/account/disconnect" }).input(AccountIdSchema).output(OkSchema),
25
29
  };
@@ -1,10 +1,31 @@
1
1
  import { oc } from "@orpc/contract";
2
+ import { z } from "zod";
2
3
  import { CapabilityIdParamSchema, ModelsSchema } from "../schemas.js";
3
4
 
4
5
  // An `endpoint` capability's picker catalog — the models the configured server itself publishes, read from its
5
6
  // own /v1/models. Every other provider's catalog is one fixed route because there is one of each; endpoints are
6
7
  // user-created and unbounded, so the id rides in the path. There is no seed floor and no static list: what a
7
8
  // model API serves is knowable only by asking it, and an empty answer is the honest report that we could not.
9
+
10
+ /* The free trial's remaining allowance, on the endpoints contract because the trial IS an endpoint — the one
11
+ * the daemon provisions rather than the user (agent-catalog.ts TRIAL_ENDPOINT_ID). It is a separate read from
12
+ * the catalog above because it answers a different question and changes on a different clock: the model list is
13
+ * a property of the upstream, while this is a property of the ACCOUNT and moves with every message sent.
14
+ *
15
+ * `available` false is the ordinary answer, not an error: most sandboxes run against a platform that serves no
16
+ * trial, and the picker simply has no trial row to badge.
17
+ */
18
+ export const TrialStatusSchema = z.object({
19
+ available: z.boolean(),
20
+ allowance: z.number().int().nonnegative(),
21
+ used: z.number().int().nonnegative(),
22
+ remaining: z.number().int().nonnegative(),
23
+ // ISO stamp of the next reset, absent until the platform has answered once.
24
+ resetsAt: z.string().optional(),
25
+ });
26
+ export type TrialStatusResponse = z.infer<typeof TrialStatusSchema>;
27
+
8
28
  export const endpointsContract = {
9
29
  models: oc.route({ method: "GET", path: "/endpoints/{id}/models" }).input(CapabilityIdParamSchema).output(ModelsSchema),
30
+ trial: oc.route({ method: "GET", path: "/endpoints/trial/status" }).output(TrialStatusSchema),
10
31
  };
@@ -6,8 +6,17 @@ import {
6
6
  ExtensionProcessStatusSchema,
7
7
  ExtensionSettingsInputSchema,
8
8
  ExtensionSettingsSchema,
9
+ ExtensionReadinessSchema,
10
+ ExtensionUpdateActionSchema,
11
+ ExtensionUpdateAppliedSchema,
12
+ ExtensionUpdatePolicyInputSchema,
13
+ ExtensionUpdatePreviewSchema,
14
+ ExtensionUpdatesCheckedSchema,
15
+ ExtensionUsageInputSchema,
9
16
  ExtensionsListSchema,
10
17
  OkSchema,
18
+ WorkspaceExtensionCreatedSchema,
19
+ WorkspaceExtensionCreateSchema,
11
20
  } from "../schemas.js";
12
21
 
13
22
  // Installed extensions resolved to their approved manifests — what the web extension host boots from. The
@@ -16,12 +25,45 @@ import {
16
25
  // declared are refused, the same honesty rule the host applies to runtime view/command registrations.
17
26
  export const extensionsContract = {
18
27
  list: oc.route({ method: "GET", path: "/extensions" }).output(ExtensionsListSchema),
28
+ // Author a new extension in place: writes a running one into .intentic/workspace-extensions/<name>/. The only
29
+ // creating route here, and it exists because that directory is otherwise reachable exclusively through an
30
+ // agent's file tools — which is a fine way to CHANGE an extension and a poor way to meet the idea of one.
31
+ create: oc.route({ method: "POST", path: "/extensions/workspace" }).input(WorkspaceExtensionCreateSchema).output(WorkspaceExtensionCreatedSchema),
19
32
  settings: oc.route({ method: "GET", path: "/extensions/{id}/settings" }).input(CapabilityIdParamSchema).output(ExtensionSettingsSchema),
20
33
  setSettings: oc.route({ method: "POST", path: "/extensions/{id}/settings" }).input(ExtensionSettingsInputSchema).output(OkSchema),
21
34
  // The owner's on/off switch. Disabling stops the extension's declared processes here and now; its agent
22
35
  // plugin dir and PATH entry are rebuilt per turn, and an `environment` fragment only at the next image
23
36
  // rebuild — the Extensions tab states which of those an extension actually has.
24
37
  setEnabled: oc.route({ method: "POST", path: "/extensions/{id}/enabled" }).input(ExtensionEnabledInputSchema).output(OkSchema),
38
+ // The host reporting which declared routes it just let through. Written by the browser because that is where
39
+ // the permission gate runs (apiImpl.ts) — the daemon sees an extension's traffic as ordinary authenticated
40
+ // requests and cannot tell which extension, or which declared entry, any of it belongs to.
41
+ recordUsage: oc.route({ method: "POST", path: "/extensions/{id}/usage" }).input(ExtensionUsageInputSchema).output(OkSchema),
42
+ /* Whether this extension is fit for somebody else to run — the checks answerable from its files alone. Read
43
+ * on demand rather than carried on the list: it reads the bundle off disk per extension, and it is looked at
44
+ * when an author is about to publish, not every time the tab renders. */
45
+ readiness: oc.route({ method: "GET", path: "/extensions/{id}/readiness" }).input(CapabilityIdParamSchema).output(ExtensionReadinessSchema),
46
+ /* The update lifecycle for a GIT-INSTALLED extension. The list carries what the periodic registry check
47
+ * found (update/advisory/health per row); these are the verbs around it. `checkUpdates` runs the comparison
48
+ * now (the tab's "check now"). `updatePreview` stages the offered sha and answers with the version story +
49
+ * the mechanical powers diff — the read BEFORE the click, costing one throwaway clone like a registry
50
+ * browse. `applyUpdate` is the transaction: re-clone, validate, quiesce, swap (keeping the outgoing
51
+ * checkout one back), restart, health-watch — on the EXISTING capability config, so a private-source token
52
+ * survives what a bare re-add would lose. `revert` swaps the kept-previous checkout back. Update and revert
53
+ * change the code that runs, so like install they are owner-only. */
54
+ checkUpdates: oc.route({ method: "POST", path: "/extensions/updates/check" }).output(ExtensionUpdatesCheckedSchema),
55
+ updatePreview: oc
56
+ .route({ method: "POST", path: "/extensions/{id}/update/preview" })
57
+ .input(ExtensionUpdateActionSchema)
58
+ .output(ExtensionUpdatePreviewSchema),
59
+ applyUpdate: oc
60
+ .route({ method: "POST", path: "/extensions/{id}/update" })
61
+ .input(ExtensionUpdateActionSchema)
62
+ .output(ExtensionUpdateAppliedSchema),
63
+ revert: oc.route({ method: "POST", path: "/extensions/{id}/revert" }).input(CapabilityIdParamSchema).output(ExtensionUpdateAppliedSchema),
64
+ // The owner's standing answer per extension (notify / agent / auto, and the advisory opt-out) — see
65
+ // ExtensionUpdatePolicySchema for what each rung means.
66
+ setUpdatePolicy: oc.route({ method: "POST", path: "/extensions/{id}/update-policy" }).input(ExtensionUpdatePolicyInputSchema).output(OkSchema),
25
67
  // Declared background processes (contributes.processes): tmux-managed through the panel machinery
26
68
  // (session `panel-ext-<id>-<name>`, PORT-assigned, optional tunneled preview route).
27
69
  processStatus: oc