acrylblends 1.1.0 → 1.2.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 (241) hide show
  1. package/README.md +9 -0
  2. package/dist/index.d.ts +45 -1
  3. package/dist/index.js +1980 -27
  4. package/dist/index.js.map +1 -1
  5. package/docs/framework/agents/index.md +1 -1
  6. package/docs/framework/getting-started/desktop.md +23 -4
  7. package/docs/framework/guides/create-acrylblends.md +1 -1
  8. package/docs/framework/workshop/README.md +138 -152
  9. package/docs/framework/workshop/facilitator.md +69 -101
  10. package/legacy/README.md +9 -0
  11. package/legacy/agent/157ce3db1112/src/app.ts +98 -0
  12. package/legacy/agent/157ce3db1112/src/routes.ts +63 -0
  13. package/legacy/agent/24c681a00fca/src/app.ts +160 -0
  14. package/legacy/agent/24c681a00fca/src/routes.ts +83 -0
  15. package/legacy/agent/2e790da977b2/src/app.ts +98 -0
  16. package/legacy/agent/2e790da977b2/src/routes.ts +63 -0
  17. package/legacy/agent/6307045545dc/src/app.ts +168 -0
  18. package/legacy/agent/6307045545dc/src/routes.ts +83 -0
  19. package/legacy/agent/6a5fb9c15c53/src/app.ts +158 -0
  20. package/legacy/agent/6a5fb9c15c53/src/routes.ts +83 -0
  21. package/legacy/agent/71fb54a5a942/src/app.ts +111 -0
  22. package/legacy/agent/71fb54a5a942/src/routes.ts +63 -0
  23. package/legacy/agent/74ece148269c/src/app.ts +168 -0
  24. package/legacy/agent/74ece148269c/src/routes.ts +93 -0
  25. package/legacy/agent/855359304707/src/app.ts +168 -0
  26. package/legacy/agent/855359304707/src/routes.ts +126 -0
  27. package/legacy/agent/b5ef3c786ae4/src/app.ts +127 -0
  28. package/legacy/agent/b5ef3c786ae4/src/routes.ts +76 -0
  29. package/legacy/agent/c2f30958043b/src/app.ts +98 -0
  30. package/legacy/agent/c2f30958043b/src/routes.ts +53 -0
  31. package/legacy/blank/5a91c558af22/src/routes.ts +18 -0
  32. package/legacy/blank/9c650cf52bf3/src/routes.ts +14 -0
  33. package/legacy/blank/aea589283445/src/routes.ts +16 -0
  34. package/legacy/blank/b2cbb759c889/src/routes.ts +16 -0
  35. package/legacy/blank/d6d371c9a65e/src/routes.ts +18 -0
  36. package/legacy/chat/7076cc821e2c/src/app.ts +72 -0
  37. package/legacy/chat/7076cc821e2c/src/routes.ts +13 -0
  38. package/legacy/chat/7dbe6d5170df/src/app.ts +107 -0
  39. package/legacy/chat/7dbe6d5170df/src/routes.ts +19 -0
  40. package/legacy/chat/a9a9ab44c58e/src/app.ts +107 -0
  41. package/legacy/chat/a9a9ab44c58e/src/routes.ts +15 -0
  42. package/legacy/chat/f20ad9a51b06/src/app.ts +106 -0
  43. package/legacy/chat/f20ad9a51b06/src/routes.ts +15 -0
  44. package/legacy/layouts.json +524 -0
  45. package/legacy/nodejs/1d477f37aaf4/src/app.ts +92 -0
  46. package/legacy/nodejs/1d477f37aaf4/src/routes.ts +40 -0
  47. package/legacy/nodejs/b1bd9462496c/src/app.ts +61 -0
  48. package/legacy/nodejs/b1bd9462496c/src/routes.ts +37 -0
  49. package/legacy/nodejs/d5bab75076ef/src/app.ts +95 -0
  50. package/legacy/nodejs/d5bab75076ef/src/routes.ts +40 -0
  51. package/legacy/notes/4edc36949c15/src/app.ts +107 -0
  52. package/legacy/notes/4edc36949c15/src/routes.ts +14 -0
  53. package/legacy/notes/6cab2d09747c/src/app.ts +72 -0
  54. package/legacy/notes/6cab2d09747c/src/routes.ts +12 -0
  55. package/legacy/notes/7ca2dfed623a/src/app.ts +106 -0
  56. package/legacy/notes/7ca2dfed623a/src/routes.ts +14 -0
  57. package/legacy/todo/02e613b3625c/src/app.ts +71 -0
  58. package/legacy/todo/02e613b3625c/src/todo.ts +123 -0
  59. package/legacy/todo/45694a8d9e15/src/app.ts +71 -0
  60. package/legacy/todo/45694a8d9e15/src/todo.ts +96 -0
  61. package/legacy/todo/5923d3a6099f/src/app.ts +71 -0
  62. package/legacy/todo/5923d3a6099f/src/todo.ts +125 -0
  63. package/legacy/todo/849a18d66dc1/src/app.ts +91 -0
  64. package/legacy/todo/849a18d66dc1/src/todo.ts +125 -0
  65. package/legacy/todo/8e64211312f3/src/app.ts +71 -0
  66. package/legacy/todo/8e64211312f3/src/todo.ts +125 -0
  67. package/legacy/todo/b79078a87565/src/app.ts +71 -0
  68. package/legacy/todo/b79078a87565/src/todo.ts +125 -0
  69. package/lib/{audit-BFpULBCj.d.ts → audit-Bok_UNCn.d.ts} +26 -3
  70. package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
  71. package/lib/cordis/door.d.ts +42 -7
  72. package/lib/cordis/door.js +204 -60
  73. package/lib/cordis/inspect.js +1 -1
  74. package/lib/cordis/node-server.d.ts +101 -3
  75. package/lib/cordis/node-server.js +3 -3
  76. package/lib/cordis/router-http.d.ts +7 -1
  77. package/lib/cordis/router-http.js +44 -3
  78. package/lib/cordis/router.d.ts +1 -1
  79. package/lib/cordis/router.js +1 -1
  80. package/lib/cordis/state.js +1 -1
  81. package/lib/door/host.d.ts +2 -2
  82. package/lib/door/host.js +1 -1
  83. package/lib/door/page.d.ts +10 -1
  84. package/lib/door/page.js +46 -7
  85. package/lib/door/protocol.d.ts +1 -1
  86. package/lib/drivers.d.ts +10 -1
  87. package/lib/{index-DMOtA-NJ.d.ts → index-DGq7H0lb.d.ts} +11 -1
  88. package/lib/node-server.d.ts +7 -1
  89. package/lib/node-server.js +2 -2
  90. package/lib/{protocol-a2aqrp2X.d.ts → protocol-BeIYCgTo.d.ts} +4 -0
  91. package/lib/{snapshot-text-BwM6T2nG.js → snapshot-text-CXTnjxGx.js} +87 -7
  92. package/lib/src-CtkIX_Gg.js +427 -0
  93. package/lib/{src-BTBm4OPD.js → src-iBJ2ysV9.js} +77 -14
  94. package/lib/{test-websocket-BLeEvybj.js → test-websocket-CQOSvBth.js} +17 -7
  95. package/lib/{test-websocket-DyeS50Tr.d.ts → test-websocket-GUTAkFQT.d.ts} +3 -1
  96. package/package.json +2 -1
  97. package/plugins/acryl-door/src/host/channel.ts +86 -9
  98. package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
  99. package/plugins/acryl-door/src/host/door-file.ts +7 -2
  100. package/plugins/acryl-door/src/host/door.ts +53 -23
  101. package/plugins/acryl-door/src/host/page-routes.ts +3 -1
  102. package/plugins/acryl-door/src/host/page-socket.ts +18 -8
  103. package/plugins/acryl-door/src/host/verbs.ts +3 -0
  104. package/plugins/acryl-door/src/index.ts +166 -29
  105. package/plugins/acryl-door/src/page/base.ts +32 -0
  106. package/plugins/acryl-door/src/page/channel.ts +3 -1
  107. package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
  108. package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
  109. package/plugins/acryl-door/src/page/driver/driver.ts +32 -4
  110. package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
  111. package/plugins/acryl-door/src/page/index.ts +41 -2
  112. package/plugins/acryl-door/src/page/outline.ts +56 -0
  113. package/plugins/acryl-door/src/page/protocol.ts +4 -0
  114. package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
  115. package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
  116. package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
  117. package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
  118. package/plugins/acryl-node-server/src/client-modules.ts +7 -1
  119. package/plugins/acryl-node-server/src/index.ts +304 -16
  120. package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
  121. package/plugins/acryl-page/plugin.json +12 -0
  122. package/plugins/acryl-page/src/index.ts +163 -0
  123. package/plugins/acryl-router/src/service.ts +77 -14
  124. package/plugins/acryl-router-http/src/index.ts +15 -1
  125. package/plugins/acryl-router-http/src/serve.ts +31 -0
  126. package/plugins/acryl-web-server/plugin.json +14 -0
  127. package/plugins/acryl-web-server/src/index.ts +164 -0
  128. package/plugins/agent/agent.ts +38 -7
  129. package/plugins/agent/index.ts +1 -1
  130. package/plugins/agent/model.ts +10 -2
  131. package/plugins/agent/tools.ts +9 -3
  132. package/plugins/agent-panel/client/describe.ts +57 -0
  133. package/plugins/agent-panel/client/driver-pick.ts +22 -0
  134. package/plugins/agent-panel/client/frame.ts +51 -0
  135. package/plugins/agent-panel/client/main.ts +238 -26
  136. package/plugins/agent-panel/drivers.ts +7 -2
  137. package/plugins/agent-panel/headless.ts +5 -2
  138. package/plugins/agent-panel/index.ts +146 -57
  139. package/plugins/agent-panel/mount.ts +27 -0
  140. package/plugins/agent-panel/visitors.ts +231 -0
  141. package/plugins/desktop/frame.ts +231 -0
  142. package/plugins/desktop/launch.ts +44 -9
  143. package/plugins/desktop/main.ts +100 -32
  144. package/plugins/desktop/window.ts +21 -4
  145. package/plugins/driver-pty/client/terminal.ts +4 -1
  146. package/plugins/driver-pty/index.ts +38 -11
  147. package/plugins/driver-pty/spawn.ts +38 -0
  148. package/plugins/driver-pty/terminal.ts +2 -0
  149. package/plugins/extensions/index.ts +26 -1
  150. package/plugins/extensions/resolve.ts +13 -0
  151. package/plugins/feedback/client.d.ts +22 -0
  152. package/plugins/feedback/client.js +471 -0
  153. package/plugins/feedback/index.ts +143 -0
  154. package/plugins/feedback/plugin.json +11 -0
  155. package/plugins/feedback/store.ts +307 -0
  156. package/plugins/home/index.ts +122 -0
  157. package/plugins/home/plugin.json +11 -0
  158. package/plugins/instance/index.ts +6 -0
  159. package/plugins/manifest/app-plugins.ts +86 -0
  160. package/plugins/manifest/index.ts +12 -2
  161. package/plugins/manifest/snapshot.ts +275 -0
  162. package/plugins/openai/index.ts +64 -5
  163. package/plugins/openai/prompt.ts +23 -0
  164. package/shared/AGENTS.core.md +36 -16
  165. package/shared/skills/acryl-add-ui/SKILL.md +19 -0
  166. package/shared/skills/acryl-app/SKILL.md +13 -0
  167. package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
  168. package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
  169. package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
  170. package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
  171. package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
  172. package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
  173. package/shared/skills/cordis-plugin/SKILL.md +26 -0
  174. package/templates/agent/AGENTS.md +1 -1
  175. package/templates/agent/blend.yaml +4 -0
  176. package/templates/agent/gitignore +1 -0
  177. package/templates/agent/package.json +1 -0
  178. package/templates/agent/src/app.ts +150 -33
  179. package/templates/agent/src/frame.ts +17 -0
  180. package/templates/agent/src/main.ts +5 -3
  181. package/templates/agent/src/routes.ts +38 -19
  182. package/templates/agent/template.json +2 -0
  183. package/templates/agent/test/app.test.ts +45 -3
  184. package/templates/blank/AGENTS.md +3 -2
  185. package/templates/blank/blend.yaml +11 -0
  186. package/templates/blank/gitignore +1 -0
  187. package/templates/blank/package.json +1 -0
  188. package/templates/blank/src/app.ts +100 -29
  189. package/templates/blank/src/door.ts +7 -0
  190. package/templates/blank/src/first-page.ts +5 -0
  191. package/templates/blank/src/frame.ts +17 -0
  192. package/templates/blank/src/routes.ts +65 -8
  193. package/templates/blank/template.json +14 -1
  194. package/templates/blank/test/app.test.ts +103 -16
  195. package/templates/chat/blend.yaml +2 -1
  196. package/templates/chat/gitignore +1 -0
  197. package/templates/chat/package.json +1 -0
  198. package/templates/chat/src/app.ts +72 -22
  199. package/templates/chat/src/chat-app.ts +11 -2
  200. package/templates/chat/src/client/chat.tsx +4 -2
  201. package/templates/chat/src/frame.ts +17 -0
  202. package/templates/chat/src/page.tsx +6 -4
  203. package/templates/chat/src/routes.ts +14 -4
  204. package/templates/chat/src/runtime.ts +9 -3
  205. package/templates/chat/template.json +3 -1
  206. package/templates/chat/test/chat.test.ts +46 -1
  207. package/templates/chat/test/server.test.ts +17 -3
  208. package/templates/nodejs/blend.yaml +2 -1
  209. package/templates/nodejs/gitignore +1 -0
  210. package/templates/nodejs/package.json +1 -0
  211. package/templates/nodejs/src/app.ts +76 -21
  212. package/templates/nodejs/src/frame.ts +17 -0
  213. package/templates/nodejs/src/page.tsx +3 -3
  214. package/templates/nodejs/src/routes.ts +32 -13
  215. package/templates/nodejs/template.json +3 -1
  216. package/templates/nodejs/test/app.test.ts +38 -3
  217. package/templates/nodejs/test/server.test.ts +17 -3
  218. package/templates/notes/blend.yaml +2 -1
  219. package/templates/notes/gitignore +1 -0
  220. package/templates/notes/package.json +1 -0
  221. package/templates/notes/src/app.ts +72 -22
  222. package/templates/notes/src/frame.ts +17 -0
  223. package/templates/notes/src/notes-app.ts +24 -17
  224. package/templates/notes/src/routes.ts +11 -4
  225. package/templates/notes/src/runtime.ts +9 -3
  226. package/templates/notes/src/views.tsx +26 -17
  227. package/templates/notes/template.json +3 -1
  228. package/templates/notes/test/notes.test.ts +39 -1
  229. package/templates/notes/test/server.test.ts +17 -3
  230. package/templates/todo/blend.yaml +3 -0
  231. package/templates/todo/gitignore +1 -0
  232. package/templates/todo/package.json +1 -0
  233. package/templates/todo/src/app.ts +81 -27
  234. package/templates/todo/src/door.ts +7 -0
  235. package/templates/todo/src/frame.ts +17 -0
  236. package/templates/todo/src/page.ts +13 -6
  237. package/templates/todo/src/todo.ts +18 -6
  238. package/templates/todo/template.json +11 -1
  239. package/templates/todo/test/todo.test.ts +67 -9
  240. package/lib/src-LX0E4ec4.js +0 -117
  241. package/shared/SKILL.md +0 -14
