@intentic/sandbox-contract 1.245.0 → 1.247.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 (247) hide show
  1. package/README.md +17 -1
  2. package/dist/batch-runs.d.ts +2 -0
  3. package/dist/batch-runs.d.ts.map +1 -1
  4. package/dist/batch-runs.js +1 -0
  5. package/dist/batch-runs.js.map +1 -1
  6. package/dist/command-classes.d.ts +6 -3
  7. package/dist/command-classes.d.ts.map +1 -1
  8. package/dist/command-classes.js +43 -18
  9. package/dist/command-classes.js.map +1 -1
  10. package/dist/contracts/{cursor.contract.d.ts → accounts.contract.d.ts} +102 -3
  11. package/dist/contracts/accounts.contract.d.ts.map +1 -0
  12. package/dist/contracts/accounts.contract.js +61 -0
  13. package/dist/contracts/accounts.contract.js.map +1 -0
  14. package/dist/contracts/agent.contract.d.ts +19 -0
  15. package/dist/contracts/agent.contract.d.ts.map +1 -1
  16. package/dist/contracts/agents.contract.d.ts +121 -0
  17. package/dist/contracts/agents.contract.d.ts.map +1 -1
  18. package/dist/contracts/agents.contract.js +4 -4
  19. package/dist/contracts/agents.contract.js.map +1 -1
  20. package/dist/contracts/ci.contract.d.ts +2 -0
  21. package/dist/contracts/ci.contract.d.ts.map +1 -1
  22. package/dist/contracts/host.contract.d.ts +35 -0
  23. package/dist/contracts/host.contract.d.ts.map +1 -1
  24. package/dist/contracts/host.contract.js +3 -2
  25. package/dist/contracts/host.contract.js.map +1 -1
  26. package/dist/contracts/personas.contract.d.ts +4 -2
  27. package/dist/contracts/personas.contract.d.ts.map +1 -1
  28. package/dist/contracts/runner.contract.d.ts +2 -2
  29. package/dist/contracts/settings.contract.d.ts +58 -20
  30. package/dist/contracts/settings.contract.d.ts.map +1 -1
  31. package/dist/contracts/system.contract.d.ts +80 -30
  32. package/dist/contracts/system.contract.d.ts.map +1 -1
  33. package/dist/contracts/system.contract.js +26 -17
  34. package/dist/contracts/system.contract.js.map +1 -1
  35. package/dist/contracts/usage.contract.d.ts +22 -0
  36. package/dist/contracts/usage.contract.d.ts.map +1 -1
  37. package/dist/contracts/usage.contract.js +19 -0
  38. package/dist/contracts/usage.contract.js.map +1 -1
  39. package/dist/definition.d.ts +20 -28
  40. package/dist/definition.d.ts.map +1 -1
  41. package/dist/documents.d.ts +0 -1
  42. package/dist/documents.d.ts.map +1 -1
  43. package/dist/documents.js +1 -2
  44. package/dist/documents.js.map +1 -1
  45. package/dist/embed.d.ts +23 -0
  46. package/dist/embed.d.ts.map +1 -0
  47. package/dist/embed.js +84 -0
  48. package/dist/embed.js.map +1 -0
  49. package/dist/events.d.ts +21 -0
  50. package/dist/events.d.ts.map +1 -1
  51. package/dist/events.js +5 -2
  52. package/dist/events.js.map +1 -1
  53. package/dist/fast-tier.js +1 -1
  54. package/dist/fast-tier.js.map +1 -1
  55. package/dist/history-state.d.ts.map +1 -1
  56. package/dist/history-state.js +2 -0
  57. package/dist/history-state.js.map +1 -1
  58. package/dist/index.d.ts +453 -306
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +7 -15
  61. package/dist/index.js.map +1 -1
  62. package/dist/model-pins.d.ts +17 -0
  63. package/dist/model-pins.d.ts.map +1 -0
  64. package/dist/{quick-model.js → model-pins.js} +18 -11
  65. package/dist/model-pins.js.map +1 -0
  66. package/dist/model-roles.d.ts +144 -0
  67. package/dist/model-roles.d.ts.map +1 -0
  68. package/dist/model-roles.js +129 -0
  69. package/dist/model-roles.js.map +1 -0
  70. package/dist/peer-dial.d.ts +33 -0
  71. package/dist/peer-dial.d.ts.map +1 -0
  72. package/dist/peer-dial.js +79 -0
  73. package/dist/peer-dial.js.map +1 -0
  74. package/dist/peer-mcp-server.d.ts +36 -0
  75. package/dist/peer-mcp-server.d.ts.map +1 -0
  76. package/dist/peer-mcp-server.js +71 -0
  77. package/dist/peer-mcp-server.js.map +1 -0
  78. package/dist/provider-specs.d.ts +38 -20
  79. package/dist/provider-specs.d.ts.map +1 -1
  80. package/dist/provider-specs.js +39 -13
  81. package/dist/provider-specs.js.map +1 -1
  82. package/dist/runtime-state.d.ts +1 -1
  83. package/dist/runtime-state.js +1 -1
  84. package/dist/runtime-state.js.map +1 -1
  85. package/dist/safety-policy.d.ts +12 -3
  86. package/dist/safety-policy.d.ts.map +1 -1
  87. package/dist/safety-policy.js +30 -5
  88. package/dist/safety-policy.js.map +1 -1
  89. package/dist/schemas/agent.d.ts +27 -8
  90. package/dist/schemas/agent.d.ts.map +1 -1
  91. package/dist/schemas/agent.js +10 -4
  92. package/dist/schemas/agent.js.map +1 -1
  93. package/dist/schemas/agents.d.ts +42 -0
  94. package/dist/schemas/agents.d.ts.map +1 -1
  95. package/dist/schemas/agents.js +25 -4
  96. package/dist/schemas/agents.js.map +1 -1
  97. package/dist/schemas/automations.d.ts +11 -2
  98. package/dist/schemas/automations.d.ts.map +1 -1
  99. package/dist/schemas/automations.js +1 -1
  100. package/dist/schemas/automations.js.map +1 -1
  101. package/dist/schemas/ci.d.ts +6 -0
  102. package/dist/schemas/ci.d.ts.map +1 -1
  103. package/dist/schemas/ci.js +3 -2
  104. package/dist/schemas/ci.js.map +1 -1
  105. package/dist/schemas/context.d.ts +30 -0
  106. package/dist/schemas/context.d.ts.map +1 -0
  107. package/dist/schemas/context.js +34 -0
  108. package/dist/schemas/context.js.map +1 -0
  109. package/dist/schemas/{computers.d.ts → devices.d.ts} +154 -60
  110. package/dist/schemas/devices.d.ts.map +1 -0
  111. package/dist/schemas/devices.js +157 -0
  112. package/dist/schemas/devices.js.map +1 -0
  113. package/dist/schemas/hosts.d.ts +12 -0
  114. package/dist/schemas/hosts.d.ts.map +1 -1
  115. package/dist/schemas/hosts.js +1 -0
  116. package/dist/schemas/hosts.js.map +1 -1
  117. package/dist/schemas/issues.d.ts +0 -5
  118. package/dist/schemas/issues.d.ts.map +1 -1
  119. package/dist/schemas/issues.js +0 -1
  120. package/dist/schemas/issues.js.map +1 -1
  121. package/dist/schemas/personas.d.ts +5 -3
  122. package/dist/schemas/personas.d.ts.map +1 -1
  123. package/dist/schemas/personas.js +3 -2
  124. package/dist/schemas/personas.js.map +1 -1
  125. package/dist/schemas/plan-limits.d.ts +20 -0
  126. package/dist/schemas/plan-limits.d.ts.map +1 -1
  127. package/dist/schemas/plan-limits.js +21 -0
  128. package/dist/schemas/plan-limits.js.map +1 -1
  129. package/dist/schemas/provider-oauth.d.ts +48 -16
  130. package/dist/schemas/provider-oauth.d.ts.map +1 -1
  131. package/dist/schemas/provider-oauth.js +22 -20
  132. package/dist/schemas/provider-oauth.js.map +1 -1
  133. package/dist/schemas/settings.d.ts +42 -16
  134. package/dist/schemas/settings.d.ts.map +1 -1
  135. package/dist/schemas/settings.js +22 -28
  136. package/dist/schemas/settings.js.map +1 -1
  137. package/dist/schemas/terminal.js +9 -9
  138. package/dist/schemas/terminal.js.map +1 -1
  139. package/dist/schemas/usage.d.ts +5 -2
  140. package/dist/schemas/usage.d.ts.map +1 -1
  141. package/dist/schemas/usage.js +5 -2
  142. package/dist/schemas/usage.js.map +1 -1
  143. package/dist/shell-regions.d.ts +4 -0
  144. package/dist/shell-regions.d.ts.map +1 -0
  145. package/dist/shell-regions.js +156 -0
  146. package/dist/shell-regions.js.map +1 -0
  147. package/dist/workspace-state.d.ts +8 -0
  148. package/dist/workspace-state.d.ts.map +1 -1
  149. package/dist/workspace-state.js +13 -5
  150. package/dist/workspace-state.js.map +1 -1
  151. package/package.json +37 -4
  152. package/src/agent-catalog.ts +2 -2
  153. package/src/arrival.ts +3 -3
  154. package/src/batch-runs.test.ts +10 -5
  155. package/src/batch-runs.ts +10 -3
  156. package/src/command-classes.test.ts +195 -71
  157. package/src/command-classes.ts +148 -46
  158. package/src/contracts/accounts.contract.ts +94 -0
  159. package/src/contracts/agents.contract.ts +4 -3
  160. package/src/contracts/exit.contract.ts +2 -2
  161. package/src/contracts/host.contract.ts +17 -5
  162. package/src/contracts/settings.contract.ts +1 -1
  163. package/src/contracts/system.contract.ts +43 -24
  164. package/src/contracts/usage.contract.ts +31 -0
  165. package/src/contracts/vpn.contract.ts +2 -2
  166. package/src/documents.test.ts +2 -1
  167. package/src/documents.ts +7 -11
  168. package/src/embed.test.ts +68 -0
  169. package/src/embed.ts +164 -0
  170. package/src/events.ts +31 -4
  171. package/src/fast-tier.test.ts +1 -1
  172. package/src/fast-tier.ts +5 -5
  173. package/src/history-state.ts +12 -3
  174. package/src/host-protocol.ts +2 -2
  175. package/src/index.ts +8 -16
  176. package/src/model-order.ts +1 -1
  177. package/src/{quick-model.test.ts → model-pins.test.ts} +73 -29
  178. package/src/model-pins.ts +183 -0
  179. package/src/model-roles.ts +224 -0
  180. package/src/peer-dial.test.ts +203 -0
  181. package/src/peer-dial.ts +163 -0
  182. package/src/peer-mcp-server.test.ts +104 -0
  183. package/src/peer-mcp-server.ts +144 -0
  184. package/src/plan-pools.ts +1 -1
  185. package/src/prompt-complexity.test.ts +1 -1
  186. package/src/prompt-complexity.ts +2 -2
  187. package/src/provider-specs.test.ts +45 -18
  188. package/src/provider-specs.ts +147 -67
  189. package/src/routes.test.ts +6 -3
  190. package/src/runner-protocol.ts +1 -1
  191. package/src/runtime-state.ts +2 -2
  192. package/src/safety-policy.test.ts +88 -0
  193. package/src/safety-policy.ts +84 -14
  194. package/src/schemas/agent.ts +83 -30
  195. package/src/schemas/agents.ts +67 -6
  196. package/src/schemas/automations.ts +6 -4
  197. package/src/schemas/capabilities.ts +4 -4
  198. package/src/schemas/ci.ts +23 -6
  199. package/src/schemas/context.ts +87 -0
  200. package/src/schemas/{computers.ts → devices.ts} +190 -107
  201. package/src/schemas/hosts.ts +5 -1
  202. package/src/schemas/issues.ts +0 -4
  203. package/src/schemas/personas.ts +8 -3
  204. package/src/schemas/plan-limits.ts +50 -0
  205. package/src/schemas/provider-oauth.ts +49 -52
  206. package/src/schemas/settings.ts +105 -140
  207. package/src/schemas/terminal.ts +12 -12
  208. package/src/schemas/usage.ts +62 -27
  209. package/src/schemas/version-seam.test.ts +0 -1
  210. package/src/shell-regions.ts +289 -0
  211. package/src/versions.ts +2 -2
  212. package/src/webext-links.ts +2 -2
  213. package/src/webext-protocol.ts +2 -2
  214. package/src/workspace-state.test.ts +48 -1
  215. package/src/workspace-state.ts +48 -11
  216. package/dist/agent-run-model.d.ts +0 -4
  217. package/dist/agent-run-model.d.ts.map +0 -1
  218. package/dist/agent-run-model.js +0 -13
  219. package/dist/agent-run-model.js.map +0 -1
  220. package/dist/contracts/claude.contract.d.ts +0 -91
  221. package/dist/contracts/claude.contract.d.ts.map +0 -1
  222. package/dist/contracts/claude.contract.js +0 -50
  223. package/dist/contracts/claude.contract.js.map +0 -1
  224. package/dist/contracts/cursor.contract.d.ts.map +0 -1
  225. package/dist/contracts/cursor.contract.js +0 -50
  226. package/dist/contracts/cursor.contract.js.map +0 -1
  227. package/dist/contracts/grok.contract.d.ts +0 -36
  228. package/dist/contracts/grok.contract.d.ts.map +0 -1
  229. package/dist/contracts/grok.contract.js +0 -31
  230. package/dist/contracts/grok.contract.js.map +0 -1
  231. package/dist/contracts/keys.contract.d.ts +0 -81
  232. package/dist/contracts/keys.contract.d.ts.map +0 -1
  233. package/dist/contracts/keys.contract.js +0 -51
  234. package/dist/contracts/keys.contract.js.map +0 -1
  235. package/dist/quick-model.d.ts +0 -15
  236. package/dist/quick-model.d.ts.map +0 -1
  237. package/dist/quick-model.js.map +0 -1
  238. package/dist/schemas/computers.d.ts.map +0 -1
  239. package/dist/schemas/computers.js +0 -135
  240. package/dist/schemas/computers.js.map +0 -1
  241. package/src/agent-run-model.test.ts +0 -76
  242. package/src/agent-run-model.ts +0 -65
  243. package/src/contracts/claude.contract.ts +0 -71
  244. package/src/contracts/cursor.contract.ts +0 -74
  245. package/src/contracts/grok.contract.ts +0 -41
  246. package/src/contracts/keys.contract.ts +0 -79
  247. package/src/quick-model.ts +0 -155
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
- import { KEY_PROVIDERS, NATIVE_PROVIDERS } from "../provider-specs.js";
2
+ import { ModelRoleSchema } from "../model-roles.js";
3
+ import { NATIVE_PROVIDERS } from "../provider-specs.js";
3
4
  import { AgentPlacementSchema } from "../runner-protocol.js";
