@intentic/sandbox-contract 1.304.0 → 1.305.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 (328) hide show
  1. package/README.md +5 -3
  2. package/dist/contracts/agent.contract.d.ts +195 -41
  3. package/dist/contracts/agent.contract.d.ts.map +1 -1
  4. package/dist/contracts/agent.contract.js +8 -8
  5. package/dist/contracts/agent.contract.js.map +1 -1
  6. package/dist/contracts/agents.contract.d.ts +203 -3
  7. package/dist/contracts/agents.contract.d.ts.map +1 -1
  8. package/dist/contracts/agents.contract.js +30 -2
  9. package/dist/contracts/agents.contract.js.map +1 -1
  10. package/dist/contracts/capabilities.contract.d.ts +18 -5
  11. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  12. package/dist/contracts/capabilities.contract.js +3 -3
  13. package/dist/contracts/capabilities.contract.js.map +1 -1
  14. package/dist/contracts/{host.contract.d.ts → device.contract.d.ts} +75 -2
  15. package/dist/contracts/device.contract.d.ts.map +1 -0
  16. package/dist/contracts/device.contract.js +17 -0
  17. package/dist/contracts/device.contract.js.map +1 -0
  18. package/dist/contracts/extensions.contract.d.ts +2 -2
  19. package/dist/contracts/git.contract.d.ts +24 -0
  20. package/dist/contracts/git.contract.d.ts.map +1 -1
  21. package/dist/contracts/panels.contract.d.ts +1 -1
  22. package/dist/contracts/personas.contract.d.ts +0 -9
  23. package/dist/contracts/personas.contract.d.ts.map +1 -1
  24. package/dist/contracts/personas.contract.js +1 -10
  25. package/dist/contracts/personas.contract.js.map +1 -1
  26. package/dist/contracts/runner.contract.d.ts +138 -98
  27. package/dist/contracts/runner.contract.d.ts.map +1 -1
  28. package/dist/contracts/runner.contract.js +1 -0
  29. package/dist/contracts/runner.contract.js.map +1 -1
  30. package/dist/contracts/secrets.contract.js +2 -2
  31. package/dist/contracts/secrets.contract.js.map +1 -1
  32. package/dist/contracts/sessions.contract.d.ts +23 -3
  33. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  34. package/dist/contracts/settings.contract.d.ts +0 -23
  35. package/dist/contracts/settings.contract.d.ts.map +1 -1
  36. package/dist/contracts/system.contract.d.ts +106 -8
  37. package/dist/contracts/system.contract.d.ts.map +1 -1
  38. package/dist/contracts/system.contract.js +11 -2
  39. package/dist/contracts/system.contract.js.map +1 -1
  40. package/dist/events/agent-events.d.ts +85 -10
  41. package/dist/events/agent-events.d.ts.map +1 -1
  42. package/dist/events/agent-events.js +26 -10
  43. package/dist/events/agent-events.js.map +1 -1
  44. package/dist/events/agent-words.d.ts +20 -0
  45. package/dist/events/agent-words.d.ts.map +1 -0
  46. package/dist/events/agent-words.js +82 -0
  47. package/dist/events/agent-words.js.map +1 -0
  48. package/dist/events/{cards.d.ts → requests.d.ts} +18 -18
  49. package/dist/events/requests.d.ts.map +1 -0
  50. package/dist/events/{cards.js → requests.js} +26 -26
  51. package/dist/events/requests.js.map +1 -0
  52. package/dist/events/resume.d.ts +1 -0
  53. package/dist/events/resume.d.ts.map +1 -1
  54. package/dist/events/resume.js +2 -0
  55. package/dist/events/resume.js.map +1 -1
  56. package/dist/events/system-events.d.ts +19 -3
  57. package/dist/events/system-events.d.ts.map +1 -1
  58. package/dist/events/transcript.d.ts +165 -25
  59. package/dist/events/transcript.d.ts.map +1 -1
  60. package/dist/events/transcript.js +34 -15
  61. package/dist/events/transcript.js.map +1 -1
  62. package/dist/events/watch-wake.d.ts.map +1 -1
  63. package/dist/events/watch-wake.js +4 -0
  64. package/dist/events/watch-wake.js.map +1 -1
  65. package/dist/index.d.ts +581 -99
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +9 -4
  68. package/dist/index.js.map +1 -1
  69. package/dist/models/model-offer.d.ts +2 -1
  70. package/dist/models/model-offer.d.ts.map +1 -1
  71. package/dist/models/model-offer.js +5 -4
  72. package/dist/models/model-offer.js.map +1 -1
  73. package/dist/models/model-roles.d.ts +2 -9
  74. package/dist/models/model-roles.d.ts.map +1 -1
  75. package/dist/models/model-roles.js +2 -9
  76. package/dist/models/model-roles.js.map +1 -1
  77. package/dist/policy/command-classes.d.ts +1 -0
  78. package/dist/policy/command-classes.d.ts.map +1 -1
  79. package/dist/policy/command-classes.js +8 -0
  80. package/dist/policy/command-classes.js.map +1 -1
  81. package/dist/policy/request-status.d.ts +5 -0
  82. package/dist/policy/request-status.d.ts.map +1 -0
  83. package/dist/policy/{card-status.js → request-status.js} +3 -3
  84. package/dist/policy/request-status.js.map +1 -0
  85. package/dist/policy/safety-policy.d.ts +1 -1
  86. package/dist/policy/safety-policy.d.ts.map +1 -1
  87. package/dist/policy/safety-policy.js +1 -0
  88. package/dist/policy/safety-policy.js.map +1 -1
  89. package/dist/policy/turned-away.d.ts +2 -0
  90. package/dist/policy/turned-away.d.ts.map +1 -0
  91. package/dist/policy/turned-away.js +13 -0
  92. package/dist/policy/turned-away.js.map +1 -0
  93. package/dist/protocol/peer-dial.d.ts +9 -0
  94. package/dist/protocol/peer-dial.d.ts.map +1 -1
  95. package/dist/protocol/peer-dial.js +12 -4
  96. package/dist/protocol/peer-dial.js.map +1 -1
  97. package/dist/protocol/peer-mcp-server.d.ts +1 -0
  98. package/dist/protocol/peer-mcp-server.d.ts.map +1 -1
  99. package/dist/protocol/peer-mcp-server.js +14 -1
  100. package/dist/protocol/peer-mcp-server.js.map +1 -1
  101. package/dist/schemas/agent.d.ts +2 -1
  102. package/dist/schemas/agent.d.ts.map +1 -1
  103. package/dist/schemas/agent.js +7 -0
  104. package/dist/schemas/agent.js.map +1 -1
  105. package/dist/schemas/agents.d.ts +26 -0
  106. package/dist/schemas/agents.d.ts.map +1 -1
  107. package/dist/schemas/agents.js +14 -0
  108. package/dist/schemas/agents.js.map +1 -1
  109. package/dist/schemas/automations.d.ts +8 -0
  110. package/dist/schemas/automations.d.ts.map +1 -1
  111. package/dist/schemas/capabilities.d.ts +25 -12
  112. package/dist/schemas/capabilities.d.ts.map +1 -1
  113. package/dist/schemas/capabilities.js +11 -6
  114. package/dist/schemas/capabilities.js.map +1 -1
  115. package/dist/schemas/chat-route.d.ts +49 -0
  116. package/dist/schemas/chat-route.d.ts.map +1 -0
  117. package/dist/schemas/{model-route.js → chat-route.js} +17 -5
  118. package/dist/schemas/chat-route.js.map +1 -0
  119. package/dist/schemas/context-trim.d.ts +9 -0
  120. package/dist/schemas/context-trim.d.ts.map +1 -0
  121. package/dist/schemas/context-trim.js +18 -0
  122. package/dist/schemas/context-trim.js.map +1 -0
  123. package/dist/schemas/devices.d.ts +21 -8
  124. package/dist/schemas/devices.d.ts.map +1 -1
  125. package/dist/schemas/devices.js +8 -9
  126. package/dist/schemas/devices.js.map +1 -1
  127. package/dist/schemas/extension-updates.d.ts +4 -4
  128. package/dist/schemas/extension-updates.js +1 -1
  129. package/dist/schemas/extension-updates.js.map +1 -1
  130. package/dist/schemas/git/git.d.ts +83 -0
  131. package/dist/schemas/git/git.d.ts.map +1 -1
  132. package/dist/schemas/git/git.js +40 -0
  133. package/dist/schemas/git/git.js.map +1 -1
  134. package/dist/schemas/hosts.d.ts +36 -5
  135. package/dist/schemas/hosts.d.ts.map +1 -1
  136. package/dist/schemas/hosts.js +7 -5
  137. package/dist/schemas/hosts.js.map +1 -1
  138. package/dist/schemas/metrics.d.ts +108 -0
  139. package/dist/schemas/metrics.d.ts.map +1 -0
  140. package/dist/schemas/metrics.js +77 -0
  141. package/dist/schemas/metrics.js.map +1 -0
  142. package/dist/schemas/panels.d.ts +2 -2
  143. package/dist/schemas/panels.js +1 -1
  144. package/dist/schemas/panels.js.map +1 -1
  145. package/dist/schemas/personas.d.ts +1 -13
  146. package/dist/schemas/personas.d.ts.map +1 -1
  147. package/dist/schemas/personas.js +3 -20
  148. package/dist/schemas/personas.js.map +1 -1
  149. package/dist/schemas/providers/plan-limits.d.ts +1 -0
  150. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  151. package/dist/schemas/providers/plan-limits.js +6 -0
  152. package/dist/schemas/providers/plan-limits.js.map +1 -1
  153. package/dist/schemas/secrets.d.ts +1 -1
  154. package/dist/schemas/secrets.d.ts.map +1 -1
  155. package/dist/schemas/secrets.js +1 -1
  156. package/dist/schemas/secrets.js.map +1 -1
  157. package/dist/schemas/settings.d.ts +0 -42
  158. package/dist/schemas/settings.d.ts.map +1 -1
  159. package/dist/schemas/settings.js +1 -16
  160. package/dist/schemas/settings.js.map +1 -1
  161. package/dist/schemas/shared.d.ts +2 -2
  162. package/dist/schemas/shared.d.ts.map +1 -1
  163. package/dist/schemas/shared.js +3 -3
  164. package/dist/schemas/shared.js.map +1 -1
  165. package/dist/schemas/system-prompt.d.ts +104 -0
  166. package/dist/schemas/system-prompt.d.ts.map +1 -0
  167. package/dist/schemas/system-prompt.js +25 -0
  168. package/dist/schemas/system-prompt.js.map +1 -0
  169. package/dist/schemas/workflows.d.ts +2 -2
  170. package/dist/schemas/workflows.d.ts.map +1 -1
  171. package/dist/schemas/workflows.js +2 -2
  172. package/dist/schemas/workflows.js.map +1 -1
  173. package/dist/state/definition.d.ts +18 -10
  174. package/dist/state/definition.d.ts.map +1 -1
  175. package/dist/state/fix-attempt-plan.d.ts +4 -0
  176. package/dist/state/fix-attempt-plan.d.ts.map +1 -1
  177. package/dist/state/fix-attempt-plan.js +3 -2
  178. package/dist/state/fix-attempt-plan.js.map +1 -1
  179. package/dist/state/fix-stance.d.ts +1 -0
  180. package/dist/state/fix-stance.d.ts.map +1 -1
  181. package/dist/state/fix-stance.js +11 -0
  182. package/dist/state/fix-stance.js.map +1 -1
  183. package/dist/state/history-state.d.ts.map +1 -1
  184. package/dist/state/history-state.js +8 -1
  185. package/dist/state/history-state.js.map +1 -1
  186. package/dist/state/runtime-state.d.ts +0 -1
  187. package/dist/state/runtime-state.d.ts.map +1 -1
  188. package/dist/state/runtime-state.js +0 -1
  189. package/dist/state/runtime-state.js.map +1 -1
  190. package/dist/state/workspace-state.d.ts +4 -4
  191. package/dist/state/workspace-state.d.ts.map +1 -1
  192. package/dist/state/workspace-state.js +4 -4
  193. package/dist/state/workspace-state.js.map +1 -1
  194. package/dist/text/documents.d.ts +2 -2
  195. package/dist/text/documents.d.ts.map +1 -1
  196. package/dist/text/documents.js.map +1 -1
  197. package/dist/text/mentions.d.ts +1 -0
  198. package/dist/text/mentions.d.ts.map +1 -1
  199. package/dist/text/mentions.js +5 -1
  200. package/dist/text/mentions.js.map +1 -1
  201. package/dist/text/transcript-fold.d.ts +8 -3
  202. package/dist/text/transcript-fold.d.ts.map +1 -1
  203. package/dist/text/transcript-fold.js +83 -28
  204. package/dist/text/transcript-fold.js.map +1 -1
  205. package/package.json +9 -9
  206. package/src/chores/chores.test.ts +1 -1
  207. package/src/chores/digest.test.ts +1 -1
  208. package/src/chores/probes.test.ts +5 -2
  209. package/src/chores/stack.test.ts +1 -1
  210. package/src/chores/verdict.test.ts +6 -5
  211. package/src/contracts/agent.contract.ts +10 -9
  212. package/src/contracts/agents.contract.ts +35 -2
  213. package/src/contracts/capabilities.contract.ts +3 -3
  214. package/src/contracts/{host.contract.ts → device.contract.ts} +8 -6
  215. package/src/contracts/personas.contract.ts +1 -13
  216. package/src/contracts/runner.contract.ts +3 -2
  217. package/src/contracts/secrets.contract.ts +2 -2
  218. package/src/contracts/system.contract.ts +13 -2
  219. package/src/contracts/webext.contract.ts +1 -1
  220. package/src/events/agent-events.ts +37 -14
  221. package/src/events/agent-words.test.ts +68 -0
  222. package/src/events/agent-words.ts +124 -0
  223. package/src/events/{cards.ts → requests.ts} +44 -44
  224. package/src/events/resume.test.ts +8 -1
  225. package/src/events/resume.ts +4 -0
  226. package/src/events/transcript.ts +51 -22
  227. package/src/events/watch-wake.test.ts +3 -2
  228. package/src/events/watch-wake.ts +4 -0
  229. package/src/ids/conversation-ids.test.ts +3 -3
  230. package/src/ids/hostnames.test.ts +1 -1
  231. package/src/ids/share-paths.test.ts +1 -1
  232. package/src/ids/tunnel-ids.test.ts +1 -1
  233. package/src/index.ts +9 -4
  234. package/src/models/agent-catalog.test.ts +1 -1
  235. package/src/models/capability-ledger.test.ts +1 -1
  236. package/src/models/model-offer.test.ts +9 -2
  237. package/src/models/model-offer.ts +10 -7
  238. package/src/models/model-order.test.ts +1 -1
  239. package/src/models/model-pins.test.ts +1 -1
  240. package/src/models/model-roles.test.ts +1 -1
  241. package/src/models/model-roles.ts +6 -12
  242. package/src/models/plan-pools.test.ts +1 -1
  243. package/src/models/provider-specs.test.ts +3 -3
  244. package/src/policy/batch-runs.test.ts +1 -1
  245. package/src/policy/command-classes.test.ts +37 -2
  246. package/src/policy/command-classes.ts +16 -0
  247. package/src/policy/credential-material.test.ts +6 -10
  248. package/src/policy/fence-paths.test.ts +1 -1
  249. package/src/policy/overlay-lint.test.ts +1 -1
  250. package/src/policy/owner-ticket.test.ts +1 -1
  251. package/src/policy/persona-home.test.ts +1 -1
  252. package/src/policy/{card-status.ts → request-status.ts} +6 -6
  253. package/src/policy/safety-policy.test.ts +1 -1
  254. package/src/policy/safety-policy.ts +1 -0
  255. package/src/policy/search-globs.test.ts +1 -1
  256. package/src/policy/turned-away.ts +18 -0
  257. package/src/protocol/container-requirements.test.ts +1 -1
  258. package/src/protocol/ingress-contract.test.ts +8 -2
  259. package/src/protocol/ingress-protocol.test.ts +5 -4
  260. package/src/protocol/peer-dial.test.ts +111 -41
  261. package/src/protocol/peer-dial.ts +39 -4
  262. package/src/protocol/peer-mcp-server.test.ts +37 -7
  263. package/src/protocol/peer-mcp-server.ts +22 -1
  264. package/src/protocol/routes.test.ts +4 -2
  265. package/src/schemas/agent.ts +13 -0
  266. package/src/schemas/agents.ts +25 -0
  267. package/src/schemas/areas.test.ts +2 -8
  268. package/src/schemas/automations.ts +5 -5
  269. package/src/schemas/capabilities.ts +20 -9
  270. package/src/schemas/chat-route.ts +61 -0
  271. package/src/schemas/context-trim.test.ts +38 -0
  272. package/src/schemas/context-trim.ts +29 -0
  273. package/src/schemas/dev-rebuild-log.test.ts +1 -1
  274. package/src/schemas/devices.ts +25 -22
  275. package/src/schemas/extension-updates.ts +1 -1
  276. package/src/schemas/git/git.test.ts +11 -0
  277. package/src/schemas/git/git.ts +66 -0
  278. package/src/schemas/hosts.ts +15 -8
  279. package/src/schemas/issues.ts +1 -1
  280. package/src/schemas/metrics.ts +107 -0
  281. package/src/schemas/panels.ts +2 -2
  282. package/src/schemas/personas.ts +5 -28
  283. package/src/schemas/providers/plan-limits.ts +9 -0
  284. package/src/schemas/secrets.ts +1 -1
  285. package/src/schemas/settings.test.ts +1 -1
  286. package/src/schemas/settings.ts +9 -36
  287. package/src/schemas/shared.ts +6 -6
  288. package/src/schemas/system-prompt.ts +41 -0
  289. package/src/schemas/version-seam.test.ts +1 -1
  290. package/src/schemas/webext.ts +1 -1
  291. package/src/schemas/workflows.ts +4 -4
  292. package/src/state/contract-lock.test.ts +15 -11
  293. package/src/state/fix-attempt-plan.test.ts +10 -1
  294. package/src/state/fix-attempt-plan.ts +6 -2
  295. package/src/state/fix-stance.test.ts +10 -1
  296. package/src/state/fix-stance.ts +13 -0
  297. package/src/state/history-state.ts +17 -1
  298. package/src/state/runtime-state.test.ts +3 -9
  299. package/src/state/runtime-state.ts +0 -4
  300. package/src/state/versions.test.ts +1 -1
  301. package/src/state/workspace-state.test.ts +12 -7
  302. package/src/state/workspace-state.ts +4 -4
  303. package/src/text/archives.test.ts +1 -1
  304. package/src/text/documents.test.ts +1 -1
  305. package/src/text/documents.ts +2 -2
  306. package/src/text/embed.test.ts +17 -6
  307. package/src/text/emoji.test.ts +1 -1
  308. package/src/text/mentions.test.ts +20 -2
  309. package/src/text/mentions.ts +10 -1
  310. package/src/text/path-refs.test.ts +1 -1
  311. package/src/text/title.test.ts +1 -1
  312. package/src/text/transcript-fold.test.ts +183 -1
  313. package/src/text/transcript-fold.ts +146 -39
  314. package/src/text/whisper.test.ts +1 -1
  315. package/src/text/workflow-faults.test.ts +1 -1
  316. package/src/time/zone.test.ts +6 -6
  317. package/dist/contracts/host.contract.d.ts.map +0 -1
  318. package/dist/contracts/host.contract.js +0 -16
  319. package/dist/contracts/host.contract.js.map +0 -1
  320. package/dist/events/cards.d.ts.map +0 -1
  321. package/dist/events/cards.js.map +0 -1
  322. package/dist/policy/card-status.d.ts +0 -5
  323. package/dist/policy/card-status.d.ts.map +0 -1
  324. package/dist/policy/card-status.js.map +0 -1
  325. package/dist/schemas/model-route.d.ts +0 -27
  326. package/dist/schemas/model-route.d.ts.map +0 -1
  327. package/dist/schemas/model-route.js.map +0 -1
  328. package/src/schemas/model-route.ts +0 -43
