@astrale-os/cli 1.0.0-beta.0 → 1.0.0-beta.10

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 (322) hide show
  1. package/README.md +23 -12
  2. package/dist/astrale.js +51494 -56307
  3. package/dist/public/connect-core.js +2028 -26846
  4. package/dist/public/keys/index.js +1755 -40694
  5. package/dist/public/paths/index.js +859 -39518
  6. package/dist/types/admin/binding.d.ts +19 -0
  7. package/dist/types/admin/instance/client.d.ts +22 -0
  8. package/dist/types/admin/instance/index.d.ts +2 -0
  9. package/dist/types/admin/instance/model.d.ts +5 -12
  10. package/dist/types/connection/auth.d.ts +3 -0
  11. package/dist/types/connection/call.d.ts +3 -0
  12. package/dist/types/connection/command.d.ts +27 -0
  13. package/dist/types/connection/credential.d.ts +14 -0
  14. package/dist/types/connection/errors.d.ts +1 -0
  15. package/dist/types/connection/exchange.d.ts +9 -0
  16. package/dist/types/connection/failure/classify.d.ts +2 -0
  17. package/dist/types/connection/failure/index.d.ts +5 -0
  18. package/dist/types/connection/failure/model.d.ts +25 -0
  19. package/dist/types/connection/failure/render.d.ts +3 -0
  20. package/dist/types/connection/index.d.ts +8 -0
  21. package/dist/types/connection/reasons.d.ts +37 -0
  22. package/dist/types/connection/self.d.ts +37 -0
  23. package/dist/types/connection/session.d.ts +31 -0
  24. package/dist/types/connection/target.d.ts +38 -0
  25. package/dist/types/errors.d.ts +1 -1
  26. package/dist/types/lib/admin-instance.d.ts +17 -0
  27. package/dist/types/lib/admin-target.d.ts +6 -2
  28. package/dist/types/lib/failure-debug.d.ts +1 -0
  29. package/dist/types/lib/idp.d.ts +7 -0
  30. package/dist/types/lib/instance-target.d.ts +1 -1
  31. package/dist/types/lib/instance.d.ts +10 -2
  32. package/dist/types/lib/log.d.ts +5 -3
  33. package/dist/types/lib/meta.d.ts +2 -2
  34. package/dist/types/lib/proc.d.ts +43 -0
  35. package/dist/types/lib/self.d.ts +9 -0
  36. package/dist/types/lib/update.d.ts +131 -0
  37. package/package.json +8 -10
  38. package/src/__tests__/fixtures/publication.ts +20 -0
  39. package/src/admin/__tests__/binding.test.ts +31 -0
  40. package/src/admin/__tests__/fixture.ts +101 -0
  41. package/src/admin/binding.ts +98 -0
  42. package/src/admin/catalog/.spec/api.d.ts +2 -2
  43. package/src/admin/catalog/__tests__/client.test.ts +20 -49
  44. package/src/admin/catalog/client.ts +36 -32
  45. package/src/admin/catalog/model.ts +3 -4
  46. package/src/admin/graph/.spec/api.d.ts +4 -10
  47. package/src/admin/graph/nodes.ts +1 -1
  48. package/src/admin/instance/.spec/api.d.ts +2 -2
  49. package/src/admin/instance/__tests__/client.test.ts +40 -163
  50. package/src/admin/instance/client.ts +35 -137
  51. package/src/admin/instance/index.ts +1 -2
  52. package/src/admin/instance/model.ts +11 -20
  53. package/src/commands/__tests__/admin-instance.test.ts +11 -8
  54. package/src/commands/__tests__/auth-login.test.ts +54 -1
  55. package/src/commands/__tests__/call-describe.test.ts +46 -33
  56. package/src/commands/__tests__/call.test.ts +5 -1
  57. package/src/commands/__tests__/domain-install-operation.test.ts +12 -7
  58. package/src/commands/__tests__/domain-install-owned.test.ts +6 -2
  59. package/src/commands/__tests__/domain-list.test.ts +63 -4
  60. package/src/commands/__tests__/install-identity-override.test.ts +19 -19
  61. package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
  62. package/src/commands/__tests__/instance-status.test.ts +139 -0
  63. package/src/commands/__tests__/read-commands.test.ts +35 -10
  64. package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
  65. package/src/commands/__tests__/update.test.ts +95 -0
  66. package/src/commands/call-describe.ts +60 -68
  67. package/src/commands/call.ts +43 -56
  68. package/src/commands/domain/install.ts +13 -17
  69. package/src/commands/domain/list.ts +18 -13
  70. package/src/commands/domain/publish.ts +14 -11
  71. package/src/commands/domain/uninstall.ts +2 -2
  72. package/src/commands/get.ts +14 -15
  73. package/src/commands/identity/__tests__/register.test.ts +3 -3
  74. package/src/commands/identity/register.ts +5 -4
  75. package/src/commands/instance/create.ts +8 -16
  76. package/src/commands/instance/delete.ts +12 -9
  77. package/src/commands/instance/list.ts +2 -2
  78. package/src/commands/instance/status.ts +66 -20
  79. package/src/commands/instance/use.ts +16 -12
  80. package/src/commands/introspect.ts +9 -16
  81. package/src/commands/logs.ts +24 -25
  82. package/src/commands/mutate.ts +6 -5
  83. package/src/commands/query.ts +27 -30
  84. package/src/commands/studio.ts +20 -0
  85. package/src/commands/token.ts +8 -7
  86. package/src/commands/update.ts +68 -22
  87. package/src/commands/view.ts +7 -6
  88. package/src/connection/.spec/api.d.ts +7 -8
  89. package/src/connection/.spec/architecture.md +5 -4
  90. package/src/connection/.spec/flows/session.ts +2 -2
  91. package/src/connection/.spec/laws/connection.ts +6 -2
  92. package/src/connection/.spec/layout.ts +1 -0
  93. package/src/connection/__tests__/credential.test.ts +20 -1
  94. package/src/connection/__tests__/errors.test.ts +43 -32
  95. package/src/connection/__tests__/exchange.test.ts +36 -6
  96. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  97. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  98. package/src/connection/__tests__/self.test.ts +31 -11
  99. package/src/connection/__tests__/session.test.ts +3 -3
  100. package/src/connection/__tests__/target.test.ts +1 -0
  101. package/src/connection/auth.ts +13 -0
  102. package/src/connection/call.ts +1 -1
  103. package/src/connection/credential.ts +2 -2
  104. package/src/connection/errors.ts +1 -355
  105. package/src/connection/exchange.ts +52 -27
  106. package/src/connection/failure/classify.ts +110 -0
  107. package/src/connection/failure/index.ts +18 -0
  108. package/src/connection/failure/model.ts +28 -0
  109. package/src/connection/failure/render.ts +116 -0
  110. package/src/connection/reasons.ts +16 -0
  111. package/src/connection/self.ts +39 -21
  112. package/src/connection/session.ts +19 -8
  113. package/src/errors.ts +3 -2
  114. package/src/graph/.spec/api.d.ts +12 -5
  115. package/src/graph/.spec/layout.ts +9 -1
  116. package/src/graph/__tests__/mutation.test.ts +2 -2
  117. package/src/graph/__tests__/query.test.ts +25 -6
  118. package/src/graph/class.ts +30 -0
  119. package/src/graph/index.ts +1 -0
  120. package/src/graph/query.ts +21 -32
  121. package/src/identity/.spec/api.d.ts +2 -2
  122. package/src/identity/__tests__/registration.test.ts +1 -1
  123. package/src/identity/registration.ts +3 -3
  124. package/src/keys/__tests__/keys.test.ts +4 -0
  125. package/src/keys/credential.ts +1 -0
  126. package/src/lib/__tests__/admin-target.test.ts +12 -0
  127. package/src/lib/__tests__/command-dx.test.ts +1 -1
  128. package/src/lib/__tests__/domain-publication.test.ts +138 -0
  129. package/src/lib/__tests__/idp-session.test.ts +22 -0
  130. package/src/lib/__tests__/idp.test.ts +36 -4
  131. package/src/lib/__tests__/instance-candidates.test.ts +1 -3
  132. package/src/lib/__tests__/instance-target.test.ts +7 -39
  133. package/src/lib/__tests__/instance.test.ts +6 -0
  134. package/src/lib/__tests__/log-errors.test.ts +64 -0
  135. package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
  136. package/src/lib/__tests__/update.test.ts +113 -8
  137. package/src/lib/__tests__/view-open-intent.test.ts +0 -2
  138. package/src/lib/__tests__/view-server.test.ts +1 -1
  139. package/src/lib/__tests__/view-session.test.ts +1 -1
  140. package/src/lib/admin-instance.ts +14 -8
  141. package/src/lib/admin-target.ts +21 -2
  142. package/src/lib/command-dx.ts +9 -9
  143. package/src/lib/domain-publication.ts +104 -0
  144. package/src/lib/failure-debug.ts +26 -0
  145. package/src/lib/idp.ts +39 -4
  146. package/src/lib/instance-candidates.ts +2 -2
  147. package/src/lib/instance-target.ts +3 -14
  148. package/src/lib/instance.ts +22 -12
  149. package/src/lib/log.ts +16 -9
  150. package/src/lib/login-flow.ts +13 -3
  151. package/src/lib/meta.ts +2 -2
  152. package/src/lib/provision-instance.ts +34 -57
  153. package/src/lib/update.ts +124 -30
  154. package/src/lib/validation.ts +2 -2
  155. package/src/lib/view/resolve.ts +1 -1
  156. package/src/program/__tests__/program.test.ts +20 -4
  157. package/src/program/build.ts +2 -2
  158. package/src/setup/__tests__/instance-step.test.ts +27 -10
  159. package/src/setup/steps/instance.ts +22 -15
  160. package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-ByzoDZWH.js} +1 -1
  161. package/studio/client/dist/assets/index-B0RCgUOT.js +8 -0
  162. package/studio/client/dist/assets/index-BiGR8Oo9.css +1 -0
  163. package/studio/client/dist/assets/index-N8J8EKlB.js +81 -0
  164. package/studio/client/dist/index.html +2 -2
  165. package/studio/package.json +5 -10
  166. package/studio/server/agent/ask.test.ts +1 -1
  167. package/studio/server/agent/bridge/grant.test.ts +1 -1
  168. package/studio/server/agent/bridge/routes.test.ts +1 -1
  169. package/studio/server/agent/bridge/stdio.ts +8 -1
  170. package/studio/server/agent/conversation.test.ts +10 -5
  171. package/studio/server/agent/conversation.ts +56 -8
  172. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  173. package/studio/server/agent/harness/codex/loadout.ts +18 -21
  174. package/studio/server/agent/harness/gateway/config.ts +20 -18
  175. package/studio/server/agent/harness/selection.ts +9 -1
  176. package/studio/server/agent/prompts/anchors.test.ts +32 -36
  177. package/studio/server/agent/prompts/anchors.ts +20 -49
  178. package/studio/server/agent/prompts/system.test.ts +3 -2
  179. package/studio/server/agent/prompts/system.ts +6 -5
  180. package/studio/server/agent/prompts/turn.ts +13 -4
  181. package/studio/server/agent/routes.test.ts +1 -1
  182. package/studio/server/agent/run/completion.ts +2 -2
  183. package/studio/server/agent/run/coordinator.test.ts +1 -1
  184. package/studio/server/agent/run/preparation.ts +6 -5
  185. package/studio/server/agent/run/transcript.test.ts +35 -1
  186. package/studio/server/agent/run/transcript.ts +169 -3
  187. package/studio/server/agent/run/usage.test.ts +24 -0
  188. package/studio/server/agent/run/usage.ts +26 -1
  189. package/studio/server/api/canvas.ts +43 -0
  190. package/studio/server/api/comments.ts +85 -0
  191. package/studio/server/api/context.ts +61 -0
  192. package/studio/server/api/deployment.ts +39 -0
  193. package/studio/server/api/documents.ts +55 -0
  194. package/studio/server/api/domain.ts +59 -0
  195. package/studio/server/api/http.ts +45 -0
  196. package/studio/server/api/project.ts +57 -0
  197. package/studio/server/api/schema.ts +12 -0
  198. package/studio/server/api/views.ts +57 -0
  199. package/studio/server/api/workspace.ts +60 -0
  200. package/studio/server/api-agent-loadout.test.ts +1 -1
  201. package/studio/server/api.test.ts +147 -0
  202. package/studio/server/api.ts +20 -422
  203. package/studio/server/cache.test.ts +122 -0
  204. package/studio/server/cache.ts +162 -11
  205. package/studio/server/cli-consumers.test.ts +494 -0
  206. package/studio/server/cli.test.ts +88 -0
  207. package/studio/server/cli.ts +240 -0
  208. package/studio/server/client-package.ts +25 -1
  209. package/studio/server/domain.test.ts +50 -40
  210. package/studio/server/domain.ts +83 -52
  211. package/studio/server/environment/dotenv-preview.test.ts +39 -0
  212. package/studio/server/environment/dotenv-preview.ts +27 -0
  213. package/studio/server/environment/files.test.ts +53 -0
  214. package/studio/server/{state/env.ts → environment/files.ts} +7 -48
  215. package/studio/server/{state → handoff}/copy.ts +21 -6
  216. package/studio/server/handoff/service.test.ts +22 -0
  217. package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
  218. package/studio/server/index.ts +1 -1
  219. package/studio/server/instances/active.ts +72 -0
  220. package/studio/server/instances/deploy-record.test.ts +31 -0
  221. package/studio/server/instances/deploy-record.ts +37 -0
  222. package/studio/server/instances/deploy.ts +56 -0
  223. package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
  224. package/studio/server/instances/probe.ts +67 -0
  225. package/studio/server/instances/status.test.ts +12 -0
  226. package/studio/server/instances/status.ts +49 -0
  227. package/studio/server/introspect/anatomy/client-tree.ts +57 -0
  228. package/studio/server/introspect/anatomy/env-fields.ts +66 -0
  229. package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
  230. package/studio/server/introspect/anatomy/source.ts +181 -0
  231. package/studio/server/introspect/anatomy/views/routes.ts +114 -0
  232. package/studio/server/introspect/anatomy/views.ts +53 -0
  233. package/studio/server/introspect/anatomy-extras.test.ts +106 -35
  234. package/studio/server/introspect/anatomy-extras.ts +7 -726
  235. package/studio/server/introspect/anatomy.test.ts +22 -14
  236. package/studio/server/introspect/anatomy.ts +52 -28
  237. package/studio/server/introspect/bundle.ts +15 -9
  238. package/studio/server/introspect/canonical-schema.test.ts +93 -309
  239. package/studio/server/introspect/canonical-schema.ts +257 -425
  240. package/studio/server/introspect/config-preview.test.ts +32 -0
  241. package/studio/server/introspect/config-preview.ts +119 -0
  242. package/studio/server/introspect/core-extractor.ts +18 -127
  243. package/studio/server/introspect/core.ts +1 -2
  244. package/studio/server/introspect/diff.test.ts +18 -18
  245. package/studio/server/introspect/diff.ts +44 -90
  246. package/studio/server/introspect/extractor.ts +23 -116
  247. package/studio/server/introspect/hash.ts +5 -2
  248. package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
  249. package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
  250. package/studio/server/introspect/overlay.test.ts +26 -54
  251. package/studio/server/introspect/overlay.ts +10 -37
  252. package/studio/server/introspect/revision.test.ts +54 -0
  253. package/studio/server/introspect/revision.ts +49 -0
  254. package/studio/server/introspect/runtime.test.ts +83 -143
  255. package/studio/server/introspect/runtime.ts +40 -11
  256. package/studio/server/introspect/schema-ir-json.test.ts +70 -0
  257. package/studio/server/introspect/schema-ir-json.ts +151 -0
  258. package/studio/server/introspect/schema-refs.test.ts +43 -88
  259. package/studio/server/introspect/schema-refs.ts +14 -49
  260. package/studio/server/introspect/source-overlay/annotations.ts +14 -0
  261. package/studio/server/introspect/source-overlay/handlers.ts +161 -0
  262. package/studio/server/introspect/source-overlay/kernel-calls.ts +46 -0
  263. package/studio/server/introspect/source-overlay/project.ts +248 -0
  264. package/studio/server/introspect/source-overlay/spans.ts +330 -0
  265. package/studio/server/json.ts +46 -0
  266. package/studio/server/lifecycle.ts +5 -1
  267. package/studio/server/sse.test.ts +26 -0
  268. package/studio/server/sse.ts +4 -1
  269. package/studio/server/state/baseline.test.ts +223 -7
  270. package/studio/server/state/baseline.ts +148 -34
  271. package/studio/server/state/comments.test.ts +66 -1
  272. package/studio/server/state/comments.ts +173 -10
  273. package/studio/server/state/context.ts +34 -2
  274. package/studio/server/state/documents.ts +42 -1
  275. package/studio/server/state/integrations.ts +27 -1
  276. package/studio/server/state/layout.test.ts +34 -0
  277. package/studio/server/state/layout.ts +44 -17
  278. package/studio/server/state/settings.ts +29 -22
  279. package/studio/server/state/store.test.ts +122 -0
  280. package/studio/server/state/store.ts +66 -17
  281. package/studio/server/state/visibility.ts +22 -11
  282. package/studio/server/views/model.test.ts +57 -0
  283. package/studio/server/views/model.ts +66 -0
  284. package/studio/server/views/runtime.ts +40 -0
  285. package/studio/server/views/selection-repository.test.ts +31 -0
  286. package/studio/server/views/selection-repository.ts +60 -0
  287. package/studio/server/views/session.test.ts +73 -0
  288. package/studio/server/views/session.ts +166 -0
  289. package/studio/server/views/target.test.ts +78 -0
  290. package/studio/server/views/target.ts +222 -0
  291. package/studio/server/watch.test.ts +11 -11
  292. package/studio/server/watch.ts +9 -5
  293. package/studio/server/workspace/catalog.test.ts +17 -0
  294. package/studio/server/workspace/catalog.ts +43 -0
  295. package/studio/server/{state → workspace}/create.test.ts +1 -1
  296. package/studio/server/{state → workspace}/create.ts +1 -1
  297. package/studio/server/{state → workspace}/git.ts +1 -1
  298. package/studio/server/workspace/updates.ts +83 -0
  299. package/studio/server/workspace-state.ts +1 -1
  300. package/studio/server/workspace-watch.ts +2 -2
  301. package/studio/shared/contracts/README.md +16 -0
  302. package/studio/shared/contracts/agent.ts +270 -0
  303. package/studio/shared/contracts/runtime.ts +50 -0
  304. package/studio/shared/contracts/schema.ts +393 -0
  305. package/studio/shared/contracts/surface.test.ts +317 -0
  306. package/studio/shared/contracts/workspace.ts +265 -0
  307. package/studio/shared/layout.test.ts +116 -0
  308. package/studio/shared/schema/identity.test.ts +34 -0
  309. package/studio/shared/schema/identity.ts +68 -0
  310. package/studio/shared/types.ts +11 -1099
  311. package/studio/tsconfig.json +1 -1
  312. package/viewer/dist/main.js +40 -61
  313. package/.check-workspace.cjs +0 -40
  314. package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
  315. package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
  316. package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
  317. package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
  318. package/studio/server/state/catalog.ts +0 -117
  319. package/studio/server/state/instance.ts +0 -280
  320. package/studio/server/state/updates.ts +0 -63
  321. package/studio/server/state/views.test.ts +0 -286
  322. package/studio/server/state/views.ts +0 -535