4
5
  import { entryId } from "./internal.js";
5
6
  // The agent runtimes the daemon can serve, the vocabulary every surface that picks an agent shares (chat
@@ -21,11 +22,7 @@ export type AgentProvider = z.infer<typeof AgentProviderSchema>;
21
22
  // subjects are exactly the ones the daemon keeps one for. Closing it here is what makes an unknown id a 400 from
22
23
  // the contract instead of a registry lookup that reads back `undefined` and serves an empty list.
23
24
  export const NativeProviderParamSchema = z.object({ provider: z.enum(NATIVE_PROVIDERS) });
24
- // The provider naming an account on the routes that connect one by pasting a key (keys.contract.ts). Closed the
25
- // same way and for the same reason as the catalog param above, narrowed to the providers whose credential this
26
- // daemon actually stores as a key: pasting one at a provider that authenticates some other way is a 400 from
27
- // the contract rather than a handler discovering there is no store to write to.
28
- export const KeyProviderParamSchema = z.object({ provider: z.enum(KEY_PROVIDERS) });
25
+ // The provider naming an account on the routes whose sign-in mints the vendor's own key (minted.contract.ts).
29
26
  // The harness (agentic loop) a turn runs on, orthogonal to the provider. See AgentTurnSchema.harness.
30
27
  export const AgentHarnessSchema = z.enum(["native", "claude-code"]);