@@ -1,131 +1,99 @@
1
- # Facilitator notes: an app that builds and drives itself
1
+ # Facilitator notes: building inside the ACRYL frame
2
2
 
3
- Read the handout first (`docs/framework/workshop/README.md`); this page is the room around it. The workshop changed shape on 2026-10-29: the agent living in
4
- the app both builds its interface and drives it (clicks, sidebars, dialogs, settings), on the web and in a desktop window. The data layer and the template
5
- tour are asides now, and `/hello` is only the introduction to the framework core. The handout is written for **acrylblends 1.0.10 or newer**.
3
+ Read the handout first (`docs/framework/workshop/README.md`); this page is the room around it. The workshop changed shape again for the frame: the
4
+ app is created, then **opened inside ACRYL's IDE** (projects on the left, the app's page in the canvas, files/changes/terminals on the right), a
5
+ chat model works on the app from inside the frame, and coding agents run in terminal tabs in the same folder. The data layer, the desktop window and
6
+ the panel-first flow of the previous handout are gone.
7
+
8
+ The handout is written for **acrylblends 1.2.0 or newer** (the release that carries the frame). A room on an older release is on the previous handout;
9
+ check with `npx acrylblends@latest --version` before the day.
6
10
 
7
11
  ## Before the room: put this on every machine
