@rho-workspace/rho 0.5.3 → 0.5.4

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 (216) hide show
  1. package/README.md +24 -5
  2. package/dist/capabilities.d.ts +27 -0
  3. package/dist/capabilities.js +73 -1
  4. package/dist/cli.js +150 -25
  5. package/dist/contract.generated.d.ts +112 -12
  6. package/dist/contracts/owners.generated.js +103 -8
  7. package/dist/conversation-presentation.d.ts +3 -1
  8. package/dist/conversation-presentation.js +16 -6
  9. package/dist/conversations.d.ts +10 -6
  10. package/dist/conversations.js +17 -13
  11. package/dist/installs.d.ts +11 -0
  12. package/dist/installs.js +179 -0
  13. package/dist/project-notes.js +2 -0
  14. package/dist/protocol.js +7 -5
  15. package/dist/screen-check-skill.d.ts +2 -0
  16. package/dist/screen-check-skill.js +39 -0
  17. package/dist/server.d.ts +2 -0
  18. package/dist/server.js +50 -7
  19. package/dist/web/.vite/manifest.json +51 -51
  20. package/dist/web/assets/{choices-B-y2OYvE.js → choices-DU9kHlyQ.js} +1 -1
  21. package/dist/web/assets/{console-e1P5qYQ5.js → console-Dbdo-5oO.js} +1 -1
  22. package/dist/web/assets/{dist-Dd3iAGFy.js → dist-B2V0VDO0.js} +1 -1
  23. package/dist/web/assets/{dist-CH56yNDn.js → dist-BV3hWfn_.js} +1 -1
  24. package/dist/web/assets/{dist-CBM2oq6g.js → dist-CLNF2g-E.js} +1 -1
  25. package/dist/web/assets/{dist-DEorbOW0.js → dist-D25Vn17i.js} +1 -1
  26. package/dist/web/assets/{explore-in-muWXOGOY.js → explore-in-8Q5ftt-E.js} +1 -1
  27. package/dist/web/assets/{graph-CK-df5lh.js → graph-hS2kvHTI.js} +1 -1
  28. package/dist/web/assets/{index-BYuKpyoi.js → index-BHQIaDsx.js} +220 -121
  29. package/dist/web/assets/index-Cj1_iqyK.css +1 -0
  30. package/dist/web/assets/{notes-meaning-B3HFbh84.js → notes-meaning-DdPbtNzJ.js} +1 -1
  31. package/dist/web/assets/{overview-D1Ev2OC_.js → overview-BV6ZO5TO.js} +3 -3
  32. package/dist/web/assets/{preview-HbmQKUGY.js → preview-n5lOOdjN.js} +1 -1
  33. package/dist/web/assets/{review-BVh9jWMB.js → review-CmWou4Iv.js} +1 -1
  34. package/dist/web/assets/{skills-DPaEYkln.js → skills-Dy7n3L9d.js} +1 -1
  35. package/dist/web/assets/{skills-work-dvjfy_tu.js → skills-work-Cj0W6b-I.js} +1 -1
  36. package/dist/web/assets/{studio-process-BYm8mjRJ.js → studio-process-QOY_n2_8.js} +1 -1
  37. package/dist/web/assets/{timeline-events-CDZ7wiJX.js → timeline-events-DmX1BCh3.js} +1 -1
  38. package/dist/web/assets/{work-turns-BeCt303c.js → work-turns-Co0C71rJ.js} +1 -1
  39. package/dist/web/assets/{workspace-d5Hw_ClZ.js → workspace-BKFefRfJ.js} +2 -2
  40. package/dist/web/composition.json +73 -73
  41. package/dist/web/index.html +2 -2
  42. package/dist/web/renderer.json +1 -1
  43. package/docs/FRONTEND.md +30 -14
  44. package/docs/HEADLESS.md +59 -13
  45. package/example/pa14-qs/.rho/rho/conversations/c-67f77cfebaba2cea.json +1 -1
  46. package/example/pa14-qs/.rho/rho/conversations/c-91aa2e46b1ac1a6c.json +1 -1
  47. package/example/pa14-qs/.rho/rho/conversations/c-fa24dfd80dfe4bcb.json +1 -1
  48. package/node_modules/@hono/node-server/dist/index.js +11 -0
  49. package/node_modules/@hono/node-server/dist/index.mjs +11 -0
  50. package/node_modules/@hono/node-server/dist/listener.js +11 -0
  51. package/node_modules/@hono/node-server/dist/listener.mjs +11 -0
  52. package/node_modules/@hono/node-server/dist/request.js +11 -0
  53. package/node_modules/@hono/node-server/dist/request.mjs +11 -0
  54. package/node_modules/@hono/node-server/dist/server.js +11 -0
  55. package/node_modules/@hono/node-server/dist/server.mjs +11 -0
  56. package/node_modules/@hono/node-server/dist/vercel.js +11 -0
  57. package/node_modules/@hono/node-server/dist/vercel.mjs +11 -0
  58. package/node_modules/@hono/node-server/package.json +1 -1
  59. package/node_modules/@rho-workspace/plugin-agent/README.md +3 -1
  60. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +14 -3
  61. package/node_modules/@rho-workspace/plugin-agent/dist/conversation.d.ts +1 -0
  62. package/node_modules/@rho-workspace/plugin-agent/dist/conversation.js +1 -0
  63. package/node_modules/@rho-workspace/plugin-agent/dist/generic-acp.d.ts +11 -0
  64. package/node_modules/@rho-workspace/plugin-agent/dist/generic-acp.js +7 -0
  65. package/node_modules/@rho-workspace/plugin-agent/dist/mcp.js +5 -1
  66. package/node_modules/@rho-workspace/plugin-agent/package.json +1 -1
  67. package/node_modules/@rho-workspace/plugin-annotations/package.json +1 -1
  68. package/node_modules/@rho-workspace/plugin-documents/README.md +4 -0
  69. package/node_modules/@rho-workspace/plugin-documents/dist/index.d.ts +3 -0
  70. package/node_modules/@rho-workspace/plugin-documents/dist/index.js +1 -1
  71. package/node_modules/@rho-workspace/plugin-documents/package.json +1 -1
  72. package/node_modules/@rho-workspace/plugin-kernel/README.md +71 -10
  73. package/node_modules/@rho-workspace/plugin-kernel/dist/code.d.ts +2 -0
  74. package/node_modules/@rho-workspace/plugin-kernel/dist/code.js +4 -1
  75. package/node_modules/@rho-workspace/plugin-kernel/dist/contract.js +5 -0
  76. package/node_modules/@rho-workspace/plugin-kernel/dist/index.d.ts +7 -0
  77. package/node_modules/@rho-workspace/plugin-kernel/dist/index.js +55 -8
  78. package/node_modules/@rho-workspace/plugin-kernel/dist/lineage.d.ts +9 -6
  79. package/node_modules/@rho-workspace/plugin-kernel/dist/lineage.js +18 -41
  80. package/node_modules/@rho-workspace/plugin-kernel/dist/operations.js +5 -2
  81. package/node_modules/@rho-workspace/plugin-kernel/dist/script-flow.d.ts +39 -0
  82. package/node_modules/@rho-workspace/plugin-kernel/dist/script-flow.js +661 -0
  83. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.d.ts +3 -0
  84. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.js +10 -0
  85. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-flow.d.ts +38 -4
  86. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-flow.js +194 -97
  87. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook-work.js +1 -2
  88. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.d.ts +10 -2
  89. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.js +79 -18
  90. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-styles.js +115 -45
  91. package/node_modules/@rho-workspace/plugin-kernel/dist/ui.d.ts +1 -1
  92. package/node_modules/@rho-workspace/plugin-kernel/dist/ui.js +1 -1
  93. package/node_modules/@rho-workspace/plugin-kernel/dist/value-graph.d.ts +63 -0
  94. package/node_modules/@rho-workspace/plugin-kernel/dist/value-graph.js +344 -0
  95. package/node_modules/@rho-workspace/plugin-kernel/package.json +1 -1
  96. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.R +6 -3
  97. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.py +6 -3
  98. package/node_modules/express-rate-limit/dist/index.cjs +1 -1
  99. package/node_modules/express-rate-limit/dist/index.mjs +1 -1
  100. package/node_modules/express-rate-limit/package.json +1 -1
  101. package/node_modules/ip-address/dist/ipv6.js +1 -1
  102. package/node_modules/ip-address/package.json +1 -1
  103. package/node_modules/playwright-core/LICENSE +202 -0
  104. package/node_modules/playwright-core/NOTICE +5 -0
  105. package/node_modules/playwright-core/ThirdPartyNotices.txt +13 -0
  106. package/node_modules/playwright-core/bin/install_media_pack.ps1 +5 -0
  107. package/node_modules/playwright-core/bin/install_webkit_wsl.ps1 +33 -0
  108. package/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh +42 -0
  109. package/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh +13 -0
  110. package/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 +24 -0
  111. package/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh +42 -0
  112. package/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh +12 -0
  113. package/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 +24 -0
  114. package/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh +48 -0
  115. package/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh +11 -0
  116. package/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 +23 -0
  117. package/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh +48 -0
  118. package/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh +11 -0
  119. package/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 +23 -0
  120. package/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh +48 -0
  121. package/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh +11 -0
  122. package/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 +24 -0
  123. package/node_modules/playwright-core/browsers.json +52 -0
  124. package/node_modules/playwright-core/cli.js +22 -0
  125. package/node_modules/playwright-core/index.js +17 -0
  126. package/node_modules/playwright-core/index.mjs +28 -0
  127. package/node_modules/playwright-core/lib/bootstrap.js +149 -0
  128. package/node_modules/playwright-core/lib/coreBundle.js +78041 -0
  129. package/node_modules/playwright-core/lib/entry/cliDaemon.js +5 -0
  130. package/node_modules/playwright-core/lib/entry/dashboardApp.js +3 -0
  131. package/node_modules/playwright-core/lib/entry/mcp.js +10 -0
  132. package/node_modules/playwright-core/lib/entry/oopBrowserDownload.js +3 -0
  133. package/node_modules/playwright-core/lib/package.js +50 -0
  134. package/node_modules/playwright-core/lib/server/chromium/appIcon.png +0 -0
  135. package/node_modules/playwright-core/lib/server/electron/loader.js +123 -0
  136. package/node_modules/playwright-core/lib/serverRegistry.js +1939 -0
  137. package/node_modules/playwright-core/lib/serverRegistry.js.LICENSE +65 -0
  138. package/node_modules/playwright-core/lib/tools/cli-client/channelSessions.js +141 -0
  139. package/node_modules/playwright-core/lib/tools/cli-client/cli.js +7 -0
  140. package/node_modules/playwright-core/lib/tools/cli-client/help.json +808 -0
  141. package/node_modules/playwright-core/lib/tools/cli-client/minimist.js +128 -0
  142. package/node_modules/playwright-core/lib/tools/cli-client/output.js +352 -0
  143. package/node_modules/playwright-core/lib/tools/cli-client/program.js +422 -0
  144. package/node_modules/playwright-core/lib/tools/cli-client/registry.js +180 -0
  145. package/node_modules/playwright-core/lib/tools/cli-client/session.js +261 -0
  146. package/node_modules/playwright-core/lib/tools/dashboard/appIcon.png +0 -0
  147. package/node_modules/playwright-core/lib/tools/utils/extension.js +112 -0
  148. package/node_modules/playwright-core/lib/tools/utils/socketConnection.js +108 -0
  149. package/node_modules/playwright-core/lib/utilsBundle.js +86078 -0
  150. package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +1890 -0
  151. package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule--QdMvsKi.css +1 -0
  152. package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule-CQ8RZGBm.js +32 -0
  153. package/node_modules/playwright-core/lib/vite/dashboard/assets/codicon-DCmgc-ay.ttf +0 -0
  154. package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-1bWoP6pv.svg +1 -0
  155. package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-beta-k3eOH_eK.svg +1 -0
  156. package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-nightly-Cp5nfeDT.svg +1 -0
  157. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-AwgwcYie.css +1 -0
  158. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-hRvO4yU2.js +12 -0
  159. package/node_modules/playwright-core/lib/vite/dashboard/assets/safari-na3_-uQk.svg +1 -0
  160. package/node_modules/playwright-core/lib/vite/dashboard/index.html +29 -0
  161. package/node_modules/playwright-core/lib/vite/dashboard/playwright-logo.svg +24 -0
  162. package/node_modules/playwright-core/lib/vite/htmlReport/index.html +16 -0
  163. package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -0
  164. package/node_modules/playwright-core/lib/vite/htmlReport/report.js +32 -0
  165. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
  166. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DK72OMRi.js +32 -0
  167. package/node_modules/playwright-core/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf +0 -0
  168. package/node_modules/playwright-core/lib/vite/recorder/assets/index-BZpYJZ-P.css +1 -0
  169. package/node_modules/playwright-core/lib/vite/recorder/assets/index-Cn0mmDuv.js +129 -0
  170. package/node_modules/playwright-core/lib/vite/recorder/index.html +29 -0
  171. package/node_modules/playwright-core/lib/vite/recorder/playwright-logo.svg +9 -0
  172. package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-BEB2e8M3.js +32 -0
  173. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-D-g2vrvf.js +184 -0
  174. package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
  175. package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-B794rD2Z.js +7 -0
  176. package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
  177. package/node_modules/playwright-core/lib/vite/traceViewer/codicon.DCmgc-ay.ttf +0 -0
  178. package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.C8BeHuxd.css +1 -0
  179. package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
  180. package/node_modules/playwright-core/lib/vite/traceViewer/index.html +44 -0
  181. package/node_modules/playwright-core/lib/vite/traceViewer/index.oNry3Hjv.js +1 -0
  182. package/node_modules/playwright-core/lib/vite/traceViewer/manifest.webmanifest +16 -0
  183. package/node_modules/playwright-core/lib/vite/traceViewer/playwright-logo.svg +9 -0
  184. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
  185. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +10 -0
  186. package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +4 -0
  187. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CYwC_gH1.js +5 -0
  188. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CyMfwkXJ.css +1 -0
  189. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +18 -0
  190. package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
  191. package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
  192. package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
  193. package/node_modules/playwright-core/lib/xdg-open +1267 -0
  194. package/node_modules/playwright-core/package.json +34 -0
  195. package/package.json +10 -6
  196. package/protocol/index.json +17 -17
  197. package/protocol/operations/conversation.settings.check.json +147 -4
  198. package/protocol/operations/conversation.settings.read.json +145 -3
  199. package/protocol/operations/conversation.settings.update.json +166 -4
  200. package/protocol/operations/conversation.start.json +2 -1
  201. package/protocol/operations/kernel.interrupt.json +20 -0
  202. package/protocol/operations/kernel.read.json +30 -0
  203. package/protocol/operations/kernel.run.json +36 -1
  204. package/protocol/operations/kernel.stop.json +10 -0
  205. package/scripts/browser-checks.mjs +53 -0
  206. package/scripts/screen-check.mjs +193 -0
  207. package/vendor/composition.json +102 -102
  208. package/vendor/rho-workspace-plugin-agent-0.4.28.tgz +0 -0
  209. package/vendor/rho-workspace-plugin-annotations-0.5.2.tgz +0 -0
  210. package/vendor/rho-workspace-plugin-documents-0.4.11.tgz +0 -0
  211. package/vendor/rho-workspace-plugin-kernel-0.5.5.tgz +0 -0
  212. package/web/theme.css +17 -2
  213. package/dist/web/assets/index-DBd77u4a.css +0 -1
  214. package/vendor/rho-workspace-plugin-agent-0.4.27.tgz +0 -0
  215. package/vendor/rho-workspace-plugin-documents-0.4.10.tgz +0 -0
  216. package/vendor/rho-workspace-plugin-kernel-0.5.2.tgz +0 -0
