@intentic/sandbox-contract 1.239.0 → 1.242.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 +483 -1
  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 +99 -101
  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 +211 -1
  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 +277 -1
  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 +2947 -991
  103. package/dist/events.d.ts.map +1 -1
  104. package/dist/events.js +233 -82
  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 +2532 -1054
  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 +12 -0
  134. package/dist/plan-pools.d.ts.map +1 -0
  135. package/dist/plan-pools.js +30 -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 +31 -5
  173. package/dist/schemas/computers.d.ts.map +1 -1
  174. package/dist/schemas/computers.js +7 -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 +508 -198
  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 +82 -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 +52 -11
  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
@@ -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.
@@ -1,21 +1,47 @@
1
1
  import { z } from "zod";
2
+ import { TRANSLATOR_PROVIDERS, type TranslatorProvider } from "../provider-specs.js";
2
3
  import { AgentHarnessSchema, AgentProviderSchema, EditorContextSchema } from "./agent.js";
3
4
  // Declared ABOVE both account shapes because both carry it: headroom is one idea in this product, not a Claude
4
5
  // idea that other providers imitate. A native account (OauthAccount) and a routed subscription
5
6
  // (TranslatorAccount) differ in who holds the credential and how the reading is taken, never in what a
6
7
  // reading IS, so every surface that draws a percentage reads this one type and no other.
7
8
 
9
+ /* WHICH MODELS A POOL GATES, said by the reader that parsed it and carried on the wire, so every surface and
10
+ * every picker answers "does this pool stand between me and THIS model" from one fact instead of six rules.
11
+ *
12
+ * A plan is not one allowance. Google meters Gemini separately from the Claude and GPT models it serves off the
13
+ * same sign-in; a Claude plan carries a per-model weekly slice ("Opus", "Fable") beside its all-models pools;
14
+ * ChatGPT publishes a code-review limit that no chat turn spends. Before this field the relation lived in six
15
+ * places that disagreed (a static Google table, a Claude kind list, a max-over-everything ranking, a fullest-
16
+ * window reset, and two client-side matchers), so a Google account with its Gemini pool spent drew a red ring
17
+ * over Claude Opus, and a Claude account with its Opus slice spent was ranked "spent" for a Haiku call.
18
+ *
19
+ * "all" every model on this plan spends it: the 5-hour and weekly pools, Kimi's throttles.
20
+ * "none" nothing a turn here runs spends it: a code-review limit, a surface-scoped pool for another
21
+ * product. Still shown, never binding.
22
+ * { models } a slice scoped to the models the names match. Names are matched as runs of whole words against
23
+ * the model id AND its label ("opus" is in "claude-opus-4-6" and in "Claude Opus 4.6"; "gemini"
24
+ * is in "gemini-3-pro"), see plan-pools.ts, because the plan names a pool by the vendor's word for
25
+ * the tier and nothing else connects the two.
26
+ *
27
+ * Decided by the READER, never inferred later: the payload is the only place the grouping is known, and it is
28
+ * the plan's to change, so the fact travels with the reading it describes. */
29
+ export const WindowGatesSchema = z.union([z.literal("all"), z.literal("none"), z.object({ models: z.array(z.string().min(1)).min(1) })]);
30
+ export type WindowGates = z.infer<typeof WindowGatesSchema>;
31
+
8
32
  // One plan-limit pool. `kind` is the provider's own key ('five_hour' | 'seven_day' | 'seven_day_opus' |
9
33
  // 'seven_day_sonnet' | 'model:Fable' | …) rather than an enum we'd have to keep in step with the provider: an
10
34
  // unrecognised pool is shown under its raw key, which is far better than being silently folded into a
11
35
  // neighbour. `label` is the provider's OWN display name where it supplies one (the per-model buckets do), it
12
36
  // wins over anything we'd infer, because the model names in a plan's limits are the provider's to rename.
13
- // `resetsAt` is epoch SECONDS (matching the SDK's frame).
37
+ // `resetsAt` is epoch SECONDS (matching the SDK's frame). `gates` says which models the pool stands in the way
38
+ // of, see WindowGatesSchema.
14
39
  export const UsageWindowSchema = z.object({
15
40
  kind: z.string(),
16
41
  label: z.string().optional(),
17
42
  utilization: z.number(), // 0-100
18
43
  resetsAt: z.number().optional(),
44
+ gates: WindowGatesSchema,
19
45
  });