8
12
 
9
13
  | What | Why | How |
10
14
  | --- | --- | --- |
11
15
  | The preflight passing | it checks Node, npm, git, an agent with its login or key, a free port, and on Windows PowerShell and long paths | `npx acrylblends@latest preflight` ends with `Ready for the workshop.` |
12
- | Node.js 22.19 or newer (24 and 26 tested) | the app runs TypeScript directly | `node -v`; the app's own `scripts/check-node.cjs` refuses an older Node with a clear line |
13
- | git, and a GitHub account with `gh auth login` | the last block forks the workshop registry and opens a pull request | `git --version`, `gh auth status` |
14
- | One coding agent, logged in | the panel drives it; `codex`, `claude` and `pi` all worked | `codex login` once (and a model the account supports), or run `claude` once, or pi with `DEEPSEEK_API_KEY` set in the terminal that starts the app |
15
- | A prepared app with Electron already downloaded | the desktop block is 15 minutes and the download is the slowest part | run the handout's blocks 1 and 4 once per machine, then keep the folder as `~/acryl-workshop/prepared/` |
16
- | A warm npm cache | the first `npm create` and the Electron download dominate the first ten minutes | the prepared app does it |
16
+ | Node.js 22.19 or newer (24 and 26 tested) | the app runs TypeScript directly | `node -v`; the app's `scripts/check-node.cjs` refuses an older Node with a clear line |
17
+ | git, and a GitHub account with `gh auth login` | the first `npm run dev` makes the folder a git repository (Files and Changes need one); the last block forks the registry | `git --version`, `gh auth status` |
18
+ | One coding agent, logged in | block 5 starts it in a terminal tab; `claude`, `codex` and `pi` all worked | `codex login` once, or run `claude` once, or pi with `DEEPSEEK_API_KEY` in the terminal that starts `npm run dev` |
19
+ | One model key | block 3 adds it in Settings > Models; without it the chat shows a connect card | a DeepSeek key from <https://platform.deepseek.com>, or a Claude/ChatGPT subscription through Sign in with an account |
20
+ | A prepared app with `npm install` done | the first install brings the frame and the chat engine (about 500 MB together); a room on one Wi-Fi should not do it 20 times | run the handout's block 1 once per machine and keep the folder as `~/acryl-workshop/prepared/` |
21
+ | A warm npm cache | the first install dominates block 1 | the prepared app does it |
17
22
 