package/README.md CHANGED
@@ -39,8 +39,9 @@ contract. Electron is reserved for a later desktop client that reuses the WebUI
39
39
  and component boundaries. Functional views use CodeMirror 6, AG Grid Community
40
40
  and ECharts.
41
41
 
42
- Install the packed tarball, start `rho`, and connect an external Agent with
43
- `rho --grant` ([Install and start](docs/HEADLESS.md#install-and-start)). Projects,
42
+ Install it with `npm install -g @rho-workspace/rho` on Node 22.22 or later, below 27, start
43
+ `rho`, and connect an external Agent with `rho --grant`
44
+ ([Install and start](docs/HEADLESS.md#install-and-start)). Projects,
44
45
  files, execution records and workspaces are available through the local WebUI.
45
46
  The Agent uses the same scoped capabilities; a browser request copied to an external
46
47
  Agent is explicitly a handoff.
@@ -50,8 +51,25 @@ plans and visual rules. See [Frontend](docs/FRONTEND.md) for the current interfa
50
51
  [Status](docs/STATUS.md) for executed checks and limits, and the
51
52
  [documentation index](docs/README.md) for backend and release information.
52
53
 
54
+ ## Set up with an Agent
55
+
56
+ A person can ask Claude Code or Codex, in any language:
57
+
58
+ > Read https://yulab-smu.top/Rho/doc/setup.md and follow its steps to install and configure Rho for me.
59
+
60
+ or: 请阅读 https://yulab-smu.top/Rho/doc/setup.md 文档,按照步骤为我安装并配置 Rho。
61
+
62
+ That page is [SETUP.md](SETUP.md). The Agent checks the machine, installs the npm package and
63
+ starts Rho on `~/Rho/projects` and `~/Rho/state`. It then installs the Agent Skill in
64
+ [skills/rho](skills/rho/SKILL.md) into its own skills folder, connects itself to the example
65
+ project and says what is missing. The Skill teaches an Agent to install and connect Rho, call
66
+ its tools without guessing, and run an analysis a person can follow. It is published beside the
67
+ page at <https://yulab-smu.top/Rho/skills/rho/SKILL.md>.
68
+
53
69
  ## Documentation
54
70
 
71
+ <https://yulab-smu.top/Rho/> runs the real interface on the shipped example project,
72
+ replaying a recorded session without a backend, and gives the sentence above for an Agent.
55
73
  The documentation is published at <https://yulab-smu.top/Rho/docs/>. The site is
56
74
  generated from this repository's `docs/` directory, which stays the only source of
57
75
  truth: every page names the file it came from and links to it.
@@ -60,12 +78,13 @@ truth: every page names the file it came from and links to it.
60
78
  npm run docs:dev # assemble and serve locally
61
79
  npm run docs:build # assemble, then build the static site into site/.vitepress/dist
62
80
  npm run docs:check # fail if a document in docs/ is neither published nor withheld
81
+ npm run pages:build # front page, demo and documentation into site/.pages (add -- --home for the front page alone)
82
+ npm run pages:check # walk the demo and the front page in a browser
83
+ npm run pages:capture # record the demo again from this checkout's service (after npm run build)
63
84
  ```
64
85
 
65
86
  `@rho-workspace/rho` is published to npm. The registry entry for 0.5.2 carries no
66
- README, so its package page shows no description; from 0.5.3 the packer ships this README. Desktop installers on the
67
- [downloads page](https://yulab-smu.top/Rho/) are the earlier 0.4.0 line; this npm line
68
- is the current delivery.
87
+ README, so its package page shows no description; from 0.5.3 the packer ships this README.
69
88
 
70
89
  For a new component view, start with [Plugin authoring](docs/PLUGIN-AUTHORING.md):
71
90
  generate a typed package, preview it independently and select its compiled entry
@@ -64,6 +64,33 @@ export declare const personFilesSkill: {
64
64
  description: string;
65
65
  };
66
66
  };
67
+ export declare const analysisRunSkillDeclaration: {
68
+ id: string;
69
+ version: string;
70
+ content: string;
71
+ digest: string;
72
+ intent: string;
73
+ requires: string[];
74
+ example: {
75
+ method: string;
76
+ input: {
77
+ language: string;
78
+ code: string;
79
+ purpose: string;
80
+ changeId: string;
81
+ waitMs: number;
82
+ };
83
+ };
84
+ failureRecipes: {
85
+ kind: string;
86
+ message: string;
87
+ next: string;
88
+ }[];
89
+ verify: {
90
+ command: string;
91
+ description: string;
92
+ };
93
+ };
67
94
  export declare const taskMapSkill: {
68
95
  id: string;
69
96
  version: string;
@@ -1,6 +1,7 @@
1
1
  import * as path from 'node:path';
2
2
  import { atomicJson, canonical, digest, now, readJson, requireThat, stateDirectory } from '@rho-workspace/plugin-contracts';
3
3
  import { operations } from './protocol.js';
4
+ import { screenCheckSkill } from './screen-check-skill.js';
4
5
  export const capabilityLimits = { maxReceipts: 256 };
5
6
  // The umbrella Skill: what this application is, the order to work in, and the rules
6
7
  // that used to be discovered by failing. It names where the detail lives instead of
@@ -85,6 +86,77 @@ const personFiles = {
85
86
  };
86
87
  export const rhoWorkspaceSkill = { ...applicationSkill, digest: skillDigest };
87
88
  export const personFilesSkill = personFiles;
89
+ // Written after five real analyses (bulk RNA-seq, snRNA-seq, GeoMx DSP, bulk plus a
90
+ // metabolome, and a multi-omics cohort) ran through this application. None of them
91
+ // finished unattended at the time: turns ended on a call limit or on a wait nothing
92
+ // wakes, calls went into looking for what may be done rather than doing it, and results
93
+ // were not always written before the turn ended. Several of those stops have since been
94
+ // fixed in this application, so what follows is written as the practice that survived
95
+ // them rather than as a workaround for one build's limits.
96
+ const analysisRunContent = [
97
+ 'A whole analysis has to survive interruption. Rho keeps no step cap and no wall time',
98
+ 'by default, and a long analysis may still be stopped by the person or by the Agent',
99
+ 'client, so write results as you go instead of holding them until the end.',
100
+ '',
101
+ '1. Spend calls on the analysis, not on discovery. Resolve the operations you will use',
102
+ ' once each (kernel.run, kernel.read, kernel.interrupt, resource.list) and reuse the',
103
+ ' contractRef you were given. In the runs behind this Skill one Agent spent 36',
104
+ ' capability.index calls in a single analysis and ended with only its quality-control',
105
+ ' table written.',
106
+ '2. Do not guess what a result reference contains. A refused read means the shape is',
107
+ ' wrong, not that the result is missing: resolve the operation that produced it and',
108
+ ' read the page or range it names instead of trying sibling paths.',
109
+ '3. Nothing wakes you later. No monitor, timer or event resumes your turn: a cell still',
110
+ ' running when you stop is left running for nobody, and ending your reply is not',
111
+ ' proof the analysis finished. Pass a waitMs long enough for the slowest step (a',
112
+ ' normalisation over 90,000 nuclei can take many minutes), or read the cell with',
113
+ ' kernel.read in your own next call.',
114
+ '4. Load once, build on it. A session keeps what a cell defines, so read the data in one',
115
+ ' cell and derive in the next. A service restart does not keep those objects: the code',
116
+ ' and its outputs stay, the values in memory do not, so keep a script that rebuilds',
117
+ ' what matters.',
118
+ '5. Prefer a few large cells to many small ones, and keep each short enough to finish. A',
119
+ ' cell that hangs costs time: interrupt it with kernel.interrupt and continue with the',
120
+ ' part that is cheap to redo.',
121
+ '6. Draw to an explicit device. In these runs a ggsave call in the shared session hung',
122
+ ' and the figure had to be redrawn with png() and dev.off(): open the device, plot,',
123
+ ' close it, and let the figure land in a file you name.',
124
+ '7. Write each file once, complete, and save it to disk before you rely on it. One script',
125
+ ' written in full and then run costs less than the same script edited piece by piece,',
126
+ ' and a file on disk is the only version that outlives a stop.',
127
+ '8. Check packages in one call, and do not install into a temporary library. Name what is',
128
+ ' missing, and when you substitute a gene-set source or an annotation build, say so',
129
+ ' beside the result, because that choice changes the numbers.',
130
+ '9. Say what did not reproduce, and why. When a published count cannot be matched, name',
131
+ ' the undefined choice (a threshold, a direction, a model) and compute the cheap',
132
+ ' alternatives beside your main one instead of tuning until it fits.',
133
+ '10. Report the state you actually reached. Say which outputs exist on disk, which cell',
134
+ ' was still running, and what a stopped turn left behind; the person continues from',
135
+ ' that record rather than from a claim that the work is done.',
136
+ ].join('\n');
137
+ const analysisRunSkill = {
138
+ id: 'rho.analysis-run', version: '1.1', content: analysisRunContent,
139
+ digest: digest(analysisRunContent),
140
+ intent: 'Run a whole analysis through the live sessions so a stop never loses the work.',
141
+ requires: ['kernel.run', 'kernel.read', 'kernel.interrupt', 'resource.list'],
142
+ example: {
143
+ method: 'kernel.run',
144
+ input: {
145
+ language: 'r',
146
+ code: 'counts <- read.csv("data/counts.csv", row.names = 1); dim(counts)',
147
+ purpose: 'Load the count matrix once and keep it for later cells',
148
+ changeId: 'load-counts',
149
+ waitMs: 120000
150
+ }
151
+ },
152
+ failureRecipes: [
153
+ { kind: 'not_found', message: 'Skill is not available in this project and session.', next: 'Read skill.list for the Skills this project and session expose; a Skill owned by a disabled plugin is not listed.' },
154
+ { kind: 'stale', message: 'Skill content digest changed.', next: 'Read the Skill again and apply the current text; do not act on a cached copy.' },
155
+ { kind: 'limit', message: 'Too many cells are waiting.', next: 'Wait for the running cell or interrupt it, then continue in a new cell with a new changeId.' }
156
+ ],
157
+ verify: { command: 'npm run test:notebook', description: 'Runs R and Python cells, keeps their output, plots and variables, interrupts and stops sessions, and keeps identity across a restart.' }
158
+ };
159
+ export const analysisRunSkillDeclaration = analysisRunSkill;
88
160
  const taskMapContent = [
89
161
  'Describe the whole task yourself: Rho fixes no closed list of kinds or states.',
90
162
  '',
@@ -240,7 +312,7 @@ export class Capabilities {
240
312
  }
241
313
  async skills(principal) {
242
314
  const state = await this.state();
243
- const result = [rhoWorkspaceSkill, personFilesSkill, taskMapSkill];
315
+ const result = [rhoWorkspaceSkill, personFilesSkill, taskMapSkill, analysisRunSkill, screenCheckSkill];
244
316
  for (const descriptor of this.plugins.descriptors()) {
245
317
  const status = this.pluginStatus(descriptor.id, state);
246
318
  for (const skill of descriptor.skills ?? []) {
package/dist/cli.js CHANGED
@@ -10,12 +10,39 @@ function openBrowser(target) {
10
10
  const [command, args] = process.platform === 'darwin' ? ['open', [target]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '""', target]] : ['xdg-open', [target]];
11
11
  spawn(command, args, { stdio: 'ignore', detached: true }).on('error', () => process.stderr.write('Could not open a browser. Open openUrl from the output instead.\n')).unref();
12
12
  }
13
+ // A generic ACP agent is any program that speaks ACP v1 over stdio. Its command is either a
14
+ // JSON argv array (safest for paths with spaces) or a simple command line.
15
+ function parseAcpCommand(spec) {
16
+ const text = (spec ?? '').trim();
17
+ if (!text)
18
+ throw new Error('--agent-backend acp needs --acp-command: a JSON argv array such as \'["/usr/bin/node","/path/agent.js"]\', or a command line.');
19
+ if (text.startsWith('[')) {
20
+ let parts;
21
+ try {
22
+ parts = JSON.parse(text);
23
+ }
24
+ catch {
25
+ throw new Error('--acp-command is not valid JSON.');
26
+ }
27
+ if (!Array.isArray(parts) || !parts.length || parts.some(x => typeof x !== 'string'))
28
+ throw new Error('--acp-command JSON must be a non-empty array of strings.');
29
+ return { file: parts[0], args: parts.slice(1) };
30
+ }
31
+ const parts = (text.match(/"[^"]*"|'[^']*'|\S+/g) ?? []).map(p => p.replace(/^(["'])(.*)\1$/, '$2'));
32
+ if (!parts.length)
33
+ throw new Error('--acp-command is empty.');
34
+ return { file: parts[0], args: parts.slice(1) };
35
+ }
13
36
  const usage = `Verify a read-only evidence package:
14
37
  rho --verify-evidence PATH
38
+ Start Rho on ~/Rho/projects and ~/Rho/state, and say where it is and what is missing:
39
+ rho
15
40
  Start Rho:
16
- rho --root PROJECTS_DIRECTORY --state-dir SERVICE_STATE [--core ABSOLUTE_CORE_BINARY] [--rscript ABSOLUTE_RSCRIPT] [--python PYTHON] [--ssh SSH] [--serverctl SERVERCTL] [--claude CLAUDE_CODE_BINARY] [--codex CODEX_BINARY] [--agent-backend claude-acp|codex-acp|claude-cli] [--port 0] [--limits JSON_FILE] [--plugins documents,files,...] [--example on|off] [--open]
41
+ rho --root PROJECTS_DIRECTORY --state-dir SERVICE_STATE [--core ABSOLUTE_CORE_BINARY] [--rscript ABSOLUTE_RSCRIPT] [--python PYTHON] [--ssh SSH] [--serverctl SERVERCTL] [--claude CLAUDE_CODE_BINARY] [--codex CODEX_BINARY] [--agent-backend claude-acp|codex-acp|claude-cli|acp] [--acp-command JSON_ARGV] [--port PORT] [--limits JSON_FILE] [--plugins documents,files,...] [--example on|off] [--open]
17
42
  A narrower browser link (optional):
18
43
  rho --pair PRIVATE_CONNECTION_FILE [--root PROJECTS_RELATIVE_DIRECTORY] [--open]
44
+ Add Rho to your own Claude Code or Codex for one project, as the project page's button does:
45
+ rho --grant PRIVATE_CONNECTION_FILE --project PROJECTS_RELATIVE_DIRECTORY --agent claude-code|codex
19
46
  Give an Agent access to one project, and the command that adds it to Claude Code:
20
47
  rho --grant PRIVATE_CONNECTION_FILE --project PROJECTS_RELATIVE_DIRECTORY [--session NAME]
21
48
  Serve that access to the Agent over MCP (stdio):
@@ -24,13 +51,27 @@ Observe an external ACP client (use this as its agent command):
24
51
  rho --acp /absolute/project --acp-agent claude|codex
25
52
  --example off starts Rho without the project it ships; a new state otherwise gets that project
26
53
  once, copied into the projects directory. --open opens the printed url in the default browser.
54
+ Without --port a start takes the port this state's last start had, and a new one only when that
55
+ port is taken; --port 0 always takes a new one.
27
56
  Opening that url on this machine needs no link, code or session credential; --pair makes a
28
57
  narrower, expiring link for one folder.
29
58
  PRIVATE_CONNECTION_FILE is the connectionFile the running service printed; it and every
30
59
  connection file are credentials.
31
60
  `;
32
- const options = { example: { type: 'string' }, 'verify-evidence': { type: 'string' }, acp: { type: 'string' }, 'acp-agent': { type: 'string' }, root: { type: 'string' }, 'state-dir': { type: 'string' }, core: { type: 'string' }, rscript: { type: 'string' }, python: { type: 'string' }, ssh: { type: 'string' }, serverctl: { type: 'string' }, claude: { type: 'string' }, codex: { type: 'string' }, 'agent-backend': { type: 'string' }, port: { type: 'string' }, limits: { type: 'string' }, plugins: { type: 'string' }, pair: { type: 'string' }, open: { type: 'boolean' },
33
- grant: { type: 'string' }, project: { type: 'string' }, session: { type: 'string' }, mcp: { type: 'string' }, help: { type: 'boolean', short: 'h' } };
61
+ const options = { example: { type: 'string' }, 'verify-evidence': { type: 'string' }, acp: { type: 'string' }, 'acp-agent': { type: 'string' }, root: { type: 'string' }, 'state-dir': { type: 'string' }, core: { type: 'string' }, rscript: { type: 'string' }, python: { type: 'string' }, ssh: { type: 'string' }, serverctl: { type: 'string' }, claude: { type: 'string' }, codex: { type: 'string' }, 'agent-backend': { type: 'string' }, 'acp-command': { type: 'string' }, port: { type: 'string' }, limits: { type: 'string' }, plugins: { type: 'string' }, pair: { type: 'string' }, open: { type: 'boolean' },
62
+ grant: { type: 'string' }, project: { type: 'string' }, session: { type: 'string' }, agent: { type: 'string' }, mcp: { type: 'string' }, help: { type: 'boolean', short: 'h' } };
63
+ // A command that is refused says why and what to do. A service that is not running is the
64
+ // common case for --grant and --pair; anything without a known reason keeps its stack.
65
+ function refuse(error, url) {
66
+ const e = error;
67
+ if (e?.cause?.code === 'ECONNREFUSED' && url)
68
+ process.stderr.write(`Rho is not running at ${url}. Start it with the same --state-dir, then run this again.\n`);
69
+ else if (e?.error?.message || e?.kind || e?.syscall)
70
+ process.stderr.write((e.error?.message ?? e.message) + '\n');
71
+ else
72
+ throw error;
73
+ process.exit(1);
74
+ }
34
75
  let parsed;
35
76
  try {
36
77
  parsed = parseArgs({ options });
@@ -77,40 +118,84 @@ else {
77
118
  process.stderr.write('--grant needs --project, the project folder relative to the service root.\n\n' + usage);
78
119
  process.exit(2);
79
120
  }
121
+ if (values.agent !== undefined && !['claude-code', 'codex'].includes(values.agent)) {
122
+ process.stderr.write('Choose --agent claude-code or codex.\n');
123
+ process.exit(2);
124
+ }
125
+ if (values.agent !== undefined && values.session !== undefined) {
126
+ process.stderr.write('--agent names the session itself; leave out --session.\n');
127
+ process.exit(2);
128
+ }
80
129
  const { RhoClient } = await import('./client.js');
81
- const client = await RhoClient.fromFile(path.resolve(values.grant));
82
- const project = await client.call('project.open', { path: values.project });
83
- const session = values.session ?? 'claude-code';
84
- const granted = await client.call('session.grant', { projectId: project.id, externalSessionId: session });
85
- const { mcpCommand, mcpName } = await import('./agent-connect.js');
86
- const command = mcpCommand(granted.connectionFile), name = mcpName(project.root);
87
- const quote = (value) => /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
88
- process.stdout.write(JSON.stringify({ projectId: project.id, session, connectionFile: granted.connectionFile, methods: granted.methods.length,
89
- mcp: { name, command: command[0], args: command.slice(1) }, claude: ['claude', 'mcp', 'add', name, '--', ...command].map(quote).join(' ') }) + '\n');
90
- process.exit(0);
130
+ const client = await RhoClient.fromFile(path.resolve(values.grant)).catch(error => refuse(error));
131
+ try {
132
+ const project = await client.call('project.open', { path: values.project });
133
+ // The same as the project page's button: the grant that Agent keeps, then its own
134
+ // `mcp add`, recorded so the page shows it under that Agent.
135
+ if (values.agent) {
136
+ const connected = await client.call('session.connect', { projectId: project.id, agent: values.agent });
137
+ process.stdout.write(JSON.stringify({ projectId: project.id, root: project.root, agent: values.agent, methods: connected.methods, granted: connected.granted, added: connected.added }) + '\n');
138
+ process.exit(0);
139
+ }
140
+ const session = values.session ?? 'claude-code';
141
+ const granted = await client.call('session.grant', { projectId: project.id, externalSessionId: session });
142
+ const { mcpCommand, mcpName } = await import('./agent-connect.js');
143
+ const command = mcpCommand(granted.connectionFile), name = mcpName(project.root);
144
+ const quote = (value) => /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
145
+ process.stdout.write(JSON.stringify({ projectId: project.id, session, connectionFile: granted.connectionFile, methods: granted.methods.length,
146
+ mcp: { name, command: command[0], args: command.slice(1) }, claude: ['claude', 'mcp', 'add', name, '--', ...command].map(quote).join(' ') }) + '\n');
147
+ process.exit(0);
148
+ }
149
+ catch (error) {
150
+ refuse(error, client.connection.url);
151
+ }
91
152
  }
92
153
  if (values.pair) {
93
154
  const { RhoClient } = await import('./client.js');
94
- const client = await RhoClient.fromFile(path.resolve(values.pair));
95
- const pairing = await client.call('session.browser.pair', values.root ? { path: values.root } : {});
96
- const openUrl = link(client.connection.url, pairing.code);
97
- process.stdout.write(JSON.stringify({ url: client.connection.url, openUrl, pairingCode: pairing.code, pairingExpiresAt: pairing.expiresAt }) + '\n');
98
- if (values.open)
99
- openBrowser(openUrl);
100
- process.exit(0);
155
+ const client = await RhoClient.fromFile(path.resolve(values.pair)).catch(error => refuse(error));
156
+ try {
157
+ const pairing = await client.call('session.browser.pair', values.root ? { path: values.root } : {});
158
+ const openUrl = link(client.connection.url, pairing.code);
159
+ process.stdout.write(JSON.stringify({ url: client.connection.url, openUrl, pairingCode: pairing.code, pairingExpiresAt: pairing.expiresAt }) + '\n');
160
+ if (values.open)
161
+ openBrowser(openUrl);
162
+ process.exit(0);
163
+ }
164
+ catch (error) {
165
+ refuse(error, client.connection.url);
166
+ }
101
167
  }
102
- if (!values.root || !values['state-dir']) {
168
+ // A person who runs `npx @rho-workspace/rho` with no arguments wants the application, not a
169
+ // usage page: start it on their own folders, say in plain words where it is, and open it when
170
+ // this is an interactive terminal. Every flagged start is unchanged, so scripts and
171
+ // acceptance runs keep the output they read.
172
+ const quickStart = !values.root && !values['state-dir'];
173
+ if (!quickStart && (!values.root || !values['state-dir'])) {
103
174
  process.stderr.write(usage);
104
175
  process.exit(2);
105
176
  }
177
+ let quickRoot = '', quickState = '';
178
+ if (quickStart) {
179
+ const { homedir } = await import('node:os');
180
+ quickRoot = path.join(homedir(), 'Rho', 'projects');
181
+ quickState = path.join(homedir(), 'Rho', 'state');
182
+ await fs.mkdir(quickRoot, { recursive: true });
183
+ await fs.mkdir(quickState, { recursive: true });
184
+ }
185
+ const projectsRoot = values.root ?? quickRoot, stateDirectory = values['state-dir'] ?? quickState;
106
186
  const limits = values.limits ? JSON.parse(await fs.readFile(values.limits, 'utf8')) : {};
107
187
  const allowed = new Set(['maxRequestBytes', 'maxEventBytes', 'eventRetention', 'maxSubscriptions', 'maxQueue', 'maxContexts', 'maxBindings', 'maxActivities', 'maxContextBytes', 'maxRetainedMetadataBytes', 'maxConcurrentActions', 'maxOperations', 'maxLogPageBytes', 'maxCodeBytes', 'maxReconciled', 'documents', 'resources', 'workspace', 'r', 'annotations', 'interactions', 'capabilityLimits', 'browser', 'nativeFolders', 'conversations', 'evidence', 'console']);
108
188
  if (Object.keys(limits).some(k => !allowed.has(k)))
109
189
  throw new Error('Unknown service budget in --limits file.');
110
190
  const { RhoService } = await import('./server.js'), { runtime } = await import('./runtime.js');
111
191
  const agentBackend = values['agent-backend'] ?? process.env.RHO_AGENT_BACKEND ?? 'claude-acp';
112
- if (!['claude-acp', 'codex-acp', 'claude-cli'].includes(agentBackend))
113
- throw new Error('Choose --agent-backend claude-acp, codex-acp or claude-cli.');
192
+ if (!['claude-acp', 'codex-acp', 'claude-cli', 'acp'].includes(agentBackend))
193
+ throw new Error('Choose --agent-backend claude-acp, codex-acp, claude-cli or acp.');
194
+ const acpCommand = agentBackend === 'acp' ? parseAcpCommand(values['acp-command'] ?? process.env.RHO_ACP_COMMAND) : undefined;
195
+ if (values.port !== undefined && !(/^\d+$/.test(values.port) && Number(values.port) <= 65535)) {
196
+ process.stderr.write('Choose --port 0 to 65535, or leave it out.\n');
197
+ process.exit(2);
198
+ }
114
199
  // A Core that cannot be used (none for this platform, a changed digest) is read by a person.
115
200
  const native = await runtime(values.core, values.rscript).catch(e => { process.stderr.write((e instanceof Error ? e.message : String(e)) + '\n'); process.exit(1); });
116
201
  // A person starting Rho sees the project it ships with; a composition that starts a service
@@ -118,7 +203,12 @@ else {
118
203
  if (values.example !== undefined && !['on', 'off'].includes(values.example))
119
204
  throw new Error('Choose --example on or off.');
120
205
  const example = values.example !== 'off';
121
- const service = await new RhoService({ example, ...limits, root: path.resolve(values.root), stateDir: path.resolve(values['state-dir']), ...native, agentBackend: agentBackend, codex: values.codex ?? process.env.RHO_CODEX ?? 'codex', claude: values.claude ?? process.env.RHO_CLAUDE ?? 'claude', python: values.python ?? process.env.RHO_PYTHON, ssh: values.ssh ?? process.env.RHO_SSH, serverctl: values.serverctl ?? process.env.RHO_SERVERCTL, port: Number(values.port ?? 0), ...(values.plugins !== undefined ? { plugins: values.plugins ? values.plugins.split(',') : [] } : {}) }).start();
206
+ const service = await new RhoService({ example, ...limits, root: path.resolve(projectsRoot), stateDir: path.resolve(stateDirectory), ...native, agentBackend: agentBackend, acpCommand, codex: values.codex ?? process.env.RHO_CODEX ?? 'codex', claude: values.claude ?? process.env.RHO_CLAUDE ?? 'claude', python: values.python ?? process.env.RHO_PYTHON, ssh: values.ssh ?? process.env.RHO_SSH, serverctl: values.serverctl ?? process.env.RHO_SERVERCTL, port: values.port === undefined ? undefined : Number(values.port), ...(values.plugins !== undefined ? { plugins: values.plugins ? values.plugins.split(',') : [] } : {}) }).start().catch(async (error) => {
207
+ // Starting Rho again on a state it already runs on is the common refusal: say where it runs.
208
+ const held = error?.details?.stateDirectory;
209
+ const running = typeof held === 'string' ? await fs.readFile(path.join(held, 'connection.json'), 'utf8').then(text => String(JSON.parse(text).url), () => '') : '';
210
+ return refuse(running ? Object.assign(error, { message: `Rho is already running for ${held} at ${running}. Open that url, or stop that service before starting it again.` }) : error);
211
+ });
122
212
  // The narrow conclusion a person needs on one line: what was selected, where state
123
213
  // lives, and how to open it. It stays inside `ready` so a parser reading line one is
124
214
  // unaffected. Opening this url on this machine is the access; no link is minted.
@@ -127,11 +217,46 @@ else {
127
217
  url: service.url, connectionFile: path.join(service.state.directory, 'connection.json'), apiVersion: '2.0',
128
218
  openUrl: service.url,
129
219
  startup: {
130
- projects: path.resolve(values.root), state: path.resolve(values['state-dir']),
220
+ projects: path.resolve(projectsRoot), state: path.resolve(stateDirectory),
131
221
  core: service.bridge.binary, rscript: service.bridge.rscript,
132
222
  note: 'Open this url in a browser on this machine. Execution stays off until an Agent submits a run; project.doctor answers what is missing.'
133
223
  }
134
224
  } }) + '\n');
225
+ // What a person needs, after the machine line: where it is, and what is missing to work in it.
226
+ // A missing Agent or R does not stop the workspace; it decides what the person can do next.
227
+ if (quickStart) {
228
+ const say = (text) => process.stdout.write(text + '\n');
229
+ say('');
230
+ say('Rho is running. Open this address in a browser on this machine:');
231
+ say(' ' + service.url);
232
+ say('');
233
+ say('Projects folder: ' + quickRoot);
234
+ say('Service state: ' + quickState);
235
+ say('');
236
+ const agentCommand = await import('@rho-workspace/plugin-agent/acp-command').catch(() => null);
237
+ // The selection the service itself got: an explicit flag, then the environment, then the name
238
+ // on PATH. Reporting "installed" for a client the person pointed at a missing path is worse
239
+ // than saying nothing.
240
+ const claudePath = values.claude ?? process.env.RHO_CLAUDE, codexPath = values.codex ?? process.env.RHO_CODEX;
241
+ const env = { ...process.env, ...(claudePath ? { CLAUDE_CODE_EXECUTABLE: claudePath } : {}), ...(codexPath ? { CODEX_PATH: codexPath } : {}) };
242
+ // An ACP agent named with --acp-command is the operator's own command; it is named, not checked.
243
+ if (acpCommand)
244
+ say('Agent: ' + acpCommand.file + ', named with --acp-command.');
245
+ else {
246
+ const missing = agentCommand ? agentCommand.missingClient(agentBackend === 'codex-acp' ? 'codex' : 'claude', env) : null;
247
+ say(missing
248
+ ? 'Agent: ' + missing + ' Until then folders can be opened and files read here; the Agent cannot run in them.'
249
+ : 'Agent: ' + (agentBackend === 'codex-acp' ? 'Codex' : 'Claude Code') + ' is installed; a conversation can start in any project.');
250
+ }
251
+ const hasR = await fs.access(service.bridge.rscript).then(() => true, () => false);
252
+ say(hasR
253
+ ? 'R: ' + service.bridge.rscript
254
+ : 'R: Rscript was not found. Reading, editing and viewing work; running R needs R installed (or --rscript).');
255
+ say('');
256
+ say('Stop it with Ctrl+C. Start it again with: npx @rho-workspace/rho');
257
+ if (process.stdout.isTTY)
258
+ openBrowser(service.url);
259
+ }
135
260
  if (values.open)
136
261
  openBrowser(service.url);
137
262
  let closing = false;