@intentic/sandbox-contract 1.240.0 → 1.243.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 (310) hide show
  1. package/README.md +52 -16
  2. package/dist/agent-catalog.d.ts +5 -28
  3. package/dist/agent-catalog.d.ts.map +1 -1
  4. package/dist/agent-catalog.js +21 -157
  5. package/dist/agent-catalog.js.map +1 -1
  6. package/dist/agent-run-model.d.ts +3 -2
  7. package/dist/agent-run-model.d.ts.map +1 -1
  8. package/dist/agent-run-model.js +9 -6
  9. package/dist/agent-run-model.js.map +1 -1
  10. package/dist/agent-runtimes.d.ts +27 -0
  11. package/dist/agent-runtimes.d.ts.map +1 -0
  12. package/dist/agent-runtimes.js +113 -0
  13. package/dist/agent-runtimes.js.map +1 -0
  14. package/dist/approvals-execution.d.ts +11 -0
  15. package/dist/approvals-execution.d.ts.map +1 -0
  16. package/dist/approvals-execution.js +44 -0
  17. package/dist/approvals-execution.js.map +1 -0
  18. package/dist/arrival.d.ts +151 -0
  19. package/dist/arrival.d.ts.map +1 -0
  20. package/dist/arrival.js +56 -0
  21. package/dist/arrival.js.map +1 -0
  22. package/dist/card-status.d.ts +5 -0
  23. package/dist/card-status.d.ts.map +1 -0
  24. package/dist/card-status.js +74 -0
  25. package/dist/card-status.js.map +1 -0
  26. package/dist/chores/chores.d.ts.map +1 -1
  27. package/dist/chores/chores.js +1 -1
  28. package/dist/chores/chores.js.map +1 -1
  29. package/dist/command-classes.d.ts +0 -1
  30. package/dist/command-classes.d.ts.map +1 -1
  31. package/dist/command-classes.js +9 -2
  32. package/dist/command-classes.js.map +1 -1
  33. package/dist/command-run.d.ts +10 -0
  34. package/dist/command-run.d.ts.map +1 -0
  35. package/dist/command-run.js +46 -0
  36. package/dist/command-run.js.map +1 -0
  37. package/dist/contracts/agent.contract.d.ts +991 -771
  38. package/dist/contracts/agent.contract.d.ts.map +1 -1
  39. package/dist/contracts/agents.contract.d.ts +345 -337
  40. package/dist/contracts/agents.contract.d.ts.map +1 -1
  41. package/dist/contracts/agents.contract.js +20 -2
  42. package/dist/contracts/agents.contract.js.map +1 -1
  43. package/dist/contracts/approvals.contract.d.ts +99 -0
  44. package/dist/contracts/approvals.contract.d.ts.map +1 -0
  45. package/dist/contracts/approvals.contract.js +32 -0
  46. package/dist/contracts/approvals.contract.js.map +1 -0
  47. package/dist/contracts/ci.contract.d.ts +1 -4
  48. package/dist/contracts/ci.contract.d.ts.map +1 -1
  49. package/dist/contracts/ci.contract.js +1 -9
  50. package/dist/contracts/ci.contract.js.map +1 -1
  51. package/dist/contracts/claude.contract.d.ts +9 -0
  52. package/dist/contracts/claude.contract.d.ts.map +1 -1
  53. package/dist/contracts/cursor.contract.d.ts +6 -0
  54. package/dist/contracts/cursor.contract.d.ts.map +1 -1
  55. package/dist/contracts/git.contract.d.ts +63 -1
  56. package/dist/contracts/git.contract.d.ts.map +1 -1
  57. package/dist/contracts/git.contract.js +22 -4
  58. package/dist/contracts/git.contract.js.map +1 -1
  59. package/dist/contracts/grok.contract.d.ts +3 -0
  60. package/dist/contracts/grok.contract.d.ts.map +1 -1
  61. package/dist/contracts/keys.contract.d.ts +81 -0
  62. package/dist/contracts/keys.contract.d.ts.map +1 -0
  63. package/dist/contracts/keys.contract.js +51 -0
  64. package/dist/contracts/keys.contract.js.map +1 -0
  65. package/dist/contracts/panels.contract.d.ts +7 -0
  66. package/dist/contracts/panels.contract.d.ts.map +1 -1
  67. package/dist/contracts/prepush.contract.js +2 -2
  68. package/dist/contracts/providers.contract.d.ts +2 -0
  69. package/dist/contracts/providers.contract.d.ts.map +1 -1
  70. package/dist/contracts/runner.contract.d.ts +84 -86
  71. package/dist/contracts/runner.contract.d.ts.map +1 -1
  72. package/dist/contracts/safety.contract.d.ts +35 -0
  73. package/dist/contracts/safety.contract.d.ts.map +1 -0
  74. package/dist/contracts/safety.contract.js +32 -0
  75. package/dist/contracts/safety.contract.js.map +1 -0
  76. package/dist/contracts/sessions.contract.d.ts +73 -337
  77. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  78. package/dist/contracts/settings.contract.d.ts +30 -32
  79. package/dist/contracts/settings.contract.d.ts.map +1 -1
  80. package/dist/contracts/system.contract.d.ts +139 -337
  81. package/dist/contracts/system.contract.d.ts.map +1 -1
  82. package/dist/contracts/translator.contract.d.ts +31 -3
  83. package/dist/contracts/translator.contract.d.ts.map +1 -1
  84. package/dist/contracts/usage.contract.d.ts +29 -20
  85. package/dist/contracts/usage.contract.d.ts.map +1 -1
  86. package/dist/contracts/usage.contract.js +13 -0
  87. package/dist/contracts/usage.contract.js.map +1 -1
  88. package/dist/contracts/workspace.contract.d.ts +8 -0
  89. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  90. package/dist/conversation-ids.d.ts +2 -0
  91. package/dist/conversation-ids.d.ts.map +1 -1
  92. package/dist/conversation-ids.js +8 -0
  93. package/dist/conversation-ids.js.map +1 -1
  94. package/dist/credential-material.d.ts +1 -0
  95. package/dist/credential-material.d.ts.map +1 -1
  96. package/dist/credential-material.js +24 -2
  97. package/dist/credential-material.js.map +1 -1
  98. package/dist/definition.d.ts +51 -93
  99. package/dist/definition.d.ts.map +1 -1
  100. package/dist/definition.js +6 -28
  101. package/dist/definition.js.map +1 -1
  102. package/dist/events.d.ts +1831 -2285
  103. package/dist/events.d.ts.map +1 -1
  104. package/dist/events.js +155 -55
  105. package/dist/events.js.map +1 -1
  106. package/dist/history-state.d.ts.map +1 -1
  107. package/dist/history-state.js +1 -0
  108. package/dist/history-state.js.map +1 -1
  109. package/dist/index.d.ts +1959 -1903
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +20 -5
  112. package/dist/index.js.map +1 -1
  113. package/dist/ingress-protocol.d.ts +13 -1
  114. package/dist/ingress-protocol.d.ts.map +1 -1
  115. package/dist/ingress-protocol.js +19 -0
  116. package/dist/ingress-protocol.js.map +1 -1
  117. package/dist/mentions.d.ts +3 -0
  118. package/dist/mentions.d.ts.map +1 -0
  119. package/dist/mentions.js +13 -0
  120. package/dist/mentions.js.map +1 -0
  121. package/dist/needs-action.d.ts +7 -0
  122. package/dist/needs-action.d.ts.map +1 -0
  123. package/dist/needs-action.js +3 -0
  124. package/dist/needs-action.js.map +1 -0
  125. package/dist/overlay-lint.d.ts +7 -0
  126. package/dist/overlay-lint.d.ts.map +1 -0
  127. package/dist/overlay-lint.js +68 -0
  128. package/dist/overlay-lint.js.map +1 -0
  129. package/dist/owner-ticket.d.ts +18 -0
  130. package/dist/owner-ticket.d.ts.map +1 -0
  131. package/dist/owner-ticket.js +51 -0
  132. package/dist/owner-ticket.js.map +1 -0
  133. package/dist/plan-pools.d.ts +13 -0
  134. package/dist/plan-pools.d.ts.map +1 -0
  135. package/dist/plan-pools.js +31 -0
  136. package/dist/plan-pools.js.map +1 -0
  137. package/dist/provider-specs.d.ts +228 -0
  138. package/dist/provider-specs.d.ts.map +1 -0
  139. package/dist/provider-specs.js +124 -0
  140. package/dist/provider-specs.js.map +1 -0
  141. package/dist/quick-model.d.ts.map +1 -1
  142. package/dist/quick-model.js +2 -1
  143. package/dist/quick-model.js.map +1 -1
  144. package/dist/runner-protocol.js +2 -2
  145. package/dist/runner-protocol.js.map +1 -1
  146. package/dist/runtime-state.d.ts +2 -2
  147. package/dist/runtime-state.d.ts.map +1 -1
  148. package/dist/runtime-state.js +1 -1
  149. package/dist/runtime-state.js.map +1 -1
  150. package/dist/safety-policy.d.ts +49 -0
  151. package/dist/safety-policy.d.ts.map +1 -0
  152. package/dist/safety-policy.js +52 -0
  153. package/dist/safety-policy.js.map +1 -0
  154. package/dist/schemas/agent.d.ts +21 -3
  155. package/dist/schemas/agent.d.ts.map +1 -1
  156. package/dist/schemas/agent.js +18 -8
  157. package/dist/schemas/agent.js.map +1 -1
  158. package/dist/schemas/agents.d.ts +19 -0
  159. package/dist/schemas/agents.d.ts.map +1 -1
  160. package/dist/schemas/agents.js +15 -0
  161. package/dist/schemas/agents.js.map +1 -1
  162. package/dist/schemas/approvals.d.ts +241 -0
  163. package/dist/schemas/approvals.d.ts.map +1 -0
  164. package/dist/schemas/approvals.js +63 -0
  165. package/dist/schemas/approvals.js.map +1 -0
  166. package/dist/schemas/automations.d.ts +5 -0
  167. package/dist/schemas/automations.d.ts.map +1 -1
  168. package/dist/schemas/ci.d.ts +5 -9
  169. package/dist/schemas/ci.d.ts.map +1 -1
  170. package/dist/schemas/ci.js +3 -10
  171. package/dist/schemas/ci.js.map +1 -1
  172. package/dist/schemas/computers.d.ts +40 -5
  173. package/dist/schemas/computers.d.ts.map +1 -1
  174. package/dist/schemas/computers.js +16 -5
  175. package/dist/schemas/computers.js.map +1 -1
  176. package/dist/schemas/environment.d.ts +11 -131
  177. package/dist/schemas/environment.d.ts.map +1 -1
  178. package/dist/schemas/environment.js +5 -42
  179. package/dist/schemas/environment.js.map +1 -1
  180. package/dist/schemas/git-history.d.ts +4 -0
  181. package/dist/schemas/git-history.d.ts.map +1 -1
  182. package/dist/schemas/git.d.ts +179 -0
  183. package/dist/schemas/git.d.ts.map +1 -1
  184. package/dist/schemas/git.js +39 -1
  185. package/dist/schemas/git.js.map +1 -1
  186. package/dist/schemas/panels.d.ts +21 -0
  187. package/dist/schemas/panels.d.ts.map +1 -1
  188. package/dist/schemas/panels.js +5 -0
  189. package/dist/schemas/panels.js.map +1 -1
  190. package/dist/schemas/plan-limits.d.ts +50 -3
  191. package/dist/schemas/plan-limits.d.ts.map +1 -1
  192. package/dist/schemas/plan-limits.js +11 -6
  193. package/dist/schemas/plan-limits.js.map +1 -1
  194. package/dist/schemas/provider-oauth.d.ts +6 -0
  195. package/dist/schemas/provider-oauth.d.ts.map +1 -1
  196. package/dist/schemas/provider-subscriptions.d.ts +2 -1
  197. package/dist/schemas/provider-subscriptions.d.ts.map +1 -1
  198. package/dist/schemas/provider-subscriptions.js +2 -1
  199. package/dist/schemas/provider-subscriptions.js.map +1 -1
  200. package/dist/schemas/settings.d.ts +29 -16
  201. package/dist/schemas/settings.d.ts.map +1 -1
  202. package/dist/schemas/settings.js +13 -20
  203. package/dist/schemas/settings.js.map +1 -1
  204. package/dist/schemas/system.d.ts +3 -0
  205. package/dist/schemas/system.d.ts.map +1 -1
  206. package/dist/schemas/system.js +5 -1
  207. package/dist/schemas/system.js.map +1 -1
  208. package/dist/schemas/workspace-repos.d.ts +14 -0
  209. package/dist/schemas/workspace-repos.d.ts.map +1 -1
  210. package/dist/schemas/workspace-repos.js +5 -0
  211. package/dist/schemas/workspace-repos.js.map +1 -1
  212. package/dist/schemas/workspace-tree.d.ts +1 -0
  213. package/dist/schemas/workspace-tree.d.ts.map +1 -1
  214. package/dist/schemas/workspace-tree.js +3 -0
  215. package/dist/schemas/workspace-tree.js.map +1 -1
  216. package/dist/transcript-fold.d.ts +31 -0
  217. package/dist/transcript-fold.d.ts.map +1 -0
  218. package/dist/transcript-fold.js +457 -0
  219. package/dist/transcript-fold.js.map +1 -0
  220. package/dist/workspace-state.d.ts +20 -3
  221. package/dist/workspace-state.d.ts.map +1 -1
  222. package/dist/workspace-state.js +22 -2
  223. package/dist/workspace-state.js.map +1 -1
  224. package/package.json +27 -5
  225. package/src/agent-catalog.test.ts +1 -1
  226. package/src/agent-catalog.ts +76 -555
  227. package/src/agent-run-model.test.ts +31 -15
  228. package/src/agent-run-model.ts +26 -12
  229. package/src/agent-runtimes.ts +419 -0
  230. package/src/approvals-execution.ts +96 -0
  231. package/src/arrival.ts +160 -0
  232. package/src/capability-ledger.test.ts +6 -4
  233. package/src/card-status.ts +93 -0
  234. package/src/chores/chores.ts +6 -3
  235. package/src/command-classes.test.ts +25 -0
  236. package/src/command-classes.ts +47 -5
  237. package/src/command-run.ts +78 -0
  238. package/src/contract-lock.test.ts +1 -1
  239. package/src/contract-lock.ts +2 -2
  240. package/src/contracts/agents.contract.ts +35 -2
  241. package/src/contracts/approvals.contract.ts +37 -0
  242. package/src/contracts/ci.contract.ts +0 -13
  243. package/src/contracts/git.contract.ts +28 -3
  244. package/src/contracts/keys.contract.ts +79 -0
  245. package/src/contracts/prepush.contract.ts +3 -3
  246. package/src/contracts/safety.contract.ts +48 -0
  247. package/src/contracts/usage.contract.ts +22 -0
  248. package/src/conversation-ids.test.ts +31 -1
  249. package/src/conversation-ids.ts +37 -3
  250. package/src/credential-material.test.ts +69 -1
  251. package/src/credential-material.ts +86 -5
  252. package/src/definition.ts +25 -53
  253. package/src/events.ts +374 -173
  254. package/src/history-state.ts +6 -0
  255. package/src/index.ts +24 -5
  256. package/src/ingress-protocol.ts +52 -1
  257. package/src/mentions.test.ts +22 -0
  258. package/src/mentions.ts +25 -0
  259. package/src/needs-action.ts +14 -0
  260. package/src/overlay-lint.test.ts +89 -0
  261. package/src/overlay-lint.ts +116 -0
  262. package/src/owner-ticket.test.ts +54 -0
  263. package/src/owner-ticket.ts +102 -0
  264. package/src/plan-pools.test.ts +71 -0
  265. package/src/plan-pools.ts +92 -0
  266. package/src/provider-specs.test.ts +130 -0
  267. package/src/provider-specs.ts +352 -0
  268. package/src/quick-model.ts +9 -5
  269. package/src/runner-protocol.ts +2 -2
  270. package/src/runtime-state.ts +6 -6
  271. package/src/safety-policy.ts +165 -0
  272. package/src/schemas/agent.ts +62 -17
  273. package/src/schemas/agents.ts +71 -0
  274. package/src/schemas/approvals.ts +174 -0
  275. package/src/schemas/ci.ts +19 -26
  276. package/src/schemas/computers.ts +91 -15
  277. package/src/schemas/environment.ts +21 -100
  278. package/src/schemas/git.ts +121 -6
  279. package/src/schemas/issues.ts +1 -1
  280. package/src/schemas/panels.ts +20 -0
  281. package/src/schemas/plan-limits.ts +58 -11
  282. package/src/schemas/provider-subscriptions.ts +7 -2
  283. package/src/schemas/settings.ts +67 -93
  284. package/src/schemas/system.ts +12 -1
  285. package/src/schemas/workspace-repos.ts +9 -0
  286. package/src/schemas/workspace-tree.ts +11 -0
  287. package/src/transcript-fold.test.ts +493 -0
  288. package/src/transcript-fold.ts +655 -0
  289. package/src/workspace-state.test.ts +77 -29
  290. package/src/workspace-state.ts +99 -31
  291. package/dist/contracts/drafts.contract.d.ts +0 -57
  292. package/dist/contracts/drafts.contract.d.ts.map +0 -1
  293. package/dist/contracts/drafts.contract.js +0 -32
  294. package/dist/contracts/drafts.contract.js.map +0 -1
  295. package/dist/definition-action.d.ts +0 -7
  296. package/dist/definition-action.d.ts.map +0 -1
  297. package/dist/definition-action.js +0 -3
  298. package/dist/definition-action.js.map +0 -1
  299. package/dist/publish-drafts.d.ts +0 -7
  300. package/dist/publish-drafts.d.ts.map +0 -1
  301. package/dist/publish-drafts.js +0 -20
  302. package/dist/publish-drafts.js.map +0 -1
  303. package/dist/schemas/drafts.d.ts +0 -84
  304. package/dist/schemas/drafts.d.ts.map +0 -1
  305. package/dist/schemas/drafts.js +0 -44
  306. package/dist/schemas/drafts.js.map +0 -1
  307. package/src/contracts/drafts.contract.ts +0 -35
  308. package/src/definition-action.ts +0 -8
  309. package/src/publish-drafts.ts +0 -59
  310. package/src/schemas/drafts.ts +0 -108
