@intentic/sandbox-contract 1.307.0 → 1.308.2

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 (260) hide show
  1. package/README.md +12 -0
  2. package/dist/contracts/accounts.contract.d.ts +6 -6
  3. package/dist/contracts/accounts.contract.js +7 -7
  4. package/dist/contracts/accounts.contract.js.map +1 -1
  5. package/dist/contracts/activity.contract.d.ts +2 -2
  6. package/dist/contracts/activity.contract.js +3 -3
  7. package/dist/contracts/activity.contract.js.map +1 -1
  8. package/dist/contracts/agent.contract.d.ts +10 -10
  9. package/dist/contracts/agent.contract.d.ts.map +1 -1
  10. package/dist/contracts/agent.contract.js +21 -11
  11. package/dist/contracts/agent.contract.js.map +1 -1
  12. package/dist/contracts/agents.contract.d.ts +41 -27
  13. package/dist/contracts/agents.contract.d.ts.map +1 -1
  14. package/dist/contracts/agents.contract.js +44 -28
  15. package/dist/contracts/agents.contract.js.map +1 -1
  16. package/dist/contracts/approvals.contract.d.ts +3 -3
  17. package/dist/contracts/approvals.contract.js +4 -4
  18. package/dist/contracts/approvals.contract.js.map +1 -1
  19. package/dist/contracts/areas.contract.d.ts +3 -3
  20. package/dist/contracts/areas.contract.d.ts.map +1 -1
  21. package/dist/contracts/areas.contract.js +5 -4
  22. package/dist/contracts/areas.contract.js.map +1 -1
  23. package/dist/contracts/automations.contract.d.ts +11 -11
  24. package/dist/contracts/automations.contract.js +12 -12
  25. package/dist/contracts/automations.contract.js.map +1 -1
  26. package/dist/contracts/capabilities.contract.d.ts +13 -13
  27. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  28. package/dist/contracts/capabilities.contract.js +18 -14
  29. package/dist/contracts/capabilities.contract.js.map +1 -1
  30. package/dist/contracts/chores.contract.d.ts +3 -3
  31. package/dist/contracts/chores.contract.js +4 -4
  32. package/dist/contracts/chores.contract.js.map +1 -1
  33. package/dist/contracts/ci.contract.d.ts +5 -5
  34. package/dist/contracts/ci.contract.js +6 -6
  35. package/dist/contracts/ci.contract.js.map +1 -1
  36. package/dist/contracts/diff.contract.d.ts +1 -1
  37. package/dist/contracts/diff.contract.js +2 -2
  38. package/dist/contracts/diff.contract.js.map +1 -1
  39. package/dist/contracts/endpoints.contract.d.ts +4 -4
  40. package/dist/contracts/endpoints.contract.js +5 -5
  41. package/dist/contracts/endpoints.contract.js.map +1 -1
  42. package/dist/contracts/exit.contract.d.ts +7 -7
  43. package/dist/contracts/exit.contract.d.ts.map +1 -1
  44. package/dist/contracts/exit.contract.js +9 -8
  45. package/dist/contracts/exit.contract.js.map +1 -1
  46. package/dist/contracts/extensions.contract.d.ts +17 -17
  47. package/dist/contracts/extensions.contract.js +18 -18
  48. package/dist/contracts/extensions.contract.js.map +1 -1
  49. package/dist/contracts/git.contract.d.ts +47 -45
  50. package/dist/contracts/git.contract.d.ts.map +1 -1
  51. package/dist/contracts/git.contract.js +46 -46
  52. package/dist/contracts/git.contract.js.map +1 -1
  53. package/dist/contracts/history.contract.d.ts +4 -4
  54. package/dist/contracts/history.contract.js +5 -5
  55. package/dist/contracts/history.contract.js.map +1 -1
  56. package/dist/contracts/intentic.contract.d.ts +3 -3
  57. package/dist/contracts/intentic.contract.d.ts.map +1 -1
  58. package/dist/contracts/intentic.contract.js +5 -4
  59. package/dist/contracts/intentic.contract.js.map +1 -1
  60. package/dist/contracts/inventory.contract.d.ts +3 -3
  61. package/dist/contracts/inventory.contract.js +4 -4
  62. package/dist/contracts/inventory.contract.js.map +1 -1
  63. package/dist/contracts/issues.contract.d.ts +5 -5
  64. package/dist/contracts/issues.contract.js +6 -6
  65. package/dist/contracts/issues.contract.js.map +1 -1
  66. package/dist/contracts/logs.contract.d.ts +3 -3
  67. package/dist/contracts/logs.contract.d.ts.map +1 -1
  68. package/dist/contracts/logs.contract.js +6 -4
  69. package/dist/contracts/logs.contract.js.map +1 -1
  70. package/dist/contracts/loops.contract.d.ts +6 -6
  71. package/dist/contracts/loops.contract.js +7 -7
  72. package/dist/contracts/loops.contract.js.map +1 -1
  73. package/dist/contracts/netdisk.contract.d.ts +3 -3
  74. package/dist/contracts/netdisk.contract.d.ts.map +1 -1
  75. package/dist/contracts/netdisk.contract.js +5 -4
  76. package/dist/contracts/netdisk.contract.js.map +1 -1
  77. package/dist/contracts/panels.contract.d.ts +3 -3
  78. package/dist/contracts/panels.contract.js +4 -4
  79. package/dist/contracts/panels.contract.js.map +1 -1
  80. package/dist/contracts/personas.contract.d.ts +8 -8
  81. package/dist/contracts/personas.contract.d.ts.map +1 -1
  82. package/dist/contracts/personas.contract.js +10 -9
  83. package/dist/contracts/personas.contract.js.map +1 -1
  84. package/dist/contracts/ports.contract.d.ts +3 -3
  85. package/dist/contracts/ports.contract.d.ts.map +1 -1
  86. package/dist/contracts/ports.contract.js +5 -4
  87. package/dist/contracts/ports.contract.js.map +1 -1
  88. package/dist/contracts/prepush.contract.d.ts +3 -3
  89. package/dist/contracts/prepush.contract.js +4 -4
  90. package/dist/contracts/prepush.contract.js.map +1 -1
  91. package/dist/contracts/providers.contract.d.ts +2 -2
  92. package/dist/contracts/providers.contract.d.ts.map +1 -1
  93. package/dist/contracts/providers.contract.js +5 -3
  94. package/dist/contracts/providers.contract.js.map +1 -1
  95. package/dist/contracts/public.contract.d.ts +3 -3
  96. package/dist/contracts/public.contract.js +4 -4
  97. package/dist/contracts/public.contract.js.map +1 -1
  98. package/dist/contracts/push.contract.d.ts +4 -4
  99. package/dist/contracts/push.contract.d.ts.map +1 -1
  100. package/dist/contracts/push.contract.js +9 -5
  101. package/dist/contracts/push.contract.js.map +1 -1
  102. package/dist/contracts/safety.contract.d.ts +3 -3
  103. package/dist/contracts/safety.contract.js +4 -4
  104. package/dist/contracts/safety.contract.js.map +1 -1
  105. package/dist/contracts/secrets.contract.d.ts +9 -9
  106. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  107. package/dist/contracts/secrets.contract.js +13 -10
  108. package/dist/contracts/secrets.contract.js.map +1 -1
  109. package/dist/contracts/sessions.contract.d.ts +2 -2
  110. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  111. package/dist/contracts/sessions.contract.js +5 -3
  112. package/dist/contracts/sessions.contract.js.map +1 -1
  113. package/dist/contracts/settings.contract.d.ts +11 -9
  114. package/dist/contracts/settings.contract.d.ts.map +1 -1
  115. package/dist/contracts/settings.contract.js +11 -10
  116. package/dist/contracts/settings.contract.js.map +1 -1
  117. package/dist/contracts/share.contract.d.ts +4 -4
  118. package/dist/contracts/share.contract.js +5 -5
  119. package/dist/contracts/share.contract.js.map +1 -1
  120. package/dist/contracts/skills.contract.d.ts +5 -5
  121. package/dist/contracts/skills.contract.js +6 -6
  122. package/dist/contracts/skills.contract.js.map +1 -1
  123. package/dist/contracts/system.contract.d.ts +22 -19
  124. package/dist/contracts/system.contract.d.ts.map +1 -1
  125. package/dist/contracts/system.contract.js +27 -20
  126. package/dist/contracts/system.contract.js.map +1 -1
  127. package/dist/contracts/translator.contract.d.ts +5 -5
  128. package/dist/contracts/translator.contract.js +6 -6
  129. package/dist/contracts/translator.contract.js.map +1 -1
  130. package/dist/contracts/usage.contract.d.ts +4 -4
  131. package/dist/contracts/usage.contract.js +5 -5
  132. package/dist/contracts/usage.contract.js.map +1 -1
  133. package/dist/contracts/vpn.contract.d.ts +4 -4
  134. package/dist/contracts/vpn.contract.d.ts.map +1 -1
  135. package/dist/contracts/vpn.contract.js +6 -5
  136. package/dist/contracts/vpn.contract.js.map +1 -1
  137. package/dist/contracts/workflows.contract.d.ts +9 -9
  138. package/dist/contracts/workflows.contract.js +10 -10
  139. package/dist/contracts/workflows.contract.js.map +1 -1
  140. package/dist/contracts/workspace.contract.d.ts +30 -30
  141. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  142. package/dist/contracts/workspace.contract.js +46 -31
  143. package/dist/contracts/workspace.contract.js.map +1 -1
  144. package/dist/events/land-breakage.d.ts +4 -0
  145. package/dist/events/land-breakage.d.ts.map +1 -0
  146. package/dist/events/land-breakage.js +4 -0
  147. package/dist/events/land-breakage.js.map +1 -0
  148. package/dist/events/system-events.d.ts +2 -0
  149. package/dist/events/system-events.d.ts.map +1 -1
  150. package/dist/index.d.ts +9379 -9350
  151. package/dist/index.d.ts.map +1 -1
  152. package/dist/index.js +13 -1
  153. package/dist/index.js.map +1 -1
  154. package/dist/protocol/procedure-types.d.ts +10 -0
  155. package/dist/protocol/procedure-types.d.ts.map +1 -0
  156. package/dist/protocol/procedure-types.js +2 -0
  157. package/dist/protocol/procedure-types.js.map +1 -0
  158. package/dist/protocol/raw-routes.d.ts +417 -0
  159. package/dist/protocol/raw-routes.d.ts.map +1 -0
  160. package/dist/protocol/raw-routes.js +140 -0
  161. package/dist/protocol/raw-routes.js.map +1 -0
  162. package/dist/protocol/route-meta.d.ts +18 -0
  163. package/dist/protocol/route-meta.d.ts.map +1 -0
  164. package/dist/protocol/route-meta.js +3 -0
  165. package/dist/protocol/route-meta.js.map +1 -0
  166. package/dist/protocol/routes.d.ts +3 -0
  167. package/dist/protocol/routes.d.ts.map +1 -1
  168. package/dist/protocol/routes.js +47 -14
  169. package/dist/protocol/routes.js.map +1 -1
  170. package/dist/schemas/agent.d.ts +33 -0
  171. package/dist/schemas/agent.d.ts.map +1 -1
  172. package/dist/schemas/agent.js +22 -3
  173. package/dist/schemas/agent.js.map +1 -1
  174. package/dist/schemas/agents.d.ts +4 -0
  175. package/dist/schemas/agents.d.ts.map +1 -1
  176. package/dist/schemas/agents.js +4 -0
  177. package/dist/schemas/agents.js.map +1 -1
  178. package/dist/schemas/automations.d.ts +1 -0
  179. package/dist/schemas/automations.d.ts.map +1 -1
  180. package/dist/schemas/devices.d.ts.map +1 -1
  181. package/dist/schemas/devices.js +2 -2
  182. package/dist/schemas/devices.js.map +1 -1
  183. package/dist/schemas/git/git.d.ts +3 -0
  184. package/dist/schemas/git/git.d.ts.map +1 -1
  185. package/dist/schemas/hosts.d.ts +1 -0
  186. package/dist/schemas/hosts.d.ts.map +1 -1
  187. package/dist/schemas/hosts.js +1 -0
  188. package/dist/schemas/hosts.js.map +1 -1
  189. package/dist/schemas/settings.d.ts +1 -0
  190. package/dist/schemas/settings.d.ts.map +1 -1
  191. package/dist/schemas/settings.js +4 -0
  192. package/dist/schemas/settings.js.map +1 -1
  193. package/dist/state/definition.d.ts +4 -0
  194. package/dist/state/definition.d.ts.map +1 -1
  195. package/dist/state/history-state.d.ts.map +1 -1
  196. package/dist/state/history-state.js +2 -12
  197. package/dist/state/history-state.js.map +1 -1
  198. package/dist/state/landed-commit.d.ts +4 -0
  199. package/dist/state/landed-commit.d.ts.map +1 -0
  200. package/dist/state/landed-commit.js +12 -0
  201. package/dist/state/landed-commit.js.map +1 -0
  202. package/package.json +5 -5
  203. package/src/contracts/accounts.contract.ts +7 -7
  204. package/src/contracts/activity.contract.ts +3 -3
  205. package/src/contracts/agent.contract.ts +25 -11
  206. package/src/contracts/agents.contract.ts +48 -30
  207. package/src/contracts/approvals.contract.ts +4 -4
  208. package/src/contracts/areas.contract.ts +5 -4
  209. package/src/contracts/automations.contract.ts +12 -12
  210. package/src/contracts/capabilities.contract.ts +23 -14
  211. package/src/contracts/chores.contract.ts +4 -4
  212. package/src/contracts/ci.contract.ts +6 -6
  213. package/src/contracts/diff.contract.ts +2 -2
  214. package/src/contracts/endpoints.contract.ts +5 -5
  215. package/src/contracts/exit.contract.ts +11 -8
  216. package/src/contracts/extensions.contract.ts +18 -18
  217. package/src/contracts/git.contract.ts +46 -46
  218. package/src/contracts/history.contract.ts +5 -5
  219. package/src/contracts/intentic.contract.ts +5 -4
  220. package/src/contracts/inventory.contract.ts +4 -4
  221. package/src/contracts/issues.contract.ts +6 -6
  222. package/src/contracts/logs.contract.ts +8 -4
  223. package/src/contracts/loops.contract.ts +7 -7
  224. package/src/contracts/netdisk.contract.ts +7 -4
  225. package/src/contracts/panels.contract.ts +4 -4
  226. package/src/contracts/personas.contract.ts +10 -9
  227. package/src/contracts/ports.contract.ts +6 -4
  228. package/src/contracts/prepush.contract.ts +4 -4
  229. package/src/contracts/providers.contract.ts +5 -3
  230. package/src/contracts/public.contract.ts +4 -4
  231. package/src/contracts/push.contract.ts +12 -5
  232. package/src/contracts/safety.contract.ts +4 -4
  233. package/src/contracts/secrets.contract.ts +16 -10
  234. package/src/contracts/sessions.contract.ts +6 -3
  235. package/src/contracts/settings.contract.ts +11 -10
  236. package/src/contracts/share.contract.ts +5 -5
  237. package/src/contracts/skills.contract.ts +6 -6
  238. package/src/contracts/system.contract.ts +32 -20
  239. package/src/contracts/translator.contract.ts +6 -6
  240. package/src/contracts/usage.contract.ts +5 -5
  241. package/src/contracts/vpn.contract.ts +8 -5
  242. package/src/contracts/workflows.contract.ts +10 -10
  243. package/src/contracts/workspace.contract.ts +49 -31
  244. package/src/events/land-breakage.ts +11 -0
  245. package/src/index.ts +23 -1
  246. package/src/models/capability-ledger.test.ts +1 -1
  247. package/src/protocol/procedure-types.ts +24 -0
  248. package/src/protocol/raw-routes.ts +191 -0
  249. package/src/protocol/route-meta.ts +39 -0
  250. package/src/protocol/routes.test.ts +65 -2
  251. package/src/protocol/routes.ts +88 -27
  252. package/src/schemas/agent.test.ts +66 -0
  253. package/src/schemas/agent.ts +184 -154
  254. package/src/schemas/agents.ts +6 -0
  255. package/src/schemas/devices.ts +2 -2
  256. package/src/schemas/hosts.ts +4 -0
  257. package/src/schemas/settings.ts +9 -0
  258. package/src/state/history-state.ts +14 -29
  259. package/src/state/landed-commit.test.ts +18 -0
  260. package/src/state/landed-commit.ts +17 -0