31
28
  export type AgentHarness = z.infer<typeof AgentHarnessSchema>;
@@ -87,23 +84,55 @@ export type WakeSource = z.infer<typeof WakeSourceSchema>;
87
84
  // One admission verdict the owner can configure: let it run, hold it for approval, or refuse it outright.
88
85
  export const AdmissionRuleSchema = z.enum(["allow", "hold", "deny"]);
89
86
  export type AdmissionRule = z.infer<typeof AdmissionRuleSchema>;
87
+ /* WHERE A COMMAND WOULD RUN, and it changes what half of these classes MEAN.
88
+ *
89
+ * The same string is two different acts depending on which machine reads it. `rm -rf /usr` inside this
90
+ * container deletes files that come back with the image; on somebody's laptop it ends the laptop. A named
91
+ * Docker volume here is a dev database the agent created three commands ago; there it is whatever they run.
92
+ * `/work` and `/history` are this product's own trees and mean nothing on a device; `/Users` and `C:` are a
93
+ * device's and never appear here.
94
+ *
95
+ * So the catalog is read with a locus (command-classes.ts CommandContext), and the classes below are the
96
+ * answer to "what would this DO", asked of a stated place. Callers do not get to omit it: a default would be
97
+ * one of these two answers applied silently to the other machine, which is the bug this type exists to end. */
98
+ export const CommandLocusSchema = z.enum([
99
+ // This sandbox's own shell: a disposable container, /work a git worktree, /history every other agent's.
100
+ "sandbox",
101
+ // One of the owner's own computers, reached through the machine agent. Nothing here is disposable.
102
+ "device",
103
+ ]);
104
+ export type CommandLocus = z.infer<typeof CommandLocusSchema>;
90
105
  /* WHAT KIND OF THING A SHELL COMMAND IS, the command gate's key space, the second layer under the admission
91
106
  * floor above. The floor decides whether a session may START; these decide whether one PARTICULAR command
92
107
  * inside a running session may go ahead, which is the only question left once the agent is already working.
93
108
  *
94
- * Five, chosen for one property: everything in them is hard or impossible to take back, so a person seeing it
95
- * once beats an audit trail read afterwards. Everything else an agent runs, builds, tests, greps, edits, is
96
- * recoverable in a container that is itself disposable, and gating it would be friction bought with nothing. */
109
+ * Six, chosen for one property: everything in them is hard or impossible to take back SOMEWHERE, so a person
110
+ * seeing it once beats an audit trail read afterwards. Everything else an agent runs, builds, tests, greps,
111
+ * edits, is recoverable in a container that is itself disposable, and gating it would be friction bought with
112
+ * nothing. "Somewhere" is the locus above: which class a command lands in is asked of a place. */
97
113
  export const CommandClassSchema = z.enum([
98
114
  // Rewrites or discards committed work: force-push, hard reset, force-delete a branch, clean -f, filter-branch.
99
115
  "git.destructive",
100
116
  // Recursive-force deletion (`rm -rf`), and its spelling in a script (`fs.rm(p, { recursive: true })`).
101
117
  "files.destructive",
102
- /* State nothing here brings back: a formatted or overwritten disk, a deleted Docker volume, a recursive
103
- * delete aimed at a root rather than at something inside one. The only class the daemon holds where the
104
- * owner wrote no rule, which is why it is separate from files.destructive rather than a shade of it:
105
- * `rm -rf build` is ordinary work in a disposable container and `rm -rf /` is the end of the machine. */
118
+ /* State nothing brings back AT THIS LOCUS: a formatted or overwritten disk, and a recursive delete aimed
119
+ * at a root rather than at something inside one. The class the daemon holds where the owner wrote no rule,
120
+ * which is why it is separate from files.destructive rather than a shade of it: `rm -rf build` is ordinary
121
+ * work in a disposable container and `rm -rf /` is the end of the machine.
122
+ *
123
+ * WHICH ROOTS COUNT IS THE LOCUS'S ANSWER, not a constant. In the sandbox it is `/` and `/history` — the
124
+ * filesystem itself, and the one tree holding work this turn cannot recreate because it is other
125
+ * conversations'. `/usr`, `/etc`, `/work` are NOT roots here: the container comes back from its image and
126
+ * the worktree's delta is uncommitted changes, both of which cost an afternoon rather than everything. On a
127
+ * device the full list applies, because nothing there is rebuilt from an image. */
106
128
  "system.destructive",
129
+ /* A CONTAINER VOLUME OR THE DATA IN IT: `docker volume rm`, `system prune`, `compose down -v`. Split out of
130
+ * system.destructive because the two loci disagree about it more sharply than about anything else in this
131
+ * enum. In the sandbox these reach the NESTED engine (the host's socket is never mounted, see
132
+ * capabilities/handlers/docker.ts), so the blast radius is dev databases the agent has been working
133
+ * against, and tearing down a smoke-test stack is ordinary work. Sent to somebody's own computer it is
134
+ * whatever they run on it, and their policy says never. */
135
+ "container.state",
107
136
  /* READS credential material: a `{{secret:NAME}}` reference (which becomes the value on the way into the
108
137
  * process), or a file that actually holds one — a dotenv, a private key, ~/.aws/credentials, an npmrc.
109
138
  * "Actually" is load-bearing and is checked rather than assumed where the caller can open the file: an
@@ -239,6 +268,23 @@ export const AgentTurnSchema = z
239
268
  .describe(
240
269
  "Whether this turn's work merges into the workspace when it finishes. Overrides the conversation's own setting for this turn only.",
241
270
  ),
271
+ /* WHAT STARTED THIS TURN, when it was not a person at a composer, and therefore which of the owner's
272
+ * model lists answers for it (settings.modelRoles, model-roles.ts declares the roles).
273
+ *
274
+ * IT NAMES THE JOB, NOT THE TIER. Before this field there was one list for every unattended turn, so a
275
+ * production incident and a documentation sweep were configured together, and a surface added tomorrow
276
+ * inherited that tier by saying nothing. Now a starter says what it IS, and the owner gets to answer
277
+ * per job: cheap for the sweep, frontier for the incident.
278
+ *
279
+ * ONLY FILLS A SILENCE. The daemon applies the role's list to a turn that named no model AND no
280
+ * provider (turn-resume.ts): a caret pick, an acceptance run's own choice, a workflow step pinned to a
281
+ * provider — all of those already answered the question, and this must not overrule them.
282
+ *
283
+ * A `run` role, always. The `helper` roles never reach here: a one-shot is not a turn, it has no
284
+ * conversation and no worktree, and it asks its own role directly at the seam that spends it. */
285
+ runRole: ModelRoleSchema.optional().describe(
286
+ "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.",
287
+ ),
242
288
  // Set ONLY by the daemon's own automation dispatchers: this turn opens a conversation on behalf of an
243
289
  // outside message rather than a user. Recorded on the registry entry so the fleet can say where the
244
290
  // agent came from. Requires conversationId, there is nothing to record it on otherwise.
@@ -283,7 +329,7 @@ export const AgentTurnSchema = z
283
329
  * opposite defaults, the chat wants the provider's own catalog default, an unattended run wants the
284
330
  * tier its owner chose for work that spends money while they are not watching.
285
331
  *
286
- * The daemon fills `agent`/`model` and the pinned entry's own knobs from agentRunModels for any turn that says
332
+ * The daemon fills `agent`/`model` and the pinned entry's own knobs from the turn's own role list for any turn that says
287
333
  * this and names none of them (startConversationTurn), walking that list until one can actually be
288
334
  * started. Naming one still wins: every surface-started run now carries a caret that overrides the list
289
335
  * for that run alone, and Acceptance picks per run because it fans a session out per story. Either way
@@ -388,14 +434,14 @@ export type AgentTurn = z.infer<typeof AgentTurnSchema>;
388
434
  * Shared rather than re-declared per route because every surface that starts an agent for the user now carries
389
435
  * that caret, and they must all mean the same thing by it: the pair rides onto the turn as `agent`/`model`, and
390
436
  * the daemon's own fill step then leaves it alone (turn-resume.ts fills only what is absent). ABSENT is the
391
- * ordinary case and the one to keep cheap, nobody touched the caret, so `agentRunModels` answers.
437
+ * ordinary case and the one to keep cheap, nobody touched the caret, so the turn's `runRole` list answers.
392
438
  *
393
439
  * Both halves or neither, because a model id is only meaningful to the provider that vends it: half a pick
394
440
  * would send a Codex model id to Claude. Routes that accept this pass it through verbatim; a model this build
395
441
  * has never heard of is a supported pick, since the picker offers a custom-id escape hatch.
396
442
  *
397
443
  * AND THE TIER IT RUNS AT, because naming a model is only half of what the standing setting says. A pinned
398
- * entry carries its own effort (AgentRunPinSchema), and the daemon applies the pin's knobs ONLY to a turn that
444
+ * entry carries its own effort (ModelPinSchema), and the daemon applies the pin's knobs ONLY to a turn that
399
445
  * named no model (turn-resume.ts): so a caret that could re-point the model but not the tier moved every
400
446
  * override onto the provider's own default effort, and the one moment somebody reaches for the caret is the
401
447
  * failure that just beat the standing order. Optional, and absent means absent, the turn goes out without an
@@ -414,29 +460,36 @@ export const AgentRunPickSchema = z
414
460
  })
