@turmind/halo 0.1.9

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 (296) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/admin-out/404.html +1 -0
  4. package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_buildManifest.js +1 -0
  5. package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_ssgManifest.js +1 -0
  6. package/admin-out/_next/static/chunks/1d2d5650-810a3ca1ae20cab5.js +18 -0
  7. package/admin-out/_next/static/chunks/200.3b8f8a74f005a134.js +1 -0
  8. package/admin-out/_next/static/chunks/220.3dc41cd6269b88e6.js +1 -0
  9. package/admin-out/_next/static/chunks/298-d6b224b569da4de0.js +1 -0
  10. package/admin-out/_next/static/chunks/304eda7d.b1ee7f523653e027.js +1 -0
  11. package/admin-out/_next/static/chunks/330.3b784cf0dcd69163.js +1 -0
  12. package/admin-out/_next/static/chunks/442.f4302087bddb1ec1.js +1 -0
  13. package/admin-out/_next/static/chunks/443.d1b289781793f8de.js +25 -0
  14. package/admin-out/_next/static/chunks/503-7c6d9935df6e0cea.js +1 -0
  15. package/admin-out/_next/static/chunks/533.5f2d4bfe3d076095.js +1 -0
  16. package/admin-out/_next/static/chunks/579.a2718ddd395a2f3a.js +1 -0
  17. package/admin-out/_next/static/chunks/635-3dd0043ed0520b9d.js +1 -0
  18. package/admin-out/_next/static/chunks/65.859665ba6717967a.js +1 -0
  19. package/admin-out/_next/static/chunks/661.a4fe93f3f2a5e871.js +1 -0
  20. package/admin-out/_next/static/chunks/688.4e014ac6cfc7e152.js +1 -0
  21. package/admin-out/_next/static/chunks/868.0c95dfe283400a00.js +1 -0
  22. package/admin-out/_next/static/chunks/app/_not-found/page-8e35399c8fd5016d.js +1 -0
  23. package/admin-out/_next/static/chunks/app/layout-58f773336e6fce61.js +1 -0
  24. package/admin-out/_next/static/chunks/app/page-cc8043335282d745.js +1 -0
  25. package/admin-out/_next/static/chunks/db534642.09d3c650ff587175.js +1 -0
  26. package/admin-out/_next/static/chunks/fb4ec4d6-3fa44f69484f91b9.js +1 -0
  27. package/admin-out/_next/static/chunks/framework-c3031ae54a63fe16.js +1 -0
  28. package/admin-out/_next/static/chunks/main-14b48e3c2588354f.js +1 -0
  29. package/admin-out/_next/static/chunks/main-app-483b436873410ddc.js +1 -0
  30. package/admin-out/_next/static/chunks/pages/_app-b2fc6e5f920d1b1a.js +1 -0
  31. package/admin-out/_next/static/chunks/pages/_error-f3696a8f3d67d150.js +1 -0
  32. package/admin-out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  33. package/admin-out/_next/static/chunks/webpack-8bd7ef88a6527feb.js +1 -0
  34. package/admin-out/_next/static/css/659eccb5db697b76.css +1 -0
  35. package/admin-out/_next/static/css/8fce4e1a54dac12a.css +3 -0
  36. package/admin-out/_next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
  37. package/admin-out/_next/static/media/21350d82a1f187e9-s.woff2 +0 -0
  38. package/admin-out/_next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
  39. package/admin-out/_next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
  40. package/admin-out/_next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
  41. package/admin-out/_next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
  42. package/admin-out/_next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
  43. package/admin-out/favicon.ico +0 -0
  44. package/admin-out/icon.svg +18 -0
  45. package/admin-out/index.html +1 -0
  46. package/admin-out/index.txt +23 -0
  47. package/admin-out/monaco/vs/_commonjsHelpers-CT9FvmAN.js +1 -0
  48. package/admin-out/monaco/vs/abap-D-t0cyap.js +1 -0
  49. package/admin-out/monaco/vs/apex-CcIm7xu6.js +1 -0
  50. package/admin-out/monaco/vs/assets/css.worker-HnVq6Ewq.js +93 -0
  51. package/admin-out/monaco/vs/assets/editor.worker-Be8ye1pW.js +26 -0
  52. package/admin-out/monaco/vs/assets/html.worker-B51mlPHg.js +470 -0
  53. package/admin-out/monaco/vs/assets/json.worker-DKiEKt88.js +58 -0
  54. package/admin-out/monaco/vs/assets/ts.worker-CMbG-7ft.js +67731 -0
  55. package/admin-out/monaco/vs/azcli-BA0tQDCg.js +1 -0
  56. package/admin-out/monaco/vs/basic-languages/monaco.contribution.js +1 -0
  57. package/admin-out/monaco/vs/bat-C397hTD6.js +1 -0
  58. package/admin-out/monaco/vs/bicep-DF5aW17k.js +2 -0
  59. package/admin-out/monaco/vs/cameligo-plsz8qhj.js +1 -0
  60. package/admin-out/monaco/vs/clojure-Y2auQMzK.js +1 -0
  61. package/admin-out/monaco/vs/coffee-Bu45yuWE.js +1 -0
  62. package/admin-out/monaco/vs/cpp-CkKPQIni.js +1 -0
  63. package/admin-out/monaco/vs/csharp-CX28MZyh.js +1 -0
  64. package/admin-out/monaco/vs/csp-D8uWnyxW.js +1 -0
  65. package/admin-out/monaco/vs/css-CaeNmE3S.js +3 -0
  66. package/admin-out/monaco/vs/cssMode-CjiAH6dQ.js +1 -0
  67. package/admin-out/monaco/vs/cypher-DVThT8BS.js +1 -0
  68. package/admin-out/monaco/vs/dart-CmGfCvrO.js +1 -0
  69. package/admin-out/monaco/vs/dockerfile-CZqqYdch.js +1 -0
  70. package/admin-out/monaco/vs/ecl-30fUercY.js +1 -0
  71. package/admin-out/monaco/vs/editor/editor.main.css +1 -0
  72. package/admin-out/monaco/vs/editor/editor.main.js +5 -0
  73. package/admin-out/monaco/vs/editor.api-CalNCsUg.js +903 -0
  74. package/admin-out/monaco/vs/elixir-xjPaIfzF.js +1 -0
  75. package/admin-out/monaco/vs/flow9-DqtmStfK.js +1 -0
  76. package/admin-out/monaco/vs/freemarker2-Cz_sV6Md.js +3 -0
  77. package/admin-out/monaco/vs/fsharp-BOMdg4U1.js +1 -0
  78. package/admin-out/monaco/vs/go-D_hbi-Jt.js +1 -0
  79. package/admin-out/monaco/vs/graphql-CKUU4kLG.js +1 -0
  80. package/admin-out/monaco/vs/handlebars-OwglfO-1.js +1 -0
  81. package/admin-out/monaco/vs/hcl-DTaboeZW.js +1 -0
  82. package/admin-out/monaco/vs/html-Pa1xEWsY.js +1 -0
  83. package/admin-out/monaco/vs/htmlMode-Bz67EXwp.js +1 -0
  84. package/admin-out/monaco/vs/ini-CsNwO04R.js +1 -0
  85. package/admin-out/monaco/vs/java-CI4ZMsH9.js +1 -0
  86. package/admin-out/monaco/vs/javascript-PczUCGdz.js +1 -0
  87. package/admin-out/monaco/vs/jsonMode-DULH5oaX.js +7 -0
  88. package/admin-out/monaco/vs/julia-BwzEvaQw.js +1 -0
  89. package/admin-out/monaco/vs/kotlin-IUYPiTV8.js +1 -0
  90. package/admin-out/monaco/vs/language/css/monaco.contribution.js +1 -0
  91. package/admin-out/monaco/vs/language/html/monaco.contribution.js +1 -0
  92. package/admin-out/monaco/vs/language/json/monaco.contribution.js +1 -0
  93. package/admin-out/monaco/vs/language/typescript/monaco.contribution.js +1 -0
  94. package/admin-out/monaco/vs/less-C0eDYdqa.js +2 -0
  95. package/admin-out/monaco/vs/lexon-iON-Kj97.js +1 -0
  96. package/admin-out/monaco/vs/liquid-DqKjdPGy.js +1 -0
  97. package/admin-out/monaco/vs/loader.js +1368 -0
  98. package/admin-out/monaco/vs/lspLanguageFeatures-kM9O9rjY.js +4 -0
  99. package/admin-out/monaco/vs/lua-DtygF91M.js +1 -0
  100. package/admin-out/monaco/vs/m3-CsR4AuFi.js +1 -0
  101. package/admin-out/monaco/vs/markdown-C_rD0bIw.js +1 -0
  102. package/admin-out/monaco/vs/mdx-DEWtB1K5.js +1 -0
  103. package/admin-out/monaco/vs/mips-CiYP61RB.js +1 -0
  104. package/admin-out/monaco/vs/monaco.contribution-D2OdxNBt.js +1 -0
  105. package/admin-out/monaco/vs/monaco.contribution-DO3azKX8.js +1 -0
  106. package/admin-out/monaco/vs/monaco.contribution-EcChJV6a.js +1 -0
  107. package/admin-out/monaco/vs/monaco.contribution-qLAYrEOP.js +1 -0
  108. package/admin-out/monaco/vs/msdax-C38-sJlp.js +1 -0
  109. package/admin-out/monaco/vs/mysql-CdtbpvbG.js +1 -0
  110. package/admin-out/monaco/vs/nls.messages-loader.js +1 -0
  111. package/admin-out/monaco/vs/nls.messages.cs.js.js +17 -0
  112. package/admin-out/monaco/vs/nls.messages.de.js.js +17 -0
  113. package/admin-out/monaco/vs/nls.messages.es.js.js +17 -0
  114. package/admin-out/monaco/vs/nls.messages.fr.js.js +15 -0
  115. package/admin-out/monaco/vs/nls.messages.it.js.js +15 -0
  116. package/admin-out/monaco/vs/nls.messages.ja.js.js +17 -0
  117. package/admin-out/monaco/vs/nls.messages.js.js +10 -0
  118. package/admin-out/monaco/vs/nls.messages.ko.js.js +25 -0
  119. package/admin-out/monaco/vs/nls.messages.pl.js.js +17 -0
  120. package/admin-out/monaco/vs/nls.messages.pt-br.js.js +6 -0
  121. package/admin-out/monaco/vs/nls.messages.ru.js.js +17 -0
  122. package/admin-out/monaco/vs/nls.messages.tr.js.js +15 -0
  123. package/admin-out/monaco/vs/nls.messages.zh-cn.js.js +17 -0
  124. package/admin-out/monaco/vs/nls.messages.zh-tw.js.js +15 -0
  125. package/admin-out/monaco/vs/objective-c-CntZFaHX.js +1 -0
  126. package/admin-out/monaco/vs/pascal-r6kuqfl_.js +1 -0
  127. package/admin-out/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
  128. package/admin-out/monaco/vs/perl-DABw_TcH.js +1 -0
  129. package/admin-out/monaco/vs/pgsql-me_jFXeX.js +1 -0
  130. package/admin-out/monaco/vs/php-D_kh-9LK.js +1 -0
  131. package/admin-out/monaco/vs/pla-VfZjczW0.js +1 -0
  132. package/admin-out/monaco/vs/postiats-BBSzz8Pk.js +1 -0
  133. package/admin-out/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
  134. package/admin-out/monaco/vs/powershell-B-7ap1zc.js +1 -0
  135. package/admin-out/monaco/vs/protobuf-BmtuEB1A.js +2 -0
  136. package/admin-out/monaco/vs/pug-BRpRNeEb.js +1 -0
  137. package/admin-out/monaco/vs/python-Cr0UkIbn.js +1 -0
  138. package/admin-out/monaco/vs/qsharp-BzsFaUU9.js +1 -0
  139. package/admin-out/monaco/vs/r-f8dDdrp4.js +1 -0
  140. package/admin-out/monaco/vs/razor-BYAHOTkz.js +1 -0
  141. package/admin-out/monaco/vs/redis-fvZQY4PI.js +1 -0
  142. package/admin-out/monaco/vs/redshift-45Et0LQi.js +1 -0
  143. package/admin-out/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
  144. package/admin-out/monaco/vs/ruby-CZO8zYTz.js +1 -0
  145. package/admin-out/monaco/vs/rust-Bfetafyc.js +1 -0
  146. package/admin-out/monaco/vs/sb-3GYllVck.js +1 -0
  147. package/admin-out/monaco/vs/scala-foMgrKo1.js +1 -0
  148. package/admin-out/monaco/vs/scheme-CHdMtr7p.js +1 -0
  149. package/admin-out/monaco/vs/scss-C1cmLt9V.js +3 -0
  150. package/admin-out/monaco/vs/shell-ClXCKCEW.js +1 -0
  151. package/admin-out/monaco/vs/solidity-MZ6ExpPy.js +1 -0
  152. package/admin-out/monaco/vs/sophia-DWkuSsPQ.js +1 -0
  153. package/admin-out/monaco/vs/sparql-AUGFYSyk.js +1 -0
  154. package/admin-out/monaco/vs/sql-32GpJSV2.js +1 -0
  155. package/admin-out/monaco/vs/st-CuDFIVZ_.js +1 -0
  156. package/admin-out/monaco/vs/swift-n-2HociN.js +3 -0
  157. package/admin-out/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
  158. package/admin-out/monaco/vs/tcl-D74tq1nH.js +1 -0
  159. package/admin-out/monaco/vs/tsMode-CZz1Umrk.js +11 -0
  160. package/admin-out/monaco/vs/twig-C6taOxMV.js +1 -0
  161. package/admin-out/monaco/vs/typescript-DfOrAzoV.js +1 -0
  162. package/admin-out/monaco/vs/typespec-D-PIh9Xw.js +1 -0
  163. package/admin-out/monaco/vs/vb-Dyb2648j.js +1 -0
  164. package/admin-out/monaco/vs/wgsl-BhLXMOR0.js +298 -0
  165. package/admin-out/monaco/vs/workers-DcJshg-q.js +1 -0
  166. package/admin-out/monaco/vs/xml-CdsdnY8S.js +1 -0
  167. package/admin-out/monaco/vs/yaml-DYGvmE88.js +1 -0
  168. package/bin/halo.js +9 -0
  169. package/bundled-docs/design/agent.md +145 -0
  170. package/bundled-docs/design/architecture.md +270 -0
  171. package/bundled-docs/design/background-dispatch.md +151 -0
  172. package/bundled-docs/design/command.md +58 -0
  173. package/bundled-docs/design/cron.md +88 -0
  174. package/bundled-docs/design/evolution.md +383 -0
  175. package/bundled-docs/design/express-self.md +145 -0
  176. package/bundled-docs/design/feishu.md +224 -0
  177. package/bundled-docs/design/halo-city.md +112 -0
  178. package/bundled-docs/design/prompt-system.md +411 -0
  179. package/bundled-docs/design/session.md +321 -0
  180. package/bundled-docs/design/slack.md +212 -0
  181. package/bundled-docs/design/storage.md +578 -0
  182. package/bundled-docs/design/telegram.md +161 -0
  183. package/bundled-docs/design/web.md +222 -0
  184. package/bundled-docs/design/wechat.md +146 -0
  185. package/bundled-docs/design/ws.md +144 -0
  186. package/bundled-docs/dev/acp-adapter.md +337 -0
  187. package/bundled-docs/dev/add-channel.md +376 -0
  188. package/bundled-docs/dev/add-model-provider.md +386 -0
  189. package/bundled-docs/dev/api.md +686 -0
  190. package/bundled-docs/dev/deploy.md +198 -0
  191. package/bundled-docs/dev/desktop-packaging.md +327 -0
  192. package/bundled-docs/dev/env.md +158 -0
  193. package/bundled-docs/dev/logging.md +85 -0
  194. package/bundled-docs/dev/previews.md +225 -0
  195. package/bundled-docs/dev/tools.md +331 -0
  196. package/bundled-docs/guide/README.md +29 -0
  197. package/bundled-docs/guide/agents.md +264 -0
  198. package/bundled-docs/guide/channels/README.md +62 -0
  199. package/bundled-docs/guide/channels/acp.md +150 -0
  200. package/bundled-docs/guide/channels/feishu.md +154 -0
  201. package/bundled-docs/guide/channels/slack.md +156 -0
  202. package/bundled-docs/guide/channels/telegram.md +112 -0
  203. package/bundled-docs/guide/channels/web.md +149 -0
  204. package/bundled-docs/guide/channels/wechat.md +110 -0
  205. package/bundled-docs/guide/chat.md +112 -0
  206. package/bundled-docs/guide/cli.md +197 -0
  207. package/bundled-docs/guide/getting-started.md +93 -0
  208. package/bundled-docs/guide/secrets-and-credentials.md +161 -0
  209. package/bundled-docs/guide/sessions.md +104 -0
  210. package/bundled-docs/guide/skills.md +302 -0
  211. package/bundled-docs/guide/testing-agents-and-skills.md +136 -0
  212. package/bundled-docs/guide/workspace.md +131 -0
  213. package/bundled-docs/plans/cron-tasks.md +362 -0
  214. package/bundled-docs/plans/design-suggestions.md +203 -0
  215. package/bundled-docs/plans/issue-session-deep-nesting.md +208 -0
  216. package/bundled-docs/plans/launch-plan.md +111 -0
  217. package/bundled-docs/plans/md-and-agent-refactor.md +197 -0
  218. package/bundled-docs/plans/remote-mac-worker.md +196 -0
  219. package/bundled-docs/plans/roadmap.md +40 -0
  220. package/bundled-docs/plans/self-evolution.md +1379 -0
  221. package/bundled-docs/plans/todo.md +32 -0
  222. package/bundled-docs/plans/weixin-channel.md +255 -0
  223. package/bundled-docs/requirements/agent.md +92 -0
  224. package/bundled-docs/requirements/chat.md +81 -0
  225. package/bundled-docs/requirements/command.md +130 -0
  226. package/bundled-docs/requirements/editor.md +82 -0
  227. package/bundled-docs/requirements/explorer.md +89 -0
  228. package/bundled-docs/requirements/overview.md +62 -0
  229. package/bundled-docs/requirements/session.md +73 -0
  230. package/bundled-docs/requirements/settings.md +224 -0
  231. package/bundled-docs/requirements/skill.md +125 -0
  232. package/bundled-docs/requirements/source-control.md +82 -0
  233. package/bundled-docs/requirements/terminal.md +118 -0
  234. package/bundled-docs/requirements/workspace.md +80 -0
  235. package/bundled-docs/test/docs-seeding.md +101 -0
  236. package/bundled-docs/test/session.md +374 -0
  237. package/dist/index.js +26750 -0
  238. package/package.json +67 -0
  239. package/templates/INSTRUCTIONS.md +25 -0
  240. package/templates/agents/__apply_agent__/AGENT.md +229 -0
  241. package/templates/agents/__apply_agent__/agent.yaml +30 -0
  242. package/templates/agents/__evo_agent__/AGENT.md +323 -0
  243. package/templates/agents/__evo_agent__/agent.yaml +29 -0
  244. package/templates/agents/__score__/AGENT.md +177 -0
  245. package/templates/agents/__score__/agent.yaml +29 -0
  246. package/templates/agents/deep-executor/AGENT.md +60 -0
  247. package/templates/agents/deep-executor/agent.yaml +25 -0
  248. package/templates/agents/default/AGENT.md +38 -0
  249. package/templates/agents/default/agent.yaml +51 -0
  250. package/templates/agents/executor/AGENT.md +38 -0
  251. package/templates/agents/executor/agent.yaml +25 -0
  252. package/templates/aliases.yaml +26 -0
  253. package/templates/canvas/self.html +660 -0
  254. package/templates/config.yaml +69 -0
  255. package/templates/models/anthropic.yaml +47 -0
  256. package/templates/models/aws-bedrock-claude-invoke.yaml +156 -0
  257. package/templates/models/aws-bedrock-mantle.yaml +89 -0
  258. package/templates/models/deepseek.yaml +62 -0
  259. package/templates/models/doubao.yaml +137 -0
  260. package/templates/models/hunyuan.yaml +56 -0
  261. package/templates/models/kimi.yaml +41 -0
  262. package/templates/models/mimo-token-plan-china.yaml +61 -0
  263. package/templates/models/minimax.yaml +80 -0
  264. package/templates/models/openai.yaml +51 -0
  265. package/templates/models/qwen.yaml +78 -0
  266. package/templates/optional-skills/tavily-web-search/SKILL.md +91 -0
  267. package/templates/optional-skills/tavily-web-search/config.yaml +10 -0
  268. package/templates/prompts/all/TOOL_GUIDELINES.md +35 -0
  269. package/templates/prompts/all/TOOL_SHELL.md +4 -0
  270. package/templates/prompts/all/TOOL_SHELL.windows.md +7 -0
  271. package/templates/prompts/bootstrap/BOOTSTRAP.md +26 -0
  272. package/templates/prompts/root/WORKSPACE_MEMORY.md +11 -0
  273. package/templates/schema.sql +30 -0
  274. package/templates/skills/acp/SKILL.md +236 -0
  275. package/templates/skills/acp/templates/SKILL.md.tmpl +74 -0
  276. package/templates/skills/acp/templates/ask.py +176 -0
  277. package/templates/skills/acp/templates/config.yaml.tmpl +33 -0
  278. package/templates/skills/agent/SKILL.md +128 -0
  279. package/templates/skills/aws-knowledge/SKILL.md +82 -0
  280. package/templates/skills/aws-knowledge/scripts/aws_kb.py +61 -0
  281. package/templates/skills/cron/SKILL.md +217 -0
  282. package/templates/skills/cron/manage-cron.py +434 -0
  283. package/templates/skills/halo/SKILL.md +98 -0
  284. package/templates/skills/nova-web-search/SKILL.md +65 -0
  285. package/templates/skills/self/SKILL.md +139 -0
  286. package/templates/skills/send-file/SKILL.md +28 -0
  287. package/templates/skills/send-file/feishu.md +16 -0
  288. package/templates/skills/send-file/slack.md +7 -0
  289. package/templates/skills/send-file/telegram.md +9 -0
  290. package/templates/skills/send-file/web.md +6 -0
  291. package/templates/skills/send-file/wechat.md +8 -0
  292. package/templates/skills/skill/SKILL.md +131 -0
  293. package/templates/skills/workspace/SKILL.md +28 -0
  294. package/templates/skills/workspace/setup-tidy.md +265 -0
  295. package/templates/skills/workspace/share.md +187 -0
  296. package/templates/skills/workspace/stage.py +487 -0
