@intentic/sandbox-contract 1.176.2 → 1.209.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 (340) hide show
  1. package/README.md +65 -0
  2. package/dist/agent-catalog.d.ts +15 -2
  3. package/dist/agent-catalog.d.ts.map +1 -1
  4. package/dist/agent-catalog.js +50 -1
  5. package/dist/agent-catalog.js.map +1 -1
  6. package/dist/agent-run-model.d.ts +3 -0
  7. package/dist/agent-run-model.d.ts.map +1 -0
  8. package/dist/agent-run-model.js +10 -0
  9. package/dist/agent-run-model.js.map +1 -0
  10. package/dist/capability-env.d.ts +2 -0
  11. package/dist/capability-env.d.ts.map +1 -0
  12. package/dist/capability-env.js +2 -0
  13. package/dist/capability-env.js.map +1 -0
  14. package/dist/chores/chores.d.ts +9 -0
  15. package/dist/chores/chores.d.ts.map +1 -1
  16. package/dist/chores/chores.js +257 -18
  17. package/dist/chores/chores.js.map +1 -1
  18. package/dist/chores/digest.js +0 -0
  19. package/dist/chores/digest.js.map +1 -1
  20. package/dist/chores/extension-update.d.ts +9 -0
  21. package/dist/chores/extension-update.d.ts.map +1 -0
  22. package/dist/chores/extension-update.js +15 -0
  23. package/dist/chores/extension-update.js.map +1 -0
  24. package/dist/chores/fix-deps.d.ts +10 -0
  25. package/dist/chores/fix-deps.d.ts.map +1 -0
  26. package/dist/chores/fix-deps.js +18 -0
  27. package/dist/chores/fix-deps.js.map +1 -0
  28. package/dist/chores/index.d.ts +7 -2
  29. package/dist/chores/index.d.ts.map +1 -1
  30. package/dist/chores/index.js +4 -1
  31. package/dist/chores/index.js.map +1 -1
  32. package/dist/chores/probes.d.ts +1 -0
  33. package/dist/chores/probes.d.ts.map +1 -1
  34. package/dist/chores/probes.js +131 -3
  35. package/dist/chores/probes.js.map +1 -1
  36. package/dist/chores/stack.d.ts +27 -0
  37. package/dist/chores/stack.d.ts.map +1 -0
  38. package/dist/chores/stack.js +121 -0
  39. package/dist/chores/stack.js.map +1 -0
  40. package/dist/chores/verdict.d.ts +2 -1
  41. package/dist/chores/verdict.d.ts.map +1 -1
  42. package/dist/chores/verdict.js +57 -7
  43. package/dist/chores/verdict.js.map +1 -1
  44. package/dist/contract-lock.d.ts +3 -0
  45. package/dist/contract-lock.d.ts.map +1 -0
  46. package/dist/contract-lock.js +35 -0
  47. package/dist/contract-lock.js.map +1 -0
  48. package/dist/contracts/activity.contract.d.ts +10 -0
  49. package/dist/contracts/activity.contract.d.ts.map +1 -1
  50. package/dist/contracts/agent.contract.d.ts +230 -20
  51. package/dist/contracts/agent.contract.d.ts.map +1 -1
  52. package/dist/contracts/agent.contract.js +2 -1
  53. package/dist/contracts/agent.contract.js.map +1 -1
  54. package/dist/contracts/agents.contract.d.ts +847 -1
  55. package/dist/contracts/agents.contract.d.ts.map +1 -1
  56. package/dist/contracts/agents.contract.js +3 -1
  57. package/dist/contracts/agents.contract.js.map +1 -1
  58. package/dist/contracts/automations.contract.d.ts +89 -0
  59. package/dist/contracts/automations.contract.d.ts.map +1 -1
  60. package/dist/contracts/automations.contract.js +3 -1
  61. package/dist/contracts/automations.contract.js.map +1 -1
  62. package/dist/contracts/capabilities.contract.d.ts +108 -32
  63. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  64. package/dist/contracts/capabilities.contract.js +5 -1
  65. package/dist/contracts/capabilities.contract.js.map +1 -1
  66. package/dist/contracts/chores.contract.d.ts +43 -0
  67. package/dist/contracts/chores.contract.d.ts.map +1 -1
  68. package/dist/contracts/ci.contract.d.ts +5 -0
  69. package/dist/contracts/ci.contract.d.ts.map +1 -1
  70. package/dist/contracts/ci.contract.js +2 -2
  71. package/dist/contracts/ci.contract.js.map +1 -1
  72. package/dist/contracts/claude.contract.d.ts +3 -14
  73. package/dist/contracts/claude.contract.d.ts.map +1 -1
  74. package/dist/contracts/claude.contract.js +2 -3
  75. package/dist/contracts/claude.contract.js.map +1 -1
  76. package/dist/contracts/drafts.contract.d.ts +4 -0
  77. package/dist/contracts/drafts.contract.d.ts.map +1 -1
  78. package/dist/contracts/endpoints.contract.d.ts +28 -12
  79. package/dist/contracts/endpoints.contract.d.ts.map +1 -1
  80. package/dist/contracts/endpoints.contract.js +9 -0
  81. package/dist/contracts/endpoints.contract.js.map +1 -1
  82. package/dist/contracts/extensions.contract.d.ts +399 -69
  83. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  84. package/dist/contracts/extensions.contract.js +15 -1
  85. package/dist/contracts/extensions.contract.js.map +1 -1
  86. package/dist/contracts/git.contract.d.ts +209 -11
  87. package/dist/contracts/git.contract.d.ts.map +1 -1
  88. package/dist/contracts/git.contract.js +12 -2
  89. package/dist/contracts/git.contract.js.map +1 -1
  90. package/dist/contracts/grok.contract.d.ts +0 -13
  91. package/dist/contracts/grok.contract.d.ts.map +1 -1
  92. package/dist/contracts/grok.contract.js +1 -2
  93. package/dist/contracts/grok.contract.js.map +1 -1
  94. package/dist/contracts/host.contract.d.ts +39 -0
  95. package/dist/contracts/host.contract.d.ts.map +1 -1
  96. package/dist/contracts/host.contract.js +3 -2
  97. package/dist/contracts/host.contract.js.map +1 -1
  98. package/dist/contracts/loops.contract.d.ts +343 -0
  99. package/dist/contracts/loops.contract.d.ts.map +1 -0
  100. package/dist/contracts/loops.contract.js +11 -0
  101. package/dist/contracts/loops.contract.js.map +1 -0
  102. package/dist/contracts/panels.contract.d.ts +6 -0
  103. package/dist/contracts/panels.contract.d.ts.map +1 -1
  104. package/dist/contracts/personas.contract.d.ts +112 -0
  105. package/dist/contracts/personas.contract.d.ts.map +1 -0
  106. package/dist/contracts/personas.contract.js +13 -0
  107. package/dist/contracts/personas.contract.js.map +1 -0
  108. package/dist/contracts/ports.contract.d.ts +1 -0
  109. package/dist/contracts/ports.contract.d.ts.map +1 -1
  110. package/dist/contracts/prepush.contract.d.ts +1 -0
  111. package/dist/contracts/prepush.contract.d.ts.map +1 -1
  112. package/dist/contracts/prepush.contract.js.map +1 -1
  113. package/dist/contracts/{codex.contract.d.ts → providers.contract.d.ts} +11 -3
  114. package/dist/contracts/providers.contract.d.ts.map +1 -0
  115. package/dist/contracts/providers.contract.js +6 -0
  116. package/dist/contracts/providers.contract.js.map +1 -0
  117. package/dist/contracts/public.contract.d.ts +24 -0
  118. package/dist/contracts/public.contract.d.ts.map +1 -0
  119. package/dist/contracts/public.contract.js +8 -0
  120. package/dist/contracts/public.contract.js.map +1 -0
  121. package/dist/contracts/push.contract.d.ts +3 -3
  122. package/dist/contracts/secrets.contract.d.ts +9 -0
  123. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  124. package/dist/contracts/sessions.contract.d.ts +16 -1
  125. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  126. package/dist/contracts/sessions.contract.js +1 -1
  127. package/dist/contracts/sessions.contract.js.map +1 -1
  128. package/dist/contracts/settings.contract.d.ts +352 -58
  129. package/dist/contracts/settings.contract.d.ts.map +1 -1
  130. package/dist/contracts/settings.contract.js +2 -1
  131. package/dist/contracts/settings.contract.js.map +1 -1
  132. package/dist/contracts/share.contract.d.ts +55 -0
  133. package/dist/contracts/share.contract.d.ts.map +1 -0
  134. package/dist/contracts/share.contract.js +9 -0
  135. package/dist/contracts/share.contract.js.map +1 -0
  136. package/dist/contracts/skills.contract.d.ts +41 -0
  137. package/dist/contracts/skills.contract.d.ts.map +1 -0
  138. package/dist/contracts/skills.contract.js +9 -0
  139. package/dist/contracts/skills.contract.js.map +1 -0
  140. package/dist/contracts/system.contract.d.ts +218 -22
  141. package/dist/contracts/system.contract.d.ts.map +1 -1
  142. package/dist/contracts/system.contract.js +10 -2
  143. package/dist/contracts/system.contract.js.map +1 -1
  144. package/dist/contracts/workflows.contract.d.ts +575 -0
  145. package/dist/contracts/workflows.contract.d.ts.map +1 -0
  146. package/dist/contracts/workflows.contract.js +13 -0
  147. package/dist/contracts/workflows.contract.js.map +1 -0
  148. package/dist/contracts/workspace.contract.d.ts +18 -4
  149. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  150. package/dist/contracts/workspace.contract.js +6 -3
  151. package/dist/contracts/workspace.contract.js.map +1 -1
  152. package/dist/conversation-ids.d.ts +2 -0
  153. package/dist/conversation-ids.d.ts.map +1 -0
  154. package/dist/conversation-ids.js +113 -0
  155. package/dist/conversation-ids.js.map +1 -0
  156. package/dist/events.d.ts +523 -17
  157. package/dist/events.d.ts.map +1 -1
  158. package/dist/events.js +106 -13
  159. package/dist/events.js.map +1 -1
  160. package/dist/history-state.d.ts +3 -0
  161. package/dist/history-state.d.ts.map +1 -0
  162. package/dist/history-state.js +53 -0
  163. package/dist/history-state.js.map +1 -0
  164. package/dist/hostnames.d.ts +4 -0
  165. package/dist/hostnames.d.ts.map +1 -1
  166. package/dist/hostnames.js +7 -2
  167. package/dist/hostnames.js.map +1 -1
  168. package/dist/index.d.ts +3793 -478
  169. package/dist/index.d.ts.map +1 -1
  170. package/dist/index.js +41 -17
  171. package/dist/index.js.map +1 -1
  172. package/dist/listener-protocol.d.ts +77 -0
  173. package/dist/listener-protocol.d.ts.map +1 -0
  174. package/dist/listener-protocol.js +33 -0
  175. package/dist/listener-protocol.js.map +1 -0
  176. package/dist/model-order.d.ts +1 -0
  177. package/dist/model-order.d.ts.map +1 -1
  178. package/dist/model-order.js +16 -0
  179. package/dist/model-order.js.map +1 -1
  180. package/dist/output-fields.d.ts +30 -0
  181. package/dist/output-fields.d.ts.map +1 -0
  182. package/dist/output-fields.js +57 -0
  183. package/dist/output-fields.js.map +1 -0
  184. package/dist/path-refs.d.ts.map +1 -1
  185. package/dist/path-refs.js.map +1 -1
  186. package/dist/publish-drafts.d.ts +7 -0
  187. package/dist/publish-drafts.d.ts.map +1 -0
  188. package/dist/publish-drafts.js +20 -0
  189. package/dist/publish-drafts.js.map +1 -0
  190. package/dist/quick-model.d.ts +2 -1
  191. package/dist/quick-model.d.ts.map +1 -1
  192. package/dist/quick-model.js +18 -14
  193. package/dist/quick-model.js.map +1 -1
  194. package/dist/routes.d.ts +3 -0
  195. package/dist/routes.d.ts.map +1 -1
  196. package/dist/routes.js +63 -0
  197. package/dist/routes.js.map +1 -1
  198. package/dist/runtime-state.d.ts +33 -0
  199. package/dist/runtime-state.d.ts.map +1 -0
  200. package/dist/runtime-state.js +14 -0
  201. package/dist/runtime-state.js.map +1 -0
  202. package/dist/schemas.d.ts +4938 -866
  203. package/dist/schemas.d.ts.map +1 -1
  204. package/dist/schemas.js +1011 -192
  205. package/dist/schemas.js.map +1 -1
  206. package/dist/search-globs.d.ts +6 -0
  207. package/dist/search-globs.d.ts.map +1 -0
  208. package/dist/search-globs.js +39 -0
  209. package/dist/search-globs.js.map +1 -0
  210. package/dist/share-paths.d.ts +8 -0
  211. package/dist/share-paths.d.ts.map +1 -0
  212. package/dist/share-paths.js +21 -0
  213. package/dist/share-paths.js.map +1 -0
  214. package/dist/state-portability.d.ts +8 -0
  215. package/dist/state-portability.d.ts.map +1 -0
  216. package/dist/state-portability.js +4 -0
  217. package/dist/state-portability.js.map +1 -0
  218. package/dist/title.d.ts.map +1 -1
  219. package/dist/title.js +1 -1
  220. package/dist/title.js.map +1 -1
  221. package/dist/tunnel-ids.d.ts +1 -0
  222. package/dist/tunnel-ids.d.ts.map +1 -1
  223. package/dist/tunnel-ids.js +1 -0
  224. package/dist/tunnel-ids.js.map +1 -1
  225. package/dist/versions.d.ts +4 -0
  226. package/dist/versions.d.ts.map +1 -0
  227. package/dist/versions.js +15 -0
  228. package/dist/versions.js.map +1 -0
  229. package/dist/workflow-faults.d.ts +4 -0
  230. package/dist/workflow-faults.d.ts.map +1 -0
  231. package/dist/workflow-faults.js +115 -0
  232. package/dist/workflow-faults.js.map +1 -0
  233. package/dist/workspace-state.d.ts +282 -3
  234. package/dist/workspace-state.d.ts.map +1 -1
  235. package/dist/workspace-state.js +198 -12
  236. package/dist/workspace-state.js.map +1 -1
  237. package/package.json +30 -6
  238. package/src/agent-catalog.test.ts +147 -7
  239. package/src/agent-catalog.ts +233 -21
  240. package/src/agent-run-model.test.ts +60 -0
  241. package/src/agent-run-model.ts +51 -0
  242. package/src/capability-env.ts +10 -0
  243. package/src/capability-ledger.test.ts +110 -0
  244. package/src/chores/chores.test.ts +32 -0
  245. package/src/chores/chores.ts +456 -32
  246. package/src/chores/digest.ts +0 -0
  247. package/src/chores/extension-update.ts +37 -0
  248. package/src/chores/fix-deps.ts +40 -0
  249. package/src/chores/index.ts +7 -2
  250. package/src/chores/probes.test.ts +155 -3
  251. package/src/chores/probes.ts +209 -4
  252. package/src/chores/stack.test.ts +126 -0
  253. package/src/chores/stack.ts +244 -0
  254. package/src/chores/verdict.test.ts +357 -19
  255. package/src/chores/verdict.ts +102 -22
  256. package/src/contract-lock.test.ts +17 -0
  257. package/src/contract-lock.ts +66 -0
  258. package/src/contracts/agent.contract.ts +6 -0
  259. package/src/contracts/agents.contract.ts +12 -0
  260. package/src/contracts/automations.contract.ts +17 -1
  261. package/src/contracts/capabilities.contract.ts +22 -0
  262. package/src/contracts/ci.contract.ts +10 -2
  263. package/src/contracts/claude.contract.ts +8 -4
  264. package/src/contracts/endpoints.contract.ts +21 -0
  265. package/src/contracts/extensions.contract.ts +42 -0
  266. package/src/contracts/git.contract.ts +33 -6
  267. package/src/contracts/grok.contract.ts +4 -3
  268. package/src/contracts/host.contract.ts +13 -2
  269. package/src/contracts/loops.contract.ts +62 -0
  270. package/src/contracts/personas.contract.ts +56 -0
  271. package/src/contracts/prepush.contract.ts +4 -3
  272. package/src/contracts/providers.contract.ts +23 -0
  273. package/src/contracts/public.contract.ts +14 -0
  274. package/src/contracts/sessions.contract.ts +5 -1
  275. package/src/contracts/settings.contract.ts +5 -0
  276. package/src/contracts/share.contract.ts +20 -0
  277. package/src/contracts/skills.contract.ts +24 -0
  278. package/src/contracts/system.contract.ts +27 -3
  279. package/src/contracts/workflows.contract.ts +78 -0
  280. package/src/contracts/workspace.contract.ts +10 -2
  281. package/src/conversation-ids.test.ts +25 -0
  282. package/src/conversation-ids.ts +160 -0
  283. package/src/events.test.ts +20 -1
  284. package/src/events.ts +404 -43
  285. package/src/history-state.ts +129 -0
  286. package/src/hostnames.ts +20 -6
  287. package/src/index.ts +60 -17
  288. package/src/listener-protocol.ts +96 -0
  289. package/src/model-order.test.ts +32 -1
  290. package/src/model-order.ts +56 -3
  291. package/src/output-fields.ts +111 -0
  292. package/src/path-refs.test.ts +4 -4
  293. package/src/path-refs.ts +5 -5
  294. package/src/publish-drafts.ts +59 -0
  295. package/src/quick-model.test.ts +70 -27
  296. package/src/quick-model.ts +76 -40
  297. package/src/routes.test.ts +91 -3
  298. package/src/routes.ts +135 -0
  299. package/src/runtime-state.test.ts +45 -0
  300. package/src/runtime-state.ts +124 -0
  301. package/src/schemas.test.ts +16 -29
  302. package/src/schemas.ts +3526 -553
  303. package/src/search-globs.test.ts +46 -0
  304. package/src/search-globs.ts +76 -0
  305. package/src/share-paths.test.ts +48 -0
  306. package/src/share-paths.ts +68 -0
  307. package/src/state-portability.ts +56 -0
  308. package/src/title.test.ts +20 -1
  309. package/src/title.ts +12 -3
  310. package/src/tunnel-ids.ts +15 -1
  311. package/src/versions.test.ts +39 -0
  312. package/src/versions.ts +48 -0
  313. package/src/workflow-faults.test.ts +127 -0
  314. package/src/workflow-faults.ts +188 -0
  315. package/src/workspace-state.test.ts +189 -17
  316. package/src/workspace-state.ts +500 -30
  317. package/dist/contracts/codex.contract.d.ts.map +0 -1
  318. package/dist/contracts/codex.contract.js +0 -6
  319. package/dist/contracts/codex.contract.js.map +0 -1
  320. package/dist/contracts/gemini.contract.d.ts +0 -16
  321. package/dist/contracts/gemini.contract.d.ts.map +0 -1
  322. package/dist/contracts/gemini.contract.js +0 -6
  323. package/dist/contracts/gemini.contract.js.map +0 -1
  324. package/dist/contracts/kimi.contract.d.ts +0 -16
  325. package/dist/contracts/kimi.contract.d.ts.map +0 -1
  326. package/dist/contracts/kimi.contract.js +0 -6
  327. package/dist/contracts/kimi.contract.js.map +0 -1
  328. package/dist/contracts/komodo.contract.d.ts +0 -125
  329. package/dist/contracts/komodo.contract.d.ts.map +0 -1
  330. package/dist/contracts/komodo.contract.js +0 -14
  331. package/dist/contracts/komodo.contract.js.map +0 -1
  332. package/dist/contracts/memory.contract.d.ts +0 -34
  333. package/dist/contracts/memory.contract.d.ts.map +0 -1
  334. package/dist/contracts/memory.contract.js +0 -9
  335. package/dist/contracts/memory.contract.js.map +0 -1
  336. package/src/contracts/codex.contract.ts +0 -10
  337. package/src/contracts/gemini.contract.ts +0 -11
  338. package/src/contracts/kimi.contract.ts +0 -9
  339. package/src/contracts/komodo.contract.ts +0 -36
  340. package/src/contracts/memory.contract.ts +0 -13