415
461
  .optional();
416
462
  export type AgentRunPick = z.infer<typeof AgentRunPickSchema>;
417
- /* A MODEL PINNED FOR EVERY SURFACE-STARTED RUN, one entry of settings.agentRunModels: the standing version of
418
- * the pick above, and not merely which model but HOW it is to be run.
463
+ /* ONE ENTRY OF ONE ROLE'S MODEL LIST (settings.modelRoles): the standing version of the pick above, and not
464
+ * merely which model but HOW it is to be run.
419
465
  *
420
- * THE KNOBS RIDE THE ENTRY RATHER THAN THE LIST, which is the whole reason this is an object where the setting
421
- * used to hold a `${provider}:${model}` string. The reasoning effort was a single field beside the list, so one
422
- * tier answered for every model in it — and the entries of that list are deliberately NOT interchangeable: it
423
- * is a frontier pin with the cheap account underneath that catches it when the first is spent. A tier scale is
424
- * a property of the MODEL as well ('max' is off Kimi's scale entirely, and off Claude's own the moment thinking
466
+ * THE KNOBS RIDE THE ENTRY RATHER THAN THE LIST, which is the whole reason this is an object rather than a
467
+ * `${provider}:${model}` string. The reasoning effort was once a single field beside a list, so one tier
468
+ * answered for every model in it — and the entries of such a list are deliberately NOT interchangeable: it is a
469
+ * frontier pin with the cheap account underneath that catches it when the first is spent. A tier scale is a
470
+ * property of the MODEL as well ('max' is off Kimi's scale entirely, and off Claude's own the moment thinking
425
471
  * is switched off), so a shared effort was either off-scale for half the list or the lowest common rung for all
426
- * of it. Each entry now carries what the composer's picker configures for the turn in front of you.
472
+ * of it. Each entry carries what the composer's picker configures for the turn in front of you.
473
+ *
474
+ * THE SAME SHAPE FOR EVERY ROLE, one-shot helpers included, and that is a deliberate widening. A commit
475
+ * message or a session title used to be pinnable by model alone, on the argument that the daemon runs those
476
+ * with reasoning off and no effort, so a control for either would be a switch with nothing behind it. True of
477
+ * the machinery, and it made the machinery the argument: an owner who pins a reasoning model to their commit
478
+ * subjects was paying that model's price to have its distinguishing feature suppressed. The knobs now travel
479
+ * through the one-shot path too, so an entry means the same thing wherever it is written.
427
480
  *
428
- * EVERY FIELD BUT THE PAIR IS OPTIONAL, AND ABSENT MEANS ABSENT: the turn goes out without the field and the
481
+ * EVERY FIELD BUT THE PAIR IS OPTIONAL, AND ABSENT MEANS ABSENT: the work goes out without the field and the
429
482
  * provider's own default answers, exactly as an unconfigured pin always did. Nothing here invents a "low".
430
483
  *
431
484
  * NO TIER HOLD, and its absence is the rule rather than an omission: automatic tier selection gates on
432
- * `unattended` (prompt-complexity.ts), so a surface-started run is never downgraded in the first place and a
433
- * veto over it would be a control whose state can make no difference to anything.
485
+ * `unattended` (prompt-complexity.ts), so a role-started run is never downgraded in the first place and a veto
486
+ * over it would be a control whose state can make no difference to anything.
434
487
  *
435
488
  * The pair is BOTH HALVES for the reason the pick above is: a model id is only meaningful to the provider that
436
489
  * vends it, so half a pin would send a Codex id to Claude. Taken verbatim, never validated against a catalog:
437
490
  * the picker offers a custom-id escape hatch, so a model this build has never heard of is a supported pin. */