@@ -20,6 +20,9 @@ export const RESUME_NOTES = {
20
20
  carried: `The model provider's usage allowance ran out while this turn was running, which stopped it, and it has been sent again on a different account of the same provider, in this same session: everything you knew is still here. ${REPEATED}`,
21
21
  refused:
22
22
  "The model provider refused the previous attempt at this request outright, because its usage allowance was spent: no part of the request below was read or acted on, and nothing has been done towards it. It has been sent again, and starts from the beginning. Where the sandbox has measured earlier work on this branch, it is in the note headed 'Where the work stands'.",
23
+ // Turned away before the model saw a word (a memory hold, a dead credential, a model the plan lacks), with the
24
+ // sandbox keeping the turn: nothing to continue from, so the request starts at its beginning.
25
+ door: "The previous attempt at this request was turned away before any of it reached you: no part of the request below was read or acted on. It has now been sent, and starts from the beginning.",
23
26
  // A turn parked on the user when the daemon died; what follows the note is their actual answer, not a repeat.
24
27
  answered:
25
28
  "The sandbox restarted while this conversation was waiting for the user to respond; it is back, and their response follows below: continue from where the session left off.",
@@ -52,6 +55,7 @@ const RESUME_DISCLOSURES: Record<ResumeReason, ResumeDisclosure> = {
52
55
  switched: { kind: "notice", text: "Sent again on the switched account after the allowance ran out mid-turn, in a fresh session." },
53
56
  carried: { kind: "notice", text: "Sent again on the switched account after the allowance ran out mid-turn, carrying the session with it." },
54
57
  refused: { kind: "notice", text: "Sent again after the allowance refused it: nothing had run." },
58
+ door: { kind: "notice", text: "Sent again: the first attempt was turned away before anything ran." },
55
59
  answered: { kind: "note", note: { title: "Picked back up after a sandbox restart", text: RESUME_NOTES.answered } },
56
60
  };
57
61
 
@@ -2,14 +2,14 @@ import { z } from "zod";
2
2
  import { AgentHarnessSchema, AgentProviderSchema } from "../schemas/agent.js";
3
3
  import { ShareDetailSchema } from "../schemas/share.js";
4
4
  import { SubagentKindSchema, SubagentStatusSchema, SubagentVerificationSchema } from "../schemas/terminal.js";
5
- import type { ToolCallContent, ToolCallLocation, ToolCallStatus, ToolKind} from "./cards.js";
6
- import { browserHelpCard, capabilityOfferCard, CapabilityOutcomeSchema, credentialOfferCard, CredentialReceiptSchema, paymentOfferCard, PaymentReceiptSchema, PermissionAskSchema, permissionCard, planCard, questionCard, terminalHelpCard, TodoItemSchema, ToolCallContentSchema, ToolCallLocationSchema, ToolCallStatusSchema, ToolKindSchema } from "./cards.js";
5
+ import type { ToolCallContent, ToolCallLocation, ToolCallStatus, ToolKind} from "./requests.js";
6
+ import { browserHelpRequest, capabilityOfferRequest, CapabilityOutcomeSchema, credentialOfferRequest, CredentialReceiptSchema, paymentOfferRequest, PaymentReceiptSchema, PermissionAskSchema, permissionRequest, planRequest, questionRequest, terminalHelpRequest, TodoItemSchema, ToolCallContentSchema, ToolCallLocationSchema, ToolCallStatusSchema, ToolKindSchema } from "./requests.js";
7
7
 
8
8
  // A conversation as recorded and replayed: the rows, the cards they carry, and the patches that change them while a
9
9
  // turn runs. One shape for the live row and the recorded one, since they're the same row.
10
10
 
11
11
  // Transcript cards.
12
- // Status enums for a parked card, settled by the fold from the reply that released it (card-status.ts), riding the row
12
+ // Status enums for a parked card, settled by the fold from the reply that released it (request-status.ts), riding the row
13
13
  // rather than the reply. `cancelled` means nobody answered, not a decision; `pending` is still parked.
14
14
  export const PlanStatusSchema = z.enum(["pending", "approved", "rejected", "cancelled"]);
15
15
  export type PlanStatus = z.infer<typeof PlanStatusSchema>;
@@ -25,10 +25,10 @@ export type OfferStatus = z.infer<typeof OfferStatusSchema>;
25
25
  export const CapabilityOfferStatusSchema = z.enum(["pending", "connecting", "skipped", "cancelled"]);
26
26
  export type CapabilityOfferStatus = z.infer<typeof CapabilityOfferStatusSchema>;
27
27
 
28
- export const TranscriptPlanSchema = z.object({ ...planCard, status: PlanStatusSchema.describe("Where the decision stands.") });
28
+ export const TranscriptPlanSchema = z.object({ ...planRequest, status: PlanStatusSchema.describe("Where the decision stands.") });
29
29
  export type TranscriptPlan = z.infer<typeof TranscriptPlanSchema>;
30
30
  export const TranscriptQuestionSchema = z.object({
31
- ...questionCard,
31
+ ...questionRequest,
32
32
  status: QuestionStatusSchema.describe("Where the answer stands."),
33
33
  answers: z
34
34
  .record(z.string(), z.array(z.string()))
@@ -37,26 +37,26 @@ export const TranscriptQuestionSchema = z.object({
37
37
  });
38
38
  export type TranscriptQuestion = z.infer<typeof TranscriptQuestionSchema>;
39
39
  // `explain`, the judge's sentence, arrives via PermissionAskSchema at raise time; nothing patches it in after.
40
- export const TranscriptPermissionSchema = PermissionAskSchema.extend({ ...permissionCard, status: PermissionStatusSchema.describe("Where the decision stands.") });
40
+ export const TranscriptPermissionSchema = PermissionAskSchema.extend({ ...permissionRequest, status: PermissionStatusSchema.describe("Where the decision stands.") });
41
41
  export type TranscriptPermission = z.infer<typeof TranscriptPermissionSchema>;
42
- export const TranscriptBrowserHelpSchema = z.object({ ...browserHelpCard, status: HelpStatusSchema.describe("How the hand-over ended.") });
42
+ export const TranscriptBrowserHelpSchema = z.object({ ...browserHelpRequest, status: HelpStatusSchema.describe("How the hand-over ended.") });
43
43
  export type TranscriptBrowserHelp = z.infer<typeof TranscriptBrowserHelpSchema>;
44
- export const TranscriptTerminalHelpSchema = z.object({ ...terminalHelpCard, status: HelpStatusSchema.describe("How the hand-over ended.") });
44
+ export const TranscriptTerminalHelpSchema = z.object({ ...terminalHelpRequest, status: HelpStatusSchema.describe("How the hand-over ended.") });
45
45
  export type TranscriptTerminalHelp = z.infer<typeof TranscriptTerminalHelpSchema>;
46
46
  export const TranscriptCapabilityOfferSchema = z.object({
47
- ...capabilityOfferCard,
47
+ ...capabilityOfferRequest,
48
48
  status: CapabilityOfferStatusSchema.describe("Where the decision stands."),
49
49
  outcome: CapabilityOutcomeSchema.optional().describe("How an accepted ask's setup ended (the capability_outcome frame)."),
50
50
  });
51
51
  export type TranscriptCapabilityOffer = z.infer<typeof TranscriptCapabilityOfferSchema>;
52
52
  export const TranscriptPaymentOfferSchema = z.object({
53
- ...paymentOfferCard,
53
+ ...paymentOfferRequest,
54
54
  status: OfferStatusSchema.describe("Where the decision stands."),
55
55
  receipt: PaymentReceiptSchema.optional().describe("How the approved payment ended (the payment_receipt frame)."),
56
56
  });
57
57
  export type TranscriptPaymentOffer = z.infer<typeof TranscriptPaymentOfferSchema>;
58
58
  export const TranscriptCredentialOfferSchema = z.object({
59
- ...credentialOfferCard,
59
+ ...credentialOfferRequest,
60
60
  status: OfferStatusSchema.describe("Where the decision stands."),
61
61
  receipt: CredentialReceiptSchema.optional().describe("Who released it, or that somebody refused (the credential_receipt frame)."),
62
62
  });
@@ -131,9 +131,8 @@ export interface TranscriptTool {
131
131
  subagent?: TranscriptSubagent | undefined;
132
132
  }
133
133
 
134
- // Three endings that wake a conversation off a condition watch: the first two are promised when it is armed, the third
135
- // is the daemon's own (a deadline that passed while it was down). Composing and parsing the wake is watch-wake.ts.
136
- export const WatchOutcomeSchema = z.enum(["met", "timeout", "restart-expired"]);
134
+ // Every ending that wakes a conversation off a condition watch; composing and parsing the wake is watch-wake.ts.
135
+ export const WatchOutcomeSchema = z.enum(["met", "timeout", "restart-expired", "broken"]);
137
136
  export type WatchOutcome = z.infer<typeof WatchOutcomeSchema>;
138
137
 
139
138
  // A condition watch waking the conversation, as the row carries it. The wake arrives as an ordinary turn prompt that
@@ -148,6 +147,25 @@ export const TranscriptWatchWakeSchema = z.object({
148
147
  });
149
148
  export type TranscriptWatchWake = z.infer<typeof TranscriptWatchWakeSchema>;
150
149
 
150
+ // Another agent's words that reached this conversation as a prompt; the row keeps the prompt and names the sender.
151
+ export const TranscriptAgentWordsSchema = z.object({
152
+ kind: z.enum(["peer", "child"]).describe("Who sent it: another conversation in the workspace, or a child agent this one started."),
153
+ from: z.string().describe("The sending conversation's id."),
154
+ title: z.string().optional().describe("The sender's title, when it had one."),
155
+ failed: z.boolean().optional().describe("A child's report on a turn that failed rather than finished."),
156
+ sent: z.string().describe("The whole prompt the model received, disclosed under the row."),
157
+ });
158
+ export type TranscriptAgentWords = z.infer<typeof TranscriptAgentWordsSchema>;
159
+
160
+ // A command the agent left running past its own call; how it is doing now is live state on the card (AgentSummary.jobs).
161
+ export const TranscriptBackgroundJobSchema = z.object({
162
+ id: z.string().describe("The daemon's handle for the job, which its live state on the conversation's card is keyed by."),
163
+ label: z.string().describe("What the job is, in the agent's own words when it gave any, else its command on one line."),
164
+ command: z.string().describe("The command as the agent wrote it, folded to one line."),
165
+ startedAt: z.number().describe("When it started, in milliseconds."),
166
+ });
167
+ export type TranscriptBackgroundJob = z.infer<typeof TranscriptBackgroundJobSchema>;
168
+
151
169
  // One note the daemon put before a user's message: the model reads `text`, the chat draws `title` on a row that opens
152
170
  // to it. Shared by the live frame and the restored transcript, so it reads the same either way.
153
171
  export const TurnNoteSchema = z.object({
@@ -226,14 +244,23 @@ export const TranscriptRowSchema = z.object({
226
244
  .describe(
227
245
  "A person wrote this in the agent's voice, with no turn behind it. Marked for the human re-reading the conversation months later, so their own words do not pass as the agent's. The agent itself never sees the mark.",
228
246
  ),
229
- // The one-press follow-up this notice offers, by name; the chat decides what it does, and whether it stands.
247
+ // The follow-up this notice offers, by name; the chat decides what it does, and whether it stands. `sendAnyway` sends
248
+ // the held message; `sandboxMemory` offers that and a raise of the sandbox's memory; `sendAgain` sends a turn the
249
+ // sandbox kept after a refusal no raise would clear.
230
250
  noticeAction: z
231
- .enum(["landHold", "depsInstall", "watchStop"])
251
+ .enum(["landHold", "depsInstall", "watchStop", "sandboxMemory", "sendAnyway", "sendAgain"])
232
252
  .optional()
233
253
  .describe("A one-press follow-up this notice offers, by name. The chat decides what it does and whether it still applies."),
254
+ // Where a refused turn's words wait decides what the notice's press does: the composer's queue, or the sandbox.
255
+ sandboxHeld: z
256
+ .boolean()
257
+ .optional()
258
+ .describe(
259
+ "The sandbox kept this refused turn whole, its message still above, so the notice's press runs that turn again instead of sending anything from the composer, which never held these words.",
260
+ ),
234
261
  // An unfinished wait this notice describes, by name; whether it's still running is live state, not stored here.
235
262
  noticeWait: z
236
- .enum(["credentialRenewal", "personaRoute", "modelRoute", "watch"])
263
+ .enum(["credentialRenewal", "chatRoute", "watch"])
237
264
  .optional()
238
265
  .describe("The wait this notice describes, by name, so a reader can say whether it is still on."),
239
266
  // Which one, for a wait whose kind can have several in flight at once; without it two armed watches settle together.
@@ -250,6 +277,8 @@ export const TranscriptRowSchema = z.object({
250
277
  capabilityOffer: TranscriptCapabilityOfferSchema.optional().describe("The capability setup this row asked for, the decision, and the outcome."),
251
278
  paymentOffer: TranscriptPaymentOfferSchema.optional().describe("The payment this row asked for, the decision, and the receipt."),
252
279
  watchWake: TranscriptWatchWakeSchema.optional().describe("The condition watch that woke this conversation, and the prompt it was woken with."),
280
+ agentWords: TranscriptAgentWordsSchema.optional().describe("Another agent's words that reached this conversation, whose they are, and the prompt they came as."),
281
+ backgroundJob: TranscriptBackgroundJobSchema.optional().describe("The background job this row marks the start of."),
253
282
  credentialOffer: TranscriptCredentialOfferSchema.optional().describe(
254
283
  "The gated credential this row asked to use, who may release it, and who did.",
255
284
  ),
@@ -257,7 +286,7 @@ export const TranscriptRowSchema = z.object({
257
286
  export type TranscriptRow = z.infer<typeof TranscriptRowSchema>;
258
287
 
259
288
  // Every card field, as one list, for readers asking whether a row holds a card at all.
260
- export const CARD_FIELDS = [
289
+ export const REQUEST_FIELDS = [
261
290
  "plan",
262
291
  "question",
263
292
  "permission",
@@ -267,12 +296,12 @@ export const CARD_FIELDS = [
267
296
  "paymentOffer",
268
297
  "credentialOffer",
269
298
  ] as const;
270
- export type CardField = (typeof CARD_FIELDS)[number];
271
- export type TranscriptCards = Pick<TranscriptRow, CardField>;
299
+ export type RequestField = (typeof REQUEST_FIELDS)[number];
300
+ export type TranscriptRequests = Pick<TranscriptRow, RequestField>;
272
301
  // Whether a row holds a card at all, answered or not.
273
- export const holdsCard = (row: TranscriptCards): boolean => CARD_FIELDS.some((field) => row[field] !== undefined);
302
+ export const holdsRequest = (row: TranscriptRequests): boolean => REQUEST_FIELDS.some((field) => row[field] !== undefined);
274
303
  // Whether a row is holding the turn open on a card nobody has answered.
275
- export const isAwaitingDecision = (row: TranscriptCards): boolean => CARD_FIELDS.some((field) => row[field]?.status === "pending");
304
+ export const isAwaitingDecision = (row: TranscriptRequests): boolean => REQUEST_FIELDS.some((field) => row[field]?.status === "pending");
276
305
 
277
306
  // One change to a run's rows, so a client keeps rows rather than frames. Prose/thinking append to a row; a `tool` card
278
307
  // replaces whole by id; everything else replaces its row; `drop` removes a row that opened and never wrote.
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, it, expect } from "bun:test";
2
2
  import type { WatchOutcome } from "./transcript.js";
3
3
  import { watchWakeOf, watchWakePrompt, watchWakeRow, type WatchWakeFields } from "./watch-wake.js";
4
4
 
@@ -13,7 +13,7 @@ const fields = (over: Partial<WatchWakeFields> = {}): WatchWakeFields => ({
13
13
  ...over,
14
14
  });
15
15
 
16
- const OUTCOMES: readonly WatchOutcome[] = ["met", "timeout", "restart-expired"];
16
+ const OUTCOMES: WatchOutcome[] = ["met", "timeout", "restart-expired", "broken"];
17
17
 
18
18
  describe("watch wake", () => {
19
19
  // The composer and the parser are the same piece of knowledge; this is what holds them together when either is
@@ -52,6 +52,7 @@ describe("watch wake", () => {
52
52
  const textOf = (outcome: WatchOutcome): string => watchWakeRow(watchWakePrompt(fields({ outcome })))?.text ?? "";
53
53
  expect(textOf("timeout")).toBe("CI run 316 on intentic/intentic — the watch gave up after 43m.");
54
54
  expect(textOf("restart-expired")).toBe("CI run 316 on intentic/intentic — the watch stopped when the sandbox restarted, after 43m.");
55
+ expect(textOf("broken")).toBe("CI run 316 on intentic/intentic — the watch stopped after 43m: its check can no longer run.");
55
56
  });
56
57
 
57
58
  it("ignores a prompt that is not a wake, so any reader can ask without checking first", () => {
@@ -11,6 +11,7 @@ const OPENINGS: Record<WatchOutcome, string> = {
11
11
  timeout: "Watch timed out: the deadline passed and the check never exited 0. Decide whether to re-arm it, investigate the check, or report back.",
12
12
  "restart-expired":
13
13
  "Watch stopped: its deadline passed while the daemon was restarting, so it went unchecked for part of that window. It has just been re-checked once and the condition still does not hold. Decide whether to re-arm it, investigate the check, or report back.",
14
+ broken: "Watch stopped: its check can no longer run at all, so it was disarmed rather than left waiting on a condition it could never see. The reason is in the output below. Decide whether to re-arm it with a check that can run, or report back.",
14
15
  };
15
16
 
16
17
  // Labels the prompt writes and the parser reads back. `Watching` and `Elapsed` are load-bearing on both sides; the rest
@@ -51,6 +52,9 @@ const headline = (outcome: WatchOutcome, note: string, elapsed: string): string
51
52
  if (outcome === "timeout") {
52
53
  return `${note} — the watch gave up after ${elapsed}.`;
53
54
  }
55
+ if (outcome === "broken") {
56
+ return `${note} — the watch stopped after ${elapsed}: its check can no longer run.`;
57
+ }
54
58
  return `${note} — the watch stopped when the sandbox restarted, after ${elapsed}.`;
55
59
  };
56
60
 
@@ -1,4 +1,4 @@
1
- import { afterEach, expect, test, vi } from "vitest";
1
+ import { test, expect, afterEach, spyOn, jest } from "bun:test";
2
2
  import {
3
3
  CI_FIX_PREFIX,
4
4
  ciFixConversationId,
@@ -15,7 +15,7 @@ import { ConversationIdSchema } from "../schemas/agent.js";
15
15
 
16
16
  const mockRandomValues = (values: readonly number[]) => {
17
17
  const remaining = [...values];
18
- const getRandomValues = vi.spyOn(crypto, "getRandomValues").mockImplementation(<T extends ArrayBufferView | null>(array: T): T => {
18
+ const getRandomValues = spyOn(crypto, "getRandomValues").mockImplementation(<T extends ArrayBufferView | null>(array: T): T => {
19
19
  if (!(array instanceof Uint32Array)) {
20
20
  throw new TypeError(`Expected a Uint32Array`);
21
21
  }
@@ -29,7 +29,7 @@ const mockRandomValues = (values: readonly number[]) => {
29
29
  return { getRandomValues, remaining };
30
30
  };
31
31
 
32
- afterEach(() => vi.restoreAllMocks());
32
+ afterEach(() => jest.restoreAllMocks());
33
33
 
34
34
  test("every generated id passes the conversation-id guard", () => {
35
35
  for (let index = 0; index < 2_000; index += 1) {
@@ -1,4 +1,4 @@
1
- import { expect, test } from "vitest";
1
+ import { test, expect } from "bun:test";
2
2
  import {
3
3
  labelHostname,
4
4
  panelFromHost,
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, it, expect } from "bun:test";
2
2
  import { SHARE_ID, shareId, shareStem } from "./share-paths.js";
3
3
 
4
4
  /* The id a shared conversation is filed under is two things at once: the readable half of a link somebody pastes. */
@@ -1,4 +1,4 @@
1
- import { expect, test } from "vitest";
1
+ import { test, expect } from "bun:test";
2
2
  import { hostSshIdFromToken, PORT_SLOT_COUNT, portSlotsFromToken, sandboxIdFromToken } from "./tunnel-ids.js";
3
3
 
4
4
  const TOKEN = "connect-token-one";
package/src/index.ts CHANGED
@@ -71,7 +71,7 @@ export { diffContract } from "./contracts/diff.contract.js";
71
71
  export { gitContract } from "./contracts/git.contract.js";
72
72
  export { historyContract } from "./contracts/history.contract.js";
73
73
  // Not part of `sandboxContract` below: spoken over a device's WebSocket, with the machine implementing it.
74
- export { hostContract } from "./contracts/host.contract.js";
74
+ export { deviceContract } from "./contracts/device.contract.js";
75
75
  // Same inversion, spoken over a browser extension's socket, with the extension implementing it.
76
76
  export { webextContract } from "./contracts/webext.contract.js";
77
77
  // Same inversion again: spoken over a runner's WebSocket, with the runner implementing it.
@@ -103,13 +103,15 @@ export { vpnContract } from "./contracts/vpn.contract.js";
103
103
  export { workflowsContract } from "./contracts/workflows.contract.js";
104
104
  export { workspaceContract } from "./contracts/workspace.contract.js";
105
105
  export * from "./events/agent-events.js";
106
- export * from "./events/cards.js";
106
+ export * from "./events/agent-words.js";
107
+ export * from "./events/requests.js";
107
108
  export * from "./events/resume.js";
108
109
  export * from "./events/system-events.js";
109
110
  export * from "./events/transcript.js";
110
111
  export * from "./events/verify-nudge.js";
111
112
  export * from "./events/watch-wake.js";
112
- export * from "./policy/card-status.js";
113
+ export * from "./policy/request-status.js";
114
+ export * from "./policy/turned-away.js";
113
115
  export * from "./text/mentions.js";
114
116
  export * from "./protocol/sse.js";
115
117
  export * from "./protocol/routes.js";
@@ -163,9 +165,11 @@ export * from "./schemas/agents.js";
163
165
  export * from "./schemas/approvals.js";
164
166
  export * from "./schemas/automations.js";
165
167
  export * from "./schemas/capabilities.js";
168
+ export * from "./schemas/chat-route.js";
166
169
  export * from "./schemas/ci.js";
167
170
  export * from "./schemas/providers/claude-gate.js";
168
171
  export * from "./schemas/codebase-health.js";
172
+ export * from "./schemas/context-trim.js";
169
173
  export * from "./schemas/devices.js";
170
174
  export * from "./schemas/engines.js";
171
175
  export * from "./schemas/environment.js";
@@ -184,7 +188,7 @@ export * from "./schemas/logs.js";
184
188
  export * from "./schemas/loops.js";
185
189
  export * from "./schemas/maintenance.js";
186
190
  export * from "./schemas/marketplace.js";
187
- export * from "./schemas/model-route.js";
191
+ export * from "./schemas/metrics.js";
188
192
  export * from "./schemas/panels.js";
189
193
  export * from "./schemas/personas.js";
190
194
  export * from "./schemas/providers/plan-limits.js";
@@ -200,6 +204,7 @@ export * from "./schemas/sessions.js";
200
204
  export * from "./schemas/settings.js";
201
205
  export * from "./schemas/share.js";
202
206
  export * from "./schemas/shared.js";
207
+ export * from "./schemas/system-prompt.js";
203
208
  export * from "./schemas/system.js";
204
209
  export * from "./schemas/terminal.js";
205
210
  export * from "./schemas/turn-break.js";
@@ -1,4 +1,4 @@
1
- import { describe, expect, it, test } from "vitest";
1
+ import { describe, it, test, expect } from "bun:test";
2
2
  import {
3
3
  accessFor,
4
4
  capabilitiesOf,
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, it, expect } from "bun:test";
2
2
  import { capabilitiesOf, limitationsOf } from "./agent-catalog.js";
3
3
  import type { AgentCapabilities } from "./agent-runtimes.js";
4
4
 
@@ -1,4 +1,4 @@
1
- import { expect, test } from "vitest";
1
+ import { test, expect } from "bun:test";
2
2
  import { type ModelOffer, offerLines, parseModelPick, renewsInWords } from "./model-offer.js";
3
3
 
4
4
  /* THE OFFER the Auto judge chooses from, and the reading of what it replies. Pure both ways: no catalog, no account. */
@@ -15,7 +15,14 @@ const OFFER: ModelOffer = {
15
15
  ],
16
16
  accounts: {
17
17
  claude: [
18
- { id: "work", label: "work@studio", windows: [{ short: "5h", left: 38, resetsAt: IN_TWO_HOURS }, { short: "wk", left: 61 }] },
18
+ {
19
+ id: "work",
20
+ label: "work@studio",
21
+ windows: [
22
+ { short: "5h", left: 38, resetsAt: IN_TWO_HOURS },
23
+ { short: "wk", left: 61 },
24
+ ],
25
+ },
19
26
  { id: "personal", windows: [] },
20
27
  ],
21
28
  codex: [],
@@ -1,6 +1,6 @@
1
1
  import { modelPinKey } from "./model-pins.js";
2
2
  import type { AgentProvider } from "../schemas/agent.js";
3
- import type { ModelPick } from "../schemas/model-route.js";
3
+ import type { ModelPick } from "../schemas/chat-route.js";
4
4
 
5
5
  // What the Auto judge is allowed to choose from, and how its reply is read back. Pure: the daemon gathers the facts
6
6
  // (catalogs, connected accounts, headroom readings) and this turns them into the lines the model sees and the
@@ -100,19 +100,22 @@ export const offerLines = (offer: ModelOffer, now: number): readonly string[] =>
100
100
  // Wrapper words a model reaches for anyway: a fence, a bullet, quotes, a trailing period.
101
101
  const FENCE_LINE = /^```/u;
102
102
  const BULLET_LINE = /^[-*•]\s+/u;
103
- const FIELD = /^(model|effort|account)\s*:\s*(.+)$/iu;
103
+ const FIELD = /^([a-z][\w-]*)\s*:\s*(.+)$/iu;
104
104
 
105
105
  const bare = (value: string): string => value.trim().replace(/^[`'"]+/u, ``).replace(/[.`'"]+$/u, ``).trim();
106
106
 
107
- // The reply's `key: value` lines, lowercased keys, bullets and fences stripped. First wins: a model that answers twice
108
- // meant its first answer, and a later line is commentary on it.
109
- const fieldsOf = (reply: string): ReadonlyMap<string, string> => {
107
+ // The reply's `key: value` lines for the keys asked about, lowercased, bullets and fences stripped. First wins: a model
108
+ // that answers twice meant its first answer, and a later line is commentary on it. Keys are passed in rather than
109
+ // fixed here, since one reply can carry the answers to several questions (chat-route.ts asks two at once), and an
110
+ // empty map is the honest way to tell a keyed reply from a bare one-word one.
111
+ export const replyFields = (reply: string, keys: readonly string[]): ReadonlyMap<string, string> => {
112
+ const wanted = new Set(keys.map((key) => key.toLowerCase()));
110
113
  const fields = new Map<string, string>();
111
114
  for (const line of reply.split(`\n`)) {
112
115
  const trimmed = line.trim().replace(BULLET_LINE, ``);
113
116
  const found = FENCE_LINE.test(trimmed) ? null : FIELD.exec(trimmed);
114
117
  const key = found?.[1]?.toLowerCase();
115
- if (key !== undefined && found?.[2] !== undefined && !fields.has(key)) {
118
+ if (key !== undefined && wanted.has(key) && found?.[2] !== undefined && !fields.has(key)) {
116
119
  fields.set(key, bare(found[2]));
117
120
  }
118
121
  }
@@ -130,7 +133,7 @@ export interface ParsedPick {
130
133
  // provider has. An unrecognised effort or account is dropped instead, keeping the model it came with — those are
131
134
  // refinements, and the turn's own defaults answer for them correctly.
132
135
  export const parseModelPick = (reply: string, offer: ModelOffer): ParsedPick => {
133
- const fields = fieldsOf(reply);
136
+ const fields = replyFields(reply, [`model`, `effort`, `account`]);
134
137
  const token = fields.get(`model`) ?? ``;
135
138
  const chosen = offer.models.find((model) => modelPinKey(model).toLowerCase() === token.toLowerCase());
136
139
  if (chosen === undefined) {
@@ -1,4 +1,4 @@
1
- import { expect, test } from "vitest";
1
+ import { test, expect } from "bun:test";
2
2
  import { compareModelIds, compareUnrankedModelIds, familyOf, namesThinking, releaseOf, tierRankOf } from "./model-order.js";
3
3
 
4
4
  // The order every provider's catalog is served and browsed in; only Anthropic's catalog arrives ranked, the rest hand
@@ -1,4 +1,4 @@
1
- import { expect, test } from "vitest";
1
+ import { test, expect } from "bun:test";
2
2
  import { type ModelSource, modelPinKey, parsePinned, readyChain } from "./model-pins.js";
3
3
  import type { ModelPin } from "../schemas/agent.js";
4
4
 
@@ -1,4 +1,4 @@
1
- import { expect, test } from "vitest";
1
+ import { test, expect } from "bun:test";
2
2
  import { MODEL_ROLE_BLOCKS, MODEL_ROLES, type ModelRoleSpec } from "./model-roles.js";
3
3
 
4
4
  // The catalog draws the settings page directly, so its properties (every role in one block) must hold of the table
@@ -58,19 +58,13 @@ export const MODEL_ROLES = [
58
58
  icon: "check-square",
59
59
  },
60
60
  {
61
- // Picks one persona id or none from a fixed list; a classification, not free text.
62
- id: "persona-router",
63
- label: "Persona routing",
64
- blurb: "Which model reads a new chat's first message and picks the persona for it.",
65
- kind: "helper",
66
- icon: "users",
67
- },
68
- {
69
- // Picks one offered `provider:model` line, an effort and an account from a fixed list; a classification, not
70
- // free text. Prompt carries live account headroom, so it must not be a model whose own allowance it is reading.
61
+ // One reading answering both halves of "what does this chat open on": an offered `provider:model` line with an
62
+ // effort and an account, and a persona id, each a classification over a fixed list rather than free text. The
63
+ // id predates the persona half and stays as it is, since it is what a settings file already carries.
64
+ // Prompt carries live account headroom, so it must not be a model whose own allowance it is reading.
71
65
  id: "model-router",
72
- label: "Auto model choice",
73
- blurb: "Which model reads a new chat's first message and picks the model, effort and account it runs on.",
66
+ label: "New chat routing",
67
+ blurb: "Which model reads a new chat's first message and picks what it opens on: the model, effort and account, and the persona.",
74
68
  kind: "helper",
75
69
  icon: "sparkles",
76
70
  },
@@ -1,4 +1,4 @@
1
- import { expect, test } from "vitest";
1
+ import { test, expect } from "bun:test";
2
2
  import { bindingWindow, gatesModel, gatingWindows, scopedWindow, windowLive, windowPeriod } from "./plan-pools.js";
3
3
  import type { AccountUsage, UsageWindow } from "../schemas/providers/plan-limits.js";
4
4
 
@@ -1,5 +1,5 @@
1
1
  import { PROVIDER_BRAND_PATHS } from "@intentic/constants";
2
- import { describe, expect, test } from "vitest";
2
+ import { describe, test, expect } from "bun:test";
3
3
  import { accessFor, capabilitiesOf, harnessChoosable, PROVIDER_ACCESS, PROVIDER_VENDOR, PROVIDERS, providerLabel } from "./agent-catalog.js";
4
4
  import {
5
5
  ACCESS_COST,
@@ -40,12 +40,12 @@ describe("every provider in the table", () => {
40
40
  if (spec.auth.kind === "translator") {
41
41
  // The CLIProxyAPI's own name for this provider, addressed by the daemon's management API.
42
42
  expect(cliProxyIdOf(id), `${id} is routed but names no CLIProxyAPI provider`).toBe(spec.auth.cliProxy);
43
- expect(KeyedProviderSchema.options, `${id} is routed but missing from KeyedProvider`).toContain(id);
43
+ expect<readonly string[]>(KeyedProviderSchema.options, `${id} is routed but missing from KeyedProvider`).toContain(id);
44
44
  expect(Object.keys(TranslatorAccountsSchema.shape), `${id} has no slot in TranslatorAccounts`).toContain(id);
45
45
  return;
46
46
  }
47
47
  if (spec.auth.kind === "minted") {
48
- expect(MINTED_PROVIDERS, `${id} is minted but missing from MINTED_PROVIDERS`).toContain(id);
48
+ expect<readonly string[]>(MINTED_PROVIDERS, `${id} is minted but missing from MINTED_PROVIDERS`).toContain(id);
49
49
  // The head of this list is what a login/start naming no variant gets; empty means a dead connect button.
50
50
  expect(spec.auth.variants.length, `${id} is minted but offers no estate to sign in to`).toBeGreaterThan(0);
51
51
  for (const variant of spec.auth.variants) {
@@ -1,5 +1,5 @@
1
1
  import { STATE_DIR } from "@intentic/constants";
2
- import { describe, expect, it } from "vitest";
2
+ import { describe, it, expect } from "bun:test";
3
3
 
4
4
  // Stands in for any role: what's tested is that the role reaches the turn, not which one it is.
5
5
  const ROLE = `maintenance-chore` as const;
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { describe, test, expect } from "bun:test";
2
2
  import { classifyCommand, type CommandContext, type CommandSpan, matchCommand } from "./command-classes.js";
3
3
 
4
4
  // Every call states a locus (now required); these default to `device`, the wider reading, so an assertion made here
@@ -475,7 +475,7 @@ describe("live", () => {
475
475
 
476
476
  test("a command substitution inside an echo is not text", () => {
477
477
  expect(live(`echo "$(rm -rf /)"`, "files.destructive")).toBe(true);
478
- expect(live("echo \"`rm -rf /`\"", "files.destructive")).toBe(true);
478
+ expect(live('echo "`rm -rf /`"', "files.destructive")).toBe(true);
479
479
  expect(live(`echo '$(rm -rf /)'`, "files.destructive")).toBe(false);
480
480
  });
481
481
 
@@ -485,3 +485,38 @@ describe("live", () => {
485
485
  }
486
486
  });
487
487
  });
488
+
489
+ // The one class whose answer depends on WHOSE checkout would move. A conversation working in a copy of its own may
490
+ // stand wherever it likes — the drift that causes is reported at turn-settled, not refused here — but a turn sharing
491
+ // the owner's tree would move the owner's checkout under them, which is not its call to make.
492
+ describe("git.branch-switch", () => {
493
+ const shared = (command: string): string[] => classify(command);
494
+ const own = (command: string): string[] => classify(command, { ownCheckout: true });
495
+
496
+ test("catches both spellings of moving a shared checkout", () => {
497
+ for (const command of ["git checkout main", "git checkout -b ci/extension-admission", "git switch main", "git switch -c feature"]) {
498
+ expect(shared(command), command).toContain("git.branch-switch");
499
+ }
500
+ });
501
+
502
+ test("the conversation's own copy is its own to move", () => {
503
+ for (const command of ["git checkout main", "git switch -c feature"]) {
504
+ expect(own(command), command).not.toContain("git.branch-switch");
505
+ }
506
+ });
507
+
508
+ // Absent, not false: a caller that has not been taught the difference must get the guard rather than a hole.
509
+ test("a caller that cannot say whose checkout it is gets the guard", () => {
510
+ expect(classify("git checkout main", {})).toContain("git.branch-switch");
511
+ });
512
+
513
+ test("checkout restoring files moves nothing, and is not this", () => {
514
+ for (const command of ["git checkout -- src/app.ts", "git checkout .", "git checkout HEAD -- package.json"]) {
515
+ expect(shared(command), command).not.toContain("git.branch-switch");
516
+ }
517
+ });
518
+
519
+ test("the card points at the verb, not the whole line", () => {
520
+ expect(marked("cd repo && git switch main && pnpm build", "git.branch-switch")).toEqual(["git switch"]);
521
+ });
522
+ });
@@ -28,6 +28,10 @@ export interface CommandContext {
28
28
  readonly locus: CommandLocus;
29
29
  // true/false/undefined for holding a credential; only false drops a class, undefined still counts as a hit.
30
30
  readonly holdsSecret?: (path: string) => boolean | undefined;
31
+ // Whether this cwd is the conversation's OWN copy, which only an isolated turn has. Absent means the tree is shared
32
+ // with its owner, the fail-safe reading: a caller that has not been taught the difference gets the guard, not a
33
+ // hole. Read only by git.branch-switch, whose whole question is whose checkout would move.
34
+ readonly ownCheckout?: boolean;
31
35
  }
32
36
 
33
37
  // The `g` twin of a table's patterns, built once: the tables stay flag-free since a surviving lastIndex is how a shared
@@ -63,6 +67,10 @@ const GIT_DESTRUCTIVE = [
63
67
  /\bgit\s+filter-branch\b/,
64
68
  ];
65
69
 
70
+ // `switch` exists only to move HEAD, so every form of it counts. `checkout` is two commands wearing one name: with
71
+ // `-- <path>` or a bare `.` it restores files and moves nothing, and those are the common ones, so they are cut out.
72
+ const GIT_BRANCH_SWITCH = [/\bgit\s+switch\b/, /\bgit\s+checkout\b(?![^|;&]*\s--\s)(?![^|;&]*\s\.(?:\s|$))/];
73
+
66
74
  // The credential is IN the command, not in a file; there is nothing for a filesystem to fact-check here.
67
75
  const SECRET_REFERENCES = [/\{\{secret:[A-Za-z0-9_./-]+\}\}/];
68
76
 
@@ -251,6 +259,7 @@ const rootDeletes = (program: string, locus: CommandLocus): CommandSpan[] => [
251
259
 
252
260
  // The g twins, built once at load: a card is minted per command held, classify runs per command typed.
253
261
  const GIT_DESTRUCTIVE_G = globally(GIT_DESTRUCTIVE);
262
+ const GIT_BRANCH_SWITCH_G = globally(GIT_BRANCH_SWITCH);
254
263
  const SECRET_REFERENCES_G = globally(SECRET_REFERENCES);
255
264
  const CREDENTIAL_PATHS_G = globally(CREDENTIAL_PATHS);
256
265
  const PACKAGE_PUBLISH_G = globally(PACKAGE_PUBLISH);
@@ -298,6 +307,8 @@ const credentialReads = (command: string, context: CommandContext): CommandSpan[
298
307
  // One function per class; empty means the command is not in it, so membership and evidence are the same walk.
299
308
  const MATCHES: Readonly<Record<CommandClass, (command: string, context: CommandContext) => CommandSpan[]>> = {
300
309
  "git.destructive": (command) => spansOf(GIT_DESTRUCTIVE_G, command),
310
+ // The only class that can answer "not here": in its own copy a conversation's checkout is its own to move.
311
+ "git.branch-switch": (command, context) => (context.ownCheckout === true ? [] : spansOf(GIT_BRANCH_SWITCH_G, command)),
301
312
  "files.destructive": (command) => [...recursiveForceRms(command), ...recursiveDeletes(command)],
302
313
  "system.destructive": (command, context) => [...spansOf(BLOCK_DEVICE_G, command), ...rootDeletes(command, context.locus)],
303
314
  "container.state": (command) => spansOf(CONTAINER_STATE_G, command),
@@ -323,6 +334,7 @@ export const classifyCommand = (command: string, context: CommandContext): Comma
323
334
  // What the card says the command would do. The class name is a settings key, not a sentence to show a person.
324
335
  export const COMMAND_CLASS_LABELS: Readonly<Record<CommandClass, string>> = {
325
336
  "git.destructive": "rewrite or discard git history",
337
+ "git.branch-switch": "move a checkout you share with this conversation onto another branch",
326
338
  "files.destructive": "delete files recursively",
327
339
  "system.destructive": "wipe a disk, or delete a whole root directory",
328
340
  "container.state": "delete a container volume or the data in it",
@@ -350,6 +362,10 @@ export const COMMAND_CLASS_PATTERNS: Readonly<Record<CommandClass, readonly Comm
350
362
  { code: "git branch -D" },
351
363
  { code: "git filter-branch" },
352
364
  ],
365
+ "git.branch-switch": [
366
+ { code: "git switch <branch>" },
367
+ { code: "git checkout <branch>", qualifier: "not `git checkout -- <path>` or `git checkout .`, which restore files and move nothing" },
368
+ ],
353
369
  "files.destructive": [
354
370
  { code: "rm -rf <path>" },
355
371
  { code: "fs.rm(<path>, { recursive: true })", qualifier: "also rmSync, rmdir, rmdirSync" },