18
- **The agent config that decides the day.** The panel's agent is configured to act rather than describe. Check on every machine that a plain request
19
- produces files, not a plan: ask the prepared app's panel "say hello in a card on the page" and watch the panel's `Working...` line and the first file
20
- appear. If it only describes, the model is waiting for permission; the handout's sentence is "do it now".
23
+ **The agent setting that decides the day.** The agents are configured to act rather than describe. Check on every machine that a plain request
24
+ produces files, not a plan: ask the prepared app's chat "add a line to the app's page" and watch the Changes panel. If it only describes, the model
25
+ is waiting for permission; the handout's sentence is "do it now".
21
26
 
22
- **Agent-specific checks.** Codex answering `model_not_found` means the account does not have the default model: pick one it supports (`/model` in codex,
23
- or `model = "..."` in `~/.codex/config.toml`). pi with DeepSeek needs `DEEPSEEK_API_KEY` in the environment of the terminal that runs `npm run dev`
24
- (the panel starts pi from the app); the preflight reports `PASS agent pi (DeepSeek key found)` when it is there. The package is
25
- `@earendil-works/pi-coding-agent` (1.4.0 on npm on 2026-10-30), installed with `npm i -g @earendil-works/pi-coding-agent`.
27
+ **Agent-specific checks.** Codex answering `model_not_found` means the account does not have the default model: pick one it supports (`/model` in
28
+ codex, or `model = "..."` in `~/.codex/config.toml`). pi with DeepSeek needs `DEEPSEEK_API_KEY` in the environment of the terminal that runs `npm
29
+ run dev` (the terminal tabs inherit it); the preflight reports `PASS agent pi (DeepSeek key found)`. The package is
30
+ `@earendil-works/pi-coding-agent`.
26
31
 
27
- ## Timings, from the 1.0.10 walks (macOS and Windows, pi on DeepSeek)
32
+ ## Timings, from the rehearsal (this Mac, packed CLI, isolated HOME)
28
33
 
29
34
  | Minutes | Block | Measured | If late |
30
35
  | --- | --- | --- | --- |
31
- | 0-10 | preflight, setup and `/hello` | preflight 12 s; create, install, dev, the route edit, `npm test` (14 tests) and `npm run check` 14 s of commands | never cut |
32
- | 10-40 | the agent builds the interface | 5:51 for pi to build, install and check the board (an earlier codex run: about 2.5 minutes) | if late, keep the board minimal (one column) |
33
- | 40-60 | the agent drives the app | 1:41 for the tick-and-filter prompt (six door calls, most of the time the model thinking), 2:07 for the New-card prompt | the drive is the point; cut the second "check its own work" prompt |
34
- | 60-75 | the same app in a window | add + install 5 s; first `npm run desktop` 10 s including the Electron download on a fast line; the drive in the window 22 s; the windows, menu and settings prompt 1:11 | pre-warm Electron or demo it |
35
- | 75-90 | deploy and publish | production start in seconds; the registry check merges in about 2 minutes | keep the publish; the deploy talk can be 3 minutes |
36
+ | 0-10 | preflight and setup | preflight ~15 s; the prepared app skips create and install | never cut |
37
+ | 10-20 | create and open the frame | the first `npm install` dominates (the frame and the chat engine); the frame's first page is up in a few seconds after that | use the prepared app and open its frame |
38
+ | 20-25 | the first change at /app | the Files view opens `src/routes.ts`; stop and `npm run dev` reconnects the frame in a few seconds; the page shows the edit | cut `npm test` to the take-home |
39
+ | 25-30 | Settings > Models | the key saves with Apply (`Saved DeepSeek (deepseek-official).`); the page uses it after a reload | paste the key in the first-run card instead, Save and continue |
40
+ | 30-50 | chat builds the board | the chat writes into the app folder and Changes fills; the restart loop shows it at /app/board | if the model is slow, cut the board to one column |
41
+ | 50-65 | a coding agent in a terminal | the agent tab starts in the app folder (its prompt is `my-app`); `.acryl/bin/acryl look` prints the page in the frame's tile | demo one agent and let the room follow |
42
+ | 65-80 | upgrade an older app | `npx acrylblends@latest upgrade` reports every file it changed (the `frame` line is the new layout), then `npm install` and `npm run dev` | keep the report, skip the second app |
43
+ | 80-90 | publish (take-home) | the registry check merges in about two minutes | leave it as the take-home |
44
+
45
+ What the rehearsal verified end to end on this Mac: a packed CLI in an isolated HOME, the app opened by the frame with the app's page in a tab
46
+ (the first-run tile), the Preview Notice dismissed once, Settings > Models saving a key and the chat sending with it (against a local mock), a
47
+ terminal tab running commands in the app folder, the app's page edited and shown after a restart, the door seeing the page (`acryl look`), and an
48
+ app made by the previous release upgraded and opened by the frame.
36
49
 
37
- The Windows walk on 1.0.10 (2026-10-30, Windows 10, Node 22, pi on DeepSeek) matched: preflight 13 s; `npm create` 8 s and install 4 s; pi built the
38
- board from the panel in 5:56; the agent opened Settings through `File > Settings...`, set the appearance and saw `Saved: Dark`, and window 1 showed it
39
- after a reload; with two windows open during a live run, `look` answered in 317-353 ms in each and the panel's Stop ended the run in 3 s; production served
40
- the board read-only with the agent routes gone.
50
+ ## What to say in the room
41
51
 