@@ -133,7 +133,19 @@ export type MachineFlowLine = z.infer<typeof MachineFlowLineSchema>;
133
133
  *
134
134
  * The machine still enforces its own switches. "Run commands" being off comes back as its own refusal, in its
135
135
  * own words, naming the control to flip — exactly as it does for the sandbox ops. */
136
- export const MachineCommandSchema = z.enum(["mirror-off", "mirror-on"]);
136
+ /* THE SET, and why the file-sync half of it is here beside the mirroring half.
137
+ *
138
+ * Both are the same gesture to the person clicking: something this computer is doing for this sandbox, turned
139
+ * off or on from the row that describes it. They were split for a while by nothing but which one had been built
140
+ * — mirroring had a button and pausing a file sync had a paragraph telling you to go and find a terminal — and
141
+ * that is exactly the gap this door exists to close.
142
+ *
143
+ * `sync-unpair` is the one that DESTROYS something, and it is deliberately the machine's `sync uninstall
144
+ * --sandbox`, not this side's idea of unpairing: the agent terminates both Mutagen sessions, drops the local
145
+ * pairing and self-revokes its enrollment on the way out, so the machine cleans up after itself rather than
146
+ * leaving a sandbox to guess what it managed to do. Revoking from the SANDBOX side (an unreachable machine, a
147
+ * laptop that is never coming back) is a different act and a different route, see the enrollment revoke. */
148
+ export const MachineCommandSchema = z.enum(["mirror-off", "mirror-on", "sync-pause", "sync-resume", "sync-unpair"]);
137
149
  export type MachineCommand = z.infer<typeof MachineCommandSchema>;