438
- export const AgentRunPinSchema = z.object({
439
- provider: AgentProviderSchema.describe("Which provider serves the run."),
491
+ export const ModelPinSchema = z.object({
492
+ provider: AgentProviderSchema.describe("Which provider serves this work."),
440
493
  model: z.string().min(1).describe("Which of its models. Both halves, because a model name only means anything to the provider that serves it."),
441
494
  effort: z
442
495
  .string()
@@ -446,7 +499,7 @@ export const AgentRunPinSchema = z.object({
446
499
  fast: z.boolean().optional().describe("Ask for this model's work at a higher rate for a higher price. A request rather than a promise."),
447
500
  harness: AgentHarnessSchema.optional().describe("Which agentic loop runs it. Leave it out to use the provider's own."),
448
501
  });
449
- export type AgentRunPin = z.infer<typeof AgentRunPinSchema>;
502
+ export type ModelPin = z.infer<typeof ModelPinSchema>;
450
503
  // POST /agent's ack: the daemon-minted id of the detached turn run it started. The turn executes daemon-side
451
504
  // regardless of any client connection; every window, the initiator included, renders it via /agent/attach.
452
505
  export const StartedTurnSchema = z.object({
@@ -95,6 +95,45 @@ export const AgentAttentionSchema = z.object({
95
95
  conflict: z.boolean().describe("Its work cannot be merged without somebody resolving a clash."),
96
96
  });
97
97
  export type AgentAttention = z.infer<typeof AgentAttentionSchema>;
98
+ /* WHAT THE LAST TURN LEFT OPEN, as the turn itself measured it at the moment it ended.
99
+ *
100
+ * Every other "needs you" on this card is a turn PARKED on somebody (AgentAttentionSchema): the agent is still
101
+ * there, waiting, and the board can say so because the pause is live. This is the opposite shape and the reason
102
+ * it is a field of its own: the turn is over, nobody is waiting, and the work stopped short anyway. That card
103
+ * reads `idle` beside a hundred others that finished what they were asked, which is how a session with three of
104
+ * seven steps still open goes back onto the board looking exactly like a session that is done.
105
+ *
106
+ * DERIVED, NEVER DECLARED, and that is the whole design constraint. Nothing here asks a model anything or asks
107
+ * an agent to report on itself: both readings are taken from frames the daemon already receives on every
108
+ * harness, once, at the finish that flushes the turn's tokens and tool counts. An agent cannot flatter this
109
+ * field, and a harness that never learned about it still fills it.
110
+ *
111
+ * Absent for the ordinary card, which is most of them: a turn that ends with its list clear and its check green
112
+ * has nothing to say here, and neither has one that kept no list and stood under no check. */
113
+ export const UnfinishedWorkSchema = z.object({
114
+ // When the turn that left it this way ended, ms since epoch. What the mark's "…, 2h ago" is measured from,
115
+ // and NOT `updatedAt`: a card touched since (a land, a rename) has moved without the work moving.
116
+ at: z.number().describe("When the turn that left this ended, in milliseconds."),
117
+ /* THE AGENT'S OWN CHECKLIST, as of that turn's last word on it (the `todos` frames). `open` counts every
118
+ * item not marked completed, `total` the whole list, and `next` names the one it would have done next, the
119
+ * in-progress item if there is one, else the first still pending.
120
+ *
121
+ * It is the agent's own account of the job, so a count here is not an inference about what it meant: it is
122
+ * the list it wrote, with items on it nobody crossed off. Absent when the conversation kept no list. */
123
+ steps: z
124
+ .object({
125
+ open: z.number().describe("Items on it that were never completed."),
126
+ total: z.number().describe("Items on the whole list."),
127
+ next: z.string().optional().describe("The one it would have done next: what it was working through, or the first still waiting."),
128
+ })
129
+ .optional()
130
+ .describe("The agent's own checklist where that turn left it. Absent for a conversation that kept no list."),
131
+ // The name of the `turn.ending` check that ran red on the way out, when one did (rules/turn-ending.ts). A
132
+ // turn gets two rounds to repair what a check reports and can then end regardless, so this is the workspace's
133
+ // own gate saying the work is not done, in a place nothing but the land used to read.
134
+ check: z.string().optional().describe("The end-of-turn check that was still failing when the turn ended, by name."),
135
+ });
136
+ export type UnfinishedWork = z.infer<typeof UnfinishedWorkSchema>;
98
137
  /* WHAT A LANDING IS CALLED, the commit message drafted from the landed diff (agents/landed-subject.ts), and
99
138
  * the whole of it: a subject, and the two trailer sentences a repo that keeps a changelog gets.
100
139
  *
@@ -143,8 +182,8 @@ export const LandedMessageSchema = z.object({
143
182
  .describe("What this change takes away, for anything already relying on it. Nearly always absent: it is for removals, not for additions."),
144
183
  });
145
184
  export type LandedMessage = z.infer<typeof LandedMessageSchema>;
146
- /* ONE MODEL'S TURN IN THE DRAFTING WALK, asked, and what became of the ask. The quick-model chain tries the
147
- * connected models in order (agent/quick-model.ts), and each rung ends one of four ways:
185
+ /* ONE MODEL'S TURN IN THE DRAFTING WALK, asked, and what became of the ask. The one-shot helper chain tries the
186
+ * connected models in order (agent/role-model.ts), and each rung ends one of four ways:
148
187
  * asking , in flight right now; `ms` absent because it is still being spent.
149
188
  * answered, it wrote the sentence, in `ms`.
150
189
  * refused , it failed or declined, in `ms`, with its own words in `reason`.
@@ -361,7 +400,7 @@ export const AgentSummarySchema = z.object({
361
400
  // The ROOT repo's short base sha, the checkout moment's display identity. Per-repo bases stay
362
401
  // daemon-internal (agents.diff already reports against them).
363
402
  base: z.string().optional().describe("The commit its private copy started from, shortened."),
364
- costUsd: z.number().optional().describe("What it has cost so far, in dollars. A helper agent's spend is its own and is not folded in here."),
403
+ costUsd: z.number().optional().describe("What it has cost so far, in dollars. A subagent's spend is its own and is not folded in here."),
365
404
  inputTokens: z.number().optional().describe("Tokens sent."),
366
405
  outputTokens: z.number().optional().describe("Tokens received."),
367
406
  contextTokens: z.number().optional().describe("How much of the window the conversation currently fills."),
@@ -414,6 +453,12 @@ export const AgentSummarySchema = z.object({
414
453
  "When somebody last opened it, in milliseconds. Newer activity than this is what makes it unread. Kept by the sandbox rather than by a browser, so clearing site data or picking up a phone does not resurrect every badge.",
415
454
  ),
416
455
  attention: AgentAttentionSchema.describe("Which kinds of waiting-for-you it is doing."),
456
+ /* What the last turn left open, when it left anything (UnfinishedWorkSchema). Beside `attention` because a
457
+ * reader asks both questions in the same glance, and apart from it because the answers have opposite
458
+ * shapes: attention is a turn parked and waiting, this is a turn gone with the job half done. */
459
+ unfinished: UnfinishedWorkSchema.optional().describe(
460
+ "What its last turn left open: steps it never completed, a check still failing. Absent for a turn that finished what it started.",
461
+ ),
417
462
  // Completed turns and lifetime tool calls, the card's msgs/tools counters.
418
463
  turns: z.number().optional().describe("Turns it has finished."),
419
464
  toolUses: z.number().optional().describe("Tools it has used, over its whole life."),
@@ -431,12 +476,12 @@ export const AgentSummarySchema = z.object({
431
476
  * the Subagents area is where it is attributed. */
432
477
  subagents: z
433
478
  .object({
434
- running: z.number().describe("Helpers working right now."),
435
- total: z.number().describe("Helpers it has started over its whole life."),
479
+ running: z.number().describe("Subagents working right now."),
480
+ total: z.number().describe("Subagents it has started over its whole life."),
436
481
  })
437
482
  .optional()
438
483
  .describe(
439
- "Helper agents this one delegated to. Absent means it never has, which is most conversations. Their spend is their own and is not folded into this conversation's cost.",
484
+ "Subagents and child agents this one delegated to. Absent means it never has, which is most conversations. Their spend is their own and is not folded into this conversation's cost.",
440
485
  ),
441
486
  // The agent's cumulative output (base → branch tip across every repo), refreshed on each land,
442
487
  // the card's "12 files · +412 −96" readout. Independent of what has landed.
@@ -573,6 +618,22 @@ export type AgentWatch = NonNullable<AgentSummary["watches"]>[number];
573
618
  // AgentsListSchema lives further down, after AutomationApprovalSchema, the fleet list carries the held wakes,
574
619
  // and zod declaration order forces the ride-along to be declared first.
575
620
  export const AgentIdSchema = z.object({ id: z.string().min(1).describe("Which conversation.") });
621
+
622
+ /* ASKING FOR ONE PAGE OF A CONVERSATION. A transcript read answers with the most recent turns and says where
623
+ * they start (`from`); handing that number back as `before` asks for the page above it, and so on to the
624
+ * beginning. Both are optional: a tab opening a chat sends neither and gets the tail.
625
+ *
626
+ * A cursor is never an error. It can be stale by the time it arrives — a rewind truncated the record under it,
627
+ * a fork re-cut it, the tab slept through both — and the daemon clamps rather than refusing, because failing
628
+ * to open a conversation is a worse answer than opening it at the end. */
629
+ export const AgentTranscriptQuerySchema = AgentIdSchema.extend({
630
+ before: z.coerce
631
+ .number()
632
+ .int()
633
+ .optional()
634
+ .describe("Return the messages before this position in the record: the `from` of the page below. Absent asks for the most recent turns."),
635
+ turns: z.coerce.number().int().min(1).max(200).optional().describe("How many of the user's turns to return, newest first. Absent takes the daemon's default."),
636
+ });
576
637
  // archive's input: the agents to take off the board. Absent `ids` ⇒ every finished agent that is archivable
577
638
  // right now (the lane header's "Clear"); unarchive always names its ids (a restore, or a bulk archive's undo).
578
639
  export const AgentArchiveSchema = z.object({
@@ -233,10 +233,12 @@ export const WebchatPublicConfigSchema = z.object({
233
233
  googleClientId: z.string().optional(),
234
234
  });
235
235
  export type WebchatPublicConfig = z.infer<typeof WebchatPublicConfigSchema>;
236
- // A proof-of-work challenge: find a nonce whose SHA-256 of `${salt}:${nonce}` starts with `difficulty` zero
237
- // bits. Issued per visitor conversation, spent on its first message.
238
- export const WebchatChallengeSchema = z.object({ salt: z.string(), difficulty: z.number().int().positive() });
239
- export type WebchatChallenge = z.infer<typeof WebchatChallengeSchema>;
236
+ /* A proof-of-work challenge: find a nonce whose SHA-256 of `${salt}:${nonce}` starts with `difficulty` zero
237
+ * bits. ONE shape for every public door (the Front Desk issues it per visitor conversation and spends it on the
238
+ * first message; the bug intake per reporter, on a written report), and one solver on the embeds' side
239
+ * (embed.ts, which declares the same two fields without zod). */
240
+ export const PowChallengeSchema = z.object({ salt: z.string(), difficulty: z.number().int().positive() });
241
+ export type PowChallenge = z.infer<typeof PowChallengeSchema>;
240
242
  // One visitor message. `conversationId` is the widget's own localStorage id, it threads the visitor's messages
241
243
  // into ONE sandbox conversation, so it is the thread key, not a secret (anyone can mint one; the origin
242
244
  // allowlist, the challenge and the rate limit are the gate).
@@ -277,7 +277,7 @@ export const IdentityConfigSchema = z.object({
277
277
  exit: z.string().optional(),
278
278
  });
279
279
  export type IdentityConfig = z.infer<typeof IdentityConfigSchema>;
280
- /* A connected COMPUTER of the user's own, the inverse of `ssh`, which reaches a server the sandbox can dial.
280
+ /* A connected DEVICE of the user's own, the inverse of `ssh`, which reaches a server the sandbox can dial.
281
281
  * A machine behind NAT can't be dialled, so it dials US: the @intentic/machine agent (installed by a one-liner,
282
282
  * enrolled with a single-use pairing token) holds one outbound WebSocket to this daemon and serves an MCP tool
283
283
  * surface, shell, files, screenshots, from the far end. The daemon tunnels the agent's JSON-RPC over it and
@@ -331,7 +331,7 @@ export const HostScopesSchema = z.object({
331
331
  * and cannot park it while somebody thinks, so the honest form of "ask me" here is "refuse until they
332
332
  * ticked it", which is exactly what a scope is.
333
333
  *
334
- * Default off, with `shell` default ON, which is the pairing to read carefully: a connected computer runs
334
+ * Default off, with `shell` default ON, which is the pairing to read carefully: a connected device runs
335
335
  * commands out of the box, because that is what people connect one for, and the ones that delete are the
336
336
  * ones they have to say yes to. */
337
337
  destructive: hostScope.default("off"),
@@ -342,7 +342,7 @@ export type HostScopes = z.infer<typeof HostScopesSchema>;
342
342
  export const HostConfigSchema = HostScopesSchema.extend({ platform: z.string().min(1) });
343
343
  /* THE USER'S OWN BROWSER, reached through the extension they installed in it: the `webext` capability's config.
344
344
  *
345
- * The sibling of `host` and deliberately not an arm of it. A connected computer runs commands; a connected
345
+ * The sibling of `host` and deliberately not an arm of it. A connected device runs commands; a connected
346
346
  * browser has one power a sandbox's own Chromium can never have, and it is the whole reason this kind exists:
347
347
  * it is ALREADY SIGNED IN, as the person, with their passkeys, their hardware second factor, their corporate
348
348
  * SSO and their genuine fingerprint. That is the set of sites the sandbox's browser cannot reach at all, and
@@ -361,7 +361,7 @@ export const WebExtScopesSchema = z.object({
361
361
  // Read a granted page: its elements, its text, its tabs. The floor of usefulness, so it defaults on; with
362
362
  // it off the connection is inert and the card says so rather than pretending.
363
363
  read: webextScope.default("on"),
364
- /* Click, type, press keys, navigate. ON by default, unlike a computer's `control`, and the difference is
364
+ /* Click, type, press keys, navigate. ON by default, unlike a device's `control`, and the difference is
365
365
  * what the two things ARE: driving a desktop is the last resort after every command-line route failed,
366
366
  * while driving the page IS this connector — a browser connection that may only look is a worse version
367
367
  * of fetching the URL. The grant that actually bounds it is per-site and lives in the browser. */
package/src/schemas/ci.ts CHANGED
@@ -9,11 +9,25 @@ import { AgentRunPickSchema } from "./agent.js";
9
9
 
10
10
  export const CiHostSchema = z.enum(["github", "gitlab"]);
11
11
  export type CiHost = z.infer<typeof CiHostSchema>;
12
- // Terminal-or-not over both vendors' vocabularies: github's status+conclusion pair and gitlab's single status
13
- // both collapse onto these five. `running` covers everything non-terminal (queued, manual, preparing …), the
14
- // view only needs "still moving" vs the three ways it stopped.
15
- export const PipelineStatusSchema = z.enum(["running", "success", "failed", "canceled", "skipped"]);
12
+ /* Both vendors' vocabularies over one enum: github's status+conclusion pair and gitlab's single status collapse
13
+ * onto these six.
14
+ *
15
+ * QUEUED IS ITS OWN STATE AND NOT A FLAVOUR OF RUNNING, which it used to be, on the reasoning that a view only
16
+ * needs "still moving" vs the three ways it stopped. What that produced is a board that spins over work nothing
17
+ * is doing: a nightly whose six self-hosted jobs are waiting for a runner that is offline reads as six jobs in
18
+ * progress, with a duration ticking up, for as long as the runner stays down. The distinction is not cosmetic,
19
+ * it is the difference between "wait" and "go look at your runners", and it is the only question a reader of a
20
+ * stuck pipeline actually has.
21
+ *
22
+ * The two are still one class for everything that asks "has this said anything yet": neither is a verdict, and
23
+ * the callers that care read them as a pair. */
24
+ export const PipelineStatusSchema = z.enum(["queued", "running", "success", "failed", "canceled", "skipped"]);
16
25
  export type PipelineStatus = z.infer<typeof PipelineStatusSchema>;
26
+ /* Whether a run or a job has yet to say anything, the pair against the three ways one stops. Here rather than
27
+ * in either consumer because both ends split on it and must split the same way: the daemon, to know that the
28
+ * span between two timestamps is not yet a duration, and the board, to count what is in flight, to keep a
29
+ * Cancel button on offer, and to leave a run out of a verdict walk. */
30
+ export const isPipelineInFlight = (status: PipelineStatus): boolean => status === "queued" || status === "running";
17
31
  export const PipelineRunSchema = z.object({
18
32
  // The workspace repo dir (the panels `repo` convention), the join key back to the tree and to triggers.
19
33
  repo: z.string().describe("Which workspace repository it belongs to."),
@@ -45,7 +59,7 @@ export const PipelineRunSchema = z.object({
45
59
  branch: z.string().describe("Which branch."),
46
60
  sha: z.string().describe("Which commit."),
47
61
  status: PipelineStatusSchema.describe(
48
- "How it is going. Running covers everything still moving, since the only distinction that matters is that against the three ways it can stop.",
62
+ "How it is going. Queued means the forge has accepted it and nothing is executing it yet, which is a different thing to wait on than a run actually in progress.",
49
63
  ),
50
64
  // The vendor's run page, the deep link out.
51
65
  url: z.string().describe("Its page on the forge."),
@@ -71,7 +85,10 @@ export type PipelineRun = z.infer<typeof PipelineRunSchema>;
71
85
  * 2. `stage`. GitLab's native sequential grouping, returned by its jobs API and used verbatim.
72
86
  * 3. The timestamps, the last resort, and GitHub's before `needs` existed: overlapping runtimes ⇒ the jobs
73
87
  * ran in parallel. Honest about when things happened, silent about what actually gated what.
74
- * Both timestamps are epoch ms; absent while a job is still queued. */
88
+ * Both timestamps are epoch ms, and a `queued` job carries NEITHER. That is an invariant the normalizers hold
89
+ * up rather than something a vendor gives: GitHub reports a `started_at` on a job that has never started, set
90
+ * to the moment the run was queued, so a job waiting an hour for a runner arrives claiming an hour of work.
91
+ * Everything downstream reads a present `startedAt` as "this began", so the lie has to be dropped at the edge. */
75
92
  export const PipelineJobSchema = z.object({
76
93
  name: z.string().describe("The job's name."),
77
94
  status: PipelineStatusSchema.describe("How it went."),
@@ -0,0 +1,87 @@
1
+ // context: which part of the workspace a conversation carries
2
+ import { z } from "zod";
3
+ import { entryId } from "./internal.js";
4
+
5
+ /* A CONVERSATION SEES A CHOSEN PART OF THE WORKSPACE, and these three schemas are how the choice is spelled.
6
+ *
7
+ * A workspace grows into more than any one session should open on: dozens of repositories, hundreds of skills,
8
+ * a shelf of reference clones. Every conversation used to get all of it, one worktree per repository, frozen at
9
+ * its first turn (sandbox agents/worktrees.ts). The cost is paid twice, in checkouts nobody reads and in a
10
+ * project map and a skill listing that describe everything to a session that needs a corner of it
11
+ * (docs/context-composition-plan.md at the workspace root measures both).
12
+ *
13
+ * THREE WORDS. A SHELF is what the owner writes: the items a session MAY take, in the order they are loaded and,
14
+ * when a cap bites, shed from the tail. A COMPOSITION is the pick for one conversation, a list of item ids in
15
+ * shelf order, stored on its registry entry. An ITEM ID names one thing in the workspace, `<kind>:<name>`.
16
+ *
17
+ * ONE KIND TODAY, `repo:`, which is the whole of what a composition can make real: a repository is in the
18
+ * conversation (a worktree of it exists) or it is not (the directory does not exist). The grammar takes a kind
19
+ * prefix so the next ones (a directory inside a repository through a sparse cone, a skill folder, a reference
20
+ * shelf entry, a capability) widen this list and change nothing else. The root repository is never an item: it
21
+ * is the workspace itself and every conversation stands in it. */
22
+ export const CONTEXT_ITEM_KINDS = ["repo"] as const;
23
+ export type ContextItemKind = (typeof CONTEXT_ITEM_KINDS)[number];
24
+
25
+ /* `<kind>:<name>`. The name is a workspace-relative path or an id: safe segments, no `..`, no empty segment, so
26
+ * joining it under a root can never escape, the same shape repo-discovery.ts accepts for a repo id. */
27
+ const ITEM = /^(repo):[a-zA-Z0-9][a-zA-Z0-9._-]*(\/[a-zA-Z0-9][a-zA-Z0-9._-]*)*$/;
28
+ export const ContextItemIdSchema = z
29
+ .string()
30
+ .min(1)
31
+ .max(200)
32
+ .regex(ITEM)
33
+ .describe("One thing in the workspace a conversation can carry, as `<kind>:<name>`. Today the kind is `repo` and the name is a repository's workspace-relative path.");
34
+ export type ContextItemId = z.infer<typeof ContextItemIdSchema>;
35
+
36
+ // The two halves of an id, for the code that has to act on the name. The schema above already proved the shape.
37
+ export const contextItem = (id: ContextItemId): { readonly kind: ContextItemKind; readonly name: string } => {
38
+ const at = id.indexOf(":");
39
+ return { kind: id.slice(0, at) as ContextItemKind, name: id.slice(at + 1) };
40
+ };
41
+
42
+ /* HOW MANY OF EACH KIND A COMPOSITION MAY HOLD. Counted per kind because the kinds cost differently: a repository
43
+ * is a checkout, a skill is a line in every prompt. Absent means no limit. A pinned item is never shed to meet
44
+ * a cap: `pinned` is the owner saying "always", and a cap that could override it would make two fields disagree
45
+ * about the same item. */
46
+ export const ContextCapsSchema = z.object({
47
+ repos: z.number().int().min(0).optional().describe("How many repositories a composition may hold. Absent means as many as the shelf allows."),
48
+ });
49
+ export type ContextCaps = z.infer<typeof ContextCapsSchema>;
50
+
51
+ /* WHAT A SESSION MAY TAKE, in the owner's order. One file per shelf under `.intentic/config/context/<id>.json`,
52
+ * tracked and carried like a persona card (workspace-state.ts), for the same reason: a shelf is a list of
53
+ * names, holds no credential, and belongs in a pull request.
54
+ *
55
+ * `allowed` IS ORDERED, and the order is the only priority there is: it is what a curator is shown, the order
56
+ * items are loaded, and the order they are shed from the tail when a cap is hit. The owner fixes the order and
57
+ * the ceiling; whoever picks (a model, a person on the card, the shelf itself when nobody picks) decides
58
+ * membership and nothing else.
59
+ *
60
+ * `pinned` is always in, whether or not it is also listed in `allowed`. `denied` wins over everything, including
61
+ * a pinned item, so a shelf that inherits a list it did not write can still take one thing off it. */
62
+ export const ContextShelfSchema = z.object({
63
+ id: entryId.describe("The shelf's id, which is also its file name."),
64
+ label: z.string().max(60).optional().describe("What to call it on screen. Absent falls back to the id."),
65
+ pinned: z.array(ContextItemIdSchema).max(200).default([]).describe("Items every composition from this shelf carries."),
66
+ allowed: z
67
+ .array(ContextItemIdSchema)
68
+ .max(500)
69
+ .default([])
70
+ .describe("Items a composition may carry, in the order they are loaded and shed. The order is the priority; nothing else is."),
71
+ denied: z.array(ContextItemIdSchema).max(200).default([]).describe("Items no composition from this shelf carries, whatever else says so."),
72
+ caps: ContextCapsSchema.optional().describe("How many of each kind a composition may hold."),
73
+ });
74
+ export type ContextShelf = z.infer<typeof ContextShelfSchema>;
75
+
76
+ /* THE PICK FOR ONE CONVERSATION, on its registry entry beside the worktree composition it decides.
77
+ *
78
+ * `items` is exactly what the conversation carries, in shelf order, and the root repository besides. A
79
+ * conversation whose entry has NO composition carries everything the workspace has, which is what every
80
+ * conversation did before shelves existed and what a conversation opened with no shelf still does. Those two
81
+ * are one state on purpose: the absence is the answer, and a stored "everything" would be a second spelling of
82
+ * it that could disagree with the first. */
83
+ export const ContextCompositionSchema = z.object({
84
+ shelf: entryId.optional().describe("Which shelf this pick was made from. Absent when the items were set without one."),
85
+ items: z.array(ContextItemIdSchema).max(500).describe("What the conversation carries, in shelf order. The root repository is always carried and never listed."),
86
+ });
87
+ export type ContextComposition = z.infer<typeof ContextCompositionSchema>;