@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,187 @@
1
+ # Share Workspace
2
+
3
+ Stage the current workspace's `.halo/` into a self-contained bundle, let the user review what
4
+ made the cut, then zip it up. The bundle is designed to drop into any other project and yield
5
+ the same Halo behavior — agents, skills, prompts, instructions — minus anything personal.
6
+
7
+ What's included: **all** workspace-scope agents/skills, plus any **global**-scope ones an
8
+ included agent references that the workspace doesn't already have (workspace wins on id clash).
9
+ Platform built-ins (default / executor / send-file / create-skill / …) are **not** bundled —
10
+ the receiver's own server seeds those on startup, so shipping them would be redundant and could
11
+ leak local edits. So the receiver gets: your customizations + needed global extensions + their
12
+ own fresh built-ins = a workspace that runs as-is.
13
+
14
+ A Python helper (`stage.py`, sibling of this file) does the heavy lifting:
15
+ file selection, secret redaction, manifest. You orchestrate it and confirm with the user.
16
+
17
+ ## Steps
18
+
19
+ ### 1. Locate the helper
20
+
21
+ The script lives at one of these paths (workspace wins if both exist):
22
+ - \`<workspace-root>/.halo/skills/workspace/stage.py\`
23
+ - \`~/.halo/global/skills/workspace/stage.py\`
24
+
25
+ The two paths above are deterministic, so \`glob\` / \`find\` searches add latency
26
+ without finding anything new. \`file_list\` the workspace skill dir first; if
27
+ no \`stage.py\` there, fall back to the global path.
28
+
29
+ ### 2. Run the staging script
30
+
31
+ The argument is the **workspace root** (the directory containing \`.halo/\`). This is the
32
+ project the user is currently in — same path you'd see in \`/workspace\`. Passing \`/home/<user>\`
33
+ or any parent traverses far more than the workspace and pulls in noise.
34
+
35
+ ```bash
36
+ shell_exec: python3 <stage.py-path> <workspace-root>
37
+ ```
38
+
39
+ Output is a JSON object on stdout summarizing what was staged. The full file contents go to
40
+ `<workspace>/.halo/tmp/share/staged/`.
41
+
42
+ ### 3. Read the manifest and summarize for the user
43
+
44
+ The script writes `<staged>/share-manifest.json`. Read it and present a digest. Keep the
45
+ output focused on what's *in* the bundle — agents, skills, redactions, suspicious flags.
46
+ You don't need to echo the staged path or generation timestamp back to the user.
47
+
48
+ ```
49
+ Agents (2):
50
+ - code-reviewer (workspace)
51
+ - researcher (global)
52
+
53
+ Skills (3):
54
+ - repo-conventions (workspace)
55
+ - aws-knowledge (global)
56
+ - nova-web-search (global)
57
+
58
+ Instructions: .halo/INSTRUCTIONS.md (global fallback)
59
+ INDEX.md → 2 docs followed: docs/architecture/auth.md, docs/dev/api.md
60
+
61
+ Prompts: all=global-fallback, root=global-fallback
62
+
63
+ Redactions:
64
+ • 1 secret field auto-replaced with {{...params.api_key}} placeholder
65
+ • 0 unambiguous leaks redacted in markdown
66
+ • 2 suspicious strings flagged for review (see below)
67
+
68
+ Excluded:
69
+ • USER.md (3 files — personal profile)
70
+ • memory/ (12 files)
71
+ • assets/ (84 files)
72
+ • sessions/, logs/, tmp/, *.db, settings.yaml — always
73
+ ```
74
+
75
+ Lead with the most important callouts: anything in the **redactions** section, anything in
76
+ **missing_skills**, and anything that looks wrong (unexpected agent included, expected one
77
+ missing).
78
+
79
+ ### 4. Walk through suspicious markdown findings
80
+
81
+ For each entry in `redactions.markdown_suspicious`, read the actual line from the staged file
82
+ and show the user. Use the **full word** ("keep" / "redact" / "edit") in the prompt — single
83
+ letters confuse the model when the user replies with just "k". Stay in the share
84
+ flow until all suspicious entries are resolved:
85
+
86
+ ```
87
+ ⚠️ Suspicious match — needs your call:
88
+
89
+ File: .halo/agents/default/AGENT.md, line 42
90
+ Pattern: email
91
+ Line: "contact alice@example.com for the staging creds"
92
+
93
+ This looks like {your interpretation: e.g. "a real email" or "a doc placeholder"}.
94
+
95
+ How should I handle it?
96
+ - Reply **keep** — leave it as-is (it's a doc example / not sensitive)
97
+ - Reply **redact** — replace with [REDACTED:email]
98
+ - Reply **edit X** — change the snippet to X
99
+ ```
100
+
101
+ When the user replies with "keep" / "k" → record decision and move to the next finding.
102
+ When they reply with "redact" / "r" → `file_edit` the *staged* file (not the workspace
103
+ file), substituting the snippet with `[REDACTED:<pattern>]`.
104
+ When they reply with "edit ..." → apply their replacement via `file_edit`.
105
+
106
+ After every suspicious entry is resolved, summarize the decisions and move to step 5. **Do not
107
+ treat short responses (`k`, `r`, `keep`, etc.) as off-topic chat** — you are still inside the
108
+ flow until the user explicitly cancels or you finish step 7.
109
+
110
+ ### 5. Hand control to the user
111
+
112
+ > "Bundle staged at `.halo/tmp/share/staged/`. Have a look — open files directly, edit anything
113
+ > sensitive, remove anything you don't want shared. When you're happy, tell me to zip it."
114
+
115
+ Wait for explicit confirmation before zipping. Auto-zipping at this step
116
+ skips the user's last review window for sensitive content.
117
+
118
+ ### 6. Zip on confirmation
119
+
120
+ ```bash
121
+ cd <workspace>/.halo/tmp/share/staged
122
+ zip -r ../workspace-$(date +%Y-%m-%d).zip .halo README.md
123
+ ```
124
+
125
+ Notes:
126
+ - `share-manifest.json` is intentionally **not** zipped — it's a review aid, not part of the bundle.
127
+ - `README.md` goes in the zip so the receiver knows what to do.
128
+
129
+ ### 7. Report
130
+
131
+ ```
132
+ Bundle ready: .halo/tmp/share/workspace-2026-05-16.zip (147 KB)
133
+
134
+ To share: send the zip. Receiver unzips into their project root, then fills in any
135
+ {{...params.<key>}} placeholders (grep the unzipped tree to find them).
136
+ ```
137
+
138
+ ## What gets included / excluded (rules the helper enforces)
139
+
140
+ **Included:**
141
+ - `<ws>/.halo/INSTRUCTIONS.md` chain (root + any sub-dir `.halo/INSTRUCTIONS.md`).
142
+ If the workspace has no root `INSTRUCTIONS.md`, falls back to `~/.halo/global/INSTRUCTIONS.md`.
143
+ - `<ws>/.halo/INDEX.md` plus only the `docs/...` paths it links to (other paths ignored).
144
+ - All workspace agents (`<ws>/.halo/agents/<id>/`), minus `USER.md`.
145
+ - Global agents that **are not in the workspace's `disabled_items`** AND **not overridden by a
146
+ workspace agent of the same id**. Those get copied into the bundle's `agents/` so the receiver
147
+ doesn't depend on having identical globals.
148
+ - Skills that are **referenced by an included agent's `skills:` list**. Workspace skill wins;
149
+ otherwise global skill (if not disabled).
150
+ - `prompts/{bootstrap,all,root}/` — workspace dir if it exists, else global fallback.
151
+
152
+ **Excluded (always):**
153
+ - Every `USER.md` (workspace, agent dirs, anywhere) — personal profile, never share.
154
+ - `memory/`, `assets/` — workspace-specific knowledge / data.
155
+ - `sessions/`, `logs/`, `tmp/`, `*.db`, `*.db-shm`, `*.db-wal` — runtime state.
156
+ - `~/.halo/secrets/settings.yaml` — secrets are redacted into agent.yaml/SKILL.md as
157
+ `{{<id>.params.<key>}}` placeholders; the receiver fills them in their own `settings.yaml`.
158
+ - Agents with `internal: true` in their `agent.yaml` (`__evo_agent__`, `__score__`,
159
+ `__apply_agent__`, etc.) — these are platform-internal, force-overwritten by the
160
+ receiver's own server on startup, so bundling them is redundant and can leak
161
+ experimental local edits.
162
+
163
+ ## Sanitization
164
+
165
+ - **YAML field redaction (auto):** Inside `agent.yaml` and SKILL.md frontmatter, fields named
166
+ `api_key`, `secret`, `token`, `password`, `access_key`, `aws_secret_access_key`, `bot_token`,
167
+ `client_secret` (case-insensitive) get their values replaced with `{{<id>.params.<field>}}`
168
+ if the value is a literal (not already a `{{...}}` or `<<ENV>>` placeholder).
169
+ - **Markdown auto-redact:** AWS access key formats (`AKIA…`, `ASIA…`) are replaced with
170
+ `[REDACTED:<type>]`.
171
+ - **Markdown flag-only:** Emails, bearer-style headers, and long base64-ish tokens are
172
+ flagged in the manifest but **not auto-replaced** — too many false positives. You walk
173
+ the user through these in step 4.
174
+
175
+ ## Patterns that go sideways
176
+
177
+ - Zipping before user confirmation — they lose the chance to catch
178
+ sensitive data the auto-redactions missed; explicit confirmation keeps
179
+ the call with them.
180
+ - Including `share-manifest.json` in the zip — clutters the receiver's
181
+ unpacked tree with a review aid that's only useful during staging.
182
+ - Including business data (anything outside `.halo/`) in the zip —
183
+ violates the sharing intent and raises security risk; the user expects
184
+ configuration only.
185
+ - Modifying the user's actual workspace files instead of the staged copy
186
+ — the original changes are irreversible, the staged copy is where
187
+ sanitization belongs.
@@ -0,0 +1,487 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Stage a Halo workspace's .halo/ contents into a shareable bundle.
4
+
5
+ Run from the workspace skill's share verb via shell_exec:
6
+ python3 stage.py <workspace>
7
+
8
+ Output:
9
+ <workspace>/.halo/tmp/share/staged/
10
+ ├── .halo/ ← mirrors target workspace's .halo/ (zip this)
11
+ ├── share-manifest.json ← review aid (NOT part of the final zip)
12
+ ├── REQUIRED_PARAMS.md ← receiver fills these in (in zip)
13
+ └── README.md ← receiver instructions (in zip)
14
+
15
+ The agent runs this, reads the manifest, summarizes for the user, awaits
16
+ confirmation, then zips `<staged>/.halo/`, REQUIRED_PARAMS.md, and README.md.
17
+ """
18
+ import argparse
19
+ import json
20
+ import os
21
+ import re
22
+ import shutil
23
+ import sqlite3
24
+ import sys
25
+ from datetime import datetime, timezone
26
+ from pathlib import Path
27
+
28
+ try:
29
+ import yaml
30
+ except ImportError:
31
+ sys.exit("Error: PyYAML required. Install with `pip install pyyaml` (or `pip3 install pyyaml`).")
32
+
33
+ # --- patterns --------------------------------------------------------------
34
+
35
+ SECRET_FIELD = re.compile(
36
+ r"^(api[-_]?key|secret|secret[-_]?key|token|password|access[-_]?key|"
37
+ r"aws[-_]?secret[-_]?access[-_]?key|bot[-_]?token|client[-_]?secret)$",
38
+ re.IGNORECASE,
39
+ )
40
+
41
+ PLACEHOLDER = re.compile(r"^\s*(\{\{[^}]+\}\}|<<[^>]+>>)\s*$")
42
+
43
+ # (label, pattern, action) — action is "redact" | "flag"
44
+ LEAK_PATTERNS = [
45
+ ("aws-access-key", re.compile(r"AKIA[0-9A-Z]{16}"), "redact"),
46
+ ("aws-session-key", re.compile(r"ASIA[0-9A-Z]{16}"), "redact"),
47
+ ("bearer-header", re.compile(r"(?i)\b(bearer|authorization)\s*[:\s]\s*[^\s]{8,}"), "flag"),
48
+ ("email", re.compile(r"\b[\w.+-]+@[\w-]+\.[\w.-]+\b"), "flag"),
49
+ # Long base64-ish token. Constrained:
50
+ # - 40+ chars from the strict base64 alphabet (no `_` or `-`, those are URL-safe variant
51
+ # and produce too many path-like false positives)
52
+ # - Contains at least one digit AND at least one upper- and one lower-case letter
53
+ # (filters out repeated-character padding in markdown rules / dividers)
54
+ # - Optional `=` padding
55
+ # This still flags real tokens (JWTs, AWS session tokens, OAuth) without snagging
56
+ # filesystem paths (which contain `/` segments and word-shaped chunks).
57
+ ("long-base64", re.compile(
58
+ r"(?=[A-Za-z0-9+/]*[A-Z])(?=[A-Za-z0-9+/]*[a-z])(?=[A-Za-z0-9+/]*\d)"
59
+ r"\b[A-Za-z0-9+/]{40,}={0,2}\b"
60
+ ), "flag"),
61
+ ]
62
+
63
+ ALWAYS_EXCLUDED = ["sessions/", "logs/", "tmp/", "*.db", "*.db-shm", "*.db-wal", "settings.yaml", "USER.md"]
64
+
65
+ # Platform built-ins. The receiver's own server force-seeds these on every
66
+ # startup (see packages/server/src/init.ts BUILTIN_AGENT_IDS / BUILTIN_SKILL_IDS),
67
+ # so a *global*-scope copy never needs to ride along in the bundle — it'd be
68
+ # redundant and would leak any local edits the sharer made to them. Keep this
69
+ # in sync with init.ts (small, rarely changes). Workspace-scope overrides of
70
+ # these ids ARE still shared — those are real per-workspace customizations.
71
+ BUILTIN_AGENT_IDS = {
72
+ "__apply_agent__", "__evo_agent__", "__score__",
73
+ "default", "executor", "deep-executor",
74
+ }
75
+ BUILTIN_SKILL_IDS = {
76
+ "agent", "skill", "workspace", "cron", "send-file", "acp",
77
+ "aws-knowledge", "nova-web-search", "self",
78
+ }
79
+
80
+ # --- helpers ---------------------------------------------------------------
81
+
82
+ def load_disabled(db_path: Path):
83
+ """Return disabled items from the workspace DB, split by scope:
84
+ (global_agents, global_skills, ws_agents, ws_skills).
85
+
86
+ A disabled item never ships in the bundle (share == "don't take it
87
+ along"). We track global and workspace scopes separately because the two
88
+ take different code paths below — workspace agents are staged from one
89
+ loop, un-overridden globals from another."""
90
+ if not db_path.exists():
91
+ return set(), set(), set(), set()
92
+ try:
93
+ conn = sqlite3.connect(db_path)
94
+ cur = conn.execute("SELECT item_type, item_id, scope FROM disabled_items")
95
+ g_agents, g_skills, w_agents, w_skills = set(), set(), set(), set()
96
+ for item_type, item_id, scope in cur:
97
+ if scope == "global":
98
+ (g_agents if item_type == "agent" else g_skills).add(item_id)
99
+ else:
100
+ (w_agents if item_type == "agent" else w_skills).add(item_id)
101
+ conn.close()
102
+ return g_agents, g_skills, w_agents, w_skills
103
+ except sqlite3.DatabaseError:
104
+ return set(), set(), set(), set()
105
+
106
+
107
+ def load_yaml(path: Path):
108
+ try:
109
+ with path.open("r", encoding="utf-8") as f:
110
+ return yaml.safe_load(f) or {}
111
+ except (OSError, yaml.YAMLError):
112
+ return None
113
+
114
+
115
+ def sanitize_yaml(data, namespace: str, file_label: str, findings: list) -> bool:
116
+ """Replace leaf string values under SECRET_FIELD keys with `{{ns.params.key}}`."""
117
+ modified = [False]
118
+
119
+ def walk(node, key_chain):
120
+ if isinstance(node, dict):
121
+ for k, v in list(node.items()):
122
+ key_str = str(k)
123
+ if isinstance(v, (dict, list)):
124
+ walk(v, key_chain + [key_str])
125
+ elif isinstance(v, str) and SECRET_FIELD.match(key_str) and v.strip() and not PLACEHOLDER.match(v):
126
+ placeholder = f"{{{{{namespace}.params.{key_str}}}}}"
127
+ node[k] = placeholder
128
+ modified[0] = True
129
+ findings.append({
130
+ "file": file_label,
131
+ "field": ".".join(key_chain + [key_str]),
132
+ "placeholder": placeholder,
133
+ })
134
+ elif isinstance(node, list):
135
+ for item in node:
136
+ walk(item, key_chain)
137
+
138
+ walk(data, [])
139
+ return modified[0]
140
+
141
+
142
+ def sanitize_markdown(text: str, file_label: str):
143
+ """Auto-redact high-confidence leaks; collect lower-confidence ones for review."""
144
+ auto, suspicious = [], []
145
+ lines = text.splitlines(keepends=True)
146
+ for i, line in enumerate(lines, 1):
147
+ new_line = line
148
+ for label, pattern, action in LEAK_PATTERNS:
149
+ for m in pattern.finditer(new_line):
150
+ excerpt = m.group(0)
151
+ if action == "redact":
152
+ new_line = new_line.replace(excerpt, f"[REDACTED:{label}]")
153
+ auto.append({"file": file_label, "line": i, "pattern": label, "before": excerpt[:60]})
154
+ else:
155
+ suspicious.append({"file": file_label, "line": i, "pattern": label, "snippet": excerpt[:80]})
156
+ lines[i - 1] = new_line
157
+ return "".join(lines), auto, suspicious
158
+
159
+
160
+ def sanitize_md_file(src: Path, dst: Path, label: str, manifest: dict):
161
+ """Read src, sanitize, write to dst, accumulate redactions into manifest."""
162
+ text = src.read_text(encoding="utf-8")
163
+ text, auto, sus = sanitize_markdown(text, label)
164
+ dst.parent.mkdir(parents=True, exist_ok=True)
165
+ dst.write_text(text, encoding="utf-8")
166
+ manifest["redactions"]["markdown_auto"].extend(auto)
167
+ manifest["redactions"]["markdown_suspicious"].extend(sus)
168
+
169
+
170
+ def index_doc_links(index_path: Path) -> list:
171
+ """[text](docs/...) markdown links from INDEX.md. Only follow docs/-prefixed paths."""
172
+ if not index_path.exists():
173
+ return []
174
+ text = index_path.read_text(encoding="utf-8")
175
+ return [m.group(1).strip() for m in re.finditer(r"\[[^\]]+\]\(([^)]+)\)", text)
176
+ if m.group(1).strip().startswith("docs/")]
177
+
178
+
179
+ # --- main ------------------------------------------------------------------
180
+
181
+ def main():
182
+ ap = argparse.ArgumentParser()
183
+ ap.add_argument("workspace", help="Workspace root (containing .halo/)")
184
+ args = ap.parse_args()
185
+
186
+ ws = Path(args.workspace).resolve()
187
+ ws_halo = ws / ".halo"
188
+ if not ws_halo.is_dir():
189
+ sys.exit(f"Error: {ws_halo} not found. Is this a Halo workspace?")
190
+
191
+ home_global = Path.home() / ".halo" / "global"
192
+
193
+ stage_root = ws_halo / "tmp" / "share" / "staged"
194
+ if stage_root.exists():
195
+ shutil.rmtree(stage_root)
196
+ stage_root.mkdir(parents=True)
197
+ stage_halo = stage_root / ".halo"
198
+ stage_halo.mkdir()
199
+
200
+ # All paths in the manifest/stdout are relative to the workspace root,
201
+ # so the bundle and review aid never leak the host's absolute layout.
202
+ manifest = {
203
+ "staged_at": str(stage_root.relative_to(ws)),
204
+ "generated_at": datetime.now(timezone.utc).isoformat(),
205
+ "included": {
206
+ "instructions": [],
207
+ "index": None,
208
+ "docs": [],
209
+ "agents": [],
210
+ "skills": [],
211
+ "prompts": {"all": None, "root": None},
212
+ },
213
+ "excluded": {
214
+ "user_md": [],
215
+ "memory_count": 0,
216
+ "assets_count": 0,
217
+ "missing_skills": [],
218
+ "always_excluded": ALWAYS_EXCLUDED,
219
+ },
220
+ "redactions": {"yaml_params": [], "markdown_auto": [], "markdown_suspicious": []},
221
+ }
222
+
223
+ disabled_agents, disabled_skills, ws_disabled_agents, ws_disabled_skills = load_disabled(ws_halo / "halo.db")
224
+
225
+ # --- INSTRUCTIONS.md (root + sub-dir chain), with global fallback if no root ws version
226
+ ws_root_instr = ws_halo / "INSTRUCTIONS.md"
227
+ if ws_root_instr.exists():
228
+ sanitize_md_file(ws_root_instr, stage_halo / "INSTRUCTIONS.md", ".halo/INSTRUCTIONS.md", manifest)
229
+ manifest["included"]["instructions"].append(".halo/INSTRUCTIONS.md")
230
+ else:
231
+ global_instr = home_global / "INSTRUCTIONS.md"
232
+ if global_instr.exists():
233
+ sanitize_md_file(global_instr, stage_halo / "INSTRUCTIONS.md", ".halo/INSTRUCTIONS.md (from global)", manifest)
234
+ manifest["included"]["instructions"].append(".halo/INSTRUCTIONS.md (global fallback)")
235
+
236
+ for sub_instr in ws.rglob(".halo/INSTRUCTIONS.md"):
237
+ if sub_instr == ws_root_instr:
238
+ continue
239
+ if "/.halo/tmp/" in str(sub_instr):
240
+ continue
241
+ rel = sub_instr.relative_to(ws)
242
+ sanitize_md_file(sub_instr, stage_root / rel, str(rel), manifest)
243
+ manifest["included"]["instructions"].append(str(rel))
244
+
245
+ # --- INDEX.md + docs/* it explicitly links to
246
+ ws_index = ws_halo / "INDEX.md"
247
+ if ws_index.exists():
248
+ sanitize_md_file(ws_index, stage_halo / "INDEX.md", ".halo/INDEX.md", manifest)
249
+ manifest["included"]["index"] = ".halo/INDEX.md"
250
+ for link in index_doc_links(ws_index):
251
+ src = ws_halo / link
252
+ if src.is_file():
253
+ sanitize_md_file(src, stage_halo / link, f".halo/{link}", manifest)
254
+ manifest["included"]["docs"].append(link)
255
+ elif src.is_dir():
256
+ for f in src.rglob("*.md"):
257
+ rel = f.relative_to(ws_halo)
258
+ sanitize_md_file(f, stage_halo / rel, f".halo/{rel}", manifest)
259
+ manifest["included"]["docs"].append(str(rel))
260
+
261
+ # --- agents (workspace + un-disabled-and-not-overridden global)
262
+ # A workspace agent disabled in this workspace is dropped from ws_agent_ids
263
+ # entirely. That mirrors runtime (loadUsableAgents skips the disabled
264
+ # record): it isn't staged, AND it no longer shadows a same-id global — so
265
+ # if the workspace disabled its own `foo`, an un-disabled global `foo` still
266
+ # ships, exactly as the running agent would fall back to it.
267
+ ws_agents_dir = ws_halo / "agents"
268
+ global_agents_dir = home_global / "agents"
269
+ ws_agent_ids = {d.name for d in ws_agents_dir.iterdir()
270
+ if ws_agents_dir.is_dir() and d.is_dir() and (d / "agent.yaml").exists()
271
+ and d.name not in ws_disabled_agents} \
272
+ if ws_agents_dir.is_dir() else set()
273
+
274
+ referenced_skills = set()
275
+
276
+ def stage_agent(agent_dir: Path, scope: str):
277
+ agent_yaml_src = agent_dir / "agent.yaml"
278
+ config = load_yaml(agent_yaml_src)
279
+ if config is None:
280
+ return
281
+ # Skip platform-internal agents (evolution, score, apply, etc.).
282
+ # They're shipped force-overwritten by the receiver's own server on
283
+ # startup, so bundling them is redundant — and shipping the workspace
284
+ # copy can leak experimental edits the user made locally.
285
+ if config.get("internal") is True:
286
+ manifest["excluded"].setdefault("internal_agents", []).append(agent_dir.name)
287
+ return
288
+ target_dir = stage_halo / "agents" / agent_dir.name
289
+ target_dir.mkdir(parents=True, exist_ok=True)
290
+ # agent.yaml — sanitize and write
291
+ yaml_findings = []
292
+ modified = sanitize_yaml(config, agent_dir.name, f".halo/agents/{agent_dir.name}/agent.yaml", yaml_findings)
293
+ if modified:
294
+ (target_dir / "agent.yaml").write_text(
295
+ yaml.safe_dump(config, sort_keys=False, allow_unicode=True),
296
+ encoding="utf-8",
297
+ )
298
+ manifest["redactions"]["yaml_params"].extend(yaml_findings)
299
+ else:
300
+ shutil.copy2(agent_yaml_src, target_dir / "agent.yaml")
301
+ # siblings (AGENT.md, etc.); skip USER.md
302
+ for f in agent_dir.iterdir():
303
+ if f.name in {"agent.yaml", "USER.md"}:
304
+ if f.name == "USER.md" and f.exists():
305
+ manifest["excluded"]["user_md"].append(f".halo/agents/{agent_dir.name}/USER.md")
306
+ continue
307
+ if f.is_file():
308
+ if f.suffix == ".md":
309
+ sanitize_md_file(f, target_dir / f.name, f".halo/agents/{agent_dir.name}/{f.name}", manifest)
310
+ else:
311
+ shutil.copy2(f, target_dir / f.name)
312
+ elif f.is_dir():
313
+ shutil.copytree(f, target_dir / f.name, dirs_exist_ok=True)
314
+ # collect skill refs
315
+ for s in (config.get("skills") or []):
316
+ if isinstance(s, str):
317
+ referenced_skills.add(s)
318
+ manifest["included"]["agents"].append({"id": agent_dir.name, "scope": scope})
319
+
320
+ for aid in sorted(ws_agent_ids):
321
+ stage_agent(ws_agents_dir / aid, "workspace")
322
+
323
+ if global_agents_dir.is_dir():
324
+ for d in sorted(global_agents_dir.iterdir()):
325
+ if not d.is_dir() or not (d / "agent.yaml").exists():
326
+ continue
327
+ if d.name in ws_agent_ids:
328
+ continue # workspace overrides
329
+ if d.name in disabled_agents:
330
+ continue # explicitly disabled in this workspace
331
+ if d.name in BUILTIN_AGENT_IDS:
332
+ continue # receiver's server seeds these itself
333
+ stage_agent(d, "global")
334
+
335
+ # --- skills
336
+ # Ship every workspace-scope skill (it's a per-workspace customization, so
337
+ # it belongs in a self-contained share — even if no agent.yaml lists it,
338
+ # e.g. slash-command-only skills). For skills an included agent references
339
+ # but the workspace doesn't have, pull the global copy — except platform
340
+ # built-ins, which the receiver's server seeds itself.
341
+ ws_skills_dir = ws_halo / "skills"
342
+ global_skills_dir = home_global / "skills"
343
+ # Same rule as agents: a workspace skill disabled in this workspace doesn't
344
+ # ship, and no longer shadows a same-id global (so an agent that references
345
+ # it still resolves to the global copy in the bundle, mirroring runtime).
346
+ ws_skill_ids = {d.name for d in ws_skills_dir.iterdir()
347
+ if ws_skills_dir.is_dir() and d.is_dir() and (d / "SKILL.md").exists()
348
+ and d.name not in ws_disabled_skills} \
349
+ if ws_skills_dir.is_dir() else set()
350
+
351
+ def stage_skill(skill_dir: Path, scope: str):
352
+ target_dir = stage_halo / "skills" / skill_dir.name
353
+ target_dir.mkdir(parents=True, exist_ok=True)
354
+ for f in skill_dir.iterdir():
355
+ if f.is_file():
356
+ if f.name == "SKILL.md":
357
+ text = f.read_text(encoding="utf-8")
358
+ fm_match = re.match(r"^---\n(.*?)\n---\n", text, re.DOTALL)
359
+ if fm_match:
360
+ fm = yaml.safe_load(fm_match.group(1)) or {}
361
+ body = text[fm_match.end():]
362
+ yaml_findings = []
363
+ modified = sanitize_yaml(fm, skill_dir.name,
364
+ f".halo/skills/{skill_dir.name}/SKILL.md (frontmatter)",
365
+ yaml_findings)
366
+ body, auto, sus = sanitize_markdown(body, f".halo/skills/{skill_dir.name}/SKILL.md")
367
+ if modified:
368
+ new_fm = yaml.safe_dump(fm, sort_keys=False, allow_unicode=True).rstrip()
369
+ text = f"---\n{new_fm}\n---\n{body}"
370
+ manifest["redactions"]["yaml_params"].extend(yaml_findings)
371
+ else:
372
+ text = f"{fm_match.group(0)}{body}"
373
+ manifest["redactions"]["markdown_auto"].extend(auto)
374
+ manifest["redactions"]["markdown_suspicious"].extend(sus)
375
+ else:
376
+ text, auto, sus = sanitize_markdown(text, f".halo/skills/{skill_dir.name}/SKILL.md")
377
+ manifest["redactions"]["markdown_auto"].extend(auto)
378
+ manifest["redactions"]["markdown_suspicious"].extend(sus)
379
+ (target_dir / "SKILL.md").write_text(text, encoding="utf-8")
380
+ elif f.suffix == ".md":
381
+ sanitize_md_file(f, target_dir / f.name, f".halo/skills/{skill_dir.name}/{f.name}", manifest)
382
+ else:
383
+ shutil.copy2(f, target_dir / f.name)
384
+ elif f.is_dir():
385
+ shutil.copytree(f, target_dir / f.name, dirs_exist_ok=True)
386
+ manifest["included"]["skills"].append({"id": skill_dir.name, "scope": scope})
387
+
388
+ # 1. all workspace-scope skills
389
+ for sid in sorted(ws_skill_ids):
390
+ stage_skill(ws_skills_dir / sid, "workspace")
391
+
392
+ # 2. agent-referenced skills missing from the workspace → global fallback
393
+ for sid in sorted(referenced_skills - ws_skill_ids):
394
+ if sid in BUILTIN_SKILL_IDS:
395
+ continue # receiver's server seeds these itself
396
+ if (global_skills_dir / sid / "SKILL.md").exists() and sid not in disabled_skills:
397
+ stage_skill(global_skills_dir / sid, "global")
398
+ else:
399
+ manifest["excluded"]["missing_skills"].append(sid)
400
+
401
+ # --- prompts (workspace per-scope, else global fallback for missing scopes)
402
+ ws_prompts = ws_halo / "prompts"
403
+ global_prompts = home_global / "prompts"
404
+ # `bootstrap` is intentionally excluded — it only fires for first-run
405
+ # users who don't have a USER.md yet, which a shared bundle never targets.
406
+ # `builtin/` (PLATFORM_KNOWLEDGE.md, etc.) is server-owned and version-tied;
407
+ # it lives outside `prompts/` and is never shared — receiver's own server
408
+ # seeds it on startup.
409
+ for scope in ["all", "root"]:
410
+ ws_scope = ws_prompts / scope
411
+ global_scope = global_prompts / scope
412
+ if ws_scope.is_dir():
413
+ shutil.copytree(ws_scope, stage_halo / "prompts" / scope, dirs_exist_ok=True)
414
+ manifest["included"]["prompts"][scope] = "workspace"
415
+ elif global_scope.is_dir():
416
+ shutil.copytree(global_scope, stage_halo / "prompts" / scope, dirs_exist_ok=True)
417
+ manifest["included"]["prompts"][scope] = "global-fallback"
418
+
419
+ # --- excluded counts
420
+ if (ws_halo / "memory").is_dir():
421
+ manifest["excluded"]["memory_count"] = sum(1 for _ in (ws_halo / "memory").rglob("*") if _.is_file())
422
+ if (ws / "assets").is_dir():
423
+ manifest["excluded"]["assets_count"] = sum(1 for _ in (ws / "assets").rglob("*") if _.is_file())
424
+ if (ws_halo / "USER.md").exists():
425
+ manifest["excluded"]["user_md"].append(".halo/USER.md")
426
+
427
+ # --- write manifest
428
+ (stage_root / "share-manifest.json").write_text(
429
+ json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8",
430
+ )
431
+
432
+ # --- README.md
433
+ # Secret-redacted fields appear inline in agent.yaml / SKILL.md as
434
+ # `{{<id>.params.<key>}}`. The receiver greps for those after unpacking;
435
+ # no separate declaration file needed.
436
+ readme = (
437
+ "# Shared Halo Workspace\n\n"
438
+ "This bundle contains a Halo workspace's `.halo/` configuration: agents, skills,\n"
439
+ "prompts, and instructions. Drop it into any project to replicate the setup.\n\n"
440
+ "## To use\n\n"
441
+ "1. Unzip into your project root. This places `.halo/` at the project root.\n"
442
+ "2. If any agent.yaml or SKILL.md contains `{{<id>.params.<key>}}` placeholders,\n"
443
+ " add the matching values to `~/.halo/secrets/settings.yaml`. Search the bundle\n"
444
+ " with `grep -rn '{{' .halo/` to find every placeholder.\n"
445
+ "3. Open the workspace in Halo — agents and skills will be ready.\n\n"
446
+ "## Inside\n\n"
447
+ f"- {len(manifest['included']['agents'])} agent(s)\n"
448
+ f"- {len(manifest['included']['skills'])} skill(s)\n"
449
+ f"- Prompts: {', '.join(f'{k}={v}' for k, v in manifest['included']['prompts'].items() if v) or '(none)'}\n"
450
+ f"- {len(manifest['included']['docs'])} doc(s) referenced from INDEX.md\n\n"
451
+ "## Excluded (privacy / portability)\n\n"
452
+ "- `USER.md` (personal profile, all scopes)\n"
453
+ "- `memory/`, `assets/` (workspace-specific)\n"
454
+ "- `sessions/`, `logs/`, `tmp/`, `*.db*`, `settings.yaml`\n\n"
455
+ "## Auto-redactions\n\n"
456
+ f"Markdown files were scanned. {len(manifest['redactions']['markdown_auto'])} unambiguous "
457
+ f"leak(s) were auto-redacted to `[REDACTED:<type>]`. "
458
+ f"{len(manifest['redactions']['markdown_suspicious'])} suspicious string(s) were flagged "
459
+ "for the maintainer to review before sharing.\n"
460
+ )
461
+ (stage_root / "README.md").write_text(readme, encoding="utf-8")
462
+
463
+ # --- summary to stdout (for the agent). Paths are relative to the
464
+ # workspace so the agent's reply doesn't leak the host's absolute layout.
465
+ print(json.dumps({
466
+ "ok": True,
467
+ "staged_at": str(stage_root.relative_to(ws)),
468
+ "manifest": str((stage_root / "share-manifest.json").relative_to(ws)),
469
+ "summary": {
470
+ "agents": [a["id"] + f" ({a['scope']})" for a in manifest["included"]["agents"]],
471
+ "skills": [s["id"] + f" ({s['scope']})" for s in manifest["included"]["skills"]],
472
+ "instructions": manifest["included"]["instructions"],
473
+ "docs_count": len(manifest["included"]["docs"]),
474
+ "prompts": manifest["included"]["prompts"],
475
+ "yaml_redactions": len(manifest["redactions"]["yaml_params"]),
476
+ "markdown_auto_redactions": len(manifest["redactions"]["markdown_auto"]),
477
+ "markdown_suspicious": len(manifest["redactions"]["markdown_suspicious"]),
478
+ "excluded_user_md": len(manifest["excluded"]["user_md"]),
479
+ "excluded_memory_files": manifest["excluded"]["memory_count"],
480
+ "excluded_asset_files": manifest["excluded"]["assets_count"],
481
+ "missing_skills": manifest["excluded"]["missing_skills"],
482
+ },
483
+ }, indent=2, ensure_ascii=False))
484
+
485
+
486
+ if __name__ == "__main__":
487
+ main()