138
150
  /* Which paired sandbox the command acts on: the machine's own id for it, as it appears in that machine's report,
139
151
  * so nothing here has to re-derive the sanitizing the agent applied. Absent means every sandbox that machine
@@ -195,7 +207,7 @@ export const MachinePairingSchema = z.object({
195
207
  /* The SECOND session's word, the one-way mirror carrying the sandbox's state dir down (sync's backupSpec).
196
208
  * Reported separately rather than folded into the status above, because the two fail independently and mean
197
209
  * different things: the first going quiet stops the owner's edits moving, the second going quiet stops their
198
- * personas, skills, automations, drafts and transcripts from surviving the sandbox. A backup that is not
210
+ * personas, skills, automations, approvals and transcripts from surviving the sandbox. A backup that is not
199
211
  * running is only dangerous while nobody knows, so it gets its own word on the line. */
200
212
  backupStatus: z.string().optional(),
201
213
  });
@@ -232,6 +244,12 @@ export type MachinePort = z.infer<typeof MachinePortSchema>;
232
244
  export const MachineWatcherSchema = z.object({
233
245
  running: z.boolean(),
234
246
  pid: z.number().int().optional(),
247
+ /* WHICH BUILD IS ACTUALLY SERVING, stamped into the pidfile by the loop that claimed it, which is the only
248
+ * place the fact exists: replacing the binary does not touch the running process, so a machine can hold a
249
+ * current agent and go on serving a months-old one indefinitely. `agents.sync` is the file, this is the
250
+ * process, and the two differing is a restart somebody is owed (see watcherBuildSkew). Absent when no loop
251
+ * is running, and when the one running predates the stamp. */
252
+ build: z.string().optional(),
235
253
  /* When the watcher last FINISHED a pass, the field that makes `running` mean something. The agent holds its
236
254
  * SSH transport listeners on its own event loop, so a failure that escapes the loop leaves a process that is
237
255
  * alive and a loop that is gone: pid present, unit "active", mirroring and the git bridge stopped. Absent
@@ -256,10 +274,18 @@ export const MachineReportSchema = z.object({
256
274
  * the same box, so it is what dedupes them into a single row. */
257
275
  hostname: z.string(),
258
276
  os: z.string(),
259
- // Which of this machine's agents are installed, and at what version, a machine running an old build is
260
- // visible rather than mysteriously lacking a field. Same argument as HostSummary.version. `host` is filled
261
- // by the daemon at merge time (it already knows it from the socket), not by the sync agent, which would have
262
- // to go reading another agent's config to guess at it.
277
+ /* Which agents this machine has, and at what version, so one on an old build is visible rather than
278
+ * mysteriously lacking a field. Same argument as HostSummary.version.
279
+ *
280
+ * `sync` is the agent INSTALLED here the file on disk — and `watcher.build` beside it is the loop running
281
+ * from that file. It used to be neither: whichever process happened to build the report stamped its own
282
+ * version here, so the same machine answered its running build to a sandbox its loop posted to and its
283
+ * installed build to one that ran `status --json` over a host capability. One field, two meanings, and the
284
+ * gap between them — a machine updated but never restarted — invisible in both.
285
+ *
286
+ * `host` is what the live socket announced (so: what is running), filled by the daemon at merge time from
287
+ * the hello frame it already holds, never by the sync agent, which would have to go reading another agent's
288
+ * config to guess at it. */
263
289
  agents: z.object({ sync: z.string().optional(), host: z.string().optional() }),
264
290
  // Filled by the READER, never the agent (see above). Empty is the resting state: no Docker on the machine,
265
291
  // or nothing has looked. Neither is an error, and neither means "no sandboxes exist".
@@ -272,6 +298,27 @@ export const MachineReportSchema = z.object({
272
298
  capturedAt: z.number(),
273
299
  });
274
300
  export type MachineReport = z.infer<typeof MachineReportSchema>;
301
+
302
+ /* THE AGENT THIS MACHINE INSTALLED AND THE ONE IT IS RUNNING, when they are not the same build — the whole of
303
+ * "you updated the agent and nothing changed", as a value.
304
+ *
305
+ * It is one comparison, and it lives HERE for the same reason watcherStalled does: the terminal (`intentic-machine
306
+ * status`) and the browser (the Computers row) both answer this question, and a machine that is behind in one and
307
+ * fine in the other is worse than either answer alone. The remedy is the same in both: restart the loop.
308
+ *
309
+ * Silent whenever either half is unknown, which covers a loop that is stopped (nothing is serving, and the row
310
+ * already says so in louder words), an agent too old to stamp its build, and a machine with no installed agent to
311
+ * compare against — none of which is a skew, and each of which would otherwise nag about a difference nobody can
312
+ * act on. */
313
+ export const watcherBuildSkew = (report: MachineReport): { readonly running: string; readonly installed: string } | undefined => {
314
+ const running = report.watcher.build;
315
+ const installed = report.agents.sync;
316
+ if (!report.watcher.running || running === undefined || installed === undefined || running === installed) {
317
+ return undefined;
318
+ }
319
+ return { running, installed };
320
+ };
321
+
275
322
  // Why a computer that is plainly THERE has no report to show. Each is a different errand for the reader, which is
276
323
  // the whole reason they are not collapsed into one "unavailable".
277
324
  export const ComputerGapSchema = z.enum([
@@ -294,13 +341,39 @@ export type ComputerGap = z.infer<typeof ComputerGapSchema>;
294
341
  * enrolled ssh key's comment vs. the capability id the user typed, so the two are reconciled on the `hostname`
295
342
  * their reports agree on, and left as separate rows when there is nothing to reconcile them by. Guessing that two
296
343
  * differently-named machines are the same one would merge two people's laptops on a shared sandbox. */
344
+ /* THE DESKTOP-SYNC ENROLLMENT BEHIND A ROW, which used to be a boolean and could not be.
345
+ *
346
+ * `syncEnrolled: true` answered "is this machine paired" and nothing a reader standing in front of the row
347
+ * actually asks next: WHICH half of desktop sync it holds (files and ports, or ports alone), whether it has
348
+ * ever used the enrollment, and how to name it when they want it gone. Those three lived on /system/sync
349
+ * instead, as one machine's worth of `syncingFrom` plus a list of `mirroredBy` names, which is the sandbox-level
350
+ * shape this view exists to stop being: one card claiming a sandbox has A desktop sync, over a list of the
351
+ * several computers that actually do.
352
+ *
353
+ * `machine` is the enrollment's own name for the box (the ssh key's comment). It is what the reports are filed
354
+ * under, and it is the id the revoke route takes — the same string, so a row can revoke exactly the enrollment
355
+ * it is drawn from. Two machines that present the same comment share one enrollment identity throughout the
356
+ * daemon (reports included); that is a pre-existing property of naming machines by their key comment, and this
357
+ * field inherits it rather than inventing a second identity that would disagree with the first. */
358
+ export const ComputerSyncSchema = z.object({
359
+ machine: z.string(),
360
+ /* Which half. "sync" is files AND ports and is SINGLE-HOLDER for the sandbox; "mirror" is ports only and any
361
+ * number of machines may hold one. The row says which, because "your laptop is paired" is read as the first
362
+ * by somebody who has the second, and then their files are not where they expect them. */
363
+ mode: z.enum(["sync", "mirror"]),
364
+ // When this machine last USED its enrollment (its watcher's own polls stamp it). Absent on one that never
365
+ // has, which is exactly what a setup that did not finish leaves behind, and must not read as healthy.
366
+ seenAt: z.number().optional(),
367
+ });
368
+ export type ComputerSync = z.infer<typeof ComputerSyncSchema>;
297
369
  export const ComputerSchema = z.object({
298
370
  // Stable row key: the reported hostname when either door produced one, else the name that door knows it by.
299
371
  key: z.string(),
300
372
  // What to call it on screen, the user's own name for the machine wherever one exists.
301
373
  label: z.string(),
302
- // Whether a desktop-sync enrollment exists for this machine (it syncs files and/or mirrors ports).
303
- syncEnrolled: z.boolean(),
374
+ // The desktop-sync enrollment this machine holds with this sandbox, absent when it has none (a computer
375
+ // reached only through its `host` capability).
376
+ sync: ComputerSyncSchema.optional(),
304
377
  // The host capability's id, when this machine is also a connected computer. Absent otherwise.
305
378
  hostId: z.string().optional(),
306
379
  // Host-capability liveness. Absent when there is no host capability, which is NOT the same as offline.
@@ -327,9 +400,16 @@ export const ComputerSchema = z.object({
327
400
  });
328
401
  export type Computer = z.infer<typeof ComputerSchema>;
329
402
  export const ComputersListSchema = z.object({ computers: z.array(ComputerSchema) });
330
- // GET /system/sync, the enrollment state the Desktop sync card is built on, plus what each enrolled machine has
331
- // said about itself. `machines` is optional because a daemon predating machine reports omits it, and an SPA is
332
- // routinely newer than the daemon it is pointed at during a rolling update.
403
+ /* GET /system/sync: what desktop sync is doing for this sandbox, WITHOUT naming any one machine as the answer.
404
+ *
405
+ * It used to carry `syncingFrom` + `syncSeenAt` + `mirroredBy`, which is the enrollment list flattened into one
406
+ * holder and a list of everybody else — the shape a card that believed a sandbox has A desktop sync needed, and
407
+ * the reason that card kept restating facts the Computers list beside it already had per machine. Every one of
408
+ * those now rides on the machine's own row (ComputerSync), where a reader can act on it.
409
+ *
410
+ * What is left is what is genuinely about the SANDBOX rather than about any computer: whether sync is possible
411
+ * here at all, whether anything at all is enrolled, and the raw reports, which is the cheap ambient read the
412
+ * rail's badge lives on (it must never fan out to somebody's laptop just to decide whether to draw a chip). */
333
413
  export const SyncStatusSchema = z.object({
334
414
  enrolled: z.boolean(),
335
415
  /* Whether this sandbox can do desktop sync at all. It used to be the SSH hostname the laptop would dial, and
@@ -338,10 +418,6 @@ export const SyncStatusSchema = z.object({
338
418
  * surface now, so a sandbox that can answer this read can also sync. Kept as a field rather than assumed,
339
419
  * because the card branches on it and a daemon too old to say is one that should not be offered sync. */
340
420
  available: z.boolean().optional(),
341
- // The single machine holding file sync, and when its heartbeat last landed.
342
- syncingFrom: z.string().optional(),
343
- syncSeenAt: z.number().optional(),
344
- mirroredBy: z.array(z.string()).optional(),
345
421
  machines: z.array(MachineReportSchema).optional(),
346
422
  });
347
423
  export type SyncStatus = z.infer<typeof SyncStatusSchema>;
@@ -70,8 +70,22 @@ export const EnvironmentRecurringSchema = z.object({
70
70
  live: z.boolean(),
71
71
  drafted: z.boolean().optional(),
72
72
  declined: z.boolean().optional(),
73
+ /* The Dockerfile step that would bake this tool, when its ecosystem has one that follows from the name
74
+ * alone (auto-drafts.ts `stepFor`). Sent rather than re-derived in the browser because it is the SAME string
75
+ * the owner would approve, and because its presence is the honest answer to "can this be fixed by pressing a
76
+ * button?": absent means the fix is a judgement call (which pip package, replaying which installer) and the
77
+ * card offers an agent instead. */
78
+ step: z.string().optional(),
73
79
  });
74
80
  export type EnvironmentRecurring = z.infer<typeof EnvironmentRecurringSchema>;
81
+ /* The owner's answer to ONE line of that list. `adopt` writes the tool's overlay draft now, without waiting for
82
+ * the sweep to re-earn it; `dismiss` tombstones it so nothing proposes it again; `restore` undoes a dismissal.
83
+ * One route rather than three because it is one decision surface about one ledger entry. */
84
+ export const EnvironmentRuntimeDecisionSchema = z.object({
85
+ tool: z.string().min(1),
86
+ decision: z.enum(["adopt", "dismiss", "restore"]),
87
+ });
88
+ export type EnvironmentRuntimeDecision = z.infer<typeof EnvironmentRuntimeDecisionSchema>;
75
89
  export const EnvironmentSchema = z.object({
76
90
  proposal: environmentFileSchema.optional(),
77
91
  // The owner-approved agent-written custom section (.intentic/config/environment.custom.Dockerfile).
@@ -142,116 +156,23 @@ export const EnvironmentItemSchema = z.object({
142
156
  export type EnvironmentItem = z.infer<typeof EnvironmentItemSchema>;
143
157
  export const EnvironmentContentsSchema = z.object({ items: z.array(EnvironmentItemSchema) });
144
158
  export type EnvironmentContents = z.infer<typeof EnvironmentContentsSchema>;
145
- /* ---- portability: exporting a sandbox's environment and restoring it into a fresh one ----
159
+ /* ---- portability: exporting a sandbox's environment ----
146
160
  *
147
161
  * A sandbox is four stores, not one: `/work` (the workspace and the daemon's manifests), `/history` (every
148
162
  * repo's real git dir, the fleet registry, the ledgers), the CONTAINER (the built overlay image plus the env
149
163
  * the run contract replays) and the AI-provider credential root. A bundle carries the first two, declared entry
150
164
  * by entry in WORKSPACE_STATE_FILES / HISTORY_STATE_FILES. It cannot carry the other two, and the honest
151
- * consequence is that an import ends in a REPORT rather than a claim of equivalence, the container has no
165
+ * consequence is that taking one in ends in a REPORT rather than a claim of equivalence, the container has no
152
166
  * docker socket, so only the host can rebuild the image the overlay describes.
153
167
  *
154
168
  * The bundle's manifest (BundleManifestSchema) lives in definition.ts beside the sandbox DEFINITION it embeds:
155
169
  * a bundle is definition + state, and keeping the two schemas together is what keeps the two export doors from
156
- * drifting into different answers about what an environment is. */
157
-
158
- // What a restore actually did. `needsAction` is the part that matters: the environment rebuild command, the
159
- // credentials to re-enter, the logins to redo, each one a thing the target cannot do for itself.
160
- export const ImportReportSchema = z.object({
161
- restored: z.object({ workspaceFiles: z.number(), historyFiles: z.number(), repos: z.array(z.string()), bytes: z.number() }),
162
- // Entries the bundle carried that this daemon refused to write (an identity file, an escaping path), empty
163
- // for any bundle a matching exporter produced, and a tamper signal when it is not.
164
- refused: z.array(z.string()),
165
- needsAction: z.array(z.object({ subject: z.string(), detail: z.string() })),
166
- });
167
- export type ImportReport = z.infer<typeof ImportReportSchema>;
168
- /* ---- migrations: importing a FOREIGN assistant's setup (Hermes, OpenClaw) ----
169
- *
170
- * A different crossing than a bundle restore, and deliberately a different surface: a bundle is our own format,
171
- * re-derived entry by entry against the state manifests, while a migration reads a directory some OTHER
172
- * program laid out (`~/.hermes`) and TRANSLATES it into native things, skills, automations, capabilities,
173
- * merged memory. Nothing foreign is executed or copied verbatim into daemon state; every item lands through the
174
- * same write paths the settings/skills/automations/capabilities surfaces use, which is what keeps an imported
175
- * setup editable and deletable in the ordinary UI the day after (docs/assistant-import-design.md).
170
+ * drifting into different answers about what an environment is.
176
171
  *
177
- * The flow is PREVIEW-FIRST, mirroring what these tools' own `migrate` commands taught their users to expect:
178
- * `plan` parses the uploaded archive into an itemized checklist and holds the upload in memory under a token;
179
- * `apply` names the ticked item ids and the token. The plan is RE-DERIVED from the held archive at apply, the
180
- * wire plan is a rendering for the owner, never the input the write trusts (restore.ts's rule, kept). */
181
- export const MigrationSourceSchema = z.enum(["hermes", "openclaw"]);
182
- export type MigrationSource = z.infer<typeof MigrationSourceSchema>;
183
- // What an item becomes here, not what it was there, the apply loop dispatches on this, and the checklist
184
- // groups by it so the owner reads "3 skills, 2 automations" rather than a foreign directory listing.
185
- export const MigrationTargetSchema = z.enum(["memory", "skill", "automation", "capability", "secret", "file"]);
186
- export type MigrationTarget = z.infer<typeof MigrationTargetSchema>;
187
- export const MigrationItemSchema = z.object({
188
- // Deterministic (derived from the source artifact, e.g. `skill:weather`), so the ids the owner ticked name
189
- // the same items when the plan is re-derived at apply.
190
- id: z.string(),
191
- target: MigrationTargetSchema,
192
- // The checklist line, plain words: "Skill, weather", "Nightly digest (9:00 every day)".
193
- label: z.string(),
194
- detail: z.string().optional(),
195
- /* The default tick. False marks the items the owner should read before taking, a server URL that points at
196
- * localhost on the OLD machine, an .env key that looks like tuning rather than a credential. They still
197
- * import fine when ticked; the flag is the adapter's judgment, not a gate. */
198
- recommended: z.boolean(),
199
- // Names of the secrets this item would store (never values, values stay in the held archive until apply,
200
- // and only move when the apply says includeSecrets). Empty for items that carry none.
201
- secrets: z.array(z.string()),
202
- });
203
- export type MigrationItem = z.infer<typeof MigrationItemSchema>;
204
- export const MigrationNeedsActionSchema = z.object({ subject: z.string(), detail: z.string() });
205
- export const MigrationPlanSchema = z.object({
206
- source: MigrationSourceSchema,
207
- // Names the held upload for the apply call. Minted per plan; a new upload replaces the held one.
208
- token: z.string(),
209
- items: z.array(MigrationItemSchema),
210
- // What the adapter saw and will not move, sessions, logs, pairing state, listed rather than silent.
211
- refused: z.array(z.string()),
212
- // What is already known not to move mechanically (channels to reconnect, a model to pick), the same
213
- // honesty ImportReportSchema carries, surfaced at PREVIEW time so the owner ticks with open eyes.
214
- needsAction: z.array(MigrationNeedsActionSchema),
215
- });
216
- export type MigrationPlan = z.infer<typeof MigrationPlanSchema>;
217
- /* One of the owner's own computers, as an import SOURCE, the answer to "where is my setup" that needs no
218
- * packing at all. Read on the card's first render for every enrolled machine, so the offer appears before the
219
- * owner has read a single instruction.
220
- *
221
- * `found` absent means "connected, and nothing to import here", which is a real answer worth rendering
222
- * quietly, not an error: the machine may simply be a different one from the machine the assistant runs on. */
223
- export const MigrationHostSchema = z.object({
224
- id: z.string(),
225
- online: z.boolean(),
226
- found: MigrationSourceSchema.optional(),
227
- // Why this machine cannot be read right now, when it cannot, offline, or its own refusal, in its words.
228
- detail: z.string().optional(),
229
- });
230
- export const MigrationHostsSchema = z.object({ hosts: z.array(MigrationHostSchema) });
231
- export type MigrationHost = z.infer<typeof MigrationHostSchema>;
232
- // Read the setup off a connected computer instead of an upload. Answers with a plan, exactly as the upload
233
- // route does, everything after this point is identical whichever door the setup came through.
234
- export const MigrationScanSchema = z.object({ host: z.string().min(1) });
235
- export const MigrationApplySchema = z.object({
236
- token: z.string(),
237
- // The ticked item ids. Ids the re-derived plan does not contain are ignored rather than erroring, the
238
- // archive is the truth, and a stale checklist must not block the items that still exist.
239
- items: z.array(z.string()),
240
- // The owner's explicit consent to move credential VALUES (mirrors the bundle export's `?secrets=`, and the
241
- // `--include-secrets` these tools' own migrate commands require). Off: secret items are skipped and
242
- // capability configs land without their keys.
243
- includeSecrets: z.boolean(),
244
- });
245
- export type MigrationApply = z.infer<typeof MigrationApplySchema>;
246
- export const MigrationReportSchema = z.object({
247
- applied: z.array(z.object({ id: z.string(), target: MigrationTargetSchema, label: z.string() })),
248
- // Items that were ticked and did not land, each with the reason, a full disk, an env store that needs
249
- // DevOps active. Distinct from `refused`, which is the class of things never attempted.
250
- failed: z.array(z.object({ id: z.string(), label: z.string(), error: z.string() })),
251
- refused: z.array(z.string()),
252
- needsAction: z.array(MigrationNeedsActionSchema),
253
- });
254
- export type MigrationReport = z.infer<typeof MigrationReportSchema>;
172
+ * ONLY THE OUTBOUND HALF IS HERE. Taking a bundle IN is not a surface of its own any more: it is one of the
173
+ * four sources the arrival pipeline reads (arrival.ts), beside a definition and the two foreign assistants,
174
+ * because all four answer the same question and used to answer it three different ways. */
175
+
255
176
  /* One export sitting in the daemon's export directory, the ARTIFACT a bundle is, rather than the request that
256
177
  * produced it. Packing takes minutes over a real workspace, so tying it to a response made it a property of one
257
178
  * browser tab: a refresh abandoned the work and left nothing to come back to. It is a file now, and every field
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { AgentProviderSchema } from "./agent.js";
3
3
  import { LandConflictSchema, LandedMessageSchema } from "./agents.js";
4
+ import { CommandRunSchema } from "./ci.js";
4
5
  import { RefNameSchema } from "./internal.js";
5
6
  import { RepoParamSchema } from "./shared.js";
6
7
  // What a commit records, three shapes, each a real git spelling. The last two are for the case where nothing
@@ -48,6 +49,34 @@ export const PushSchema = RepoParamSchema.extend({
48
49
  .optional()
49
50
  .describe("Which branch to push. Leave it out for the checked-out one. A branch with no upstream yet gets one set on this push."),
50
51
  });
52
+ /* WHO SAID NO to a push, read off git's final words (git/git.ts pushRefusal), because the three answers ask
53
+ * three different things of the owner and only one of them is an agent's to fix:
54
+ * hook , this repository's own pre-push hook refused it: the code is known to be wrong, the hook's
55
+ * output says how, and a fix is worth proposing.
56
+ * remote , the server rejected the refs: a non-fast-forward, a protected branch. Pull first, or push
57
+ * somewhere else; nothing about the code has been judged.
58
+ * transport , it never got there: credentials, a host that does not answer, a remote that is not a
59
+ * repository. The same again, and a retry is the only sensible button. */
60
+ export const PushRefusalSchema = z.enum(["hook", "remote", "transport"]);
61
+ export type PushRefusal = z.infer<typeof PushRefusalSchema>;
62
+ /* THE PUSH AS A RUN: the pre-push check's shape (CommandRunSchema) with the three things a push adds. A push is
63
+ * a command that runs this repository's pre-push hook, which for a workspace with a real gate is the whole
64
+ * suite, minutes of output; it is a terminal to watch and a verdict to poll for exactly as the check is, and
65
+ * NOT a request held open for its duration (the browser's header deadline is seconds, and a push that ran a
66
+ * suite inside its own request reported "failed" over a push that had gone). One shape for both halves of
67
+ * the push flow is what lets the browser render a refused push with the card, the terminal link and the
68
+ * proposed fix it already has for a red check. */
69
+ export const PushRunSchema = CommandRunSchema.extend({
70
+ repo: z.string().describe("The repository this run is about, the same id the routes take."),
71
+ reason: z
72
+ .string()
73
+ .optional()
74
+ .describe("Why not, in git's own words: the last verdict line, for a row that has room for one line. The whole tail is `output`."),
75
+ refusedBy: PushRefusalSchema.optional().describe(
76
+ "Who refused a failed push: this repository's pre-push hook (the code is wrong, a fix is worth proposing), the remote (pull first), or the transport (credentials, network: retry). Absent while it runs and for a push that went.",
77
+ ),
78
+ });
79
+ export type PushRun = z.infer<typeof PushRunSchema>;
51
80
  export const GitFileQuerySchema = RepoParamSchema.extend({ path: z.string().min(1).describe("The file to read, relative to the repository root.") });
52
81
  export const GitFileWriteSchema = RepoParamSchema.extend({
53
82
  path: z.string().min(1).describe("Where to write, relative to the repository root. Missing folders are created."),
@@ -107,6 +136,22 @@ export const GitChangeSchema = z.object({
107
136
  .optional()
108
137
  .describe("Lines added. Absent for a binary file, and for an untracked one, which has nothing to compare against."),
109
138
  deletions: z.number().optional().describe("Lines removed. Absent for the same reasons additions is."),
139
+ /* THE SAME CHANGE WITH THE COMMENTS TAKEN OUT, computed here rather than by whoever renders the row, and
140
+ * that is the whole point of it being on the wire. A review's diffs open on code alone, so the numbers beside
141
+ * them are the code's; working those out needs both whole sides of the file and a TextMate walk over each,
142
+ * which the app used to do per file, as the files were read — so a row arrived showing git's count and
143
+ * changed to this one the moment anything read it, which moved the row under the reader when the list was
144
+ * sorted by size. Shipped with the list, the number a reader sees first is the number it stays.
145
+ *
146
+ * Absent, not zero, when there is nothing to say: a binary file, one side too large to read, a path whose
147
+ * grammar this build does not ship, or a list too long to count whole (see git/code-counts.ts). The caller
148
+ * then shows git's own counts, which for such a file are the only honest reading anyway. */
149
+ code: z
150
+ .object({ additions: z.number(), deletions: z.number() })
151
+ .optional()
152
+ .describe(
153
+ "The same +/− with every comment stripped from both sides, which is what a review shows beside a diff that opens on code alone. Absent when the file cannot be read that way (binary, too large, or a language this build ships no grammar for): git's own counts above are then the reading.",
154
+ ),
110
155
  });
111
156
  export type GitChange = z.infer<typeof GitChangeSchema>;
112
157
  // Where a repo's checked-out branch stands against its remote. Every field is optional-or-zero because every
@@ -375,16 +420,25 @@ export const RepoModulesSchema = z.object({
375
420
  export type RepoModules = z.infer<typeof RepoModulesSchema>;
376
421
  export const WorkspaceModulesSchema = z.object({ repos: z.array(RepoModulesSchema).describe("Every repository with the packages inside it.") });
377
422
  export type WorkspaceModules = z.infer<typeof WorkspaceModulesSchema>;
378
- // One file an agent touched, plus whether that change is ALREADY in the main tree. The review lists the
379
- // agent's CUMULATIVE output (base worktree), not just the not-yet-landed remainder, because landing is not
380
- // the end of the review: a clean turn auto-lands within milliseconds, and a list scoped to the remainder shows
381
- // the user an empty panel for work they never got to look at. `landed` is what still separates the two, the
382
- // remainder is what "Land now" would apply, and the panel filters on exactly this flag.
423
+ /* One file an agent touched AND STILL DIFFERS FROM MAIN ON, plus whether the main working tree is already
424
+ * holding it. The list is what the conversation wrote measured against the main line as it stands right now,
425
+ * which takes three states to say and each one decides a different next move for the reader:
426
+ *
427
+ * · main's history has this content: the user accepted it, it is their commit, and it is no longer a
428
+ * difference against main. There is no row (see `absorbed` on the response).
429
+ * · the main working tree has it, uncommitted: the steady state seconds after a land, waiting in the Changes
430
+ * panel. A row, `landed: true`.
431
+ * · neither: never landed, or landed and then discarded. A row, `landed: false`, and this is exactly what
432
+ * "Land now" would apply.
433
+ *
434
+ * Landed work keeps its row on purpose: a clean turn auto-lands within milliseconds, so a list scoped to the
435
+ * outstanding remainder would show an empty panel for work nobody had looked at yet. What retires a row is the
436
+ * user committing it, which is the one act that says they are done reviewing it. */
383
437
  export const AgentChangeSchema = GitChangeSchema.extend({
384
438
  landed: z
385
439
  .boolean()
386
440
  .describe(
387
- "Whether this change is already in the shared tree. The list is everything the conversation wrote, not just what is left over, because a clean turn merges in milliseconds and a list of leftovers would show an empty panel for work nobody had looked at yet.",
441
+ "Whether your workspace already holds this content. Read from the tree at request time, not from what a land recorded: discard a landed file in the Changes panel and this goes back to false, which is what puts it back under Land now.",
388
442
  ),
389
443
  });
390
444
  export type AgentChange = z.infer<typeof AgentChangeSchema>;
@@ -420,6 +474,16 @@ export type AgentRepoChanges = z.infer<typeof AgentRepoChangesSchema>;
420
474
  * than the last attempt. */
421
475
  export const AgentChangesSchema = z.object({
422
476
  repos: z.array(AgentRepoChangesSchema).describe("One entry per repository the conversation touched."),
477
+ /* HOW MUCH OF THE WORK IS NO LONGER A DIFFERENCE, so an empty list can say WHICH kind of empty it is. An
478
+ * agent that has written nothing and an agent whose every file the user committed both answer with no rows,
479
+ * and they are opposite facts: one is "ask it for something", the other is "it is all in your history". A
480
+ * count rather than the rows themselves, because that is the whole of what the surface needs to pick a
481
+ * sentence, and carrying the rows would put the list back where it started. */
482
+ absorbed: z
483
+ .number()
484
+ .describe(
485
+ "How many of this conversation's files your own history already carries, and which are therefore not listed as differences any more.",
486
+ ),
423
487
  conflicts: z
424
488
  .array(LandConflictSchema)
425
489
  .optional()
@@ -428,3 +492,54 @@ export const AgentChangesSchema = z.object({
428
492
  ),
429
493
  });
430
494
  export type AgentChanges = z.infer<typeof AgentChangesSchema>;
495
+
496
+ /* WHERE THE WORK WENT once it stopped being a difference, the other half of the sentence `absorbed` above can
497
+ * only start. The review is what still differs from main, so a reader who committed an agent's every file
498
+ * arrives at an empty panel; the count lets it say WHICH kind of empty, and this says where to go and shows
499
+ * the work, which is what the reader actually came for.
500
+ *
501
+ * Deliberately its OWN read rather than more fields on the review. The review is on the panel's hot path and
502
+ * this is a `git log` per repo that only matters once history has taken something, so it is asked for exactly
503
+ * when there is an answer to have. That also keeps the review's shape honest: its rows are differences against
504
+ * main, and these are not.
505
+ *
506
+ * A COMMIT CARRIES the work, it did not necessarily author it: a path is attributed to the newest commit that
507
+ * left the agent's content there, which is where a reader should be sent to read it now. See the daemon's
508
+ * agents/landed-history.ts for the span this is measured over and for why an unattributable path is counted
509
+ * rather than assigned to a plausible commit. */
510
+ export const AgentHistoryCommitSchema = z.object({
511
+ sha: z.string().describe("The commit."),
512
+ short: z.string().describe("Its abbreviated hash, which is what a reader recognises it by."),
513
+ subject: z.string().describe("Its first line."),
514
+ author: z.string().describe("Who committed it."),
515
+ at: z.number().describe("When it was authored, in milliseconds."),
516
+ changes: z
517
+ .array(GitChangeSchema)
518
+ .describe(
519
+ "The conversation's files that this commit is the newest carrier of, as the conversation changed them. Every file appears under exactly one commit, so these counts add up to the work rather than over-counting a file that history touched twice.",
520
+ ),
521
+ });
522
+ export type AgentHistoryCommit = z.infer<typeof AgentHistoryCommitSchema>;
523
+ export const AgentRepoHistorySchema = z.object({
524
+ repo: z.string().describe("Which repository."),
525
+ commits: z.array(AgentHistoryCommitSchema).describe("The commits carrying this conversation's work there, newest first."),
526
+ /* Carried for the same reason the review's rows carry it, and read from the same tree at the same instant:
527
+ * a package the conversation created lives only in its own copy, so the shared tree cannot name it, and
528
+ * without this every file of a brand-new package groups under no package at all. */
529
+ modules: z.array(WorkspaceModuleSchema).describe("The packages of the tree these files came from, so a review can group them by package."),
530
+ });
531
+ export type AgentRepoHistory = z.infer<typeof AgentRepoHistorySchema>;
532
+ export const AgentHistorySchema = z.object({
533
+ repos: z.array(AgentRepoHistorySchema).describe("One entry per repository holding committed work of this conversation."),
534
+ /* FILES HISTORY HOLDS THAT NO COMMIT HERE ACCOUNTS FOR, reported rather than hidden. Content reaches the
535
+ * main line by roads that do not pass through a commit since the land: a cherry-pick from elsewhere,
536
+ * another conversation landing the same lines, the user typing them by hand before this one landed. Those
537
+ * files are absorbed and unattributable at once, and a surface that quietly dropped them would be claiming
538
+ * the commits it names are the whole story. */
539
+ unaccounted: z
540
+ .number()
541
+ .describe(
542
+ "How many of the conversation's absorbed files none of these commits carries. Above zero means its content reached your main line by some other road, so the commits listed are not the whole story.",
543
+ ),
544
+ });
545
+ export type AgentHistory = z.infer<typeof AgentHistorySchema>;
@@ -150,7 +150,7 @@ export type Issue = z.infer<typeof IssueSchema>;
150
150
  export const IssueSummarySchema = IssueSchema.extend({ id: entryId.describe("The issue's id, which is its fingerprint.") });
151
151
  export type IssueSummary = z.infer<typeof IssueSummarySchema>;
152
152
 
153
- // `invalid` is the same trust-boundary confession the drafts list makes, for the opposite reason: nothing but
153
+ // `invalid` is the same trust-boundary confession the approvals list makes, for the opposite reason: nothing but
154
154
  // the daemon writes these, so a file in here that will not parse is a BUG in this daemon or a half-written
155
155
  // volume, and either is worth seeing rather than silently skipping.
156
156
  export const IssuesListSchema = z.object({
@@ -5,12 +5,32 @@ import { z } from "zod";
5
5
  // preview-<panelKey>-<sandboxId>.<zone> to it) PLUS content facts, evidence the web app's extensions run their
6
6
  // detect() over, computed daemon-side in one pass so the browser never scans /work file-by-file.
7
7
 
8
+ /* WHERE A START THE SANDBOX IS RUNNING HAS GOT TO, between the click and the first byte served. The process
9
+ * manager watches the pane's foreground command every couple of seconds (processes/managed-processes.ts), and
10
+ * these four words are what that sampling can honestly say: the shell is still coming up; the install that
11
+ * runs first when node_modules is missing is still going; the dev command is running but nothing listens yet;
12
+ * or the command has already exited back to a prompt, which is the one a person needs told at once, because
13
+ * the "Preparing the preview…" it would otherwise sit behind never ends. Absent once the preview proxy has
14
+ * something to serve, and absent for anything the sandbox is not starting. */
15
+ export const PanelLaunchSchema = z.enum(["launching", "installing", "starting", "exited"]);
16
+ export type PanelLaunch = z.infer<typeof PanelLaunchSchema>;
17
+
8
18
  export const PanelSummarySchema = z.object({
9
19
  // The repo id: its root-relative dir under /work (slashes become `--` in the preview subdomain label).
10
20
  repo: z.string().describe("Which repository."),
11
21
  // Whether the repo ships a runnable dev server (a package.json `dev` script at operator/ or the root).
12
22
  hasPanel: z.boolean().describe("Whether it has anything runnable at all."),
13
23
  running: z.boolean().describe("Whether the sandbox has it running."),
24
+ /* Whether its dependencies are on disk, a node_modules at the directory Start runs in. What decides what a
25
+ * Start COSTS: seconds when true, an install first when false, and the Start screen's copy says which
26
+ * instead of promising "a few minutes" over a tree that is already installed (the starter site's is: the
27
+ * image bakes it). True for a repo with nothing runnable, which has nothing to install for. */
28
+ installed: z
29
+ .boolean()
30
+ .describe("Whether its dependencies are installed, which is what decides whether a start takes seconds or an install first."),
31
+ launch: PanelLaunchSchema.optional().describe(
32
+ "Where a start the sandbox is running has got to: its shell coming up, installing, its dev command running with nothing listening yet, or exited back to a prompt. Absent when nothing is starting and once it serves.",
33
+ ),
14
34
  // Whether anything this repo owns is answering, see `servers`. Not the same question as `running`: a panel
15
35
  // whose install is still going is running and not yet healthy, and a dev server someone started in their own
16
36
  // terminal is healthy without the daemon running it.