@@ -5,8 +5,9 @@ import { buildSystemPrompt } from './system'
5
5
  test('embedded agents receive the current SDK layout contract', () => {
6
6
  const prompt = buildSystemPrompt({ bridge: false })
7
7
 
8
- expect(prompt).toContain('implementation.ts')
8
+ expect(prompt).toContain('modular Actions and Workflows')
9
9
  expect(prompt).toContain('pre-Kernel-V2 APIs or layouts')
10
- expect(prompt).toContain('domain.ts/runtime only when this is already a legacy project')
10
+ expect(prompt).toContain('Runtime/Application entries')
11
+ expect(prompt).toContain('do not restore defineDomain or Interface-era layouts')
11
12
  expect(prompt).not.toContain('schema/ runtime/ views/')
12
13
  })
@@ -28,16 +28,17 @@ export function buildSystemPrompt(options: { bridge: boolean }): string {
28
28
  ' and **astrale-cli** when you run the `astrale` CLI. Treat those current skills and',
29
29
  ' the project itself as authoritative; do not restore pre-Kernel-V2 APIs or layouts.',
30
30
  '- Prefer the project’s existing semantic modules over inventing new structure. Current',
31
- ' SDK projects compose schema, handlers, and frontend metadata in implementation.ts;',
32
- ' wire new modules explicitly through their layer facades and that entry. Preserve',
33
- ' domain.ts/runtime only when this is already a legacy project (no folder magic).',
31
+ ' SDK projects author Schema definitions, modular Actions and Workflows, a Runtime,',
32
+ ' and an Application. Wire modules through their semantic SDK facades and explicit',
33
+ ' Runtime/Application entries; do not restore defineDomain or Interface-era layouts.',
34
34
  '- The studio re-renders automatically as you save files — never start, build, or refresh',
35
35
  ' anything for the UI to update.',
36
36
  '- Sanity-check schema/handler edits with `pnpm typecheck` (or `tsgo --noEmit`).',
37
37
  '- You ARE allowed to run the shell. When a thread asks you to **deploy or install**, do',
38
38
  ' it yourself: `pnpm prod` (managed deploy + install) or the `astrale` CLI. The user is',
39
- ' already authenticated (`astrale auth`). After deploying, VERIFY: `curl <svc-url>/meta`',
40
- ' must name this domain, and a live smoke call (create a node, call a method) should work',
39
+ ' already authenticated (`astrale auth`). After deploying, VERIFY:',
40
+ ' `curl <svc-url>/.well-known/astrale/domain.json` must publish this domain origin, and a',
41
+ ' live smoke call (create a node, call a method) should work',
41
42
  ' — report the URL and what you verified. Long-running commands are fine.',
42
43
  '- SINGLE HEADLESS TURN — you are not a resident session: there is NO background loop and NO',
43
44
  ' wake-on-completion. A shell started with run_in_background (or any “notify me when it',
@@ -1,12 +1,20 @@
1
- import type { Comment, ContextItem, DocMeta, SchemaIR, SchemaOverlay } from '../../../shared/types'
1
+ import type {
2
+ Comment,
3
+ ContextItem,
4
+ DocMeta,
5
+ SchemaIR,
6
+ SchemaOverlay,
7
+ SchemaRevision,
8
+ } from '../../../shared/types'
2
9
 
3
- import { buildCopyMarkdown } from '../../state/copy'
10
+ import { buildCopyMarkdown } from '../../handoff/copy'
4
11
  import { resolveThreadAnchors } from './anchors'
5
12
 
6
13
  export interface TurnParts {
7
14
  origin: string
8
15
  root: string
9
- schemaHash: string
16
+ renderFingerprint: string
17
+ schemaRevision?: SchemaRevision
10
18
  awaitingThreads: Comment[]
11
19
  userContext: ContextItem[]
12
20
  autoContext: ContextItem[]
@@ -22,7 +30,8 @@ export function buildTurnPrompt(parts: TurnParts): string {
22
30
  const body = buildCopyMarkdown({
23
31
  origin: parts.origin,
24
32
  root: parts.root,
25
- schemaHash: parts.schemaHash,
33
+ renderFingerprint: parts.renderFingerprint,
34
+ schemaRevision: parts.schemaRevision,
26
35
  openComments: parts.awaitingThreads,
27
36
  userContext: parts.userContext,
28
37
  autoContext: parts.autoContext,
@@ -26,7 +26,7 @@ function fixture() {
26
26
  roots.push(root)
27
27
  mkdirSync(join(root, 'schema'))
28
28
  writeFileSync(join(root, 'astrale.config.ts'), 'export default {}\n')
29
- writeFileSync(join(root, 'domain.ts'), 'export default {}\n')
29
+ writeFileSync(join(root, 'application.ts'), 'export default {}\n')
30
30
  writeFileSync(join(root, 'schema/index.ts'), 'export const Test = {}\n')
31
31
  const handle = registerDomain(root)!
32
32
  domainIds.push(handle.id)
@@ -31,7 +31,7 @@ export async function completeRun(
31
31
  settings,
32
32
  session,
33
33
  resume,
34
- schemaHash,
34
+ renderFingerprint,
35
35
  model,
36
36
  effort,
37
37
  harnessEnv,
@@ -141,7 +141,7 @@ export async function completeRun(
141
141
  result.finalText.trim()
142
142
  ) {
143
143
  try {
144
- const merged = mergeReply(root, schemaHash, result.finalText, {
144
+ const merged = mergeReply(root, renderFingerprint, result.finalText, {
145
145
  skipByComment: liveByComment,
146
146
  })
147
147
  run.merge = merged
@@ -58,7 +58,7 @@ function fixture(): DomainHandle {
58
58
  roots.push(root)
59
59
  mkdirSync(join(root, 'schema'))
60
60
  writeFileSync(join(root, 'astrale.config.ts'), 'export default {}\n')
61
- writeFileSync(join(root, 'domain.ts'), 'export default {}\n')
61
+ writeFileSync(join(root, 'application.ts'), 'export default {}\n')
62
62
  writeFileSync(join(root, 'schema/index.ts'), 'export const Test = {}\n')
63
63
  const handle = registerDomain(root)!
64
64
  domainIds.push(handle.id)
@@ -13,10 +13,10 @@ import type { Bridge } from '../bridge/grant'
13
13
  import type { AgentHarness } from '../harness/adapter'
14
14
 
15
15
  import { getBundle } from '../../cache'
16
+ import { refreshAuto } from '../../handoff/service'
16
17
  import { readComments } from '../../state/comments'
17
18
  import { readContext } from '../../state/context'
18
19
  import { listDocuments } from '../../state/documents'
19
- import { refreshAuto } from '../../state/handoff'
20
20
  import { startBridge } from '../bridge/grant'
21
21
  import { readConversation } from '../conversation'
22
22
  import { getHarness, resolveHarnessConfiguration } from '../harness/selection'
@@ -36,7 +36,7 @@ export interface PreparedRun {
36
36
  settings: StudioSettings
37
37
  session: ReturnType<typeof readConversation>
38
38
  resume?: string
39
- schemaHash: string
39
+ renderFingerprint: string
40
40
  model?: string
41
41
  effort?: AgentEffort
42
42
  harnessEnv: Record<string, string>
@@ -80,7 +80,7 @@ export async function prepareRun(
80
80
  const bundle = await getBundle(domainId)
81
81
  if (controller.signal.aborted) return { error: 'agent run canceled during setup' }
82
82
 
83
- const schemaHash = bundle?.schemaHash ?? ''
83
+ const renderFingerprint = bundle?.renderFingerprint ?? ''
84
84
  const context = readContext(root)
85
85
  const documents = listDocuments(root)
86
86
  const configuration = await resolveHarnessConfiguration(root, harness)
@@ -96,7 +96,8 @@ export async function prepareRun(
96
96
  : buildTurnPrompt({
97
97
  origin: bundle?.overlay.origin ?? domainId,
98
98
  root,
99
- schemaHash,
99
+ renderFingerprint,
100
+ schemaRevision: bundle?.schemaRevision,
100
101
  awaitingThreads: awaiting,
101
102
  userContext: context.user,
102
103
  autoContext: context.auto.filter((item) => item.includeInHandoff),
@@ -150,7 +151,7 @@ export async function prepareRun(
150
151
  settings,
151
152
  session,
152
153
  resume,
153
- schemaHash,
154
+ renderFingerprint,
154
155
  model,
155
156
  effort,
156
157
  harnessEnv,
@@ -5,6 +5,7 @@ import { join } from 'node:path'
5
5
 
6
6
  import type { AgentRun } from '../../../shared/types'
7
7
 
8
+ import { asJsonRecord } from '../../json'
8
9
  import { readJson, writeJson } from '../../state/store'
9
10
  import { currentRun, hydrateRun } from './live-state'
10
11
  import { readLastRun } from './transcript'
@@ -37,7 +38,7 @@ test('reconciles a run orphaned by restart and hydrates its terminal snapshot',
37
38
  sessionId: 'thread-1',
38
39
  error: expect.stringContaining('studio restarted'),
39
40
  })
40
- expect(readJson<AgentRun>(root, '.cache/agent/last-run.json', running).status).toBe('interrupted')
41
+ expect(readJson(root, '.cache/agent/last-run.json', asJsonRecord, {}).status).toBe('interrupted')
41
42
 
42
43
  hydrateRun(domainId, root)
43
44
  expect(currentRun(domainId)).toMatchObject({
@@ -45,3 +46,36 @@ test('reconciles a run orphaned by restart and hydrates its terminal snapshot',
45
46
  sessionId: 'thread-1',
46
47
  })
47
48
  })
49
+
50
+ test('admits future run fields but rejects malformed persisted run structure', () => {
51
+ const root = mkdtempSync(join(tmpdir(), 'studio-agent-transcript-boundary-'))
52
+ roots.push(root)
53
+ const domainId = `domain-${crypto.randomUUID()}`
54
+ writeJson(root, '.cache/agent/last-run.json', {
55
+ id: crypto.randomUUID(),
56
+ domainId,
57
+ harness: 'codex',
58
+ status: 'succeeded',
59
+ createdAt: '2026-08-20T00:00:00.000Z',
60
+ summary: 'finished work',
61
+ targetCommentIds: [],
62
+ events: [],
63
+ futureRunField: { version: 2 },
64
+ })
65
+ expect(readLastRun(domainId, root)).toMatchObject({
66
+ status: 'succeeded',
67
+ summary: 'finished work',
68
+ })
69
+
70
+ writeJson(root, '.cache/agent/last-run.json', {
71
+ id: crypto.randomUUID(),
72
+ domainId,
73
+ harness: 'codex',
74
+ status: 42,
75
+ createdAt: '2026-08-20T00:00:00.000Z',
76
+ summary: 'corrupt',
77
+ targetCommentIds: [],
78
+ events: [],
79
+ })
80
+ expect(readLastRun(domainId, root)).toBeNull()
81
+ })
@@ -1,10 +1,176 @@
1
- import type { AgentRun } from '../../../shared/types'
2
-
1
+ import {
2
+ AGENT_ACCESS_LEVELS,
3
+ AGENT_EFFORT_LEVELS,
4
+ type AgentEvent,
5
+ type AgentPromptSnapshot,
6
+ type AgentRun,
7
+ type MergeResult,
8
+ } from '../../../shared/types'
9
+ import { asBoolean, asFiniteNumber, asJsonRecord, asString, asStringArray } from '../../json'
3
10
  import { readJson, writeJson } from '../../state/store'
4
11
 
5
12
  const LAST_RUN_FILE = '.cache/agent/last-run.json'
6
13
  const runFile = (id: string) => `.cache/agent/runs/${id}.json`
7
14
 
15
+ const RUN_STATUSES = new Set<AgentRun['status']>([
16
+ 'queued',
17
+ 'running',
18
+ 'succeeded',
19
+ 'failed',
20
+ 'canceled',
21
+ 'interrupted',
22
+ ])
23
+ const EVENT_KINDS = new Set<AgentEvent['kind']>([
24
+ 'status',
25
+ 'thinking',
26
+ 'message',
27
+ 'tool',
28
+ 'reply',
29
+ 'error',
30
+ ])
31
+
32
+ function decodeAgentEvent(value: unknown): AgentEvent | undefined {
33
+ const record = asJsonRecord(value)
34
+ const id = asString(record?.id)
35
+ const ts = asString(record?.ts)
36
+ const kind = asString(record?.kind)
37
+ const text = asString(record?.text)
38
+ if (!id || !ts || !kind || !EVENT_KINDS.has(kind as AgentEvent['kind']) || text === undefined)
39
+ return undefined
40
+ const tool = asString(record?.tool)
41
+ const target = asString(record?.target)
42
+ const commentId = asString(record?.commentId)
43
+ return {
44
+ id,
45
+ ts,
46
+ kind: kind as AgentEvent['kind'],
47
+ text,
48
+ ...(tool === undefined ? {} : { tool }),
49
+ ...(target === undefined ? {} : { target }),
50
+ ...(commentId === undefined ? {} : { commentId }),
51
+ }
52
+ }
53
+
54
+ function decodeMergeResult(value: unknown): MergeResult | undefined {
55
+ const record = asJsonRecord(value)
56
+ const merged = asFiniteNumber(record?.merged)
57
+ const closed = asFiniteNumber(record?.closed)
58
+ const unknownIds = asStringArray(record?.unknownIds)
59
+ const schemaMismatch = asBoolean(record?.schemaMismatch)
60
+ if (
61
+ merged === undefined ||
62
+ !Number.isInteger(merged) ||
63
+ closed === undefined ||
64
+ !Number.isInteger(closed) ||
65
+ !unknownIds ||
66
+ schemaMismatch === undefined
67
+ ) {
68
+ return undefined
69
+ }
70
+ const pastedSchemaVersion = asString(record?.pastedSchemaVersion)
71
+ return {
72
+ merged,
73
+ closed,
74
+ unknownIds,
75
+ schemaMismatch,
76
+ ...(pastedSchemaVersion === undefined ? {} : { pastedSchemaVersion }),
77
+ }
78
+ }
79
+
80
+ function decodePrompt(value: unknown): AgentPromptSnapshot | undefined {
81
+ const record = asJsonRecord(value)
82
+ const createdAt = asString(record?.createdAt)
83
+ const systemPrompt = asString(record?.systemPrompt)
84
+ const turnPrompt = asString(record?.turnPrompt)
85
+ const firstTurn = asBoolean(record?.firstTurn)
86
+ const resumed = asBoolean(record?.resumed)
87
+ const mcpTools = asStringArray(record?.mcpTools)
88
+ if (
89
+ !createdAt ||
90
+ systemPrompt === undefined ||
91
+ turnPrompt === undefined ||
92
+ firstTurn === undefined ||
93
+ resumed === undefined ||
94
+ !mcpTools
95
+ ) {
96
+ return undefined
97
+ }
98
+ const sessionId = asString(record?.sessionId)
99
+ const model = asString(record?.model)
100
+ const effort = AGENT_EFFORT_LEVELS.find((candidate) => candidate === record?.effort)
101
+ const access = AGENT_ACCESS_LEVELS.find((candidate) => candidate === record?.access)
102
+ return {
103
+ createdAt,
104
+ systemPrompt,
105
+ turnPrompt,
106
+ firstTurn,
107
+ resumed,
108
+ mcpTools,
109
+ ...(sessionId === undefined ? {} : { sessionId }),
110
+ ...(model === undefined ? {} : { model }),
111
+ ...(effort === undefined ? {} : { effort }),
112
+ ...(access === undefined ? {} : { access }),
113
+ }
114
+ }
115
+
116
+ function decodeAgentRun(value: unknown): AgentRun | undefined {
117
+ const record = asJsonRecord(value)
118
+ const id = asString(record?.id)
119
+ const domainId = asString(record?.domainId)
120
+ const harness = asString(record?.harness)
121
+ const status = asString(record?.status)
122
+ const createdAt = asString(record?.createdAt)
123
+ const summary = asString(record?.summary)
124
+ const targetCommentIds = asStringArray(record?.targetCommentIds)
125
+ if (
126
+ !id ||
127
+ !domainId ||
128
+ !harness ||
129
+ !status ||
130
+ !RUN_STATUSES.has(status as AgentRun['status']) ||
131
+ !createdAt ||
132
+ summary === undefined ||
133
+ !targetCommentIds ||
134
+ !Array.isArray(record?.events)
135
+ ) {
136
+ return undefined
137
+ }
138
+ const events = record.events.flatMap((event) => {
139
+ const decoded = decodeAgentEvent(event)
140
+ return decoded ? [decoded] : []
141
+ })
142
+ const finishedAt = asString(record.finishedAt)
143
+ const sessionId = asString(record.sessionId)
144
+ const resumed = asBoolean(record.resumed)
145
+ const costUsd = asFiniteNumber(record.costUsd)
146
+ const numTurns = asFiniteNumber(record.numTurns)
147
+ const tokens = asFiniteNumber(record.tokens)
148
+ const error = asString(record.error)
149
+ const liveReplies = asFiniteNumber(record.liveReplies)
150
+ const merge = decodeMergeResult(record.merge)
151
+ const prompt = decodePrompt(record.prompt)
152
+ return {
153
+ id,
154
+ domainId,
155
+ harness,
156
+ status: status as AgentRun['status'],
157
+ createdAt,
158
+ summary,
159
+ targetCommentIds,
160
+ events,
161
+ ...(finishedAt === undefined ? {} : { finishedAt }),
162
+ ...(sessionId === undefined ? {} : { sessionId }),
163
+ ...(resumed === undefined ? {} : { resumed }),
164
+ ...(costUsd === undefined ? {} : { costUsd }),
165
+ ...(numTurns === undefined ? {} : { numTurns }),
166
+ ...(tokens === undefined ? {} : { tokens }),
167
+ ...(error === undefined ? {} : { error }),
168
+ ...(liveReplies === undefined ? {} : { liveReplies }),
169
+ ...(merge === undefined ? {} : { merge }),
170
+ ...(prompt === undefined ? {} : { prompt }),
171
+ }
172
+ }
173
+
8
174
  /** Persist the latest run pointer and, when terminal, its transcript. */
9
175
  export function persistRun(root: string, run: AgentRun, transcript = false): void {
10
176
  try {
@@ -17,7 +183,7 @@ export function persistRun(root: string, run: AgentRun, transcript = false): voi
17
183
 
18
184
  /** Rehydrate the latest run and reconcile one orphaned by a Studio restart. */
19
185
  export function readLastRun(domainId: string, root: string): AgentRun | null {
20
- const last = readJson<AgentRun | null>(root, LAST_RUN_FILE, null)
186
+ const last = readJson(root, LAST_RUN_FILE, decodeAgentRun, null)
21
187
  if (!last || last.domainId !== domainId) return null
22
188
  if (last.status === 'running' || last.status === 'queued') {
23
189
  last.status = 'interrupted'
@@ -5,6 +5,7 @@ import { join } from 'node:path'
5
5
 
6
6
  import type { AgentRun } from '../../../shared/types'
7
7
 
8
+ import { writeJson } from '../../state/store'
8
9
  import { readUsage, recordRun } from './usage'
9
10
 
10
11
  const roots: string[] = []
@@ -44,3 +45,26 @@ test('records only reported usage and accumulates domain totals', () => {
44
45
  lastCostUsd: 0.1,
45
46
  })
46
47
  })
48
+
49
+ test('normalizes malformed usage fields while accepting future fields', () => {
50
+ const root = mkdtempSync(join(tmpdir(), 'studio-agent-usage-boundary-'))
51
+ roots.push(root)
52
+ writeJson(root, 'usage.json', {
53
+ runs: -2,
54
+ tokens: 'many',
55
+ costUsd: -1,
56
+ lastRunAt: 42,
57
+ lastTokens: -10,
58
+ futureUsageField: { version: 2 },
59
+ })
60
+
61
+ expect(readUsage(root)).toEqual({ runs: 0, tokens: 0, costUsd: 0 })
62
+
63
+ writeJson(root, 'usage.json', {
64
+ runs: 3,
65
+ tokens: 120,
66
+ costUsd: 1.25,
67
+ futureUsageField: { version: 2 },
68
+ })
69
+ expect(readUsage(root)).toEqual({ runs: 3, tokens: 120, costUsd: 1.25 })
70
+ })
@@ -6,14 +6,39 @@
6
6
  */
7
7
  import type { AgentRun, DomainUsage } from '../../../shared/types'
8
8
 
9
+ import { asFiniteNumber, asJsonRecord, asString } from '../../json'
9
10
  import { readJson, writeJson } from '../../state/store'
10
11
 
11
12
  const PATH = 'usage.json'
12
13
 
13
14
  const EMPTY: DomainUsage = { runs: 0, tokens: 0, costUsd: 0 }
14
15
 
16
+ function nonNegative(value: unknown): number | undefined {
17
+ const number = asFiniteNumber(value)
18
+ return number !== undefined && number >= 0 ? number : undefined
19
+ }
20
+
21
+ function decodeUsage(value: unknown): DomainUsage | undefined {
22
+ const record = asJsonRecord(value)
23
+ if (!record) return undefined
24
+ const runs = nonNegative(record.runs)
25
+ const tokens = nonNegative(record.tokens)
26
+ const costUsd = nonNegative(record.costUsd)
27
+ const lastRunAt = asString(record.lastRunAt)
28
+ const lastTokens = nonNegative(record.lastTokens)
29
+ const lastCostUsd = nonNegative(record.lastCostUsd)
30
+ return {
31
+ runs: runs !== undefined && Number.isInteger(runs) ? runs : EMPTY.runs,
32
+ tokens: tokens ?? EMPTY.tokens,
33
+ costUsd: costUsd ?? EMPTY.costUsd,
34
+ ...(lastRunAt === undefined ? {} : { lastRunAt }),
35
+ ...(lastTokens === undefined ? {} : { lastTokens }),
36
+ ...(lastCostUsd === undefined ? {} : { lastCostUsd }),
37
+ }
38
+ }
39
+
15
40
  export function readUsage(root: string): DomainUsage {
16
- return { ...EMPTY, ...readJson<Partial<DomainUsage>>(root, PATH, {}) }
41
+ return readJson(root, PATH, decodeUsage, { ...EMPTY })
17
42
  }
18
43
 
19
44
  /** Fold a finished run into the running total. A run with no reported usage (e.g. it
@@ -0,0 +1,43 @@
1
+ /** Persisted canvas layout and visibility preference routes. */
2
+ import { getBundle } from '../cache'
3
+ import { readLayout, resetLayout, saveLayout, setNodePositions } from '../state/layout'
4
+ import { readVisibility, resetVisibility, saveVisibility } from '../state/visibility'
5
+ import { badRequest, json, type DomainRouteContext } from './http'
6
+
7
+ export async function handleCanvasRoute(context: DomainRouteContext): Promise<Response | null> {
8
+ const { req, rest, body, handle } = context
9
+ const id = handle.id
10
+ const root = handle.root
11
+
12
+ if (rest === '/layout') {
13
+ const bundle = await getBundle(id)
14
+ if (req.method === 'GET') return json(readLayout(root))
15
+ if (body.action === 'set') {
16
+ return json(setNodePositions(root, body.positions ?? {}, bundle?.renderFingerprint))
17
+ }
18
+ if (body.action === 'save') {
19
+ return json(saveLayout(root, body.positions ?? {}, bundle?.renderFingerprint))
20
+ }
21
+ if (body.action === 'reset') {
22
+ resetLayout(root)
23
+ return json({ ok: true })
24
+ }
25
+ return badRequest('unknown layout action')
26
+ }
27
+
28
+ if (rest === '/visibility') {
29
+ if (req.method === 'GET') return json(readVisibility(root))
30
+ if (body.action === 'set') {
31
+ return json(
32
+ saveVisibility(root, {
33
+ hidden: body.hidden ?? {},
34
+ showInheritedEdges: body.showInheritedEdges ?? true,
35
+ }),
36
+ )
37
+ }
38
+ if (body.action === 'reset') return json(resetVisibility(root))
39
+ return badRequest('unknown visibility action')
40
+ }
41
+
42
+ return null
43
+ }
@@ -0,0 +1,85 @@
1
+ /** Schema-anchored review comment routes and agent reply merge. */
2
+ import { getBundle } from '../cache'
3
+ import { schemaRefs } from '../introspect/schema-refs'
4
+ import {
5
+ addThreadEntry,
6
+ deleteComment,
7
+ editThreadEntry,
8
+ markOrphans,
9
+ mergeReply,
10
+ readComments,
11
+ setStatus,
12
+ upsertComment,
13
+ } from '../state/comments'
14
+ import { badRequest, json, notFound, type DomainRouteContext } from './http'
15
+
16
+ export async function handleCommentRoute(context: DomainRouteContext): Promise<Response | null> {
17
+ const { req, rest, body, handle, notify } = context
18
+ const id = handle.id
19
+ const root = handle.root
20
+
21
+ if (rest === '/comments') {
22
+ if (req.method === 'GET') {
23
+ const bundle = await getBundle(id)
24
+ const valid = new Set<string>(bundle ? schemaRefs(bundle) : [])
25
+ for (const key of Object.keys(bundle?.overlay.sourceSpans ?? {})) valid.add(key)
26
+ const store = readComments(root)
27
+ for (const comment of store.comments) {
28
+ for (const anchor of comment.anchorRefs) {
29
+ if (anchor.kind !== 'schema') valid.add(anchor.ref)
30
+ }
31
+ }
32
+ return json(markOrphans(root, [...valid]))
33
+ }
34
+ if (body.action === 'create') {
35
+ const bundle = await getBundle(id)
36
+ const comment = upsertComment(root, {
37
+ anchors: body.anchors ?? [],
38
+ anchorRefs: body.anchorRefs ?? [],
39
+ text: body.text,
40
+ firstRole: body.firstRole,
41
+ type: body.type,
42
+ options: body.options,
43
+ schemaVersion: bundle?.renderFingerprint,
44
+ })
45
+ notify({ type: 'comments', domainId: id })
46
+ return json(comment)
47
+ }
48
+ if (body.action === 'reply') {
49
+ const comment = addThreadEntry(root, body.id, body.entry)
50
+ notify({ type: 'comments', domainId: id })
51
+ return comment ? json(comment) : notFound()
52
+ }
53
+ if (body.action === 'edit') {
54
+ const comment = editThreadEntry(root, body.id, body.entryId, String(body.text ?? ''))
55
+ notify({ type: 'comments', domainId: id })
56
+ return comment ? json(comment) : notFound()
57
+ }
58
+ if (body.action === 'status') {
59
+ const comment = setStatus(root, body.id, body.status, body.closeNote)
60
+ notify({ type: 'comments', domainId: id })
61
+ return comment ? json(comment) : notFound()
62
+ }
63
+ if (body.action === 'delete') {
64
+ const ok = deleteComment(root, body.id)
65
+ notify({ type: 'comments', domainId: id })
66
+ return json({ ok })
67
+ }
68
+ return badRequest('unknown comments action')
69
+ }
70
+
71
+ if (rest === '/comments/merge' && req.method === 'POST') {
72
+ const bundle = await getBundle(id)
73
+ try {
74
+ const result = mergeReply(root, bundle?.renderFingerprint ?? '', String(body.text ?? ''), {
75
+ dedupeAuthorText: true,
76
+ })
77
+ notify({ type: 'comments', domainId: id })
78
+ return json(result)
79
+ } catch (error: any) {
80
+ return badRequest(String(error?.message ?? error))
81
+ }
82
+ }
83
+
84
+ return null
85
+ }
@@ -0,0 +1,61 @@
1
+ /** Authored/automatic agent context and copy-handoff routes. */
2
+ import { getBundle } from '../cache'
3
+ import { buildCopyMarkdown } from '../handoff/copy'
4
+ import { refreshAuto } from '../handoff/service'
5
+ import { readComments } from '../state/comments'
6
+ import {
7
+ addUserContext,
8
+ deleteContext,
9
+ readContext,
10
+ setAutoInclude,
11
+ updateContext,
12
+ } from '../state/context'
13
+ import { listDocuments } from '../state/documents'
14
+ import { badRequest, json, notFound, type DomainRouteContext } from './http'
15
+
16
+ export async function handleContextRoute(context: DomainRouteContext): Promise<Response | null> {
17
+ const { req, rest, body, handle, notify } = context
18
+ const id = handle.id
19
+ const root = handle.root
20
+
21
+ if (rest === '/context') {
22
+ if (req.method === 'GET') return json(readContext(root))
23
+ if (body.action === 'add') {
24
+ const item = addUserContext(root, body)
25
+ notify({ type: 'comments', domainId: id })
26
+ return json(item)
27
+ }
28
+ if (body.action === 'update') {
29
+ const item = updateContext(root, body.id, body)
30
+ return item ? json(item) : notFound()
31
+ }
32
+ if (body.action === 'delete') return json({ ok: deleteContext(root, body.id) })
33
+ if (body.action === 'include') {
34
+ const item = setAutoInclude(root, body.id, body.include)
35
+ return item ? json(item) : notFound()
36
+ }
37
+ return badRequest('unknown context action')
38
+ }
39
+
40
+ if (rest === '/copy-payload' && req.method === 'POST') {
41
+ await refreshAuto(handle)
42
+ const bundle = await getBundle(id)
43
+ const storedContext = readContext(root)
44
+ const openComments = readComments(root).comments.filter((comment) => comment.status === 'open')
45
+ const markdown = buildCopyMarkdown({
46
+ origin: bundle?.overlay.origin ?? id,
47
+ root,
48
+ renderFingerprint: bundle?.renderFingerprint ?? '',
49
+ schemaRevision: bundle?.schemaRevision,
50
+ openComments,
51
+ userContext: storedContext.user,
52
+ autoContext: body.includeAuto
53
+ ? storedContext.auto.filter((item) => item.includeInHandoff)
54
+ : [],
55
+ documents: listDocuments(root),
56
+ })
57
+ return json({ markdown, openComments: openComments.length })
58
+ }
59
+
60
+ return null
61
+ }