@@ -1,4 +1,6 @@
1
- import type { FileContribution } from "@intentic/extension-api";
1
+ import { STATE_DIR } from "@intentic/constants";
2
+ import type { FileContribution } from "@intentic/extension-manifest";
3
+ import type { StateFile } from "./state-portability.js";
2
4
 
3
5
  /* WHICH WORKSPACE FILE BACKS WHICH CORE VIEW — one declaration, read by both sides of the wire.
4
6
  *
@@ -29,16 +31,18 @@ import type { FileContribution } from "@intentic/extension-api";
29
31
  * by the automations extension, because the extension had no way to say so itself. A key belongs to whoever
30
32
  * queries it. */
31
33
 
32
- // A core entry is an extension's `contributes.files` entry plus the one thing only the core list needs: the
33
- // right to declare NO invalidations, which for a daemon-owned file is the answer more often than not.
34
- export interface WorkspaceStateFile {
35
- /* Workspace-root-relative, forward-slash — the space `workspaceChanged` paths arrive in. Matching is by
36
- * PREFIX, which lets one entry cover three shapes without a second matching rule:
37
- * - an exact file `.intentic/settings.json`
38
- * - a directory `.intentic/drafts/` (one file per draft)
39
- * - a name family `.intentic/environment.` (…Dockerfile, .custom.Dockerfile, .approved.Dockerfile)
40
- * A directory entry keeps its trailing slash so it can never prefix-match a sibling file. */
41
- readonly path: string;
34
+ /* A core entry is an extension's `contributes.files` entry plus the two things only the core list needs: the
35
+ * right to declare NO invalidations (for a daemon-owned file, the answer more often than not), and a
36
+ * portability class, because the daemon's own state is what an environment export has to reason about.
37
+ *
38
+ * `path` is workspace-root-relative, forward-slash the space `workspaceChanged` paths arrive in. Matching is
39
+ * by PREFIX, which lets one entry cover three shapes without a second matching rule:
40
+ * - an exact file `.intentic/settings.json`
41
+ * - a directory `.intentic/drafts/` (one file per draft)
42
+ * - a name family `.intentic/environment.custom.` (…Dockerfile and anything later named beside it)
43
+ * A directory entry keeps its trailing slash so it can never prefix-match a sibling file. Entries may NEST —
44
+ * see stateFileFor, which resolves the longest match rather than the first. */
45
+ export interface WorkspaceStateFile extends StateFile {
42
46
  /* The browser query keys this file's contents feed. EMPTY is a real answer, not a gap — a file the browser
43
47
  * renders nothing from, or one deliberately kept off the push path — and `why` says which. Never a prefix
44
48
  * test over `.intentic/` as a whole: one stray write must not cost every view a refetch, which is the
@@ -46,16 +50,119 @@ export interface WorkspaceStateFile {
46
50
  readonly invalidates: readonly string[];
47
51
  // Why this file has no invalidations, for the entries that declare none. Absent when it has some.
48
52
  readonly why?: string;
53
+ /* Whether this entry is TRACKED by the root repo — the third thing an entry declares, and the one an owner
54
+ * sees most directly: a tracked entry gets a diff in the Changes review and a line in `git log`, so a change
55
+ * to how this sandbox behaves can be read, reverted, and attributed.
56
+ *
57
+ * ABSENT IS THE ANSWER FOR ALMOST EVERYTHING, and deliberately so. The root repo excludes `.intentic`
58
+ * wholesale and this flag is the only thing that carves an entry back out, so a store added later is
59
+ * untracked until someone says otherwise — the same default-deny the `portability` classes are built on, for
60
+ * the same reason. An ignore-pattern list would invert it: a credential store added next month would be
61
+ * committed on its first write, and nothing would have had to change for that to happen.
62
+ *
63
+ * Only CONFIGURATION belongs here — the small, slow-moving files that decide how this sandbox behaves. Two
64
+ * kinds of entry are excluded on purpose even though they are `carry` and hold no secret:
65
+ * - LEDGERS (workflow runs, loop iterations, thread bookkeeping, permission-usage batches), which are
66
+ * rewritten on a timer or several times per step. Tracking them buries the owner's code review under
67
+ * machine noise — one of them is written every few seconds while a browser has the app open.
68
+ * - BULK (session transcripts, artifacts), which are hundreds of megabytes of constantly-rewritten
69
+ * content. They travel in a bundle; they do not belong in a diff.
70
+ * `versioned` is therefore NARROWER than `carry`, and the two answer different questions: carry is "does it
71
+ * move to a new sandbox", this is "should a human review it changing". */
72
+ readonly versioned?: true;
73
+ /* AUTHORED but not configuration — the fourth question an entry can answer, and the narrowest: is this
74
+ * human- or agent-written TEXT that a workspace search should surface? Every `versioned` entry already is
75
+ * (a setting, a persona, a skill — things the agent is asked to find and edit), so this flag exists only
76
+ * for the entries that are authored content without being config: a draft awaiting approval, a staged
77
+ * README, an extension the agent wrote in place. Everything else under `.intentic` is machine state, and
78
+ * `SEARCHABLE_STATE_PATHS` below is what lets the search engine deny the rest BY DEFAULT instead of
79
+ * hand-keeping a deny list that goes stale the day a store is added (which is how a 98 kB loop ledger and
80
+ * whole third-party extension checkouts ended up ranking in code search). */
81
+ readonly authored?: true;
82
+ /* WHO BUILDS THIS TREE, when it is not the daemon — an extension, pnpm, another process entirely. Declared
83
+ * on the entry because the coverage guard's second direction ("every declared entry is built somewhere in
84
+ * the daemon") is only meaningful for entries the daemon owns: one it can never build must say who does, or
85
+ * the guard would read the entry as dead. Absent for everything the daemon writes itself. */
86
+ readonly outsideWriter?: string;
49
87
  }
50
88
 
51
- export const WORKSPACE_STATE_FILES: readonly WorkspaceStateFile[] = [
52
- // A capability add/remove recomposes the environment overlay and can add or drop a repo's panel.
53
- { path: ".intentic/capabilities.json", invalidates: ["capabilities", "environment", "panels"] },
54
- { path: ".intentic/environment.", invalidates: ["environment"] },
55
- { path: ".intentic/settings.json", invalidates: ["settings"] },
89
+ /* Declared `as const` so the paths survive as literal types (see WorkspaceStatePath below), then published under
90
+ * the interface. Both bindings are needed and neither is redundant: the const is the only thing that can produce
91
+ * the path union, and every consumer reads entries as `WorkspaceStateFile` — an exact-literal tuple loses the
92
+ * optional members (`note`, `why`) on the entries that omit them, which is a worse type for reading than the
93
+ * interface it satisfies. One list, two views of it. */
94
+ const STATE_FILES = [
95
+ /* A capability add/remove recomposes the environment overlay and can add or drop a repo's panel.
96
+ *
97
+ * Each entry's `config` carries that capability's credential (an mcp server's token, a Komodo key, an ssh
98
+ * key), so the manifest is a secret in full. It is also what composeEnvironment reads its Dockerfile
99
+ * fragments from, which makes this the entry where the owner's export choice has the most visible
100
+ * consequence: a bundle exported WITHOUT secrets rebuilds a stock overlay, and the import report has to
101
+ * name every capability the target needs re-added before its environment matches again. */
102
+ { path: ".intentic/capabilities.json", invalidates: ["capabilities", "environment", "panels", "manifests"], portability: "secret" },
103
+
104
+ /* Which workspace-derived recommendations the owner has said "not needed" to, and the evidence each was
105
+ * declined against. It rides the `capabilities` key because the catalog is what changes when one lands, and
106
+ * it travels because a decision about what this workspace does NOT need is as much the owner's as the
107
+ * connections themselves — an export that dropped it would greet them on the target with the same
108
+ * suggestions they had already dismissed. Holds no credential: it is a card name and a file path. */
109
+ { path: ".intentic/capability-dismissals.json", invalidates: ["capabilities"], portability: "carry", versioned: true },
110
+
111
+ /* The secret use ledger — one row per moment the agent's exits spent a stored secret (a `{{secret:name}}`
112
+ * reference resolved into a shell command, a value typed into a browser field), joined onto the secrets
113
+ * inventory as each entry's "last used" (sandbox's secrets/secret-uses.ts). Holds names and destinations,
114
+ * never values — which is why it may `carry`: like the automations' run ledger, a use history is about the
115
+ * secrets, and an export that dropped it would arrive claiming none had ever been touched. */
116
+ { path: ".intentic/secret-uses.json", invalidates: ["secrets"], portability: "carry" },
117
+
118
+ /* The named personas this sandbox shows the outside world — which connected accounts each one speaks for,
119
+ * what a session wearing it may do, where it works (schemas.ts PersonaSchema). It invalidates `capabilities` as well as
120
+ * its own key because a card and the accounts it names are read together everywhere they are shown: connect
121
+ * a second Reddit and the persona list has a new candidate; remove one and a card points at nothing.
122
+ *
123
+ * It is `carry`, and that is the whole design rather than an oversight — a card is a NAME and a list of ids,
124
+ * never a credential, so it travels to a new sandbox in full while the logins it refers to stay behind. What
125
+ * arrives is a workspace that already knows it has a work-reddit and a studio-x, both visibly unconnected,
126
+ * each waiting for one sign-in. It was also the FIRST file under .intentic the root repo tracked, and the
127
+ * argument it was carved out on — a card is configuration, holds no secret, and belongs in review — is the
128
+ * one `versioned` now generalises to the rest of the config slice (personas/personas-store.ts argues it at
129
+ * length, and its reasoning is why the flag exists rather than a second hand-kept list). */
130
+ { path: ".intentic/personas.json", invalidates: ["personas", "capabilities", "manifests"], portability: "carry", versioned: true },
131
+
132
+ /* The overlay Dockerfile, four files that a single `.intentic/environment.` prefix used to cover. They are
133
+ * split here because they answer PORTABILITY differently while answering invalidation identically, and the
134
+ * split is the whole difference between an export that reproduces an environment and one that reproduces a
135
+ * stale copy of it:
136
+ * - custom is the owner-approved SOURCE OF TRUTH and the only one that must travel;
137
+ * - approved is COMPOSED from custom + the capability fragments + this container's base image, and is
138
+ * rewritten on the target's first boot — carrying it would ship a FROM naming an image the target may
139
+ * not be on (see composeEnvironment's baseImageOf);
140
+ * - the proposal and the per-tool drafts under environment.d/ are the agent's pending requests, which the
141
+ * owner has not answered yet; they travel so the question survives the move. */
142
+ { path: ".intentic/environment.custom.Dockerfile", invalidates: ["environment"], portability: "carry", versioned: true },
143
+ { path: ".intentic/environment.Dockerfile", invalidates: ["environment"], portability: "carry", versioned: true },
144
+ { path: ".intentic/environment.d/", invalidates: ["environment"], portability: "carry", versioned: true },
145
+ {
146
+ path: ".intentic/environment.approved.Dockerfile",
147
+ invalidates: ["environment"],
148
+ portability: "derived",
149
+ note: "The target composes its own overlay on first boot; rebuild it there to install the tools it names.",
150
+ },
151
+
152
+ { path: ".intentic/settings.json", invalidates: ["settings", "manifests"], portability: "carry", versioned: true },
153
+ // The rule table's last-fired stamps, beside the rules themselves. `derived` rather than `carry`: it is a
154
+ // record of what happened in THIS sandbox, and carrying it to a fresh one would date every rule to work
155
+ // that machine never did.
156
+ {
157
+ path: ".intentic/rule-firings.json",
158
+ invalidates: ["rule-firings"],
159
+ portability: "derived",
160
+ note: "Stamps of when each rule last did something; the new sandbox starts its own record.",
161
+ },
56
162
  // Written by the AGENT's file tools (the drafts skill), read by the owner's approval inbox — the one entry
57
- // here whose whole point is that a change arrives from outside the browser that renders it.
58
- { path: ".intentic/drafts/", invalidates: ["drafts"] },
163
+ // here whose whole point is that a change arrives from outside the browser that renders it. `authored`:
164
+ // a draft is text somebody wrote, and "find the reddit draft about X" is an ordinary search.
165
+ { path: ".intentic/drafts/", invalidates: ["drafts"], portability: "carry", authored: true },
59
166
  // ---- declared by the extension that renders them (contributes.files), not here ----
60
167
  // The path is the DAEMON's (automations-store writes both), the query keys are the intentic.automations
61
168
  // extension's. It declares them in its own manifest and the browser unions the two lists, so uninstalling
@@ -64,11 +171,85 @@ export const WORKSPACE_STATE_FILES: readonly WorkspaceStateFile[] = [
64
171
  path: ".intentic/automations.json",
65
172
  invalidates: [],
66
173
  why: "Declared by the intentic.automations extension's contributes.files — `automations` is its query key, not core's.",
174
+ portability: "carry",
175
+ versioned: true,
176
+ },
177
+ /* The run history, keyed by automation id — the LEDGER half of what automations.json used to be, and split
178
+ * out of it for the one reason this table's `versioned` note already gives: a tracked file must be worth
179
+ * reviewing. A scheduled automation records a run every time it fires, so every fire dirtied the manifest
180
+ * the owner reviews, and the run records went into `git log` with it — timestamps and conversation ids
181
+ * committed beside the prompt they belong to, burying an actual edit to the automation's config under
182
+ * machine noise. Config is now the only thing in the tracked file, and a fire touches nothing tracked.
183
+ *
184
+ * It is `carry` for the same reason the workflow ledger is: a run history is about the automation, not about
185
+ * the machine, and an export that dropped it would arrive claiming every automation had never run.
186
+ *
187
+ * Its invalidation is the extension's, exactly like the manifest above — and it has to be DECLARED there
188
+ * rather than inherited, because the row renders its run history from this file now: without its own entry
189
+ * a completed run would stop refreshing the view the moment it stopped living in automations.json. */
190
+ {
191
+ path: ".intentic/automation-runs.json",
192
+ invalidates: [],
193
+ why: "Declared by the intentic.automations extension's contributes.files — `automations` is its query key, not core's.",
194
+ portability: "carry",
67
195
  },
68
196
  {
69
197
  path: ".intentic/approvals/",
70
198
  invalidates: [],
71
199
  why: "Declared by the intentic.automations extension's contributes.files — `automation-approvals` is its query key, not core's.",
200
+ portability: "carry",
201
+ },
202
+ {
203
+ path: ".intentic/automations.seeded.json",
204
+ invalidates: [],
205
+ why: "Which default automations this workspace has been offered (default-automations.ts); nothing renders it — it exists so deleting a seeded automation is final.",
206
+ portability: "carry",
207
+ versioned: true,
208
+ },
209
+ /* The maintenance ledger and probe evidence, written by the daemon's chores-store and rendered by the
210
+ * intentic.maintenance extension — the automations shape exactly: the path is the daemon's, the query keys
211
+ * (`maintenance-report`, `maintenance-runs`) are the extension's own contributes.files. Point-in-time
212
+ * evidence about this workspace, so `carry` like the run ledgers: an export that dropped it would arrive
213
+ * claiming no chore had ever been checked. */
214
+ {
215
+ path: ".intentic/chores/",
216
+ invalidates: [],
217
+ why: "Declared by the intentic.maintenance extension's contributes.files — `maintenance-report`/`maintenance-runs` are its query keys, not core's.",
218
+ portability: "carry",
219
+ },
220
+ /* The documentation STAGING tree (documentation extension's paths.ts): generation writes here, the owner
221
+ * reads and approves here, publishing copies into the repo. `authored` is the whole nature of the entry —
222
+ * these are draft READMEs, drafts-shaped in every way that matters, and "find the staged page about X" is
223
+ * as ordinary a search as finding a post draft. */
224
+ {
225
+ path: ".intentic/docs/",
226
+ invalidates: [],
227
+ why: "Declared by the intentic.documentation extension's contributes.files — `documentation`/`documentation-runs` are its query keys, not core's.",
228
+ portability: "carry",
229
+ authored: true,
230
+ outsideWriter: "the intentic.documentation extension's staging writes (its paths.ts)",
231
+ },
232
+ /* The workflow designs and their run ledger became CORE keys the day runs got cards on the fleet board and
233
+ * a mode of the chat panel (web's useWorkflowRuns): those surfaces exist whether or not the workflows
234
+ * extension is enabled, so their freshness cannot ride an extension's contributes.files — an owner turning
235
+ * the extension off would have frozen the board's run cards mid-run. This push is the ONLY live feed the
236
+ * run surfaces have: the scheduler writes the ledger several times per step and nothing polls for it.
237
+ * The runs file invalidates `workflows` too, because GET /workflows embeds each design's runs
238
+ * (WorkflowSummary) — a settled step changes that answer as surely as an edited design does. */
239
+ { path: ".intentic/workflows.json", invalidates: ["workflows"], portability: "carry", versioned: true },
240
+ { path: ".intentic/workflow-runs.json", invalidates: ["workflows", "workflow-runs"], portability: "carry" },
241
+ /* The SAVED loops, which are a manifest and so the opposite of the ledger below them: a handful of entries
242
+ * a person authors, read by two surfaces at once — the workflows page that owns them, and every chat
243
+ * composer's loop picker. Those two are in different windows as often as not (a popped-out chat is its own
244
+ * window), so an edit made on the page has to reach a picker nobody is going to think to reopen. A CORE key
245
+ * rather than the workflows extension's, for the reason the workflow designs beside it are: the composer
246
+ * lists saved loops whether or not that extension is switched on. */
247
+ { path: ".intentic/loop-designs.json", invalidates: ["loop-designs"], portability: "carry", versioned: true },
248
+ {
249
+ path: ".intentic/loops.json",
250
+ invalidates: [],
251
+ why: "Ralph loops and their iteration history. Nothing observes it: where a RUNNING loop stands rides on the fleet roster (AgentSummary.loop), which the /events stream already pushes about once a second, and a second source invalidating on this file could only ever disagree with the card beside it. The iteration list of an ENDED loop is an on-demand read — nothing renders it until someone opens it (web's useLoops, which holds no query for exactly this reason).",
252
+ portability: "carry",
72
253
  },
73
254
 
74
255
  /* ---- reached by no query, for reasons that are not oversights ----
@@ -81,16 +262,24 @@ export const WORKSPACE_STATE_FILES: readonly WorkspaceStateFile[] = [
81
262
  path: ".intentic/webchat-installs.json",
82
263
  invalidates: [],
83
264
  why: "Which origins have loaded a Doorbell's widget, written on a 30s flush timer while a customer's site serves page views. The install panel that renders it fetches on open and polls itself while it is on screen, which is the whole window in which the answer changes for anyone. Pushing instead would bill every connected browser a refetch per flush, for a panel almost nobody has open.",
265
+ portability: "carry",
84
266
  },
85
267
  {
86
268
  path: ".intentic/thread-sessions.json",
87
269
  invalidates: [],
88
270
  why: "Thread bookkeeping (an inbound thread — a Doorbell visitor, a Discord or Slack channel — → sandbox conversation + provider session), written on EVERY inbound message. Nothing in the browser reads it: what a thread produces is a conversation, and the fleet board already learns about that from the agent registry's own push. Naming a key here would bill every connected browser a refetch per inbound message — the request storm this table's own note warns about — to refresh nothing it can see.",
271
+ portability: "carry",
89
272
  },
273
+ /* Values are a primitive union an extension chooses the meaning of, and "an API key for the service I talk
274
+ * to" is squarely within it — so this is classed by what it CAN hold, not by what any particular extension
275
+ * happens to put there. The alternative reads the wrong way round: a bundle that leaked one extension's
276
+ * token would have been correct about all the others. */
90
277
  {
91
278
  path: ".intentic/extension-settings.json",
92
279
  invalidates: [],
93
280
  why: "Held in a module-level shallowRef store per extension (web's extensionSettingsStore) with no query observer, and deliberately so: api.settings.get must answer SYNCHRONOUSLY from an extension's first activate() line, and the store outlives every component scope. A module-level QueryObserver is the one shape that would make invalidation refetch, and this app already ruled it out — it detaches on the queryClient.clear() at logout (see useSandbox's sandbox-list mirror). So a remote member's setting edit reaches this browser on its next load, not live.",
281
+ portability: "secret",
282
+ note: "Re-enter each extension's settings on the Extensions tab.",
94
283
  },
95
284
  /* Unlike the settings file above it, the on/off switch IS observed by a query — the Extensions tab's list,
96
285
  * which carries each row's switch position — so a flip made elsewhere (another member, the agent writing the
@@ -99,58 +288,330 @@ export const WORKSPACE_STATE_FILES: readonly WorkspaceStateFile[] = [
99
288
  {
100
289
  path: ".intentic/extension-enablement.json",
101
290
  invalidates: ["extensions"],
291
+ portability: "carry",
292
+ versioned: true,
293
+ },
294
+ /* Workspace extensions: one directory per extension, consumed straight from the workspace — no clone, no
295
+ * install moment. Written like drafts, by the agent's own file tools (which is the point: an agent authors
296
+ * an extension and it is live for the daemon and every session at once, since .intentic is shared), so this
297
+ * push is what makes one appearing or changing show up on the Extensions tab while the owner watches.
298
+ * `authored` for the same reason as drafts: this is source the agent wrote and will be asked to find —
299
+ * unlike `.intentic/extensions/` below, which is CLONES of source that lives elsewhere. */
300
+ { path: ".intentic/workspace-extensions/", invalidates: ["extensions"], portability: "carry", authored: true },
301
+ /* What the registry comparison found per installed extension (update available / advisory / post-update
302
+ * health), written by the periodic check and by the update/revert transactions — pushed to the tab because
303
+ * an advisory that auto-disabled something must not wait for a reload to be seen. */
304
+ { path: ".intentic/extension-updates.json", invalidates: ["extensions"], portability: "carry" },
305
+ /* The owner's per-extension update posture (notify / agent / auto, and the advisory opt-out). Carried:
306
+ * it is a decision about the extension, not about this machine. */
307
+ { path: ".intentic/extension-update-policy.json", invalidates: ["extensions"], portability: "carry", versioned: true },
308
+ /* Carried, because the evidence is about the extension rather than about the machine: an export that dropped
309
+ * it would arrive claiming every permission was unused, which is worse than arriving with no figures at all. */
310
+ {
311
+ path: ".intentic/extension-usage.json",
312
+ invalidates: [],
313
+ why: "Which of the routes each extension DECLARED it has actually called — the evidence behind the permissions list on its row. The one entry here whose empty set is a RATE decision rather than an architectural one: every browser with the app open reports its batch on a timer, so wiring this to the `extensions` query would refetch the whole list every few seconds for a figure nobody is watching change. The tab reads it when it loads, which is when anyone is reading it.",
314
+ portability: "carry",
102
315
  },
103
316
  {
104
317
  path: ".intentic/members.json",
105
318
  invalidates: [],
106
319
  why: "Not this view's source at all: SandboxAccess renders the PLATFORM's invite records (apiClient.invite.list), and this file is the daemon's ENFORCED copy — written first so a grant the enforcer never got is never recorded, then never read back. A change here means the two disagreed, which the write order makes fail-closed rather than stale.",
320
+ portability: "identity",
321
+ note: "Re-invite collaborators from the Access tab — a grant is the platform's record, and the target enforces its own copy.",
107
322
  },
108
323
 
109
324
  // ---- daemon-owned, nothing derives from watching them ----
325
+ /* Keep credentials and conversation state in disjoint top-level trees. Provider homes are intentionally
326
+ * classified as a single secret unit: several CLIs mix OAuth, config, and provider-native thread metadata,
327
+ * and no generic export can safely distinguish those files. The broad root also makes a newly-added provider
328
+ * secret by construction instead of relying on another hand-maintained provider-name list. */
329
+ {
330
+ path: ".intentic/auth/",
331
+ invalidates: [],
332
+ why: "AI-provider credentials and runtime homes; each account is rendered through owner-gated provider routes.",
333
+ portability: "secret",
334
+ note: "Sign the agent's AI accounts in again on the Agent tab.",
335
+ },
110
336
  /* Agent session transcripts, rewritten on every streamed token.
111
337
  *
112
338
  * The memory notes under it (`projects/<slug>/memory/**`) ARE user-facing and the /memory view polls them
113
339
  * every 30s, which is the one place in this table where a poll survives a real change feed being available.
114
340
  * It stays a poll deliberately: the watcher's exclusion is a DESCENT filter, so reaching those notes means
115
- * letting it walk `.intentic/claude` → `projects` → every project slug. Measured on the live workspace that
116
- * is +119 watched directories against ~593 today (a fifth more), with 314 continuously-rewritten transcripts
117
- * inside the newly-watched set, to make ONE memory directory live. Notes change at agent-turn cadence, so the
118
- * poll costs a request a minute and the alternative costs a permanent 20% on the watcher. */
341
+ * letting it walk `.intentic/sessions/claude` → `projects` → every project slug. Measured on the live
342
+ * workspace that is +119 watched directories against ~593 today (a fifth more), with 314 continuously-
343
+ * rewritten transcripts inside the newly-watched set, to make ONE memory directory live. Notes change at
344
+ * agent-turn cadence, so the poll costs a request a minute and the alternative costs a permanent 20% on the
345
+ * watcher. */
119
346
  {
120
- path: ".intentic/claude/",
347
+ path: ".intentic/sessions/claude/",
121
348
  invalidates: [],
122
349
  why: "Agent session transcripts — see the note above on why the memory notes under it stay polled.",
350
+ portability: "carry",
351
+ },
352
+ {
353
+ path: ".intentic/artifacts/",
354
+ invalidates: [],
355
+ why: "Durable outputs owned by conversations and extension runs: attachments, browser captures, generated images, acceptance reports, workflow step reports, voice transcripts, and loop ledgers.",
356
+ portability: "carry",
357
+ },
358
+ {
359
+ path: ".intentic/cache/",
360
+ invalidates: [],
361
+ why: "Rebuildable indexes and caches — the iq index and its vector sidecar, the whisper model; ignored by the watcher and recreated from carried workspace content.",
362
+ portability: "derived",
363
+ },
364
+ /* Connector and extension scratch, one directory per extension under runtime/extensions/<id>
365
+ * (extensionRuntimeDir below — the ONLY way an extension names a home here, so a new one lands under its
366
+ * own id by construction instead of minting a file at the .intentic root). Resume watermarks, cached
367
+ * hour-tokens, gateway discovery state: all of it either expires or re-establishes itself, and classifying
368
+ * the root once is what keeps a token an extension caches tomorrow out of bundles without a second edit. */
369
+ {
370
+ path: ".intentic/runtime/",
371
+ invalidates: [],
372
+ why: "Extension runtime scratch (watermarks, cached short-lived tokens); nothing renders it and gateways re-derive it.",
373
+ portability: "derived",
374
+ outsideWriter: "extensions, through extensionRuntimeDir below",
375
+ },
376
+ {
377
+ path: ".intentic/tmp/",
378
+ invalidates: [],
379
+ why: "Scratch that agents and tools leave behind (build logs, demo checkouts); nothing reads it after the turn that wrote it. The state janitor empties it at boot.",
380
+ portability: "derived",
381
+ },
382
+ /* Not written by the daemon at all: pnpm auto-creates its content-addressable store at the project's
383
+ * mountpoint, and `.intentic` is its own mount in an isolated turn — so an install run from under it mints
384
+ * this. Declared anyway, because the table's job is to say what everything under `.intentic` IS: hardlink
385
+ * sources a fresh install rebuilds, which an export must not ship (it reached 1.3 GB on the workspace this
386
+ * entry was written against). */
387
+ {
388
+ path: ".intentic/.pnpm-store/",
389
+ invalidates: [],
390
+ why: "pnpm's content-addressable store, auto-created by installs run from under .intentic; the next install rebuilds it.",
391
+ portability: "derived",
392
+ outsideWriter: "pnpm itself, when an install runs from under .intentic",
393
+ },
394
+ {
395
+ path: ".intentic/newest-run.json",
396
+ invalidates: [],
397
+ why: "The newest daemon version that ever ran this workspace (store/newest-run.ts) — a downgrade tripwire, about THIS sandbox the way rule-firings is.",
398
+ portability: "derived",
399
+ note: "The target stamps its own daemon version on first boot.",
400
+ },
401
+ {
402
+ path: ".intentic/verify.json",
403
+ invalidates: [],
404
+ why: "The dependency verifier's verdict memory; nothing renders it directly — outcomes reach the owner as activity entries and workspace events.",
405
+ portability: "carry",
406
+ },
407
+ {
408
+ path: ".intentic/verify/",
409
+ invalidates: [],
410
+ why: "A running check's wrapper artifacts (log + exit status), read once by the daemon when the panel finishes.",
411
+ portability: "derived",
123
412
  },
124
413
  {
125
414
  path: ".intentic/ci.json",
126
415
  invalidates: [],
127
416
  why: "Webhook secret + conclusion memory; the Pipelines view reads it through /ci/runs, not off disk.",
417
+ portability: "secret",
418
+ note: "Re-add the CI webhook on the Pipelines view — its secret is per-sandbox.",
128
419
  },
129
420
  {
130
- path: ".intentic/komodo.json",
421
+ path: ".intentic/control-tokens.json",
131
422
  invalidates: [],
132
- why: "Per-connection 'when the owner last looked at Deployments'; the view reads it through /komodo/{capability}/overview, not off disk — and it is written BY that view being opened, so invalidating on it would refetch the board in answer to the browser's own click.",
423
+ why: "Hashed control tokens (the ACP editor bridge, and anything else driving this sandbox from outside), listed on demand by the owner.",
424
+ portability: "identity",
425
+ note: "Mint fresh control tokens — the old ones authenticate against the source sandbox.",
133
426
  },
134
- { path: ".intentic/bridge-tokens.json", invalidates: [], why: "Hashed ACP bridge tokens, listed on demand by the owner." },
135
427
  {
136
428
  path: ".intentic/owner.json",
137
429
  invalidates: [],
138
430
  why: "Bound once on first use; a change here means the sandbox was re-owned, which re-authenticates anyway.",
431
+ portability: "identity",
432
+ },
433
+ {
434
+ path: ".intentic/workspace.json",
435
+ invalidates: [],
436
+ why: "The workspace identity, read from the /events hello frame rather than as a file.",
437
+ portability: "identity",
438
+ },
439
+ {
440
+ path: ".intentic/templates.json",
441
+ invalidates: [],
442
+ why: "Scaffold templates, read when the scaffold dialog opens.",
443
+ portability: "carry",
444
+ versioned: true,
139
445
  },
140
- { path: ".intentic/workspace.json", invalidates: [], why: "The workspace identity, read from the /events hello frame rather than as a file." },
141
- { path: ".intentic/templates.json", invalidates: [], why: "Scaffold templates, read when the scaffold dialog opens." },
446
+ /* Classed `derived` for size rather than for safety, and it is the one entry where that costs the owner
447
+ * something real: the profiles ARE logged-in sessions. They are also gigabytes of a store Chromium rewrites
448
+ * constantly and versions against its own build, so carrying them ships bulk that the target's Chromium may
449
+ * refuse anyway. The note is what keeps the loss visible instead of silent. */
142
450
  {
143
451
  path: ".intentic/browser/",
144
452
  invalidates: [],
145
453
  why: "Browser-login profiles: Chromium rewrites these constantly. Descent-ignored by the watcher outright.",
454
+ portability: "derived",
455
+ note: "Log the agent's browser back into any site it needs — profiles do not travel.",
146
456
  },
147
457
  {
148
458
  path: ".intentic/extensions/",
149
459
  invalidates: [],
150
460
  why: "Extension checkouts — whole git clones. The `extensions` query is driven by the capability manifest above, not by their contents.",
461
+ portability: "derived",
462
+ note: "Extensions re-clone from the capability manifest on the target's next reconcile.",
151
463
  },
152
- { path: ".intentic/plugins/", invalidates: [], why: "Agent plugin dirs, read by the SDK's loader each turn." },
153
- ];
464
+ { path: ".intentic/plugins/", invalidates: [], why: "Agent plugin dirs, read by the SDK's loader each turn.", portability: "carry" },
465
+ /* THE SKILLS THE OWNER WROTE THEMSELVES, one directory per skill — the source of truth the reconciler copies
466
+ * into `.agents/skills` for the ones currently switched on (settings.json's `skills` list). It is here rather
467
+ * than in the loaded folder for the reason the plugin dirs are: that tree holds only what is currently ON,
468
+ * and a skill switched off has to keep its text somewhere the loaders will not read it from.
469
+ *
470
+ * `versioned`, like the rest of the config slice: a skill changes how the agent behaves, so it earns a diff
471
+ * in the Changes review and a line in `git log` the same way a rule or a persona does. `carry` for the same
472
+ * reason — it is text the owner wrote, with no credential in it and nothing about this machine. */
473
+ { path: ".intentic/skills/", invalidates: ["skills"], portability: "carry", versioned: true },
474
+ /* ONE FOLDER PER PERSONA — what a session wearing that card is told, and the skills and tools only it gets.
475
+ * Laid out as a Claude Code plugin (`.claude-plugin/plugin.json`, `skills/`, `agents/`, `commands/`,
476
+ * `hooks/`, `.mcp.json`) so the runtime's own loader reads it and this daemon parses none of it, exactly as
477
+ * the plugin checkouts above are read (personas/persona-kit.ts).
478
+ *
479
+ * A SECOND ENTRY BESIDE `personas.json` RATHER THAN A FIELD INSIDE IT, because the two are different kinds
480
+ * of thing to review. The card is a name, some ids and some switches — a few lines that diff cleanly. This
481
+ * is prose and files: a system prompt, a skill, a subagent. Folding a 20k prompt into the JSON would make
482
+ * every persona edit an unreadable diff and put text somebody wrote inside a record nobody writes by hand.
483
+ *
484
+ * `versioned` and `carry` for the same reasons the card and the skills above are: it changes how the agent
485
+ * behaves, it holds no credential, and it belongs in a pull request — which is also what makes it
486
+ * searchable, since every versioned entry already is. */
487
+ { path: ".intentic/personas/", invalidates: ["personas"], portability: "carry", versioned: true },
488
+ ] as const satisfies readonly WorkspaceStateFile[];
489
+
490
+ export const WORKSPACE_STATE_FILES: readonly WorkspaceStateFile[] = STATE_FILES;
491
+
492
+ /* The entries the root repo tracks, workspace-root-relative and in declaration order — what history.ts turns
493
+ * into the negations that carve them back out of the wholesale `.intentic` exclusion.
494
+ *
495
+ * Derived rather than written down beside the exclude rule, for the reason this whole file exists: the git rule
496
+ * lives in the daemon and the classification lives here, and a second hand-kept copy of "which config is
497
+ * reviewable" is a copy that goes stale the first time someone adds a store. Marking an entry `versioned` is now
498
+ * the entire change — the exclude list follows on the next boot, in both places it is written. */
499
+ export const VERSIONED_STATE_PATHS: readonly string[] = WORKSPACE_STATE_FILES.filter((file) => file.versioned).map((file) => file.path);
500
+
501
+ /* The `.intentic` slice a workspace SEARCH may surface: configuration a person reviews (`versioned`) plus the
502
+ * authored-content dirs (`authored`) — drafts, staged docs, workspace extensions. Everything else under
503
+ * `.intentic` is machine state, and the search engine (iq's floor) denies it BY DEFAULT off this list, the
504
+ * same default-deny the portability classes are built on and for the same reason: a deny list is a list a new
505
+ * ledger is forgotten from, and the forgetting is silent — it ranked loop iteration history and cloned
506
+ * third-party extension source against the user's own code for months before this derivation existed.
507
+ *
508
+ * Note what falls out without a special case: `capabilities.json` is `secret` and unversioned, so the index
509
+ * stops copying capability tokens into search text — the exact boundary the floor already drew for `auth/`. */
510
+ export const SEARCHABLE_STATE_PATHS: readonly string[] = WORKSPACE_STATE_FILES.filter((file) => file.versioned || file.authored).map(
511
+ (file) => file.path,
512
+ );
513
+
514
+ /* THE ONE WAY AN EXTENSION NAMES ITS SCRATCH HOME — `.intentic/runtime/extensions/<id>`, workspace-relative
515
+ * and forward-slash so the browser bundle can hold it too; callers join it onto whatever root is in force.
516
+ *
517
+ * It exists for the reason statePath does one table over: before it, every gateway spelled the layout itself
518
+ * and one extension (deployments) simply didn't, minting `komodo.json` at the `.intentic` root where nothing
519
+ * classified it. An extension that composes through this helper cannot land outside its own directory, so the
520
+ * runtime/ entry's `derived` covers whatever it writes tomorrow. Extension ids are validated slugs already;
521
+ * the replace is defence in depth against a path ever being built from something else. */
522
+ export const extensionRuntimeDir = (extension: string): string => `${STATE_DIR}/runtime/extensions/${extension.replaceAll(/[^a-zA-Z0-9._-]/g, "_")}`;
523
+
524
+ /* The manifests whose problems the unreadable-manifest notice SHOWS — the handful a person hand-edits — and the
525
+ * one fact that decides it is already in the table above.
526
+ *
527
+ * Every store reads through the same `jsonFile`, so every store reports what it could not make sense of, and for
528
+ * a long time the notice showed all of them. That is wrong twice over. Its advice — fix the file and this clears
529
+ * on its own — is addressed to somebody holding an editor, which is true of `settings.json` and false of a
530
+ * daemon-written LEDGER nobody opens: a run history that stopped matching a schema the build tightened is not a
531
+ * mistake the owner made, and the card asked them to repair sixty kilobytes of machine JSON by hand. Worse, a
532
+ * file that reports into the notice without feeding the notice's QUERY leaves a complaint no write can refresh,
533
+ * so it sits on screen until the daemon restarts — which is exactly how the workflow ledger's entry became
534
+ * permanent furniture.
535
+ *
536
+ * Both follow from one rule, which is why this derives rather than lists: a file's problems are shown IFF a write
537
+ * to that file refreshes the notice. Declaring `manifests` in `invalidates` is the entire opt-in, so the edit
538
+ * that puts a file on the card is the same edit that keeps it current, and neither can be done without the
539
+ * other. A ledger that breaks still falls back and still sets its unreadable bytes aside on the next write
540
+ * (store/json-file.ts) — it just stops asking the owner to fix it. */
541
+ export const REPORTED_MANIFEST_PATHS: readonly string[] = WORKSPACE_STATE_FILES.filter((file) => file.invalidates.includes("manifests")).map(
542
+ (file) => file.path,
543
+ );
544
+
545
+ // Accepts either separator, like isLockedWorkspacePath below: the daemon holds these as platform paths and makes
546
+ // them relative at the last moment, and normalizing at each call site is the one that eventually gets forgotten.
547
+ export const isReportedManifest = (relPath: string): boolean => REPORTED_MANIFEST_PATHS.includes(relPath.replaceAll("\\", "/"));
548
+
549
+ /* Old directory names are never read or migrated. Keep that finite set in one quarantine record so access,
550
+ * export, and search cannot reinterpret abandoned machine state as ordinary workspace content after a rename.
551
+ * `artifacts` still carry; the distinction here tells portability only which retired roots are secrets or
552
+ * derived — and tells the state janitor which it may DELETE: a retired `derived` root is a rebuildable cache
553
+ * by its own classification, so leaving 466 MB of abandoned model where only a manual `rm` reaches it was
554
+ * quarantine doing half its job. Secret and artifact roots stay until an owner removes them by hand: deleting
555
+ * content is not the janitor's call, only deleting what the class already says is disposable. */
556
+ export const RETIRED_WORKSPACE_STATE_DIRS = {
557
+ secret: ["claude", "codex", "kimi", "opencode", "cliproxy"],
558
+ derived: ["iq", "extensions-runtime", "whisper", "browser/output"],
559
+ artifacts: ["attachments", "acceptance", "loops", "workflow-runs", "transcripts"],
560
+ } as const;
561
+
562
+ /* THE DAEMON'S OWN CONTROL PLANE — the entries directly under the workspace root's `.intentic/` that the file
563
+ * API refuses to read, write, move or delete for anyone, the owner included (workspace/workspace-files.ts holds
564
+ * the enforcement and the full reasoning for each name).
565
+ *
566
+ * The list lives HERE, in the package both sides import, because the browser has to draw the same rule the
567
+ * daemon enforces. It didn't, and the gap was a small piece of theatre: the explorer listed `capabilities.json`
568
+ * like any other file, opening it flashed a tab, the read came back with nothing there, and the tab closed
569
+ * itself — a refusal acted out as a glitch. A file the app will not open should say so before it is clicked,
570
+ * which takes a rule the explorer can consult, not a status code it can only react to.
571
+ *
572
+ * Naming these to the browser gives nothing away that the tree did not already publish — it listed them, sizes
573
+ * and all. What stays behind the guard is the only thing that ever mattered: the bytes. */
574
+ const LOCKED_STATE_ENTRIES: ReadonlySet<string> = new Set([
575
+ "owner.json",
576
+ "members.json",
577
+ "capabilities.json",
578
+ "ci.json",
579
+ "claude.json",
580
+ "auth",
581
+ "sessions",
582
+ "browser",
583
+ ...RETIRED_WORKSPACE_STATE_DIRS.secret,
584
+ ]);
585
+
586
+ /* Whether a workspace-root-relative path lands in that control plane — and so is shown locked rather than
587
+ * opened. Scoped deliberately tight, matching the guard: only the ROOT `.intentic` counts (a repo's own nested
588
+ * one is ordinary content) and only these entries within it, subtrees included, so a new provider dropped under
589
+ * `auth/` is covered without a second edit.
590
+ *
591
+ * The ROOT's own `.git` joins them. It is the pointer to the shadow history repo kept off the workspace so the
592
+ * agent cannot rewrite its own past; a NESTED repo's `.git` is ordinary content and stays browsable.
593
+ *
594
+ * Accepts either slash so a caller holding a platform path doesn't have to normalize first. */
595
+ export const isLockedWorkspacePath = (relPath: string): boolean => {
596
+ const segments = relPath.split(/[\\/]/).filter((segment) => segment !== "" && segment !== ".");
597
+ if (segments[0] === ".git") {
598
+ return true;
599
+ }
600
+ return segments.length >= 2 && segments[0] === STATE_DIR && LOCKED_STATE_ENTRIES.has(segments[1] ?? "");
601
+ };
602
+
603
+ /* Every path this table declares, as a type. `as const` above is what makes it one, and it is what finally makes
604
+ * the first sentence of this file's header TRUE rather than aspirational.
605
+ *
606
+ * "The daemon builds its store paths from `path`" was the design; the code did not. `composition.ts` and twenty
607
+ * files beside it spelled the same layout a SECOND way — `join(root, ".intentic", "settings.json")` — with
608
+ * nothing tying the two spellings together. Rename a store's file and this table keeps declaring the old name:
609
+ * no error, no failing test, just a view that quietly stops refreshing, which is the exact failure the table was
610
+ * written to end and the exact way drafts went missing.
611
+ *
612
+ * So the daemon joins through `statePath` (workspace/state-paths.ts), which takes one of THESE and nothing else.
613
+ * A rename is now a compile error at every site that names the file, in both packages, or it is not a rename. */
614
+ export type WorkspaceStatePath = (typeof STATE_FILES)[number]["path"];
154
615
 
155
616
  /* The query keys a batch of changed paths makes stale, deduped and stable. The browser's `/events` handler calls
156
617
  * this; keeping it here rather than in the web means the rule is unit-testable without a query client, and the
@@ -171,3 +632,12 @@ export const staleQueryKeys = (paths: readonly string[], contributed: readonly F
171
632
  .flatMap((file) => file.invalidates),
172
633
  ),
173
634
  ];
635
+
636
+ /* Every query key any watched file feeds — what a NEW /events connection invalidates wholesale (core's table
637
+ * plus the running extensions'). The file push is these keys' ONLY live feed, and a `workspaceChanged` frame
638
+ * produced while the stream was down is a frame nobody will ever resend — so each key's view would sit stale
639
+ * until the file's NEXT write, indefinitely for anything that settled while the browser was away. Re-asking on
640
+ * connect bounds the damage at one cheap read per key, which is what lets those views go entirely unpolled. */
641
+ export const fileBoundQueryKeys = (contributed: readonly FileContribution[]): readonly string[] => [
642
+ ...new Set([...WORKSPACE_STATE_FILES, ...contributed].flatMap((file) => file.invalidates)),
643
+ ];