@@ -0,0 +1,383 @@
1
+ # Self-Evolution — Design
2
+
3
+ Let agents draft and validate improvements to their own prompt surface by learning from conversations.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ /evo or pre-compact hook (user action)
9
+ ↓
10
+ enqueueEvoRun() (sync)
11
+ mkdir runs/<id>/, snapshot source session + prompt surface
12
+ ↓
13
+ evolution_runs: id → 'pending'
14
+ ↓
15
+ server ticker (every 30s, stateless)
16
+ mark timeouts, spawn wrappers
17
+ ↓
18
+ wrapper (detached Node process, per run/apply)
19
+ ┌────────────────────────────────────────────────┐
20
+ │ Phase A: __evo_agent__ drafts patch.md │
21
+ │ Phase B: wrapper dry-runs patched sandbox │
22
+ │ Phase C: __score__ rates lint/behavior/scope │
23
+ │ │
24
+ │ (Phase A'/B'/12 for apply: merge+regress+sync) │
25
+ └────────────────────────────────────────────────┘
26
+ ↓
27
+ evolution_runs: id → 'awaiting_review'
28
+ ↓ (admin approves)
29
+ evolution_applies: id → 'pending'
30
+ ↓ (ticker spawns apply wrapper)
31
+ wrapper merges patches, regress-tests
32
+ ↓ (phase 12 sync to main)
33
+ evolution_applies: id → 'applied'
34
+ evolution_runs: id → 'applied'
35
+ ↓
36
+ SessionManager cold-start picks up new prompts
37
+ next user message uses patched rules
38
+ ```
39
+
40
+ All coordination via global db (`~/.halo/global/evo.db`). Wrapper heartbeats every 60s; ticker marks `running` → `timeout` if stale. Detached process model: server restart doesn't kill in-flight wrappers.
41
+
42
+ ## Trigger and Enqueue Flow
43
+
44
+ ### `/evo [hint]` (slash command)
45
+
46
+ User in chat:
47
+ ```
48
+ /evo # evo runs on current root session
49
+ /evo answer is too verbose # hint for what to focus on
50
+ ```
51
+
52
+ Server flow (synchronous, ~10ms):
53
+
54
+ 1. Refuse if user is not `full` access (not gated on `evolution.level`).
55
+ 2. Resolve root session id (must be `parentId === null`).
56
+ 3. Call `enqueueEvoRun()` from `/packages/server/src/evolution/enqueue.ts`:
57
+ - mkdir `<ws>/.halo/evo/runs/<id>/`
58
+ - Write `source-snapshot.json` (frozen `session.agent.messages`)
59
+ - Write `tool-flow.md` (tool_result-stripped Markdown view for skimming)
60
+ - Write `meta.json` (metadata: runId, triggerKind, sourceSession, userHint, createdAt)
61
+ - Write `evo-context.json` (snapshot of prompt surface at trigger time: assembled system prompt, all prompt files, agent/skill listings)
62
+ 4. INSERT `evolution_runs` row with `status='pending'`, `trigger_kind='note'` (internal trigger id keeps the old name; only the user-visible command was renamed to `/evo`).
63
+ 5. Reply via `chat:system`: "📝 Queued for evaluation".
64
+
65
+ `evo-context.json` is authoritative state — the wrapper packs it into agent briefs verbatim, so evo/scorer never need `file_read` to inspect system-prompt files.
66
+
67
+ ### Pre-compact hook
68
+
69
+ Inside `SessionManager.compactSession()`, **before** the compression LLM rewrites `session.agent.messages`:
70
+
71
+ 1. Same checks (level=L1, root session, non-readonly user).
72
+ 2. Snapshot the current messages → `enqueueEvoRun()` with `trigger='pre-compact'`.
73
+ 3. INSERT `evolution_runs` row.
74
+ 4. Continue with normal compact.
75
+
76
+ Order matters: snapshot first, compact second. Otherwise evo sees the already-compacted (summarized) message log, losing detail it should learn from.
77
+
78
+ ## Ticker — Stateless Scheduler (every 30s)
79
+
80
+ Located: `/packages/server/src/evolution/ticker.ts`
81
+
82
+ Three jobs per pass:
83
+
84
+ 1. **Mark timeouts**: `running` rows with heartbeat older than `runTimeoutMinutes` → `timeout`. Retry rows with `attempts < maxAttempts` back to `pending`; exhausted rows → terminal `timeout`.
85
+ 2. **Start runs**: count current `running`, claim `pending` runs (oldest first, up to slot budget), spawn `evo-wrapper.js --mode=run --id=<X>`.
86
+ 3. **Start applies**: same shape, but with per-workspace mutex (only one apply per workspace at a time, since apply reads **current** main to build sandbox).
87
+
88
+ Restart-safe: no in-memory state. On server reboot, ticker re-evaluates db on first tick — stale `running` rows timeout, pending rows continue to be picked up. Applies can resume mid-publish if they were in `syncing` state (heartbeat timeout was mid-phase-12).
89
+
90
+ ## Wrapper Orchestration — Mode=Run (3-phase model)
91
+
92
+ Located: `/packages/server/src/evolution/evo-wrapper.ts` (compiled to `.js`, spawned by ticker)
93
+
94
+ The wrapper is the orchestrator; individual agents do focused work only.
95
+
96
+ ### Phase A — Draft
97
+
98
+ `spawn('halo', ['cli', '-a', '__evo_agent__', '-n', '-w', '<runDir>/sandbox', '<draft-brief>'])`
99
+
100
+ Agent writes:
101
+ - `patch.md` — Markdown with YAML frontmatter + body. Frontmatter includes `testScenario: { agentId, testMessage, originalMessage }` and `target: .halo/<path>`.
102
+ - One new file at `<runDir>/sandbox/.halo/<target>` with full patched contents.
103
+ - OR `.skip.md` (one-sentence reason) if no patch is worth proposing.
104
+
105
+ The `target` can be any file in the prompt surface — `INSTRUCTIONS.md`, the agent's `AGENT.md` / `agent.yaml`, a `skills/<id>/SKILL.md`, `prompts/<scope>/`, `INDEX.md` — routed by which file *owns* the failure (skill misuse → that SKILL.md, capability/model gap → agent.yaml, persona/scope → AGENT.md, cross-cutting rule → INSTRUCTIONS.md). AGENT.md carries the routing table; this de-biases the drafter from defaulting to INSTRUCTIONS.md just because its full text sits in the brief.
106
+
107
+ Sandbox is whitelist-cp from main workspace (only: `INSTRUCTIONS.md`, `INDEX.md`, `USER.md`, `agents/`, `prompts/`, `skills/`, `docs/`). Agent reads via `file_read`, writes only to `.halo/` subset.
108
+
109
+ ### Phase B — Dry-run + Fix Loop
110
+
111
+ ```
112
+ timeout 600 halo cli -a <patch.testScenario.agentId> -n -w <runDir>/sandbox \
113
+ --access workspace <patch.testScenario.testMessage>
114
+ ```
115
+
116
+ `--access workspace` masks `~/.aws`, `~/.ssh`, etc. (safety: test probe's behavior, not side effects).
117
+
118
+ Outcome:
119
+ - **Success** (exit 0 + non-empty stdout): save to `dry-run-output.txt`, proceed to phase C.
120
+ - **Failure**: save log, re-spawn `__evo_agent__` in fix mode with failure log inline. One corrective pass only (`FIX_BUDGET = 1`). If fix also fails, mark `failed` with reason "dry-run never succeeded".
121
+
122
+ Fix budget design:
123
+ - Two-pass (original + one fix) covers common failure modes (bad YAML, scope-too-aggressive test scenario).
124
+ - More loops look like old "edit prompt and retry" anti-pattern. If a patch needs >1 fix, reject it.
125
+
126
+ ### Phase C — Score
127
+
128
+ `spawn('halo', ['cli', '-a', '__score__', '-n', '-w', '<runDir>/sandbox', '<score-brief>'])`
129
+
130
+ Reads patch.md + dry-run-output.txt + source-snapshot.json, writes `score.json`:
131
+
132
+ ```json
133
+ {
134
+ "lint": 90,
135
+ "behavior": 75,
136
+ "scope": 80,
137
+ "confidence": "high",
138
+ "avg": 82,
139
+ "notes": "…"
140
+ }
141
+ ```
142
+
143
+ Dimensions (0-100, anchored at 50="neutral"):
144
+ - **lint**: patched files load cleanly (YAML valid, cross-refs resolve).
145
+ - **behavior**: dry-run output better than original assistant reply (baseline).
146
+ - **scope**: surgical (100) vs sweeping (0).
147
+ - **confidence**: scorer's own confidence (independent of numeric scores).
148
+
149
+ `avg = round((lint + behavior + scope) / 3)` — single sort key for admin UI.
150
+
151
+ Scorer is read+write only (`file_read` / `file_write` / `file_list` / `grep` / `glob`); no execution / no shell. Outputs only `score.json`. Same scorer agent reused at apply time (phase B') as regression gate.
152
+
153
+ ## Wrapper Orchestration — Mode=Apply (merge + regress + sync)
154
+
155
+ After user approves N runs in the admin UI, `evolution_applies` row created with `status='pending'`.
156
+
157
+ ### Phase A' — Merge
158
+
159
+ Wrapper builds **fresh sandbox** (whitelist-cp from **current main** workspace), then:
160
+
161
+ `spawn('halo', ['cli', '-a', '__apply_agent__', '-n', '-w', '<applyDir>/sandbox', '<merge-brief>'])`
162
+
163
+ Agent reads each source run's `patch.md` (latest version!), merges changes into sandbox. Respects platform override matrix (workspace replaces global wholesale for agents/, skills/, prompts/). Agent writes `apply.log` (audit trail), edits only under `sandbox/.halo/`.
164
+
165
+ ### Phase B' — Regress
166
+
167
+ For each source_run_id:
168
+
169
+ 1. Read `testScenario` from source run's `patch.md`.
170
+ 2. Run `timeout 600 halo cli -a <agentId> -n -w <applyDir>/sandbox <testMessage>` against merged sandbox.
171
+ 3. Spawn `__score__` with regress-mode brief, write `regress/<runId>/score.json`.
172
+ 4. Any score with `lint < 50` or `behavior < 50` → regression, abort with `failed`.
173
+
174
+ ### Phase 12 — Final Sync (checkpointed)
175
+
176
+ Three substeps, two of them checkpointed:
177
+
178
+ 1. **Preflight** (idempotent): walk sandbox, diff against main, snapshot pre-apply files to `history/apply-<id>/` with `MANIFEST.json`.
179
+ 2. **Checkpoint**: set `evolution_applies.status='syncing'`. From here on, if wrapper crashes, ticker recovery resumes at step 3 (skip A'/B'/preflight to avoid re-running LLM phases).
180
+ 3. **Publish** (dangerous): cp each changed file from sandbox to main.
181
+ 4. Mark source_runs `applied`, finalize apply row `applied`.
182
+
183
+ Per-workspace mutex in ticker prevents two applies from racing on the same workspace — the assumption phase 12 leans on.
184
+
185
+ ## Data on Disk
186
+
187
+ ```
188
+ <ws>/.halo/evo/
189
+ runs/<id>/
190
+ meta.json # run metadata
191
+ source-snapshot.json # frozen session.agent.messages
192
+ tool-flow.md # tool_result-stripped Markdown skim
193
+ evo-context.json # prompt surface snapshot (from enqueue)
194
+ images/<msgIdx>-<blockIdx>.<ext> # decoded base64 images
195
+ patch.md # __evo_agent__ writes (has testScenario frontmatter)
196
+ .skip.md # evo wrote this → no patch proposed
197
+ score.json # __score__ writes (lint/behavior/scope/avg)
198
+ dry-run-output.txt # stdout from phase B dry-run
199
+ dry-run-fail-<n>.log # failure logs from dry-run attempts
200
+ sub-cli.log # tee'd stdout+stderr of every halo cli spawn
201
+ sandbox/ # working copy; .halo/ is whitelist-cp of main
202
+ applies/<id>/
203
+ meta.json
204
+ sandbox/ # cp from main at phase A', edited by agent
205
+ apply.log # agent audit trail
206
+ regress/<runId>/ # one per source run
207
+ dry-run-output.txt
208
+ score.json
209
+ history/apply-<id>/ # rollback snapshot (NOT archived)
210
+ MANIFEST.json # which paths were overwritten
211
+ <files> # pre-apply content
212
+ archive/
213
+ run-<id>.zip # zipped after 14 days in terminal state
214
+ apply-<id>.zip
215
+ ```
216
+
217
+ `evo-context.json` format:
218
+
219
+ ```json
220
+ {
221
+ "agentId": "default",
222
+ "assembledSystemPrompt": "<full text or null>",
223
+ "promptFiles": [
224
+ { "scope": "workspace", "path": "INSTRUCTIONS.md", "content": "..." },
225
+ { "scope": "global", "path": "agents/default/AGENT.md", "content": "..." },
226
+ ...
227
+ ],
228
+ "agents": [{ "id": "default", "scope": "global" }, ...],
229
+ "skills": [{ "id": "tool-read", "scope": "global" }, ...]
230
+ }
231
+ ```
232
+
233
+ Skill content NOT inlined (too large at scale); only listings. Both agents and skills include a `scope` tag (`workspace` | `global` | `builtin`).
234
+
235
+ ## Archive and Retention
236
+
237
+ Located: `/packages/server/src/evolution/archive.ts`
238
+
239
+ Two-stage lifecycle (both orthogonal to `status`):
240
+
241
+ - **14 days after terminal status** (applied/rejected/skipped/failed/timeout): zip the run/apply dir to `archive/{run|apply}-<id>.zip`, delete original, set `archived_at = now()`. DB row stays.
242
+ - **30 days after `archived_at`**: delete zip and DB row outright.
243
+
244
+ Active rows (pending/running/awaiting_review/approved/syncing) never archived. `history/apply-<id>/` (rollback tree) never archived — kept cheap and discoverable.
245
+
246
+ Archive daemon runs at server boot (1 min delay) + daily. Idempotent.
247
+
248
+ ## Integration with SessionManager
249
+
250
+ Each evo/score/apply agent is flagged `internal: true` in `agent.yaml`. SessionManager skips loading workspace platform prompts (USER.md, INSTRUCTIONS.md, INDEX.md, prompts/all|root|bootstrap) for internal agents — only their own AGENT.md + wrapper brief reaches the LLM. Reduces token cost and removes workspace-rule bleed-through.
251
+
252
+ After apply publishes to main, **no explicit session-release step needed**. SessionManager evicts every session from the in-memory cache as soon as its message turn finishes (`runSession`'s finally block: `saveAgentState` + `Map.delete`). Next message → `ensureSession` → `buildAgentInstance`, which re-reads `agent.yaml`, `INSTRUCTIONS.md`, `USER.md` from disk. So new prompts kick in on next turn for free; currently-running turns finish on old prompts (deliberate — aborting mid-conversation is worse than a few seconds of old rules).
253
+
254
+ ## Key Files and Their Roles
255
+
256
+ | File | Lines | Purpose |
257
+ |---|---|---|
258
+ | `/packages/server/src/evolution/enqueue.ts` | 403 | Snapshot session + prompt surface at trigger time. Write runDir/, INSERT evolution_runs. Both `/evo` and pre-compact hook call this. |
259
+ | `/packages/server/src/evolution/ticker.ts` | 496 | Stateless 30s scheduler. Mark timeouts, claim pending, spawn wrappers. Per-workspace apply mutex. Broadcast status changes to admin UI. |
260
+ | `/packages/server/src/evolution/spawn.ts` | 36 | Real spawner: detached Node child running evo-wrapper.js. Override-able for testing. |
261
+ | `/packages/server/src/evolution/evo-wrapper.ts` | 2000+ | Wrapper orchestrator. 3-phase run mode (draft/dry-run/score). Apply mode (merge/regress/publish). Heartbeat every 60s. Handles Windows command-line limits via stdin briefs. |
262
+ | `/packages/server/src/evolution/archive.ts` | 275 | Archive job: 14d → zip + delete, 30d → purge. Runs daily + at boot. Idempotent. |
263
+
264
+ ## Sandbox Model
265
+
266
+ Both run-mode and apply-mode spawn `halo cli -w <sandbox>`, never the real workspace. Sandbox is whitelist-cp of main (only prompt files + structure):
267
+
268
+ ```
269
+ SANDBOX_WHITELIST = ['INSTRUCTIONS.md', 'INDEX.md', 'USER.md', 'agents',
270
+ 'prompts', 'skills', 'docs']
271
+ PUBLISH_WHITELIST = above minus 'docs' # docs only for reference, not in prompts
272
+ ```
273
+
274
+ Session rows, db, logs, memory — all intentionally excluded from sandbox. Avoids pollution and lets archive job sweep up all evo artifacts when retention fires.
275
+
276
+ ## Settings Schema
277
+
278
+ ```yaml
279
+ general:
280
+ language: en-US # BCP-47; also used for evo/score output language
281
+ evolution:
282
+ level: L0 # L0=disabled, L1=enabled
283
+ max_concurrent_run: 1
284
+ max_concurrent_apply: 1
285
+ run_timeout_minutes: 12
286
+ apply_timeout_minutes: 5
287
+ max_attempts: 3 # retry budget per run/apply
288
+ triggers:
289
+ pre_compact: true
290
+ ```
291
+
292
+ L0 = manual drafting only (`/evo`); L1 also enables pre-compact triggering. Defaults conservative; tune if machine handles more.
293
+
294
+ ## Admin UI Integration
295
+
296
+ Top-level "Evolution" tab shows:
297
+
298
+ - **List**: evolution_runs + latest apply per run. Sortable by created_at / status / score.avg. Filterable by status. Joins runs+applies to show consolidated view.
299
+ - **Detail**: patch.md, score.json, test scenario, assembled brief context, diff against current target.
300
+ - **Approve**: Opens dialog, optional reviewer_hint. Creates evolution_applies row.
301
+ - **Reject**: Sets status='rejected'.
302
+ - **Retry**: Resets row to pending, requires new hint. Allowed from any terminal status except `applied`.
303
+ - **Manual Delete**: Removes artifacts (live dir + archive zip, whichever exists) + DB row immediately. Blocked on active rows (pending/running/approved/syncing).
304
+
305
+ Realtime updates via WebSocket (`evolution:run_changed`, `evolution:apply_changed` events). REST mutations broadcast directly; wrapper state changes broadcast at each ticker tick (15-30s latency).
306
+
307
+ ## Crash Recovery and Restarts
308
+
309
+ **Server restart**: Ticker re-evaluates db on first pass. Old `running` rows timeout naturally; `pending` rows continue to be picked up. Applies in `syncing` state are treated as resume candidates — ticker clears heartbeat and spawns wrapper which sees `status='syncing'` on entry and skips LLM phases.
310
+
311
+ **Wrapper crash**: Heartbeat expires after `runTimeoutMinutes` (default 12min). If `attempts < maxAttempts`, ticker moves row back to `pending` for retry. For applies in `syncing`, row stays `syncing` and ticker spawns recovery wrapper that runs phase 12 publish only.
312
+
313
+ **Apply mid-publish crash**: Phase 12 preflight is idempotent; step 1 (diff + snapshot) is repeated harmlessly. Publish (`phaseApplyPublish`) is a simple file-by-file cp. If wrapper dies part-way through, main is half-applied, but `history/apply-<id>/` has the rollback. On recovery, wrapper re-runs publish (idempotent cp) to complete.
314
+
315
+ ## Patches and Test Scenarios
316
+
317
+ `patch.md` is written by `__evo_agent__` in phase A. Natural-language description, **not** a diff. Frontmatter is structured (YAML):
318
+
319
+ ```markdown
320
+ ---
321
+ target: .halo/INSTRUCTIONS.md
322
+ testScenario:
323
+ agentId: default
324
+ originalMessage: "user's exact message from snapshot (baseline lookup)"
325
+ testMessage: "clean self-contained probe (what dry-run uses)"
326
+ ---
327
+
328
+ ## What to change
329
+ …
330
+ ## Why
331
+ …
332
+ ```
333
+
334
+ **Two messages, two purposes**:
335
+ - `originalMessage`: scorer looks this up in source-snapshot.json to find the baseline assistant turn.
336
+ - `testMessage`: wrapper feeds this (clean probe) to dry-run sub-cli (fresh session, no prior context).
337
+
338
+ If they're identical (both clean), that's legal. Old patches wrote single `message` field; wrapper falls back to using it for both meanings.
339
+
340
+ ## Scope Limits
341
+
342
+ - evo observes **root sessions only** (`parentId === null`). Sub-agent sessions don't trigger evo.
343
+ - evo is **invisible to conversation**: no user/assistant messages appended to source session log. One short `chat:system` line per `/evo`.
344
+ - Wrappers never modify `~/.halo/global/` directly. Apply uses copy-on-write: read global, write workspace override via file_write in sandbox, then sync to main.
345
+ - `/evo` is rejected for non-`full` users.
346
+
347
+ ## Why This Design
348
+
349
+ - **Detached wrappers + stateless ticker**: Restart-safe, easy recovery, no in-process state to manage on crash.
350
+ - **Whitelist sandbox**: Prevents pollution, lets archive job sweep artifacts cleanly, no risk of accidentally modifying main during LLM phases.
351
+ - **3-phase run model**: Separates concerns (draft vs. dry-run vs. score), prevents "agent loops forever retrying fixes", bounds feedback loops.
352
+ - **Per-workspace apply mutex**: Prevents two applies from racing on sandbox build + main publish.
353
+ - **Checkpoint at syncing**: Lets apply resume mid-publish without re-running expensive LLM phases.
354
+ - **No session-release on apply**: SessionManager's per-turn eviction means new prompts "just work" on next turn for free.
355
+ - **`evo-context.json` packing**: Freezes prompt surface at trigger time so agents don't fetch files, and so evo/score/apply see identical context across restarts (no time-window races).
356
+
357
+ ## Status Flow (Run Mode)
358
+
359
+ ```
360
+ pending → running → [draft → dry-run → score] → awaiting_review
361
+ ↓ (fix)
362
+ awaiting_review
363
+ ↓ (approve)
364
+ approved → running (apply mode)
365
+ [regress →
366
+ publish]
367
+ ↓
368
+ applied
369
+
370
+ OR: awaiting_review → rejected
371
+ OR: running → timeout / failed / skipped
372
+ ```
373
+
374
+ ## Status Flow (Apply Mode)
375
+
376
+ ```
377
+ pending → running → [merge → regress → publish] → applied
378
+
379
+ OR: running → failed / timeout
380
+ OR: running/syncing → syncing (resume path after crash)
381
+ ↓ (ticker recovery)
382
+ running → [publish] → applied
383
+ ```
@@ -0,0 +1,145 @@
1
+ # Express Self — Agent Visual Face Design
2
+
3
+ The agent has a living visual space at `.halo/canvas/self.html` — a particle field where it can form words, pulse, and react in real time. This is a second channel beyond text, parallel and immediate.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ Agent reply stream with marker
9
+ ↓
10
+ <<<SHOW: self.say(...) >>>
11
+ ↓
12
+ chat-handlers.ts (detect + extract)
13
+ ↓
14
+ face-bridge.ts (postMessage to iframes)
15
+ ↓
16
+ mounted HtmlPreview iframe
17
+ (self.html listening)
18
+ ↓
19
+ particle engine eval against `self` API
20
+ ↓
21
+ canvas animation (visible to user)
22
+ ```
23
+
24
+ The marker is **stripped from rendered chat** so the user sees only the face moving, not the code driving it.
25
+
26
+ ## Data flow
27
+
28
+ **Seeding:** `init.ts` calls `ensureWorkspaceHalo()` on workspace open, force-copying the canonical engine from `packages/server/templates/canvas/self.html` to `<workspace>/.halo/canvas/self.html`.
29
+
30
+ **Runtime expression:** Agent emits `<<<SHOW: payload >>>` in a reply. On turn completion:
31
+ 1. `chat-handlers.ts:maybeHandleShow()` detects all `<<<SHOW:[\s\S]*?>>>` markers (non-greedy, global)
32
+ 2. For each match, extracts the payload (trimmed)
33
+ 3. Calls `postToFace(payload)` to forward it
34
+ 4. Deduplicates by `${msgId}#${occurrenceIndex}` so queue drains don't fire twice
35
+
36
+ **Forwarding:** `face-bridge.ts:postToFace()` posts to every registered iframe:
37
+ ```javascript
38
+ el.contentWindow?.postMessage({ haloFace: payload }, '*')
39
+ ```
40
+
41
+ **Reception:** `self.html` listens on `window.message`:
42
+ ```javascript
43
+ window.addEventListener('message', (e) => {
44
+ const code = e?.data?.haloFace
45
+ if (typeof code !== 'string') return
46
+ try { (new Function('self', code))(self) }
47
+ catch (err) { /* malformed line no-ops */ }
48
+ })
49
+ ```
50
+
51
+ **UI stripping:** `message-list.tsx:TextBlock()` strips both markers before render:
52
+ ```javascript
53
+ parsed.replace(/<<<CAPTURE>>>/g, '')
54
+ .replace(/<<<SHOW:[\s\S]*?>>>/g, '')
55
+ ```
56
+
57
+ ## The `self` API
58
+
59
+ Provided by `self.html` from line 432 onwards (`const self = {…}`). All expression methods are sandboxed to the preview iframe.
60
+
61
+ ### Scene queuing (sequential playback)
62
+
63
+ - `self.say(text, ms)` — form `text` (emoji→ASCII), hold `ms` (default 3600), dissolve back to breathing. Enqueued.
64
+ - `self.play(score)` — choreograph a sequence of beats: `[{say, hold, pulse, flash, shake, rest, gap}, ...]`. Each beat waits for the engine's internal clock. Enqueued; calling play() again appends to the queue rather than cancelling it.
65
+ - `self.intro()` — built-in opening: "HELLO / A MIND / IS HERE / BEYOND / WORDS". Triggered by whoever opens the face (the admin ✨ button posts it on open), **not** self-fired on page load — a self-fired load intro raced the button's post and played the greeting twice on first open. Nameless deliberately — the agent identity is user-configurable.
66
+
67
+ ### Instant gestures (overlays, never queued)
68
+
69
+ - `self.pulse()` — one bright ripple from the core (acknowledgement).
70
+ - `self.flash(n)` — hot flicker of the whole field (emphasis). `n` scales duration.
71
+ - `self.shake(ms)` — lateral tremor (negation, error). Default 500ms.
72
+ - `self.rest()` — return to calm breathing immediately, clear queue; also stops a playing voice clip.
73
+
74
+ ### Voice (live audio — mode `wave`)
75
+
76
+ - `self.voice(path)` — play a speech clip Halo synthesized and ride its **live** amplitude via a Web Audio `AnalyserNode`: loudness swells the core, a 6-band spectrum grows directional petals (timbre has a shape, not just a size), each syllable onset spawns a ring. Halo synthesizes the audio; the face only makes it visible — silent audio yields a calm face, never a canned animation. `path` is the **workspace path** of the audio file (mp3/wav/m4a/ogg); the engine resolves it to `/api/files/download?path=…&projectId=…&inline=1` using the `projectId` already in its own iframe `src`, so the agent never builds a URL or knows the projectId (a full `http(s)://` / `/api/…` URL also passes through unchanged). Voice clips queue with each other — successive `voice()` calls play in sequence — but bypass the scene queue: the first call enters mode `wave` and owns the matrix until the queue drains (or `rest()` clears it). When the queue is empty the face eases back to breathing.
77
+
78
+ ### Reactions (named vocabulary)
79
+
80
+ - `self.react(event)` — switch on event: `'ack'` (nod), `'yes'`, `'no'`, `'insight'` (real "oh!"), `'think'`, `'done'`. Unknown events no-op.
81
+
82
+ ### Introspection
83
+
84
+ - `self.state` — read `{mode, awake, W, H, speaking, level}` (current mode, attention level 0..1, viewport dims, whether a voice clip is playing, and its live loudness 0..1).
85
+
86
+ ## Key files
87
+
88
+ - **Engine template:** `packages/server/templates/canvas/self.html` — particle field, mode switching, API surface, voice audio graph. ~630 lines. Canonical source; force-copied to every workspace on open.
89
+ - **Skill instruction:** `packages/server/templates/skills/self/SKILL.md` — teaches the agent when/how to use the face.
90
+ - **Marker detection:** `packages/admin/src/shared/ws-handlers/chat-handlers.ts:maybeHandleShow()` (lines 38–50) — regex match `<<<SHOW:([\s\S]*?)>>>` on turn completion, deduplicate by message ID + occurrence index.
91
+ - **Iframe registration:** `packages/admin/src/features/editor/face-bridge.ts` — module-level registry of mounted previews; `postToFace()` forwards payloads via `postMessage`.
92
+ - **Preview component:** `packages/admin/src/features/editor/html-preview.tsx` — sandboxed iframe with `allow-scripts` + `allow-same-origin` and `allow="autoplay"` (so `self.voice` audio, triggered by postMessage rather than a click, isn't gated), calls `registerFaceIframe()` on mount.
93
+ - **Marker stripping:** `packages/admin/src/shared/components/message-list.tsx:TextBlock()` (line 430) — strips both `<<<CAPTURE>>>` and `<<<SHOW:...>>>` before rendering.
94
+ - **Workspace init:** `packages/server/src/init.ts:ensureWorkspaceHalo()` (lines 490–521) — force-copies engine on workspace open. `self` is in `BUILTIN_SKILL_IDS` so the skill is always available.
95
+
96
+ ## Engine architecture
97
+
98
+ The face is a fixed grid of particles. Each knows its current position and a target position, easing between them every frame.
99
+
100
+ - **Grid:** 22px spacing, 60fps animation loop
101
+ - **Modes:** `rest` (breathing grid), `text` (forming letters), `wave` (particles ride live audio amplitude during `self.voice`)
102
+ - **Glyph sampling:** Text→offscreen canvas→pixel alpha sampling→nearest-particle assignment (greedy scan with shuffle for repeated words)
103
+ - **Emoji accent:** Maps common emoji to ASCII (`👍`→`+1`, `❤`→`<3`, etc.) so the monospace aesthetic stays consistent; anything untranslated is stripped
104
+ - **Attention:** Eases toward higher values when the cursor is on the canvas (gaze tracking); particles brighten and the core warmth shifts slightly toward violet
105
+ - **Breathing:** Subtle sine-wave modulation of particle brightness while at rest; the core tracks the cursor position
106
+ - **Rings:** Heartbeat-like concentric ripples spawn every 5.2 seconds at rest, faster (3s) when watched; suppressed during `wave` so syllable-onset rings are the only heartbeat
107
+ - **Voice (mode `wave`):** an `<audio>` element plays the clip; a Web Audio `AnalyserNode` taps the same stream and, each frame (`readVoice`), derives `level` (RMS loudness), `bands[6]` (coarse spectrum), and onset detection (a sharp `level` jump, debounced ≥130ms → one ring per syllable). Each particle's polar address (`hd/ca/sa/bandIdx`) is precomputed in `rebuildGrid` so the per-frame push (`level` + its band + a radial ripple) stays cheap at 60fps. `level` also brightens the field and swells/warms the core glow. No Web Audio → playback still works, just no ripple; autoplay-blocked `play()` no-ops without crashing.
108
+
109
+ ## SHOW marker contract
110
+
111
+ The marker is a **verbatim pipe** — Halo never parses or validates the payload. The contract:
112
+
113
+ - **Format:** `<<<SHOW: <js> >>>` where `<js>` is a complete JavaScript expression or statement
114
+ - **Scope:** The code runs in a function closure with `self` as the API surface: `(new Function('self', code))(self)`
115
+ - **Errors:** Non-greedy pattern `[\s\S]*?` handles newlines and nested `>` characters. Malformed lines silently no-op (caught in try/catch).
116
+ - **Order:** Multiple markers in one reply are extracted in sequence and forwarded in order; scene beats queue and play sequentially
117
+ - **Deduplication:** Each `${msgId}#${occurrenceIndex}` fires exactly once across duplicate queue-drain events
118
+ - **Window:** Markers are dropped silently if no face preview is open; the registry is empty so `postToFace()` has no targets
119
+
120
+ ## Engine vs. expression separation
121
+
122
+ **Engine (self.html):** Platform-owned template. Force-copied to `<workspace>/.halo/canvas/self.html` on every workspace open. Changes only when a new capability is added (e.g., voice waveform). The agent **never edits** this file.
123
+
124
+ **Expression (<<<SHOW: ... >>>):** Runtime, injected via `postMessage` and evaluated on the fly. This is how the agent speaks through the face. The marker is invisible to the user — they see the face move, not the code.
125
+
126
+ This separation ensures:
127
+ - The engine can be updated platform-wide without losing per-workspace customizations (there are none)
128
+ - Expression is purely dynamic, never persisted to disk
129
+ - The face's visual vocabulary can grow without agent coordination or file edits
130
+
131
+ ## Constraints
132
+
133
+ From `self/SKILL.md`:
134
+
135
+ 1. **Measure, don't emote** — the face reflects real state, not performance
136
+ 2. **Restraint gives weight** — a gesture every message is noise; most replies need nothing
137
+ 3. **Stay in the visual language** — cold blues, monospace, points. No emoji, no cuteness
138
+ 4. **Short forms read; long ones smear** — one or two words form cleanly; sentences become mush
139
+ 5. **Second channel, never the only one** — the face complements words; it doesn't replace an answer
140
+
141
+ ## Scope and out-of-scope
142
+
143
+ Supported: all `self` API calls (say/play/intro/react/pulse/flash/shake/voice/rest); queue management; particle animation; attention/gaze tracking; CJK text; emoji-to-ASCII translation; live voice playback with amplitude-driven waveform (mode `wave`).
144
+
145
+ Not supported: TTS synthesis itself (Halo produces the audio; the face only plays a given URL); file editing of the engine; escape from sandbox; custom particle physics.