20
46
  export type UsageWindow = z.infer<typeof UsageWindowSchema>;
21
47
  // An account's headroom: EVERY window the provider reports, read together, plus when the reading was taken.
@@ -69,6 +95,10 @@ export const ProviderRefusalSchema = z.object({
69
95
  message: z.string().describe("The provider's own words, verbatim. The only part that says which limit or which credential."),
70
96
  // The account that was serving, when the daemon knows it (native turns only, see above).
71
97
  account: z.string().optional().describe("Which account was serving, where that is known."),
98
+ // The model the refused turn ran, so a `limit` refusal can be read against the POOL that model spends
99
+ // (UsageWindow.gates) rather than against the account's fullest pool, which on a plan that meters models
100
+ // separately is routinely a different allowance from the one that said no.
101
+ model: z.string().optional().describe("Which model the refused turn was on, where that is known."),
72
102
  });
73
103
  export type ProviderRefusal = z.infer<typeof ProviderRefusalSchema>;
74
104
  export const ProviderRefusalsSchema = z.object({
@@ -89,18 +119,35 @@ export const TranslatorAccountSchema = z.object({
89
119
  // or one that did not answer, must still render as the connected account it is, with a dot instead of a
90
120
  // ring.
91
121
  usage: AccountUsageSchema.optional(),
122
+ /* THE TRANSLATOR'S OWN VERDICT ON THE CREDENTIAL, the one live fact no quota read can produce. CLIProxyAPI
123
+ * benches an auth file the moment upstream refuses it (a quota 429, an expired token) and routes around it
124
+ * until `until`; a reading taken five minutes ago cannot know that, and a green ring over a benched file is
125
+ * exactly the gap a refusal used to be the only way to see. Absent ⇒ the proxy is routing to it. */
126
+ cooling: z
127
+ .object({
128
+ // Epoch SECONDS, like every reset on this wire. Absent when the proxy named no retry instant.
129
+ until: z.number().optional(),
130
+ // The proxy's own sentence, when it gave one.
131
+ reason: z.string().optional(),
132
+ })
133
+ .optional(),
92
134
  });
93
135
  export type TranslatorAccount = z.infer<typeof TranslatorAccountSchema>;
94
- // Which routed-provider subscriptions are connected in the translator, per provider, a LIST per provider, not
95
- // a flag: CLIProxyAPI holds any number of auth files per provider side by side and balances requests across
96
- // them, so connecting a second ChatGPT or Google account is more headroom, and each is disconnectable on its
97
- // own. Drives the account rows in Sandbox ▸ Agent.
98
- export const TranslatorAccountsSchema = z.object({
99
- codex: z.array(TranslatorAccountSchema),
100
- grok: z.array(TranslatorAccountSchema),
101
- kimi: z.array(TranslatorAccountSchema),
102
- gemini: z.array(TranslatorAccountSchema),
103
- });
136
+ /* Which routed-provider subscriptions are connected in the translator, per provider, a LIST per provider, not
137
+ * a flag: CLIProxyAPI holds any number of auth files per provider side by side and balances requests across
138
+ * them, so connecting a second ChatGPT or Google account is more headroom, and each is disconnectable on its
139
+ * own. Drives the account rows in Sandbox ▸ Agent.
140
+ *
141
+ * ONE KEY PER TRANSLATOR PROVIDER, built over the derived list rather than typed out, because this object and
142
+ * the enum beside it (KeyedProviderSchema) are the same fact twice and the browser reads a provider's slot
143
+ * without checking it exists: a provider present in the enum and missing here is `undefined.length`, at the
144
+ * exact moment somebody is asking whether they can send. */
145
+ export const TranslatorAccountsSchema = z.object(
146
+ Object.fromEntries(TRANSLATOR_PROVIDERS.map((provider) => [provider, z.array(TranslatorAccountSchema)] as const)) as Record<
147
+ TranslatorProvider,
148
+ z.ZodArray<typeof TranslatorAccountSchema>
149
+ >,
150
+ );
104
151
  export type TranslatorAccounts = z.infer<typeof TranslatorAccountsSchema>;
105
152
  // The side-channel body that un-parks a turn waiting on the user. Every interactive card, plan approval,
106
153
  // clarifying questions, a per-tool permission prompt, parks on the SAME registry keyed by `requestId`, so
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { TRANSLATOR_PROVIDERS, type TranslatorProvider } from "../provider-specs.js";
2
3
  // The providers whose model can run UNDER the Claude Code harness through the bundled translator (CLIProxyAPI),
3
4
  // which holds their SUBSCRIPTION OAuth and re-serves it behind an Anthropic endpoint. The `claude` provider is
4
5
  // absent, native Anthropic OAuth serves it directly, without the translator. Codex, Grok and Gemini also have a
@@ -7,5 +8,9 @@ import { z } from "zod";
7
8
  // Gemini is in BOTH camps and that is not a contradiction: its native runtime (OpenCode) reaches Google through
8
9
  // this same translator and these same auth files. The harness axis picks the loop; the translator is the road
9
10
  // under either.
10
- export const KeyedProviderSchema = z.enum(["codex", "grok", "kimi", "gemini"]);
11
- export type KeyedProvider = z.infer<typeof KeyedProviderSchema>;
11
+ //
12
+ // DERIVED from the provider table's `auth.kind`, never listed here: this enum and the accounts schema built on
13
+ // it (TranslatorAccountsSchema) and the daemon's CLIProxyAPI id map are three readings of one fact, and they
14
+ // used to be three hand-kept lists that had to agree.
15
+ export const KeyedProviderSchema = z.enum(TRANSLATOR_PROVIDERS);
16
+ export type KeyedProvider = TranslatorProvider;
@@ -1,6 +1,6 @@
1
1
  // settings: per-sandbox agent settings (.intentic/config/settings.json)
2
2
  import { z } from "zod";
3
- import { AdmissionPolicySchema, AdmissionRuleSchema, CommandClassSchema } from "./agent.js";
3
+ import { AdmissionPolicySchema, AdmissionRuleSchema, AgentRunPinSchema } from "./agent.js";
4
4
  // Which prompt the agent is, before this turn composes anything on top. Two built-in bases and an escape
5
5
  // hatch: Intentic's own (the default), Claude Code's preset, or the owner's text. Declared out here rather
6
6
  // than inline in the settings object because both sides of the wire branch on it, the daemon to build the
@@ -27,9 +27,14 @@ export type DependencyFreshness = z.infer<typeof DependencyFreshnessSchema>;
27
27
  * chores already wake on those and folding them into this table later must not mean renaming what users wrote.
28
28
  */
29
29
 
30
- // WHERE a rule can stand. Three, and each is a place the daemon already stopped to make a decision, this
30
+ // WHERE a rule can stand. Four, and each is a place the daemon already stopped to make a decision, this
31
31
  // names those decisions rather than inventing new ones.
32
32
  export const RuleMomentSchema = z.enum([
33
+ // The assistant has just written a file, with an edit tool or with a shell command; the tree says which. A
34
+ // command here runs on that one file (`{file}` in the command is its path) and what it prints on a non-zero
35
+ // exit rides back with the edit's own result, while the file is still in mind. The cheapest moment a defect
36
+ // can be caught at, and the one the per-edit linter and byte scan stand at.
37
+ "file.edited",
33
38
  // The assistant is about to stop. A rule here can send it back to work, which is the only moment that can.
34
39
  "turn.ending",
35
40
  // Code is about to leave the machine. A rule here gates the push on its own exit code.
@@ -56,8 +61,10 @@ export type RuleMoment = z.infer<typeof RuleMomentSchema>;
56
61
  * `verify-removals` weighs what a turn DELETED against what the repository's history says about those lines,
57
62
  * which is a question `git log` answers and no shell one-liner an owner could type would, and
58
63
  * `verify-ui-edits` weighs the rendered surfaces a turn changed against whether it ever looked at one, the one
59
- * question a passing suite is structurally unable to answer. */
60
- export const RuleBuiltinSchema = z.enum(["verify-edits", "verify-removals", "verify-ui-edits"]);
64
+ * question a passing suite is structurally unable to answer, and `verify-tests` weighs the test files a turn
65
+ * touched against two things a green suite cannot say: whether their assertions got weaker than the same files
66
+ * at HEAD, and whether a test the turn wrote would have passed before the change it covers. */
67
+ export const RuleBuiltinSchema = z.enum(["verify-edits", "verify-removals", "verify-ui-edits", "verify-tests"]);
61
68
  export type RuleBuiltin = z.infer<typeof RuleBuiltinSchema>;
62
69
  export const RuleActionSchema = z.discriminatedUnion("kind", [
63
70
  z.object({
@@ -73,6 +80,13 @@ export const RuleActionSchema = z.discriminatedUnion("kind", [
73
80
  z.object({ kind: z.literal("builtin"), name: RuleBuiltinSchema }),
74
81
  ]);
75
82
  export type RuleAction = z.infer<typeof RuleActionSchema>;
83
+ /* HOW A TURN ENDED, as the `agent.finished` moment sees it. `clean`, `error` and `conflict` are the turn's own
84
+ * fate. `checks-failed` is a clean turn whose `turn.ending` command check went red on the tree it is about to
85
+ * land: the model was told, answered, and the last run still failed. It is named separately because a landing
86
+ * rule has to be able to speak about it, and because its default is not the others': work in that state is HELD
87
+ * unless a rule names `checks-failed` and says allow (rules/rules.ts landingVerdict). */
88
+ export const RuleOutcomeSchema = z.enum(["clean", "error", "conflict", "checks-failed"]);
89
+ export type RuleOutcome = z.infer<typeof RuleOutcomeSchema>;
76
90
  // WHEN a rule narrows. Three keys, chosen because they cover the two things people reach for on day one,
77
91
  // "only this repo" and "don't bother for a docs-only change", without opening a query language. Every key
78
92
  // absent ⇒ the rule always matches at its moment, which is what the three replaced settings each did.
@@ -81,8 +95,8 @@ export const RuleConditionSchema = z.object({
81
95
  repo: z.string().min(1).optional(),
82
96
  // Globs the change has to touch for the rule to fire. Absent/empty ⇒ any.
83
97
  paths: z.array(z.string().min(1)).max(20).optional(),
84
- // How the turn ended. Absent/empty ⇒ any.
85
- outcome: z.array(z.enum(["clean", "error", "conflict"])).optional(),
98
+ // How the turn ended. Absent/empty ⇒ any, except that `checks-failed` never lands by omission.
99
+ outcome: z.array(RuleOutcomeSchema).optional(),
86
100
  });
87
101
  export type RuleCondition = z.infer<typeof RuleConditionSchema>;
88
102
  /* ONE RULE. `id` is stable and owner-visible: it is what the activity feed names when the rule fires and what
@@ -91,8 +105,10 @@ export type RuleCondition = z.infer<typeof RuleConditionSchema>;
91
105
  * WHICH ACTIONS FIT WHICH MOMENT is checked here rather than left to the consumer, because the alternative is
92
106
  * a rule that saves cleanly and then quietly does nothing, the failure mode a settings screen can least
93
107
  * afford. A verdict at `turn.ending` has nothing to decide; a command at `agent.finished` has no defined place
94
- * in the landing pass and would be a promise this stage cannot keep. */
108
+ * in the landing pass and would be a promise this stage cannot keep; an instruction at `file.edited` would be
109
+ * repeated on every save, which is noise by the third one. */
95
110
  const MOMENT_ACTIONS: Record<RuleMoment, readonly RuleAction["kind"][]> = {
111
+ "file.edited": ["command"],
96
112
  "turn.ending": ["builtin", "instruct", "command"],
97
113
  "push.starting": ["command"],
98
114
  "agent.finished": ["verdict"],
@@ -418,42 +434,9 @@ export const SandboxSettingsSchema = z.object({
418
434
  *
419
435
  * Off by default like every other flag here, and off means genuinely nothing: no hook is wired, and the
420
436
  * sandbox makes no network call it would not otherwise have made. */
421
- dependencyFreshness: DependencyFreshnessSchema.default("off")
422
- .describe(
423
- "Whether a version the agent is about to pin is checked against the package's own registry first. Facts only, or facts plus the name of a maintained replacement where the registry agrees the current choice has been abandoned. It tells the agent and lets it decide rather than refusing, because matching a version your project already uses is usually the right answer and a gate would fight it.",
424
- ),
425
- /* DOES THE TEST IT JUST WROTE ACTUALLY TEST THE CHANGE?
426
- *
427
- * A model writes a test that passes, and a passing test is the thing it was asked for, so nothing in the loop
428
- * objects. The failure this catches is the one where the test would have passed BEFORE the change too: it
429
- * exercises the code without depending on the behaviour that was added, and it will keep passing when that
430
- * behaviour is broken later. It type-checks, it lints, `pnpm verify` is green, and the suite has grown a test
431
- * that can never fail.
432
- *
433
- * The check is a differential, and it is cheap for one specific reason: in an agent turn we already know
434
- * exactly which source files changed. So the test is re-run with those files' HEAD contents served in place
435
- * of the working copies, and the answer is one bit — did it still pass? A test that passes against the old
436
- * code did not test the new code. This is one-mutant mutation testing where the mutant is "your change,
437
- * reverted", which is the cheapest useful mutant there is and the only one already known for free.
438
- *
439
- * NOTHING IS WRITTEN TO THE TREE. The HEAD copies are served through a vite `load` hook in a generated config,
440
- * never checked out over the working files — a hook that could leave a half-reverted tree behind on a crash
441
- * would be trading a whole turn's work for a lint-grade signal.
442
- *
443
- * IT REPORTS AND GETS OUT OF THE WAY, for the same reason the freshness check above does. Two legitimate
444
- * cases pass on HEAD and must not be fought: a test written BEFORE its implementation (it fails now, which
445
- * the agent can see without help), and a pure refactor, where a test that keeps passing is the entire point.
446
- * Distinguishing those from a weak test needs intent, so the fact rides back as context and the model decides.
447
- *
448
- * Scoped to source changed in the SAME package as the test, because that is what a package's own vitest run
449
- * loads from source; a cross-package import resolves to the other package's built output, where there is
450
- * nothing to swap. Off by default like every flag here, and off wires no hook at all. */
451
- testFaultDetection: z
452
- .boolean()
453
- .default(false)
454
- .describe(
455
- "After the agent writes a test, re-run it against the code as it was before this turn's changes. A test that still passes did not test the change — it will pass just as happily when that behaviour breaks. Reports what it finds and lets the agent decide, because a test written before its implementation and a pure refactor both pass this honestly.",
456
- ),
437
+ dependencyFreshness: DependencyFreshnessSchema.default("off").describe(
438
+ "Whether a version the agent is about to pin is checked against the package's own registry first. Facts only, or facts plus the name of a maintained replacement where the registry agrees the current choice has been abandoned. It tells the agent and lets it decide rather than refusing, because matching a version your project already uses is usually the right answer and a gate would fight it.",
439
+ ),
457
440
  outputCleaners: z
458
441
  .string()
459
442
  .default("")
@@ -507,11 +490,17 @@ export const SandboxSettingsSchema = z.object({
507
490
  ),
508
491
  /* WHAT AN AGENT RUN OPENS ON, the tier above quickModel, and the answer for every turn a SURFACE starts
509
492
  * rather than a person at a composer: Fix with agent on a pipeline or a deployment, a Maintenance chore, a
510
- * Documentation or Acceptance run, the fix a failed pre-push check proposes. An ORDERED list of
511
- * `${provider}:${model}` (quickModelKey) plus the reasoning effort beside it; EMPTY ⇒ whatever the chat
493
+ * Documentation or Acceptance run, the fix a failed pre-push check proposes. An ORDERED list of PINS, each
494
+ * naming a provider and model AND how that one is to be run (AgentRunPinSchema); EMPTY ⇒ whatever the chat
512
495
  * composer would have started with, which is the honest floor because it is the model the user already
513
496
  * chose to work with.
514
497
  *
498
+ * EACH ENTRY CARRIES ITS OWN REASONING AND COST KNOBS, which is why these are objects rather than the
499
+ * `${provider}:${model}` keys the two lists around them still hold. The effort used to be one field beside
500
+ * the list, answering for every model in it, and the entries of this list are the least interchangeable
501
+ * things on the page: the head is the tier the owner wants the work done at and what follows it is the
502
+ * account that catches it when the first is spent. AgentRunPinSchema has the rest of the argument.
503
+ *
515
504
  * A LIST, for the reason quickModel is one: the account at the head runs out, and every surface-started run
516
505
  * in the sandbox then fails on a credential the user cannot see from the row they pressed. Written in order,
517
506
  * the next one down catches it (turn-resume.ts walks it).
@@ -527,13 +516,12 @@ export const SandboxSettingsSchema = z.object({
527
516
  * surface added tomorrow inherits it by saying what it is instead of re-deriving where models come from. A
528
517
  * surface MAY still name one (the shared run button's caret, Acceptance's per-run pick), and that wins. */
529
518
  agentRunModels: z
530
- .array(z.string())
519
+ .array(AgentRunPinSchema)
531
520
  .max(10)
532
521
  .default([])
533
522
  .describe(
534
- "Which models run the work a screen starts rather than a person: fixing a red pipeline, a maintenance chore, an acceptance run. Tried in order, so one spent account does not take every such run down. Empty falls back to whatever the chat would have used, which is the honest floor because it is the model you already chose to work with.",
523
+ "Which models run the work a screen starts rather than a person: fixing a red pipeline, a maintenance chore, an acceptance run. Tried in order, so one spent account does not take every such run down, and each entry says how hard that model should think as well as which one it is. Empty falls back to whatever the chat would have used, which is the honest floor because it is the model you already chose to work with.",
535
524
  ),
536
- agentRunEffort: z.string().default("").describe("How hard those runs should think."),
537
525
  /* AUTOMATIC TIER SELECTION: may the daemon run an easy-looking turn on a cheaper rung of the provider the
538
526
  * user is already on, instead of on the model they picked?
539
527
  *
@@ -630,6 +618,25 @@ export const SandboxSettingsSchema = z.object({
630
618
  .describe(
631
619
  "Whether a turn killed by the model provider failing is re-run automatically, backing off between attempts. The sandbox-wide default; any one conversation can say otherwise. Off to begin with, because a retry spends your allowance on a turn you sent once and only you can say whether it was worth paying for twice. Worth turning on for a sandbox whose work mostly happens with nobody in the room.",
632
620
  ),
621
+ /* THE SANDBOX-WIDE DEFAULT for the blocker that is not a failure: "when a spent usage limit refuses a turn,
622
+ * send that turn again the moment the allowance comes back".
623
+ *
624
+ * The one automatic resume here that WAITS FOR A PUBLISHED INSTANT rather than guessing. Its neighbour above
625
+ * escalates a backoff at a provider nobody can predict; this one sleeps until the hour the provider itself
626
+ * named and fires once, at it. A limit that publishes no instant (Grok, Cursor) is never fired for at all,
627
+ * because there is nothing to wait for and a guess would be spending the user's money on arithmetic.
628
+ *
629
+ * OFF by default, and this is the setting the default matters most for. Every other blocker in this pair
630
+ * clears at nobody's expense, while this one clears into an allowance the user may have been holding back
631
+ * deliberately, so the shipped behaviour is to say when it reopens and let them decide. What arming it buys
632
+ * is the case a press cannot reach: the 2am wall on a board nobody is watching, where the alternative is a
633
+ * card that sat waiting eight hours for a press that was always going to come. */
634
+ resumeAfterLimit: z
635
+ .boolean()
636
+ .default(false)
637
+ .describe(
638
+ "Whether a turn a spent usage limit refused is sent again by itself once the allowance reopens. The sandbox-wide default; any one conversation can say otherwise. Off to begin with, because the allowance is your budget and a turn that spends it the second it comes back is not a decision to make for you. Worth turning on for a sandbox whose work mostly happens with nobody in the room.",
639
+ ),
633
640
  /* When the daemon dies under a running turn, re-run that turn once it is back (agent/turn-journal.ts records
634
641
  * every in-flight turn; the boot pass in agent/turn-resume.ts re-runs what survived). OFF by default, like
635
642
  * the outage resume above and for the same reason: a boot that re-runs turns spends the user's allowance on
@@ -697,7 +704,7 @@ export const SandboxSettingsSchema = z.object({
697
704
  * `<provider>.<type>` ("discord.message.send") with `<provider>.*` as the per-provider wildcard; exact key
698
705
  * wins. An action with no rule is allowed, the empty default wires no hook at all, so an unconfigured
699
706
  * workspace pays nothing. "hold" cannot park a running turn (nobody may be there to answer); it refuses the
700
- * live call and points the agent at the drafts outbox, which IS the held form of a send.
707
+ * live call and points the agent at the approvals queue, which IS the held form of a send.
701
708
  *
702
709
  * The CHILD-AGENT surface reads the same book: `agents.spawn` covers starting, steering and answering
703
710
  * child agents on every provider, `agents.spawn.<provider>` singles one out (the specific key wins), and
@@ -708,50 +715,17 @@ export const SandboxSettingsSchema = z.object({
708
715
  .record(z.string(), AdmissionRuleSchema)
709
716
  .default({})
710
717
  .describe("What an agent may do out in the world, per kind of action: go ahead, ask first, or never."),
711
- /* THE COMMAND GATE'S RULEBOOK, a verdict per CommandClass, for shell commands the agent runs itself. This
712
- * is the layer that still applies once a session is already running: the admission floor above decides who
713
- * may wake the agent, and after that every command it types is inside one already-admitted session.
714
- *
715
- * "hold" means what it says here, unlike in actionRules: the gate raises a permission card and the command
716
- * waits for a real answer, in EVERY posture, hooks fire under bypassPermissions, where the card machinery
717
- * on its own never would. An UNATTENDED turn has nobody to answer, so a hold there refuses instead and says
718
- * why; that is the honest form of "ask me" when there is no me.
719
- *
720
- * An unlisted class is allowed, WITH ONE FLOOR UNDER IT: the classes nothing brings back (FLOOR_CLASSES in
721
- * command-classes.ts, `system.destructive` today) are held where the owner wrote nothing, so a workspace
722
- * that has never opened this page is not one mistyped path away from a formatted disk. An explicit `allow`
723
- * still wins, it is a decision about that exact class and the floor must not override the person who made
724
- * it. Everything else stays as it was: unlisted is allowed and ordinary work is never asked about.
725
- *
726
- * Keys are the CommandClass enum, so a typo is a settings error rather than a rule that silently never
727
- * matches. */
728
- commandRules: z
729
- .partialRecord(CommandClassSchema, AdmissionRuleSchema)
730
- .default({})
731
- .describe(
732
- "What an agent may run inside the sandbox, for the six kinds of command that are hard to take back: rewriting git history, deleting recursively, wiping a disk or a container volume, reading credential files, publishing a package, reaching out to the network. Everything else is recoverable in a container that is itself disposable, and gating it would be friction bought with nothing. Leaving a kind unset is not the same as allowing it: wiping a disk is held for your approval until you say otherwise, because nothing here brings that back.",
733
- ),
734
- /* WHETHER A HELD COMMAND IS TRANSLATED BEFORE YOU ANSWER FOR IT. Off by default, and the default is the
735
- * argument: the card is already complete without this, and switching it on spends one quick-model call per
736
- * card raised, on the owner's own connected account.
737
- *
738
- * It buys the case the card is worst at. A held command is regularly a hundred-plus characters of pipeline
739
- * the agent assembled, and the question the card actually asks, "do you want this to happen", is not
740
- * answerable by reading shell quickly. The classifier already marks WHICH fragment held it (the card paints
741
- * those); a sentence is what says what the rest of it does and why the agent wanted it.
742
- *
743
- * NEVER THE AGENT'S OWN WORDS. The sentence comes from the quick model reading the command text, not from
744
- * the model being gated: a card that let the asking agent write its own justification would be a safety
745
- * prompt whose persuasive half is authored by the thing it is meant to gate.
746
- *
747
- * The command itself is never replaced by it, only folded behind a disclosure the sentence sits above, and
748
- * the marked fragments stay on the card either way, see ChatMessageView's permission card. */
749
- explainCommands: z
750
- .boolean()
751
- .default(false)
752
- .describe(
753
- "Have the quick model describe each held command in one plain sentence, above the command itself. Costs one quick-model call per card raised, on your own connected account. The command is always still there to read.",
754
- ),
718
+ /* THE COMMAND GATE IS NOT CONFIGURED HERE. It used to be: `commandRules` was a verdict per CommandClass and
719
+ * `explainCommands` decided whether a card carried a sentence. Both are gone, and the reason is the whole
720
+ * safety redesign (safety-policy.ts argues it): a regex verdict per class asked about `echo "rm -rf /"` and
721
+ * an actual delete in the same words, and no setting of six switches fixes that, because telling the two
722
+ * apart is an act of understanding rather than a threshold. What replaced them is the owner's written
723
+ * policy at .intentic/config/safety.md, read by a judge that also sees what the daemon knows about the turn,
724
+ * plus one typed hard rule the judge cannot waive. The Safety page edits that document; nothing about the
725
+ * command gate belongs in this object.
726
+ *
727
+ * The sentence on a card is no longer optional either: it is the judge's own reason for the verdict, so a
728
+ * card without one would be a card that could not say why it exists. */
755
729
  /* HOW MUCH AN AGENT MAY DELEGATE, the three ceilings the Claude Code harness enforces on its own Agent
756
730
  * tool, surfaced here because their defaults are tuned for a laptop and this is a container the owner sized.
757
731
  *