@@ -125,166 +125,167 @@ export const ForkedFromSchema = z.object({
125
125
  files: z.enum(["then", "now"]),
126
126
  });
127
127
  export type ForkedFrom = z.infer<typeof ForkedFromSchema>;
128
- export const AgentTurnSchema = z
129
- .object({
130
- prompt: z.string().describe("What to say to the agent. May be empty if you are only attaching files."),
131
- // Seeds a fresh registry entry's title; an existing entry's title always wins over this.
132
- title: z
133
- .string()
134
- .max(80)
135
- .optional()
136
- .describe("A title for a conversation this turn is opening. Ignored for a conversation that already has one."),
137
- // Already uploaded via /workspace/upload; Claude reads them via Read, Codex gets images as native input.
138
- attachments: z
139
- .array(z.string().min(1))
140
- .max(20)
141
- .optional()
142
- .describe("Files to hand the agent along with the prompt, as workspace paths. Upload them first."),
143
- // Read out of the prompt's own `@path` tokens rather than chosen: a tokenizer over pasted text guesses, so a
144
- // miss here is dropped and only a chosen attachment can refuse the turn.
145
- mentions: z
146
- .array(z.string().min(1))
147
- .max(MENTION_LIMIT)
148
- .optional()
149
- .describe(
150
- "Workspace paths the prompt mentions with `@`. Unlike attachments, one that escapes the workspace or names no file is ignored rather than refused.",
151
- ),
152
- // Which provider serves the turn, absent = claude; a sessionId resumes only on the provider that minted it.
153
- agent: AgentProviderSchema.optional().describe("Which model provider serves this turn. Leave it out for Claude."),
154
- // Which agentic loop runs the turn, absent = provider's own; "claude-code" forces the SDK loop for any
155
- // provider.
156
- harness: AgentHarnessSchema.optional().describe("Which agentic loop runs the turn. Leave it out to use each provider's own."),
157
- // Which connected account of that provider serves the turn; absent = the provider's first account.
158
- account: z.string().optional().describe("Which of that provider's connected accounts pays for the turn. Leave it out for the first one."),
159
- // Distinct from `account` (who pays): whose name the turn posts as; absent means something different for each
160
- // mode.
161
- actsAs: entryId.optional().describe("Which persona the turn speaks as out in the world. Not the same as which account pays for it."),
162
- sessionId: z.string().optional().describe("Resume this provider session instead of starting a fresh one."),
163
- // Stable id surviving provider/account/harness switches; keys the registry entry, the turn run, and the
164
- // worktree when isolated.
165
- conversationId: ConversationIdSchema.optional().describe(
166
- "The conversation this turn belongs to. You choose it, it survives model switches, and it is how you address the conversation later. Naming one that does not exist opens it.",
128
+ // The turn's fields before the cross-field refinements below, so a subset (TurnProfileSchema) can be picked from them.
129
+ const AgentTurnFieldsSchema = z.object({
130
+ prompt: z.string().describe("What to say to the agent. May be empty if you are only attaching files."),
131
+ // Seeds a fresh registry entry's title; an existing entry's title always wins over this.
132
+ title: z
133
+ .string()
134
+ .max(80)
135
+ .optional()
136
+ .describe("A title for a conversation this turn is opening. Ignored for a conversation that already has one."),
137
+ // Already uploaded via /workspace/upload; Claude reads them via Read, Codex gets images as native input.
138
+ attachments: z
139
+ .array(z.string().min(1))
140
+ .max(20)
141
+ .optional()
142
+ .describe("Files to hand the agent along with the prompt, as workspace paths. Upload them first."),
143
+ // Read out of the prompt's own `@path` tokens rather than chosen: a tokenizer over pasted text guesses, so a
144
+ // miss here is dropped and only a chosen attachment can refuse the turn.
145
+ mentions: z
146
+ .array(z.string().min(1))
147
+ .max(MENTION_LIMIT)
148
+ .optional()
149
+ .describe(
150
+ "Workspace paths the prompt mentions with `@`. Unlike attachments, one that escapes the workspace or names no file is ignored rather than refused.",
167
151
  ),
168
- // Runs in the conversation's own git worktree (created lazily) instead of shared /work; requires
169
- // conversationId.
170
- isolated: z
171
- .boolean()
172
- .optional()
173
- .describe(
174
- "Work in this conversation's own private copy of the repos rather than the shared tree, so several agents can work at once. Needs a conversation id.",
175
- ),
176
- // Decided like `isolated`: the first turn's choice, latched. A persona's own start folder wins over it.
177
- startIn: z
178
- .string()
179
- .max(200)
180
- .optional()
181
- .describe(
182
- "Which folder the conversation opens in, relative to the workspace root; the project it belongs to. Decided on the first turn. A persona that names its own start folder wins.",
183
- ),
184
- // Decided like `isolated`: the request's choice on the first turn, the registry's after. `runner` implies
185
- // isolation; absent means local.
186
- placement: AgentPlacementSchema.optional().describe(
187
- "Where this conversation runs: this sandbox (leave it out), or a paired runner by id. Decided on the first turn; later turns follow the conversation.",
152
+ // Which provider serves the turn, absent = claude; a sessionId resumes only on the provider that minted it.
153
+ agent: AgentProviderSchema.optional().describe("Which model provider serves this turn. Leave it out for Claude."),
154
+ // Which agentic loop runs the turn, absent = provider's own; "claude-code" forces the SDK loop for any
155
+ // provider.
156
+ harness: AgentHarnessSchema.optional().describe("Which agentic loop runs the turn. Leave it out to use each provider's own."),
157
+ // Which connected account of that provider serves the turn; absent = the provider's first account.
158
+ account: z.string().optional().describe("Which of that provider's connected accounts pays for the turn. Leave it out for the first one."),
159
+ // Distinct from `account` (who pays): whose name the turn posts as; absent means something different for each
160
+ // mode.
161
+ actsAs: entryId.optional().describe("Which persona the turn speaks as out in the world. Not the same as which account pays for it."),
162
+ sessionId: z.string().optional().describe("Resume this provider session instead of starting a fresh one."),
163
+ // Stable id surviving provider/account/harness switches; keys the registry entry, the turn run, and the
164
+ // worktree when isolated.
165
+ conversationId: ConversationIdSchema.optional().describe(
166
+ "The conversation this turn belongs to. You choose it, it survives model switches, and it is how you address the conversation later. Naming one that does not exist opens it.",
167
+ ),
168
+ // Runs in the conversation's own git worktree (created lazily) instead of shared /work; requires
169
+ // conversationId.
170
+ isolated: z
171
+ .boolean()
172
+ .optional()
173
+ .describe(
174
+ "Work in this conversation's own private copy of the repos rather than the shared tree, so several agents can work at once. Needs a conversation id.",
188
175
  ),
189
- // Pins a new isolated conversation's worktree to these commits; a workflow step's candidates share one snapshot
190
- // and skip the ordinary rebase.
191
- worktreeBase: z
192
- .array(RepoBaseSchema)
193
- .min(1)
194
- .max(50)
195
- .optional()
196
- .describe(
197
- "Pin a new private copy to these exact commits instead of today's workspace. Used when several agents must start from identical files.",
198
- ),
199
- // Overrides landing for this turn only; a workflow step sets false so candidates can't leak in before synthesis
200
- // compares them.
201
- autoLand: z
202
- .boolean()
203
- .optional()
204
- .describe(
205
- "Whether this turn's work merges into the workspace when it finishes. Overrides the conversation's own setting for this turn only.",
206
- ),
207
- // Which per-job model list answers when nobody named a model or provider (turn-resume.ts); names the job, not a
208
- // tier, and never overrides an explicit pick.
209
- runRole: ModelRoleSchema.optional().describe(
210
- "What started this turn, when it was not a person typing: which of the sandbox's per-job model lists answers for it. Only used when the turn names no model of its own.",
176
+ // Decided like `isolated`: the first turn's choice, latched. A persona's own start folder wins over it.
177
+ startIn: z
178
+ .string()
179
+ .max(200)
180
+ .optional()
181
+ .describe(
182
+ "Which folder the conversation opens in, relative to the workspace root; the project it belongs to. Decided on the first turn. A persona that names its own start folder wins.",
211
183
  ),
212
- // Set only by the daemon's own dispatchers, never a client; requires conversationId to record it on.
213
- origin: AgentOriginSchema.optional().describe(
214
- "Set by the sandbox alone: this turn opened a conversation on behalf of a message from outside rather than a person.",
184
+ // Decided like `isolated`: the request's choice on the first turn, the registry's after. `runner` implies
185
+ // isolation; absent means local.
186
+ placement: AgentPlacementSchema.optional().describe(
187
+ "Where this conversation runs: this sandbox (leave it out), or a paired runner by id. Decided on the first turn; later turns follow the conversation.",
188
+ ),
189
+ // Pins a new isolated conversation's worktree to these commits; a workflow step's candidates share one snapshot
190
+ // and skip the ordinary rebase.
191
+ worktreeBase: z
192
+ .array(RepoBaseSchema)
193
+ .min(1)
194
+ .max(50)
195
+ .optional()
196
+ .describe(
197
+ "Pin a new private copy to these exact commits instead of today's workspace. Used when several agents must start from identical files.",
215
198
  ),
216
- // No `history` field: a provider/account/harness switch carries no transcript over the wire; the daemon seeds
217
- // the new session from its own conversationId-keyed record.
218
- // Where a fork was cut from, on its first turn only; requires conversationId, and files:"then" implies
219
- // isolated.
220
- forkOf: z
221
- .object({
222
- conversationId: ConversationIdSchema.describe("The conversation this one was cut from."),
223
- keep: z.number().int().nonnegative().describe("How many of that conversation's messages to copy in before this turn runs."),
224
- files: z
225
- .enum(["then", "now"])
226
- .describe(
227
- 'Which files the fork opens on: "now" is the workspace as it stands, "then" is the files as they were at the cut, which needs a private copy.',
228
- ),
229
- })
230
- .optional()
231
- .describe("Where this conversation was cut from, on its first turn only. Only the client knows this, so only the client can say it."),
232
- // The browser sends the chosen model per turn; the provider token is the sandbox's own stored credential.
233
- model: z.string().optional().describe("Which model to use. Leave it out for the provider's default."),
234
- // Audience only, never model routing: `runRole` is what fills a model in. A run somebody pressed and is
235
- // watching leaves this off, however little of it a person typed.
236
- unattended: z
237
- .boolean()
238
- .optional()
239
- .describe(
240
- "Nobody is watching this turn: a schedule, a queue or another agent started it and no chat is open on it. A card that needs a person is refused rather than raised, plan mode and the terminal hand-off are withheld, and the sandbox's signed-in accounts stay out of it unless a persona carries them.",
241
- ),
242
- // Outside content caused this turn, naming the source; distinct from `unattended` (whether anyone is watching).
243
- outsideWake: z
244
- .string()
245
- .min(1)
246
- .optional()
247
- .describe(
248
- "Content from outside caused this turn, and what to call the source. It is what makes the sandbox treat the turn as carrying somebody else's words.",
249
- ),
250
- // How tool calls are gated for this turn (the SDK's permissionMode, verbatim):
251
- // plan: propose → approve → execute; the proposing half asks nothing, it only withholds writes
252
- // default: prompts per tool on the permission side channel, the one mode that interrupts anybody
253
- // bypassPermissions: runs everything
254
- // The agent can move itself between modes mid-turn, riding back as a `mode` frame.
255
- permissionMode: PermissionModeSchema.optional().describe(
256
- "How tool calls are gated: ask before each tool, propose a plan first, or run everything. The agent can move itself between these mid-turn.",
199
+ // Overrides landing for this turn only; a workflow step sets false so candidates can't leak in before synthesis
200
+ // compares them.
201
+ autoLand: z
202
+ .boolean()
203
+ .optional()
204
+ .describe(
205
+ "Whether this turn's work merges into the workspace when it finishes. Overrides the conversation's own setting for this turn only.",
257
206
  ),
258
- // Narrows the turn to these tool names (the SDK option, not the daemon's MCP `tools`/servers); absent means
259
- // everything the runtime has.
260
- allowedTools: z
261
- .array(z.string().min(1))
262
- .optional()
263
- .describe(
264
- "Narrow the turn to these tools. Leave it out for everything the runtime has. For a turn driven by an outside message this list is the real boundary, because prompt wording is only advice.",
265
- ),
266
- effort: z.string().optional().describe("How hard the model should think, where the provider offers a choice."),
267
- thinking: z.boolean().optional().describe("Whether to show the model's reasoning as it works."),
268
- // Requests the harness serve this turn faster at a higher price; a request, not a promise, the `fast_mode`
269
- // frame reports what happened.
270
- fast: z
271
- .boolean()
272
- .optional()
273
- .describe(
274
- "Ask for the same work at a higher rate for a higher price. A request rather than a promise: the answer says what actually happened.",
275
- ),
276
- // Set by the composer on the one turn whose model the Auto judge chose; the daemon only records it.
277
- autoPicked: z
278
- .boolean()
279
- .optional()
280
- .describe(
281
- "Whether this turn's model was chosen for you by reading the conversation's opening message, rather than picked by hand. Recorded so the choice can be judged later against what you did next.",
282
- ),
283
- // The opt-in editor context chip: what the user is looking at, folded into the prompt daemon-side.
284
- editorContext: EditorContextSchema.optional().describe(
285
- 'What the user has open in their editor, folded into the prompt so that pointing words like "this" resolve.',
207
+ // Which per-job model list answers when nobody named a model or provider (turn-resume.ts); names the job, not a
208
+ // tier, and never overrides an explicit pick.
209
+ runRole: ModelRoleSchema.optional().describe(
210
+ "What started this turn, when it was not a person typing: which of the sandbox's per-job model lists answers for it. Only used when the turn names no model of its own.",
211
+ ),
212
+ // Set only by the daemon's own dispatchers, never a client; requires conversationId to record it on.
213
+ origin: AgentOriginSchema.optional().describe(
214
+ "Set by the sandbox alone: this turn opened a conversation on behalf of a message from outside rather than a person.",
215
+ ),
216
+ // No `history` field: a provider/account/harness switch carries no transcript over the wire; the daemon seeds
217
+ // the new session from its own conversationId-keyed record.
218
+ // Where a fork was cut from, on its first turn only; requires conversationId, and files:"then" implies
219
+ // isolated.
220
+ forkOf: z
221
+ .object({
222
+ conversationId: ConversationIdSchema.describe("The conversation this one was cut from."),
223
+ keep: z.number().int().nonnegative().describe("How many of that conversation's messages to copy in before this turn runs."),
224
+ files: z
225
+ .enum(["then", "now"])
226
+ .describe(
227
+ 'Which files the fork opens on: "now" is the workspace as it stands, "then" is the files as they were at the cut, which needs a private copy.',
228
+ ),
229
+ })
230
+ .optional()
231
+ .describe("Where this conversation was cut from, on its first turn only. Only the client knows this, so only the client can say it."),
232
+ // The browser sends the chosen model per turn; the provider token is the sandbox's own stored credential.
233
+ model: z.string().optional().describe("Which model to use. Leave it out for the provider's default."),
234
+ // Audience only, never model routing: `runRole` is what fills a model in. A run somebody pressed and is
235
+ // watching leaves this off, however little of it a person typed.
236
+ unattended: z
237
+ .boolean()
238
+ .optional()
239
+ .describe(
240
+ "Nobody is watching this turn: a schedule, a queue or another agent started it and no chat is open on it. A card that needs a person is refused rather than raised, plan mode and the terminal hand-off are withheld, and the sandbox's signed-in accounts stay out of it unless a persona carries them.",
286
241
  ),
287
- })
242
+ // Outside content caused this turn, naming the source; distinct from `unattended` (whether anyone is watching).
243
+ outsideWake: z
244
+ .string()
245
+ .min(1)
246
+ .optional()
247
+ .describe(
248
+ "Content from outside caused this turn, and what to call the source. It is what makes the sandbox treat the turn as carrying somebody else's words.",
249
+ ),
250
+ // How tool calls are gated for this turn (the SDK's permissionMode, verbatim):
251
+ // plan: propose → approve → execute; the proposing half asks nothing, it only withholds writes
252
+ // default: prompts per tool on the permission side channel, the one mode that interrupts anybody
253
+ // bypassPermissions: runs everything
254
+ // The agent can move itself between modes mid-turn, riding back as a `mode` frame.
255
+ permissionMode: PermissionModeSchema.optional().describe(
256
+ "How tool calls are gated: ask before each tool, propose a plan first, or run everything. The agent can move itself between these mid-turn.",
257
+ ),
258
+ // Narrows the turn to these tool names (the SDK option, not the daemon's MCP `tools`/servers); absent means
259
+ // everything the runtime has.
260
+ allowedTools: z
261
+ .array(z.string().min(1))
262
+ .optional()
263
+ .describe(
264
+ "Narrow the turn to these tools. Leave it out for everything the runtime has. For a turn driven by an outside message this list is the real boundary, because prompt wording is only advice.",
265
+ ),
266
+ effort: z.string().optional().describe("How hard the model should think, where the provider offers a choice."),
267
+ thinking: z.boolean().optional().describe("Whether to show the model's reasoning as it works."),
268
+ // Requests the harness serve this turn faster at a higher price; a request, not a promise, the `fast_mode`
269
+ // frame reports what happened.
270
+ fast: z
271
+ .boolean()
272
+ .optional()
273
+ .describe(
274
+ "Ask for the same work at a higher rate for a higher price. A request rather than a promise: the answer says what actually happened.",
275
+ ),
276
+ // Set by the composer on the one turn whose model the Auto judge chose; the daemon only records it.
277
+ autoPicked: z
278
+ .boolean()
279
+ .optional()
280
+ .describe(
281
+ "Whether this turn's model was chosen for you by reading the conversation's opening message, rather than picked by hand. Recorded so the choice can be judged later against what you did next.",
282
+ ),
283
+ // The opt-in editor context chip: what the user is looking at, folded into the prompt daemon-side.
284
+ editorContext: EditorContextSchema.optional().describe(
285
+ 'What the user has open in their editor, folded into the prompt so that pointing words like "this" resolve.',
286
+ ),
287
+ });
288
+ export const AgentTurnSchema = AgentTurnFieldsSchema
288
289
  // An attachment-only send (no text) is legal; an entirely empty turn is not.
289
290
  .refine((turn) => turn.prompt.trim().length > 0 || (turn.attachments?.length ?? 0) > 0, {
290
291
  message: "prompt or attachments required",
@@ -306,6 +307,35 @@ export const AgentTurnSchema = z
306
307
  message: 'forkOf.files "then" requires isolated',
307
308
  });
308
309
  export type AgentTurn = z.infer<typeof AgentTurnSchema>;
310
+ // Which turn this is, as opposed to what it says: who serves it and how (agent, harness, account, model and its knobs),
311
+ // as whom (actsAs), where (isolated), whether anyone watches (unattended) and for which job (runRole). A turn that
312
+ // continues another (a resume, a wake, a nudge, a child's follow-up) carries this whole, never a hand-picked part.
313
+ export const TurnProfileSchema = AgentTurnFieldsSchema.pick({
314
+ agent: true,
315
+ harness: true,
316
+ account: true,
317
+ model: true,
318
+ effort: true,
319
+ thinking: true,
320
+ fast: true,
321
+ actsAs: true,
322
+ isolated: true,
323
+ unattended: true,
324
+ runRole: true,
325
+ });
326
+ export type TurnProfile = z.infer<typeof TurnProfileSchema>;
327
+ const PROFILE_KEYS = Object.keys(TurnProfileSchema.shape) as readonly (keyof TurnProfile)[];
328
+ // Read as `=== true` everywhere, so false and absent are one value; carrying a false would state a placement or an
329
+ // audience the turn never named.
330
+ const TRUE_ONLY: ReadonlySet<keyof TurnProfile> = new Set(["isolated", "unattended"]);
331
+ /* THE PROFILE A TURN STATES: absent stays absent (no `undefined` keys), and a `=== true` flag travels only when set. */
332
+ export const profileOf = (turn: TurnProfile): TurnProfile =>
333
+ Object.fromEntries(
334
+ PROFILE_KEYS.flatMap((key) => {
335
+ const value = turn[key];
336
+ return value === undefined || (TRUE_ONLY.has(key) && value !== true) ? [] : [[key, value]];
337
+ }),
338
+ ) as TurnProfile;
309
339
  /* A MODEL CHOSEN FOR ONE SURFACE-STARTED RUN, what the caret on the shared run button (<AgentRunButton>) sends along with the click that starts it. */
310
340
  export const AgentRunPickSchema = z
311
341
  .object({
@@ -109,6 +109,12 @@ export const LandedMessageSchema = z.object({
109
109
  .string()
110
110
  .optional()
111
111
  .describe("What this change takes away, for anything already relying on it. Nearly always absent: it is for removals, not for additions."),
112
+ // Present only when the conversation said why a test it weakened should stay weaker; the push gate reads it as the
113
+ // weakening's declaration.
114
+ testNote: z
115
+ .string()
116
+ .optional()
117
+ .describe("Why a test this change weakened is meant to be weaker, in the conversation's own words. Nearly always absent."),
112
118
  });
113
119
  export type LandedMessage = z.infer<typeof LandedMessageSchema>;
114
120
  // One model's turn in the drafting walk (agent/role-model.ts tries connected models in order), in spend order:
@@ -1,6 +1,6 @@
1
1
  // What one of the user's own machines is running.
2
2
  import { z } from "zod";
3
- import { hostEntryOf, hostEnvironmentOf, type DeviceFacts, DeviceFactsSchema, WSL_SYSTEM_DISTROS, WslEnvironmentSchema } from "./hosts.js";
3
+ import { hostEntryOf, hostEnvironmentOf, type DeviceFacts, DeviceFactsSchema, userDistrosOf, WslEnvironmentSchema } from "./hosts.js";
4
4
  import { DEV_VERSION } from "../state/versions.js";
5
5
  // Desktop-sync report shape shared by the agent, daemon and browser, produced only by the agent's own `deviceReport`.
6
6
  // The agent never reports `sandboxes`; the docker half is filled in by whoever reads the report, scoped to the reader's
@@ -596,7 +596,7 @@ export const pathReach = (platform: string | undefined, facts: DeviceFacts | und
596
596
  if (platform === undefined || (platform === "windows") === windowsPath(path)) {
597
597
  return { kind: "direct" };
598
598
  }
599
- const distros = platform === "windows" ? (facts?.wslDistros ?? []).filter((distro) => !WSL_SYSTEM_DISTROS.has(distro)) : [];
599
+ const distros = platform === "windows" ? userDistrosOf(facts) : [];
600
600
  const only = distros.length === 1 ? distros[0] : undefined;
601
601
  return only === undefined ? { kind: "none", distros } : { kind: "wsl", distro: only };
602
602
  };
@@ -41,6 +41,10 @@ export type DeviceFacts = z.infer<typeof DeviceFactsSchema>;
41
41
  // Docker Desktop's own distros: `wsl -l -q` lists them like any other, and none ever runs an agent or holds a checkout.
42
42
  export const WSL_SYSTEM_DISTROS: ReadonlySet<string> = new Set(["docker-desktop", "docker-desktop-data"]);
43
43
 
44
+ // The distros a Windows side lists that are the user's own: the only ones any screen offers, crosses into or boots.
45
+ export const userDistrosOf = (facts: Pick<DeviceFacts, "wslDistros"> | undefined): string[] =>
46
+ (facts?.wslDistros ?? []).filter((distro) => !WSL_SYSTEM_DISTROS.has(distro));
47
+
44
48
  // One folder, two names. Windows sees a distro's files under a UNC share and a distro sees the Windows drives under
45
49
  // /mnt, so a path handed across the boundary is translated here rather than by hand at every call site.
46
50
  export const wslPathOf = (windowsPath: string): string | undefined => {
@@ -389,6 +389,15 @@ export const SandboxSettingsSchema = z.object({
389
389
  ),
390
390
  // Worth it since the container is recreated on every update or environment approval — otherwise approving a
391
391
  // Dockerfile change costs the run that asked for it.
392
+ // What happens to breakage found after the work that caused it has left the turn: a land that turns the main tree's own
393
+ // check red is sent back to the conversation that landed it, and main's CI staying red on one failure gets a fix agent
394
+ // once pushes go quiet, with a fleet failure re-run instead. Off, both are only reported.
395
+ autoRepair: z
396
+ .boolean()
397
+ .default(true)
398
+ .describe(
399
+ "Whether breakage found after the work left its turn is repaired without asking: a land that turns the main tree's check red goes back to the conversation that landed it, and main's CI staying red on the same failure gets a fix agent once pushes go quiet. A failure on the CI fleet itself is re-run once instead. Off, both are only reported.",
400
+ ),
392
401
  autoResumeOnRestart: z
393
402
  .boolean()
394
403
  .default(false)
@@ -1,8 +1,8 @@
1
1
  import type { StateFile } from "./state-portability.js";
2
2
 
3
- // /history holds daemon state outside /work's `.intentic/`: real git dirs (in-tree .git is a pointer file), the fleet
4
- // registry, turn journal, ledgers, checkpoint scopes, isolated checkouts. Nothing here is watched, unlike `.intentic`.
5
- // history-state-coverage.test.ts enforces this list stays complete.
3
+ // /history holds daemon state outside /work's `.intentic/`: real git dirs (in-tree .git is a pointer file), the fleet's
4
+ // database and each conversation's directory, ledgers, checkpoint scopes, isolated checkouts. Nothing here is watched,
5
+ // unlike `.intentic`. history-state-coverage.test.ts enforces this list stays complete.
6
6
 
7
7
  // historyRoot-relative paths, forward-slash, matched by prefix; a directory keeps its trailing slash so it can't
8
8
  // prefix-match a sibling file.
@@ -13,22 +13,15 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
13
13
  { path: "gits/", portability: "carry" },
14
14
  // Checkpoint timeline (one bare repo per scope); lets "restore to before that turn" still reach back.
15
15
  { path: "scopes/", portability: "carry" },
16
- // The fleet: every conversation card, its branch, session ids and standing.
17
- { path: "agents.json", portability: "carry" },
18
- { path: "turns/", portability: "carry" },
19
- // Armed condition watches; carries safely: the journal holds no credential, only names re-derived on target.
20
- { path: "watches/", portability: "carry" },
21
- { path: "transcripts/", portability: "carry" },
22
- // What each conversation's last turn was told before its own words (prompt-record.ts), one file per conversation.
23
- // Carried with the transcripts it explains: a restored conversation whose prompt record stayed behind can no
24
- // longer say whether its AGENTS.md arrived or a small window trimmed it, which is the whole point of keeping it.
25
- { path: "system-prompts/", portability: "carry" },
26
- // One runtime session store per fenced conversation, holding for it what `.intentic/records/sessions/claude/` holds
27
- // for every other: its own transcripts, plans, backups and checklists, kept off /work so no other conversation's
28
- // namespace can reach them. Carried for the same reason that one is.
29
- { path: "sessions/", portability: "carry" },
30
- // What each message can restore to (a checkpoint or isolated commit); the join between transcripts and scopes.
31
- { path: "turn-checkpoints.json", portability: "carry" },
16
+ // The fleet and everything keyed by a conversation that must agree with it: each card's record, its checkout's
17
+ // per-repo provenance, what each message restores to, its turn in flight and its armed watches, plus automation
18
+ // fires in flight. Packed as a snapshot taken for the bundle, never as the live file and its WAL, and landed row
19
+ // by row into the target's own database. Watches carry safely: they hold no credential, only names.
20
+ { path: "conversations.db", portability: "carry" },
21
+ // One directory per conversation: its transcript, what its last turn was told before its own words, and a fenced
22
+ // conversation's own runtime session store (what `.intentic/records/sessions/claude/` holds for every other).
23
+ // Carried whole, like the conversation it is.
24
+ { path: "conversations/", portability: "carry" },
32
25
  // Index of published-page conversations; the pages travel with /work anyway, this makes one withdrawable.
33
26
  { path: "shares.json", portability: "carry" },
34
27
  { path: "activity.jsonl", portability: "carry" },
@@ -56,22 +49,14 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
56
49
 
57
50
  /* ---- regenerated by the target ---- */
58
51
 
59
- // Deliberately omitted: the branch lives in gits/, the registry in agents.json; regenerated on the next turn.
52
+ // Deliberately omitted: the branch lives in gits/, the registry in conversations.db; regenerated on the next turn.
60
53
  {
61
54
  path: "worktrees/",
62
55
  portability: "derived",
63
56
  note: "Each conversation re-attaches its checkout from its branch on its next turn.",
64
57
  },
65
- // Search phrase index, derived from transcripts/; rebuilt by the first boot's backfill instead of carried.
58
+ // Search phrase index, derived from the conversations' transcripts; rebuilt by the first boot's backfill instead.
66
59
  { path: "said-index/", portability: "derived", note: "Rebuilt from the carried transcripts by the first boot's backfill." },
67
- // What each conversation was last told (prompt-record.ts). Tens of kilobytes per conversation, rewritten by every
68
- // turn — and a prompt composed on THIS sandbox names its paths, devices and accounts, so carrying it would describe
69
- // the machine the bundle left rather than the one it landed on.
70
- {
71
- path: "system-prompts/",
72
- portability: "derived",
73
- note: "Each conversation records what it was told again on its next turn.",
74
- },
75
60
  // What each connected machine and browser publishes as its tools, so one that is asleep still appears in a turn
76
61
  // (peers/peer-tool-memory.ts). Re-asked of every peer the moment it reconnects, and about software that stayed
77
62
  // behind on somebody else's computer, so it is not worth carrying.
@@ -0,0 +1,18 @@
1
+ import { expect, test } from "bun:test";
2
+ import { declaredTestNote, landedCommitMessage } from "./landed-commit.js";
3
+
4
+ // The push gate reads a weakening's declaration off the commit (assertion-ratchet.mjs), so the trailer must survive
5
+ // the trip from the conversation's last word into the message a land commits under.
6
+
7
+ test("a landed message commits as its subject and one trailer paragraph, the test note included", () => {
8
+ expect(landedCommitMessage({ subject: "fix: tabs" })).toBe("fix: tabs");
9
+ expect(landedCommitMessage({ subject: "feat: tabs", note: "Tabs remember.", breaking: "Old API gone.", testNote: "Rows became a table." })).toBe(
10
+ "feat: tabs\n\nRelease-Note: Tabs remember.\nBreaking-Note: Old API gone.\nTest-Note: Rows became a table.",
11
+ );
12
+ });
13
+
14
+ test("a Test-Note is read only from a line that starts with one and says something", () => {
15
+ expect(declaredTestNote("Done.\n\nTest-Note: the prose assertion became a structural one\n")).toBe("the prose assertion became a structural one");
16
+ expect(declaredTestNote("I kept a Test-Note: nowhere near the start")).toBeUndefined();
17
+ expect(declaredTestNote("Test-Note: \n")).toBeUndefined();
18
+ });
@@ -0,0 +1,17 @@
1
+ import type { LandedMessage } from "../schemas/agents.js";
2
+
3
+ // The commit a landed message makes: its subject, then every trailer in one paragraph, since git reads only the last
4
+ // block of a message as trailers.
5
+ export const landedCommitMessage = (landed: LandedMessage): string => {
6
+ const trailers = [
7
+ landed.note === undefined ? "" : `Release-Note: ${landed.note}`,
8
+ landed.breaking === undefined ? "" : `Breaking-Note: ${landed.breaking}`,
9
+ landed.testNote === undefined ? "" : `Test-Note: ${landed.testNote}`,
10
+ ]
11
+ .filter((line) => line !== "")
12
+ .join("\n");
13
+ return [landed.subject, trailers].filter((part) => part !== "").join("\n\n");
14
+ };
15
+
16
+ // The `Test-Note:` line a conversation ended its last message with, when it declared a weakened test on purpose.
17
+ export const declaredTestNote = (said: string): string | undefined => /^Test-Note:[ \t]*(\S.*)$/m.exec(said)?.[1]?.trim();