42
- Checkpoints in the handout: 10 minutes a route answers; 40 minutes the agent's board is on screen; 60 minutes the agent has clicked through it; 75 minutes
43
- the window is open. Walk the room at those minutes and help the machine that is behind, rather than the one that is ahead.
52
+ - **The frame is the product.** The point is not "an app with a chat sidebar"; it is that ACRYL's IDE hosts the app you are building: the app is
53
+ the page in the middle, its folder is the workspace, and the agents work in that folder.
54
+ - **Settings > Models is the key's home.** The key goes to the frame's own credentials store, never the app folder or `.acrylblends` files. The
55
+ first-run card writes the same store; either path is fine.
56
+ - **The loop is stop, start, look.** A source change needs `npm run dev` again; the port is stable per app, so the frame reconnects on the same
57
+ address and the app's tab is still there. If a chat tab comes to the front after the restart, press the `my-app` tab.
58
+ - **Two kinds of agent.** The chat (DSH's own, in the frame) and the coding agents in terminal tabs are different hands on the same folder; show
59
+ both and let people pick.
60
+ - **The door.** `.acryl/bin/acryl look` prints the page with refs, `act` clicks and types. In the frame the page is the tile, and the door sees
61
+ it. This is how an agent checks its own work.
62
+ - **Production is unchanged.** The frame, the chat and the agents are development tools; `NODE_ENV=production npm start` serves the app alone.
44
63
 
45
- ## What to say in the room
64
+ ## Windows
46
65
 
47
- - **Close the browser tab before block 4.** While a browser tab of the app is open, a door action without `--window` can go to that tab: in the 1.0.10
48
- walk the agent, asked from the window's panel, moved the hidden browser tab and reported success while the window stayed on `/`. With the tab closed
49
- the same prompt drove the window in 22 s. The handout says so at the top of block 4; it stays until the door aims at the window the agent was asked
50
- from (reported to Blends).
51
- - **The Settings item.** It is in the application menu on macOS (`my-app > Settings...`, `Cmd+,`) and the first item of the File menu on Windows and Linux
52
- (`Ctrl+,`). An app gets it because it serves `/settings`; the template does. In the walk, pi found it in the app menu by itself.
53
- - **Appearance.** The template's `/settings` saves light, dark or "match the system" for the app's own pages. A window that is already open shows the
54
- change after a reload, and a page the agent built follows the system theme unless asked otherwise. On a dark-mode machine, demo with Light.
55
- - **The driving bar.** `Agent is driving: clicking` with a Stop button shows at the bottom of the page only while each action runs, a moment each; the
56
- panel's `Working... m:ss` line and its Stop button are what people see during a run.
57
- - **Every agent builds a different board.** pi's board needed a title typed before "New card" added a card, and kept its sidebar always open when the
58
- prompt did not ask for a toggle. The handout's block 2 prompt now asks for a button that opens and closes the sidebar, so block 3 has something to open.
59
- - **Production listens where it is told.** The app reads `ACRYLBLENDS_HOST` and `ACRYLBLENDS_PORT`, not `PORT`; without them it listens on `127.0.0.1` at a
60
- port chosen from its folder. A server needs `ACRYLBLENDS_HOST=0.0.0.0 ACRYLBLENDS_PORT=8080 NODE_ENV=production npm start` (verified: it answered on the
61
- LAN). If Blends ships `PORT`, the handout switches to it.
62
- - **Windows.** Windows PowerShell 5.1 has no `&&` (two lines, or `;`); PowerShell sets variables with `$env:NAME="value"`; cmd.exe's `Ctrl-C` asks
63
- `Terminate batch job (Y/N)?` (answer `Y`); cmd.exe does not expand `~` (use `%USERPROFILE%`). Node 22 prints only `Restarting 'src/main.ts'` on a save;
64
- Node 24 and 26 print `Change detected …` before it.
65
- - **F (the door through a DeepSeek model with no login).** pi with a DeepSeek key works today; do not promise more than that.
66
-
67
- ## What the 1.0.10 walk found, block by block
68
-
69
- The final macOS walk on 1.0.10 (2026-10-30), verbatim from the handout as it stood before this revision, with pi on DeepSeek. Every block passed; these
70
- are the places where the handout text differed from what the walk saw, and each one is now handled in the handout.
71
-
72
- 1. Block 1: npm test now has 14 tests (the guide gives no number). "Change detected … Restarting" matches on Node 24; on Node 22 only "Restarting"
73
- prints (seen on z370n).
74
- 2. Block 2: "keep the app's own page (/) open in a second tab, because the panel lives there" is outdated: the panel is on every page now and followed the
75
- agent to /board. The page is dark on a dark-mode Mac (screenshots s1-first-page/s3-board look different); the panel stays light on the dark page.
76
- 3. Block 3: the guide's block 2 prompt asks for no sidebar toggle, so "open the sidebar" has nothing to open (pi says so and goes on). "an indicator in
77
- the corner says the agent is driving, with a stop button": it shows only while each single door call runs (a fraction of a second), so an attendee
78
- will hardly see it; it never showed in 33 frames taken every 3 s. "Click New card twice" added no cards: pi's board needs a title first; the prompt
79
- only asks whether the counts agree. "seven door calls in about twenty seconds" was 6 calls in 1:41 with pi.
80
- 4. Block 4: the guide says "use the File menu". On macOS Settings is in the app menu (my-app>Settings..., Cmd+,); pi found it there itself. Elsewhere it
81
- is the first File item. "switch the appearance to dark" shows no change on a dark-mode Mac, and the agent-built /board ignores the app theme (its CSS
82
- follows the system); the change shows on the template's / and /settings, and in other open windows only after a reload (switching to Light in window
83
- 3, window 1 on / changed only after act reload). The TODO(1.0.4-C) paragraph is outdated: the window, menu and settings control has shipped.
84
- Important: if the browser tab from blocks 2-3 is still open, the agent's door calls from the window's panel go to that browser tab, not the window. My
85
- first in-window drive moved the hidden tab and the window stayed on /. With the tab closed it worked.
86
- 5. Block 5: "That is the same app on any Node host … one port" names no port: the app binds 127.0.0.1 on a port derived from its folder path, ignores PORT
87
- (PORT=3299 still gave 3991), and needs ACRYLBLENDS_PORT and ACRYLBLENDS_HOST (only the app's README names them; production.md does not).
66
+ Windows PowerShell 5.1 has no `&&` (two lines, or `;`); PowerShell sets variables with `$env:NAME="value"`; cmd.exe's `Ctrl-C` asks `Terminate
67
+ batch job (Y/N)?` (answer `Y`); cmd.exe does not expand `~` (use `%USERPROFILE%`). `npm install` needs the "Desktop development with C++" workload
68
+ of Visual Studio Build Tools for the terminal package; the preflight notes it.
88
69
 
89
70
  ## The no-network fallback
90
71
 
91
- 1. Prepared apps on every machine cover blocks 1-3; the agent's model still needs the network, so block 2 and 3 need a connection or a demo.
92
- 2. Block 5's registry needs GitHub; if the venue is offline, publish from your machine on a hotspot at the end, or show the recording.
72
+ 1. Prepared apps with `npm install` done cover blocks 1, 2 and the frame; the models (chat and agents) still need a connection or a demo.
73
+ 2. Block 7's registry needs GitHub; if the venue is offline, publish from your machine on a hotspot at the end, or show the recording.
93
74
  3. Keep one phone hotspot for the room.
94
75
 
95
76
  ## Demo script (five minutes, if the room is behind)
96
77
 
97
- 1. Prepared app, panel open: ask for a card with a count. Narrate the panel's `Working...` line and the page as it reloads.
98
- 2. Ask: "open the sidebar, tick one card, change the filter to done, tell me the counts." Point at the page changing and at the driving bar as each action
99
- runs.
100
- 3. Show `.acryl/audit/` afterwards: every action is a line.
101
- 4. Close the browser tab, switch to the desktop window and repeat one click there; then open Settings from the menu.
102
- 5. Show the registry listing with an app already published, and the four commands that put it there.
103
-
104
- ## Screenshots
105
-
106
- Taken on 1.0.10 on macOS in dark mode (the pages follow the system theme), 1280 wide, no personal data, tokens or real home paths:
107
-
108
- | File | Shows |
109
- | --- | --- |
110
- | `s1-first-page.webp` | the first page with the panel open (block 1) |
111
- | `s2-panel-building.webp` | the panel with the build request and `Working... 0:04` (block 2) |
112
- | `s3-board.webp` | a board an agent built (block 2; from an earlier run, kept because it shows the result best) |
113
- | `s4-agent-drives-window.webp` | the agent driving the board in the desktop window: the Done filter it just set (block 4) |
114
- | `s5-two-windows.webp` | two desktop windows: the app's page in the dark appearance, and the Settings window showing `Saved: Dark` (block 4) |
115
- | `s6-registry.webp` | the registry listing (block 5) |
116
-
117
- S4 and S5 are page captures of the desktop windows (through the window's DevTools), without the window frame; a capture with the frame needs the
118
- screen-recording permission on the machine that takes it.
78
+ 1. Prepared app, frame open on the app's page: press `+`, open **AcrylDSH Chat**, ask for a sentence on the page. Narrate the Changes panel and
79
+ the restart loop.
80
+ 2. Open a **files** tab in the right panel and show the edit the model made.
81
+ 3. `+` → **Pi** (or Claude): the agent starts in the app folder; ask it for a route; run `.acryl/bin/acryl look` in that terminal and show the
82
+ page's snapshot.
83
+ 4. Settings > Models: show where the key lives and that it is not in the app folder.
84
+ 5. Show the registry listing with an app already published.
119
85
 
120
86
  ## Resetting between sessions
121
87
 
122
- Everything a person makes lives inside their app folder and `~/acryl-workshop`. To reset a machine, delete the app folders and copy the prepared one back;
123
- nothing is installed globally except the agent, `gh` and Node.
88
+ Everything a person makes lives inside their app folder and `~/acryl-workshop`. To reset a machine, delete the app folders and copy the prepared one
89
+ back; nothing is installed globally except the agent, `gh` and Node. An app's data lives in its `.acrylblends/` (gitignored); the model key lives in
90
+ that home's credentials store, so a fresh copy starts without it.
124
91
 
125
92
  ## What is still open
126
93
 
127
- - Close-the-tab advice in block 4 until the door aims at the window the agent was asked from.
128
- - The appearance reaching open windows live, and an agent-built page following the app's appearance: reported to Blends; the handout says "reload".
129
- - `PORT` for production: if Blends ships it, the handout's server line switches from `ACRYLBLENDS_PORT` to `PORT`.
130
- - The handout's hosted-workspace section (browser-only attendees) stays TODO(M9) until that workspace is live; the one-click WebBoxes.ai deploy lives
131
- there too.
94
+ - The frame's chat tab takes the focus when it binds its session after a restart; the app's tab is one click away and the guide says so.
95
+ - The frame's address is stable across restarts: the launcher keeps the token in the app home (owner-only, atomic) and reuses it, so an open page
96
+ comes back by reloading its URL even if the browser lost its cookie. `npx acrylblends open` prints the address of the running frame;
97
+ `npx acrylblends open --new` rotates it (the old address stops working at once and the new one is printed). The frame-side banner that names the
98
+ command is still to come (after the 120 gate work).
99
+ - The one-click WebBoxes.ai deploy and the browser-only section stay TODO(M9) until that workspace is live.
@@ -0,0 +1,9 @@
1
+ # The scaffolds published acrylblends releases shipped
2
+
3
+ Generated by `scripts/build-legacy-layouts.mjs`; do not edit by hand. Each folder is a template, each hash under it one distinct scaffold (deduplicated by
4
+ content hash across releases), and `layouts.json` maps every published release that shipped templates (0.3.0 through 1.1.0, 40 releases)
5
+ to the layouts it shipped.
6
+
7
+ `acrylblends upgrade` compares an app's scaffold files with these bytes: a file the app has not edited is brought up to date, an edited one is refused (or
8
+ replaced with --force after a backup). Releases before 0.7.8 wrote src/main.ts importing './plugins/...', which cannot start; their layouts are kept so the
9
+ upgrade can say so precisely instead of writing a frame such a release's app cannot run.
@@ -0,0 +1,98 @@
1
+ import { join } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { Context, type Fiber } from '@deepseek-ai/cordis'
4
+ import * as nodeServer from './plugins/acryl-node-server/src/index.ts'
5
+ import * as router from './plugins/acryl-router/src/index.ts'
6
+ import * as state from './plugins/acryl-state/src/index.ts'
7
+ import { agent, memory, model, tools } from './plugins/agent/index.ts'
8
+ import { builder } from './plugins/builder/index.ts'
9
+ import { extensions } from './plugins/extensions/index.ts'
10
+ import { type AppInstance, instance } from './plugins/instance/index.ts'
11
+ import {
12
+ blueprintsIn,
13
+ compose,
14
+ loadManifest,
15
+ type ParameterValue,
16
+ } from './plugins/manifest/index.ts'
17
+ import { type OpenAIConfig, openai } from './plugins/openai/index.ts'
18
+ import { sandboxPlugin } from './plugins/sandbox/index.ts'
19
+ import { save } from './plugins/save/index.ts'
20
+ import { tasks } from './plugins/tasks/index.ts'
21
+ import { routes } from './routes.ts'
22
+
23
+ /** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
24
+ export const registry = {
25
+ tasks,
26
+ model,
27
+ tools,
28
+ memory,
29
+ agent,
30
+ 'acryl-router': router,
31
+ 'acryl-state': state,
32
+ extensions,
33
+ builder,
34
+ save,
35
+ sandbox: sandboxPlugin,
36
+ routes,
37
+ 'acryl-node-server': nodeServer,
38
+ }
39
+
40
+ /** The manifest next to this app's source. */
41
+ export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'blend.yaml')
42
+
43
+ /**
44
+ * The app's manifest, resolved. A Blend finds its Blueprint in `blueprints/<id>.yaml`; an app that is itself a Blueprint never looks.
45
+ * `createApp` and the tests read it the same way.
46
+ */
47
+ export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
48
+ return loadManifest(MANIFEST, {
49
+ known: Object.keys(registry),
50
+ getBlueprint: blueprintsIn(join(fileURLToPath(new URL('..', import.meta.url)), 'blueprints')),
51
+ ...(values ? { values } : {}),
52
+ })
53
+ }
54
+
55
+ export interface App {
56
+ readonly root: Context
57
+ readonly url: string
58
+ stop(): Promise<void>
59
+ }
60
+
61
+ /**
62
+ * The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. The app's own `extensions/`
63
+ * folder is trusted to install at start, and is where the builder writes.
64
+ */
65
+ export async function createApp(
66
+ chosen: AppInstance,
67
+ options: {
68
+ values?: Readonly<Record<string, ParameterValue>>
69
+ /** A real model (any OpenAI-compatible API) in place of the offline scripted one. */
70
+ model?: OpenAIConfig
71
+ } = {},
72
+ ): Promise<App> {
73
+ const manifest = readManifest(options.values)
74
+ const extensionsDir = join(chosen.root, 'extensions')
75
+ const root = new Context()
76
+ const first = root.plugin(instance, chosen)
77
+ await first.await()
78
+ const fibers: Fiber[] = [
79
+ first,
80
+ // The `model` row mounts the scripted model, or the real one when a model is configured.
81
+ ...(await compose(root, manifest, options.model ? { ...registry, model: openai } : registry, {
82
+ ...(options.model ? { model: { ...options.model } } : {}),
83
+ // Runtime values the manifest cannot hold: the manifest's own digest (every task records it), and who may install.
84
+ tasks: { manifest: `sha256:${manifest.digest}` },
85
+ // The `approval` parameter in blend.yaml (the extensions row) turns this source into one that asks.
86
+ extensions: { sources: [{ dir: extensionsDir, trust: 'install' }] },
87
+ builder: { dir: extensionsDir },
88
+ server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0, hostname: chosen.host },
89
+ })),
90
+ ]
91
+ return {
92
+ root,
93
+ url: root.nodeServer.url,
94
+ async stop() {
95
+ for (const fiber of [...fibers].reverse()) await fiber.dispose()
96
+ },
97
+ }
98
+ }
@@ -0,0 +1,63 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import { Acryl } from './plugins/acryl-router/src/index.ts'
3
+ import { bodyLimit } from './plugins/acryl-router/src/middleware/index.ts'
4
+ import type {} from './plugins/agent/index.ts'
5
+ import type {} from './plugins/extensions/index.ts'
6
+ import type {} from './plugins/tasks/index.ts'
7
+
8
+ export const APP_NAME = '__APP_NAME__'
9
+
10
+ /**
11
+ * The app's own routes on the router: `POST /ask` for the agent; `GET /extensions` and `GET /tasks` to observe what it did (every run is a
12
+ * durable task owning one task per tool call); `POST /tasks/cancel`. What changes is also streamed: `GET /api/state/tasks/events` and
13
+ * `GET /api/state/extensions/events`. The mount is removed when the plugin is disposed.
14
+ */
15
+ export const routes = {
16
+ name: 'routes',
17
+ inject: ['router', 'agent', 'extensions', 'tasks'],
18
+ apply(ctx: Context) {
19
+ // A body here is a name, an id or a question; anything bigger is refused before it is read.
20
+ const json = async (c: {
21
+ req: { json(): Promise<unknown> }
22
+ }): Promise<Record<string, unknown>> => {
23
+ const body = await c.req.json().catch(() => undefined)
24
+ return typeof body === 'object' && body !== null ? (body as Record<string, unknown>) : {}
25
+ }
26
+ const app = new Acryl()
27
+ .use(bodyLimit({ maxSize: 64 * 1024 }))
28
+ .get('/', (c) => c.json({ app: APP_NAME, ok: true }))
29
+ .get('/health', (c) => c.json({ ok: true }))
30
+ .get('/extensions', (c) =>
31
+ c.json({
32
+ installed: ctx.extensions.list(),
33
+ notInstalled: ctx.extensions.pending(),
34
+ waitingForApproval: ctx.extensions.approvals(),
35
+ }),
36
+ )
37
+ // The owner's decision on an install that is waiting (the `approval` parameter in blend.yaml).
38
+ for (const [path, decide] of [
39
+ ['/extensions/approve', (name: string) => ctx.extensions.approve(name)],
40
+ ['/extensions/deny', (name: string) => ctx.extensions.deny(name)],
41
+ ] as const) {
42
+ app.post(path, async (c) => {
43
+ const { name } = await json(c)
44
+ if (typeof name !== 'string') return c.json({ error: 'name must be a string' }, 400)
45
+ return c.json(await decide(name))
46
+ })
47
+ }
48
+ app
49
+ .get('/tasks', (c) => c.json(ctx.tasks.list().slice(0, 100)))
50
+ .post('/tasks/cancel', async (c) => {
51
+ const { id } = await json(c)
52
+ if (typeof id !== 'string' || !ctx.tasks.get(id))
53
+ return c.json({ error: 'no such task' }, 404)
54
+ return c.json({ cancelled: ctx.tasks.cancel(id) })
55
+ })
56
+ .post('/ask', async (c) => {
57
+ const { question } = await json(c)
58
+ if (typeof question !== 'string') return c.json({ error: 'question must be a string' }, 400)
59
+ return c.json(await ctx.agent.run(question))
60
+ })
61
+ ctx.router.route('/', app)
62
+ },
63
+ }
@@ -0,0 +1,160 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { Context, type Fiber } from '@deepseek-ai/cordis'
5
+ import * as door from '../plugins/acryl-door/src/index.ts'
6
+ import * as nodeServer from '../plugins/acryl-node-server/src/index.ts'
7
+ import * as router from '../plugins/acryl-router/src/index.ts'
8
+ import * as state from '../plugins/acryl-state/src/index.ts'
9
+ import { agent, memory, model, tools } from '../plugins/agent/index.ts'
10
+ import * as panel from '../plugins/agent-panel/index.ts'
11
+ import { builder } from '../plugins/builder/index.ts'
12
+ import { extensions } from '../plugins/extensions/index.ts'
13
+ import { type AppInstance, instance } from '../plugins/instance/index.ts'
14
+ import {
15
+ blueprintsIn,
16
+ compose,
17
+ loadManifest,
18
+ type ParameterValue,
19
+ } from '../plugins/manifest/index.ts'
20
+ import { type OpenAIConfig, openai } from '../plugins/openai/index.ts'
21
+ import { sandboxPlugin } from '../plugins/sandbox/index.ts'
22
+ import { save } from '../plugins/save/index.ts'
23
+ import { tasks } from '../plugins/tasks/index.ts'
24
+ import { routes } from './routes.ts'
25
+
26
+ /** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
27
+ export const registry = {
28
+ tasks,
29
+ model,
30
+ tools,
31
+ memory,
32
+ agent,
33
+ 'acryl-router': router,
34
+ 'acryl-state': state,
35
+ extensions,
36
+ builder,
37
+ save,
38
+ sandbox: sandboxPlugin,
39
+ routes,
40
+ 'acryl-node-server': nodeServer,
41
+ 'agent-panel': panel,
42
+ 'acryl-door': door,
43
+ }
44
+
45
+ /** The manifest next to this app's source. */
46
+ export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'blend.yaml')
47
+
48
+ /**
49
+ * The app's manifest, resolved. A Blend finds its Blueprint in `blueprints/<id>.yaml`; an app that is itself a Blueprint never looks.
50
+ * `createApp` and the tests read it the same way.
51
+ */
52
+ export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
53
+ return loadManifest(MANIFEST, {
54
+ known: Object.keys(registry),
55
+ getBlueprint: blueprintsIn(join(fileURLToPath(new URL('..', import.meta.url)), 'blueprints')),
56
+ ...(values ? { values } : {}),
57
+ })
58
+ }
59
+
60
+ export interface App {
61
+ readonly root: Context
62
+ /** The address the server listens on; empty when the app was made with `listen: false`. */
63
+ readonly url: string
64
+ stop(): Promise<void>
65
+ }
66
+
67
+ /** The development rows production leaves out, and what each one gives a visitor. */
68
+ const DEVELOPMENT_ROWS: Record<string, string> = {
69
+ agent: 'POST /ask runs the agent, which spends the model key and calls its tools',
70
+ builder: 'the agent can write plugin source into plugins/',
71
+ extensions:
72
+ 'a plugin installed live runs in this process, and POST /extensions/approve decides installs',
73
+ }
74
+
75
+ /**
76
+ * The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. The app's `plugins/`
77
+ * folder is trusted to install at start, and is where the builder writes.
78
+ */
79
+ export async function createApp(
80
+ chosen: AppInstance,
81
+ options: {
82
+ values?: Readonly<Record<string, ParameterValue>>
83
+ /** A real model (any OpenAI-compatible API) in place of the offline scripted one. */
84
+ model?: OpenAIConfig
85
+ /** Start the HTTP server (default). Tests pass `false` and call `app.root.router.request(path)`: it needs no port, so it also runs where a sandbox forbids listening. */
86
+ listen?: boolean
87
+ } = {},
88
+ ): Promise<App> {
89
+ const read = readManifest(options.values)
90
+ // Rows left out: the server (and so the door, which needs it) when the app is reached in process; the door in production and with ACRYL_DOOR=off (one process at a time
91
+ // serves a folder's door, so the tests that start several apps set it).
92
+ const listen = options.listen !== false
93
+ const leaveOut = new Set<string>()
94
+ if (!listen) leaveOut.add('acryl-node-server').add('acryl-door')
95
+ if (process.env.NODE_ENV === 'production' || process.env.ACRYL_DOOR === 'off')
96
+ leaveOut.add('acryl-door')
97
+ // Production: the rows that give a visitor power over the running app or the owner's model key are left out too. The agent (`POST /ask` spends the model key and runs tools),
98
+ // the builder and the extensions (live plugin install runs code in this process, and `POST /extensions/approve` decides it) are development tools. `ACRYL_PRODUCTION_KEEP`
99
+ // names the ones to keep (`agent,builder,extensions`); each one kept is announced at start, because none of them is behind a login.
100
+ if (process.env.NODE_ENV === 'production') {
101
+ const keep = new Set(
102
+ (process.env.ACRYL_PRODUCTION_KEEP ?? '')
103
+ .split(',')
104
+ .map((name) => name.trim())
105
+ .filter((name) => name !== ''),
106
+ )
107
+ for (const name of keep)
108
+ if (!(name in DEVELOPMENT_ROWS))
109
+ console.warn(
110
+ `WARNING: ACRYL_PRODUCTION_KEEP names "${name}", which is not one of ${Object.keys(DEVELOPMENT_ROWS).join(', ')}`,
111
+ )
112
+ for (const [row, danger] of Object.entries(DEVELOPMENT_ROWS)) {
113
+ if (keep.has(row))
114
+ console.warn(
115
+ `WARNING: the "${row}" row runs in production because ACRYL_PRODUCTION_KEEP names it: ${danger}. Nothing here is behind a login; put the app behind your own.`,
116
+ )
117
+ else leaveOut.add(row)
118
+ }
119
+ }
120
+ const manifest = {
121
+ ...read,
122
+ plugins: read.plugins.map((row) => (leaveOut.has(row.use) ? { ...row, disabled: true } : row)),
123
+ }
124
+ const extensionsDir = join(chosen.root, 'plugins')
125
+ // One release of grace: an app made before 0.7.8 kept what the agent wrote in `extensions/`. It is still installed, with a warning.
126
+ const legacyDir = join(chosen.root, 'extensions')
127
+ const legacy = existsSync(legacyDir)
128
+ if (legacy)
129
+ console.warn(
130
+ 'warning: extensions/ is the old folder for plugins the agent writes; move them to plugins/ ("git mv extensions/<name> plugins/<name>"). extensions/ is read for this release only.',
131
+ )
132
+ const root = new Context()
133
+ const first = root.plugin(instance, chosen)
134
+ await first.await()
135
+ const fibers: Fiber[] = [
136
+ first,
137
+ // The `model` row mounts the scripted model, or the real one when a model is configured.
138
+ ...(await compose(root, manifest, options.model ? { ...registry, model: openai } : registry, {
139
+ ...(options.model ? { model: { ...options.model } } : {}),
140
+ // Runtime values the manifest cannot hold: the manifest's own digest (every task records it), and who may install.
141
+ tasks: { manifest: `sha256:${manifest.digest}` },
142
+ // The `approval` parameter in blend.yaml (the extensions row) turns this source into one that asks.
143
+ extensions: {
144
+ sources: [
145
+ { dir: extensionsDir, trust: 'install' },
146
+ ...(legacy ? [{ dir: legacyDir, trust: 'install' as const }] : []),
147
+ ],
148
+ },
149
+ builder: { dir: extensionsDir },
150
+ server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0, hostname: chosen.host },
151
+ })),
152
+ ]
153
+ return {
154
+ root,
155
+ url: listen ? root.nodeServer.url : '',
156
+ async stop() {
157
+ for (const fiber of [...fibers].reverse()) await fiber.dispose()
158
+ },
159
+ }
160
+ }