@xingular/harness 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (397) hide show
  1. package/CHANGELOG.md +46 -5
  2. package/README.md +75 -21
  3. package/dist/cli.js +17421 -8711
  4. package/dist/sdk/accounts/accounts.d.ts +27 -3
  5. package/dist/sdk/accounts/accounts.js +78 -24
  6. package/dist/sdk/accounts/secrets.d.ts +12 -0
  7. package/dist/sdk/accounts/secrets.js +114 -7
  8. package/dist/sdk/accounts/store.d.ts +26 -1
  9. package/dist/sdk/accounts/store.js +1 -1
  10. package/dist/sdk/agents/builtin.d.ts +2 -0
  11. package/dist/sdk/agents/builtin.js +48 -0
  12. package/dist/sdk/agents/narrow.d.ts +13 -0
  13. package/dist/sdk/agents/narrow.js +29 -0
  14. package/dist/sdk/agents/planResearch.d.ts +5 -0
  15. package/dist/sdk/agents/planResearch.js +19 -0
  16. package/dist/sdk/agents/profiles.d.ts +81 -0
  17. package/dist/sdk/agents/profiles.js +226 -0
  18. package/dist/sdk/annotations/bridge.d.ts +42 -0
  19. package/dist/sdk/annotations/bridge.js +264 -0
  20. package/dist/sdk/annotations/capture.d.ts +116 -0
  21. package/dist/sdk/annotations/capture.js +308 -0
  22. package/dist/sdk/annotations/delivery.d.ts +72 -0
  23. package/dist/sdk/annotations/delivery.js +130 -0
  24. package/dist/sdk/annotations/inspect.d.ts +10 -0
  25. package/dist/sdk/annotations/inspect.js +33 -0
  26. package/dist/sdk/annotations/isolated.d.ts +6 -0
  27. package/dist/sdk/annotations/isolated.js +51 -0
  28. package/dist/sdk/annotations/pageScript.d.ts +28 -0
  29. package/dist/sdk/annotations/pageScript.js +871 -0
  30. package/dist/sdk/annotations/part.d.ts +10 -0
  31. package/dist/sdk/annotations/part.js +155 -0
  32. package/dist/sdk/annotations/queue.d.ts +4 -0
  33. package/dist/sdk/annotations/queue.js +40 -0
  34. package/dist/sdk/annotations/serverMethods.d.ts +3 -0
  35. package/dist/sdk/annotations/serverMethods.js +62 -0
  36. package/dist/sdk/annotations/store.d.ts +167 -0
  37. package/dist/sdk/annotations/store.js +394 -0
  38. package/dist/sdk/browser/backend.d.ts +16 -0
  39. package/dist/sdk/browser/backend.js +17 -0
  40. package/dist/sdk/browser/contracts.d.ts +19 -0
  41. package/dist/sdk/browser/contracts.js +116 -0
  42. package/dist/sdk/browser/index.d.ts +14 -3
  43. package/dist/sdk/browser/index.js +11 -4
  44. package/dist/sdk/browser/serverMethods.d.ts +3 -0
  45. package/dist/sdk/browser/serverMethods.js +7 -0
  46. package/dist/sdk/browser/session.d.ts +1 -0
  47. package/dist/sdk/browser/session.js +45 -9
  48. package/dist/sdk/browser/tools.d.ts +1 -4
  49. package/dist/sdk/browser/tools.js +14 -91
  50. package/dist/sdk/cli.js +24 -15
  51. package/dist/sdk/commands/list.d.ts +3 -0
  52. package/dist/sdk/commands/list.js +75 -0
  53. package/dist/sdk/commands/registry.d.ts +250 -0
  54. package/dist/sdk/commands/registry.js +47 -0
  55. package/dist/sdk/commands/routes.d.ts +16 -0
  56. package/dist/sdk/commands/routes.js +33 -0
  57. package/dist/sdk/core/bus.js +1 -1
  58. package/dist/sdk/core/commandBudget.d.ts +23 -0
  59. package/dist/sdk/core/commandBudget.js +57 -0
  60. package/dist/sdk/core/env.d.ts +1 -1
  61. package/dist/sdk/core/env.js +1 -1
  62. package/dist/sdk/core/index.js +17 -1
  63. package/dist/sdk/core/interactiveProfile.d.ts +46 -0
  64. package/dist/sdk/core/interactiveProfile.js +143 -0
  65. package/dist/sdk/core/workspace.d.ts +13 -0
  66. package/dist/sdk/core/workspace.js +88 -0
  67. package/dist/sdk/core/workspaceBashTool.d.ts +4 -0
  68. package/dist/sdk/core/workspaceBashTool.js +36 -0
  69. package/dist/sdk/core/workspaceSandbox.d.ts +31 -0
  70. package/dist/sdk/core/workspaceSandbox.js +320 -0
  71. package/dist/sdk/core/workspaceWriteOperations.d.ts +22 -0
  72. package/dist/sdk/core/workspaceWriteOperations.js +206 -0
  73. package/dist/sdk/doctor/serverMethods.d.ts +3 -0
  74. package/dist/sdk/doctor/serverMethods.js +8 -0
  75. package/dist/sdk/doctor/types.d.ts +2 -2
  76. package/dist/sdk/harness.d.ts +22 -0
  77. package/dist/sdk/harness.js +84 -5
  78. package/dist/sdk/history/recall.js +8 -8
  79. package/dist/sdk/history/serverMethods.d.ts +3 -0
  80. package/dist/sdk/history/serverMethods.js +8 -0
  81. package/dist/sdk/hub/cli.d.ts +1 -1
  82. package/dist/sdk/hub/cli.js +1 -1
  83. package/dist/sdk/hub/schema.d.ts +1 -1
  84. package/dist/sdk/hub/serverMethods.d.ts +3 -0
  85. package/dist/sdk/hub/serverMethods.js +11 -0
  86. package/dist/sdk/index.d.ts +16 -1
  87. package/dist/sdk/index.js +10 -0
  88. package/dist/sdk/mcp/connections.js +14 -7
  89. package/dist/sdk/mcp/serverMethods.d.ts +3 -0
  90. package/dist/sdk/mcp/serverMethods.js +22 -0
  91. package/dist/sdk/pi/runtime.d.ts +2 -1
  92. package/dist/sdk/pi/runtime.js +2 -2
  93. package/dist/sdk/plugins/builtin.d.ts +1 -0
  94. package/dist/sdk/plugins/builtin.js +145 -33
  95. package/dist/sdk/plugins/builtinRegistry.d.ts +60 -0
  96. package/dist/sdk/plugins/builtinRegistry.js +71 -0
  97. package/dist/sdk/plugins/catalogue.js +2 -1
  98. package/dist/sdk/plugins/cli.d.ts +1 -1
  99. package/dist/sdk/plugins/cli.js +4 -3
  100. package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
  101. package/dist/sdk/plugins/commandPrefix.js +21 -0
  102. package/dist/sdk/plugins/firstParty.d.ts +1 -1
  103. package/dist/sdk/plugins/firstParty.js +8 -2
  104. package/dist/sdk/plugins/inventory.js +4 -3
  105. package/dist/sdk/plugins/manifest.d.ts +2 -0
  106. package/dist/sdk/plugins/manifest.js +2 -1
  107. package/dist/sdk/plugins/surfaces.d.ts +5 -0
  108. package/dist/sdk/plugins/surfaces.js +11 -0
  109. package/dist/sdk/plugins/types.d.ts +15 -0
  110. package/dist/sdk/prompt/compile.d.ts +19 -2
  111. package/dist/sdk/prompt/compile.js +79 -26
  112. package/dist/sdk/prompt/standard.d.ts +17 -0
  113. package/dist/sdk/prompt/standard.js +21 -0
  114. package/dist/sdk/refine/cli.d.ts +1 -1
  115. package/dist/sdk/refine/cli.js +2 -2
  116. package/dist/sdk/refine/serverMethods.d.ts +3 -0
  117. package/dist/sdk/refine/serverMethods.js +20 -0
  118. package/dist/sdk/server/methods.js +27 -86
  119. package/dist/sdk/server/params.d.ts +4 -0
  120. package/dist/sdk/server/params.js +11 -0
  121. package/dist/sdk/server/remote.js +36 -11
  122. package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
  123. package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
  124. package/dist/sdk/sessions/continuation.d.ts +3 -0
  125. package/dist/sdk/sessions/continuation.js +5 -1
  126. package/dist/sdk/sessions/contracts.d.ts +185 -0
  127. package/dist/sdk/sessions/contracts.js +101 -4
  128. package/dist/sdk/sessions/control.d.ts +72 -0
  129. package/dist/sdk/sessions/control.js +158 -0
  130. package/dist/sdk/sessions/initialBash.d.ts +7 -0
  131. package/dist/sdk/sessions/initialBash.js +7 -0
  132. package/dist/sdk/sessions/planMode.d.ts +18 -4
  133. package/dist/sdk/sessions/planMode.js +82 -16
  134. package/dist/sdk/sessions/reasoningSummaries.d.ts +2 -2
  135. package/dist/sdk/sessions/reasoningSummaries.js +3 -3
  136. package/dist/sdk/sessions/registry.d.ts +2 -1
  137. package/dist/sdk/sessions/registry.js +6 -3
  138. package/dist/sdk/sessions/runIndex.js +7 -7
  139. package/dist/sdk/sessions/runStore.d.ts +14 -0
  140. package/dist/sdk/sessions/runStore.js +38 -1
  141. package/dist/sdk/sessions/runner.d.ts +40 -2
  142. package/dist/sdk/sessions/runner.js +163 -15
  143. package/dist/sdk/sessions/sessions.d.ts +40 -8
  144. package/dist/sdk/sessions/sessions.js +279 -27
  145. package/dist/sdk/sessions/sideQuestionServerMethods.d.ts +3 -0
  146. package/dist/sdk/sessions/sideQuestionServerMethods.js +19 -0
  147. package/dist/sdk/sessions/sideQuestions.d.ts +66 -0
  148. package/dist/sdk/sessions/sideQuestions.js +290 -0
  149. package/dist/sdk/sessions/subagentServerMethods.d.ts +3 -0
  150. package/dist/sdk/sessions/subagentServerMethods.js +41 -0
  151. package/dist/sdk/sessions/subagentService.d.ts +22 -2
  152. package/dist/sdk/sessions/subagentService.js +93 -16
  153. package/dist/sdk/sessions/subagents.d.ts +9 -1
  154. package/dist/sdk/sessions/subagents.js +19 -0
  155. package/dist/sdk/sessions/workSlice.d.ts +103 -0
  156. package/dist/sdk/sessions/workSlice.js +297 -0
  157. package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
  158. package/dist/sdk/sessions/workspaceBoundary.js +46 -0
  159. package/dist/sdk/terminal/browser.d.ts +62 -0
  160. package/dist/sdk/terminal/browser.js +41 -0
  161. package/dist/sdk/terminal/browserControl.d.ts +29 -0
  162. package/dist/sdk/terminal/browserControl.js +187 -0
  163. package/dist/sdk/terminal/commandHelp.d.ts +17 -0
  164. package/dist/sdk/terminal/commandHelp.js +20 -0
  165. package/dist/sdk/terminal/controller.d.ts +15 -1
  166. package/dist/sdk/terminal/controller.js +67 -8
  167. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +28 -0
  168. package/dist/sdk/terminal/opentui/subagentsWidget.js +52 -0
  169. package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
  170. package/dist/sdk/terminal/opentui/theme.js +87 -0
  171. package/dist/sdk/terminal/piExtension.d.ts +6 -1
  172. package/dist/sdk/terminal/piExtension.js +169 -84
  173. package/dist/sdk/terminal/socket.d.ts +6 -3
  174. package/dist/sdk/terminal/socket.js +40 -9
  175. package/dist/sdk/terminal/standalone.d.ts +9 -0
  176. package/dist/sdk/terminal/standalone.js +118 -19
  177. package/dist/sdk/terminal/subagentMonitor.d.ts +47 -0
  178. package/dist/sdk/terminal/subagentMonitor.js +152 -0
  179. package/dist/sdk/terminal/terminals.d.ts +60 -7
  180. package/dist/sdk/terminal/terminals.js +598 -47
  181. package/dist/sdk/terminal/trajectory.js +2 -2
  182. package/dist/sdk/terminal/tui/annotateCommand.d.ts +3 -0
  183. package/dist/sdk/terminal/tui/annotateCommand.js +22 -0
  184. package/dist/sdk/terminal/tui/commandPages.d.ts +7 -1
  185. package/dist/sdk/terminal/tui/commandPages.js +57 -6
  186. package/dist/sdk/terminal/tui/commands.d.ts +10 -4
  187. package/dist/sdk/terminal/tui/commands.js +269 -65
  188. package/dist/sdk/terminal/tui/dialogGeometry.d.ts +2 -2
  189. package/dist/sdk/terminal/tui/dialogGeometry.js +4 -2
  190. package/dist/sdk/terminal/tui/dialogs.d.ts +6 -1
  191. package/dist/sdk/terminal/tui/dialogs.js +9 -2
  192. package/dist/sdk/terminal/tui/list.d.ts +14 -1
  193. package/dist/sdk/terminal/tui/list.js +96 -14
  194. package/dist/sdk/terminal/tui/modelPicker.js +1 -6
  195. package/dist/sdk/terminal/tui/page.d.ts +3 -0
  196. package/dist/sdk/terminal/tui/pageOverlay.js +27 -7
  197. package/dist/sdk/terminal/tui/pages/accountsPage.d.ts +26 -4
  198. package/dist/sdk/terminal/tui/pages/accountsPage.js +264 -36
  199. package/dist/sdk/terminal/tui/pages/agentsPage.d.ts +74 -0
  200. package/dist/sdk/terminal/tui/pages/agentsPage.js +190 -0
  201. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +94 -0
  202. package/dist/sdk/terminal/tui/pages/annotatePage.js +341 -0
  203. package/dist/sdk/terminal/tui/pages/browserPage.d.ts +87 -0
  204. package/dist/sdk/terminal/tui/pages/browserPage.js +738 -0
  205. package/dist/sdk/terminal/tui/pages/doctorPage.d.ts +2 -0
  206. package/dist/sdk/terminal/tui/pages/doctorPage.js +103 -15
  207. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +75 -0
  208. package/dist/sdk/terminal/tui/pages/notesPage.js +201 -0
  209. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +0 -5
  210. package/dist/sdk/terminal/tui/pages/pluginsPage.js +9 -3
  211. package/dist/sdk/terminal/tui/pages/settingsPage.d.ts +8 -0
  212. package/dist/sdk/terminal/tui/pages/settingsPage.js +16 -1
  213. package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +24 -0
  214. package/dist/sdk/terminal/tui/pages/subagentsPage.js +152 -0
  215. package/dist/sdk/terminal/tui/pages/trajectoryPage.d.ts +12 -5
  216. package/dist/sdk/terminal/tui/pages/trajectoryPage.js +156 -83
  217. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
  218. package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
  219. package/dist/sdk/terminal/tui/pages/worktreesPage.d.ts +72 -0
  220. package/dist/sdk/terminal/tui/pages/worktreesPage.js +203 -0
  221. package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
  222. package/dist/sdk/terminal/tui/piAdapter.js +3 -5
  223. package/dist/sdk/terminal/tui/planMode.d.ts +1 -1
  224. package/dist/sdk/terminal/tui/planMode.js +1 -1
  225. package/dist/sdk/terminal/tui/pluginCommands.d.ts +16 -0
  226. package/dist/sdk/terminal/tui/pluginCommands.js +31 -0
  227. package/dist/sdk/terminal/tui/questions.d.ts +2 -0
  228. package/dist/sdk/terminal/tui/runtime.js +27 -5
  229. package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
  230. package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
  231. package/dist/sdk/terminal/tui/settingsModel.d.ts +2 -0
  232. package/dist/sdk/terminal/tui/settingsModel.js +32 -2
  233. package/dist/sdk/terminal/tui/sideQuestion.d.ts +26 -26
  234. package/dist/sdk/terminal/tui/sideQuestion.js +131 -98
  235. package/dist/sdk/terminal/tui/ui.d.ts +2 -0
  236. package/dist/sdk/terminal/tui/ui.js +1 -0
  237. package/dist/sdk/tools/askUser.js +26 -8
  238. package/dist/sdk/tools/checklist.js +3 -2
  239. package/dist/sdk/tools/checkpoint.d.ts +27 -0
  240. package/dist/sdk/tools/checkpoint.js +154 -0
  241. package/dist/sdk/tools/getCapabilities.d.ts +31 -0
  242. package/dist/sdk/tools/getCapabilities.js +34 -0
  243. package/dist/sdk/tools/harnessTools.d.ts +2 -0
  244. package/dist/sdk/tools/harnessTools.js +1 -1
  245. package/dist/sdk/tools/spawnSubagent.js +14 -4
  246. package/dist/sdk/usage/analytics.d.ts +20 -0
  247. package/dist/sdk/usage/analytics.js +151 -0
  248. package/dist/sdk/usage/ledger.d.ts +38 -0
  249. package/dist/sdk/usage/ledger.js +142 -0
  250. package/dist/sdk/usage/limits/adapters.d.ts +11 -0
  251. package/dist/sdk/usage/limits/adapters.js +349 -0
  252. package/dist/sdk/usage/limits/capture.d.ts +8 -0
  253. package/dist/sdk/usage/limits/capture.js +47 -0
  254. package/dist/sdk/usage/limits/http.d.ts +12 -0
  255. package/dist/sdk/usage/limits/http.js +51 -0
  256. package/dist/sdk/usage/limits/normalize.d.ts +15 -0
  257. package/dist/sdk/usage/limits/normalize.js +211 -0
  258. package/dist/sdk/usage/limits/service.d.ts +30 -0
  259. package/dist/sdk/usage/limits/service.js +176 -0
  260. package/dist/sdk/usage/limits/types.d.ts +65 -0
  261. package/dist/sdk/usage/limits/types.js +1 -0
  262. package/dist/sdk/usage/serverMethods.d.ts +3 -0
  263. package/dist/sdk/usage/serverMethods.js +22 -0
  264. package/dist/sdk/usage/service.d.ts +32 -0
  265. package/dist/sdk/usage/service.js +66 -0
  266. package/dist/sdk/usage/settings.d.ts +7 -0
  267. package/dist/sdk/usage/settings.js +28 -0
  268. package/dist/sdk/usage/types.d.ts +60 -0
  269. package/dist/sdk/usage/types.js +4 -0
  270. package/dist/sdk/web/part.d.ts +10 -0
  271. package/dist/sdk/web/part.js +6 -0
  272. package/dist/sdk/web/serverMethods.d.ts +3 -0
  273. package/dist/sdk/web/serverMethods.js +7 -0
  274. package/dist/sdk/wiki/serverMethods.d.ts +3 -0
  275. package/dist/sdk/wiki/serverMethods.js +26 -0
  276. package/dist/sdk/worktrees/cli.d.ts +11 -0
  277. package/dist/sdk/worktrees/cli.js +146 -0
  278. package/dist/sdk/worktrees/copy.d.ts +9 -0
  279. package/dist/sdk/worktrees/copy.js +111 -0
  280. package/dist/sdk/worktrees/git.d.ts +16 -0
  281. package/dist/sdk/worktrees/git.js +81 -0
  282. package/dist/sdk/worktrees/serverMethods.d.ts +3 -0
  283. package/dist/sdk/worktrees/serverMethods.js +30 -0
  284. package/dist/sdk/worktrees/service.d.ts +73 -0
  285. package/dist/sdk/worktrees/service.js +483 -0
  286. package/dist/sdk/worktrees/setup.d.ts +23 -0
  287. package/dist/sdk/worktrees/setup.js +65 -0
  288. package/dist/sdk/worktrees/types.d.ts +91 -0
  289. package/dist/sdk/worktrees/types.js +1 -0
  290. package/dist/sdk/world/cli.d.ts +2 -2
  291. package/dist/sdk/world/cli.js +6 -6
  292. package/dist/sdk/world/world.d.ts +3 -1
  293. package/dist/sdk/world/world.js +8 -9
  294. package/dist/terminal-opentui.js +16456 -5966
  295. package/dist/terminal-pi.js +1579 -573
  296. package/docs/README.md +22 -2
  297. package/docs/architecture/00-overview.md +28 -16
  298. package/docs/architecture/02-sessions-and-runs.md +184 -25
  299. package/docs/architecture/03-variants.md +10 -2
  300. package/docs/architecture/04-prompt.md +65 -7
  301. package/docs/architecture/06-plugins-and-capabilities.md +57 -4
  302. package/docs/architecture/07-tools-and-gate.md +112 -17
  303. package/docs/architecture/08-memory-and-history.md +4 -2
  304. package/docs/architecture/09-accounts-and-connectors.md +39 -1
  305. package/docs/architecture/10-server-protocol.md +168 -4
  306. package/docs/architecture/11-terminal.md +251 -8
  307. package/docs/architecture/12-storage.md +45 -3
  308. package/docs/architecture/13-world-tools.md +49 -6
  309. package/docs/architecture/14-surfaces-and-parity.md +136 -0
  310. package/docs/architecture/README.md +17 -4
  311. package/docs/architecture/proposals/PROP-0006-run-checklist.md +4 -2
  312. package/docs/architecture/proposals/PROP-0013-resource-browser.md +58 -0
  313. package/docs/architecture/proposals/PROP-0014-fullscreen-browser-tabs.md +58 -0
  314. package/docs/architecture/proposals/PROP-0015-one-capability-surface.md +88 -0
  315. package/docs/architecture/proposals/PROP-0016-managed-worktrees.md +156 -0
  316. package/docs/architecture/proposals/PROP-0017-page-annotations.md +131 -0
  317. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +179 -0
  318. package/docs/architecture/proposals/PROP-0019-contextual-side-questions.md +122 -0
  319. package/docs/architecture/proposals/PROP-0020-builtin-feature-plugins.md +105 -0
  320. package/docs/architecture/proposals/PROP-0021-command-namespaces-and-duplicates.md +99 -0
  321. package/docs/architecture/proposals/PROP-0022-plan-only-questions.md +102 -0
  322. package/docs/architecture/proposals/PROP-0023-xagent-cli-name.md +61 -0
  323. package/docs/architecture/proposals/PROP-0024-question-sketch-previews.md +130 -0
  324. package/docs/architecture/proposals/PROP-0025-contextual-question-wireframes.md +49 -0
  325. package/docs/architecture/proposals/PROP-0026-connected-sketch-display.md +94 -0
  326. package/docs/architecture/proposals/PROP-0027-xingular-agent-product-identity.md +60 -0
  327. package/docs/architecture/proposals/PROP-0028-normalize-question-wireframes.md +55 -0
  328. package/docs/architecture/proposals/PROP-0029-agent-profiles.md +124 -0
  329. package/docs/architecture/proposals/PROP-0030-subagent-title.md +86 -0
  330. package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
  331. package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
  332. package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
  333. package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
  334. package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
  335. package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
  336. package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
  337. package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
  338. package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
  339. package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
  340. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +247 -0
  341. package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
  342. package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
  343. package/docs/architecture/proposals/README.md +39 -0
  344. package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
  345. package/docs/decisions/ADR-0009-xharness-command.md +6 -5
  346. package/docs/decisions/ADR-0014-public-release-baseline.md +3 -2
  347. package/docs/decisions/ADR-0015-resource-browser.md +28 -0
  348. package/docs/decisions/ADR-0016-fullscreen-browser-tabs.md +31 -0
  349. package/docs/decisions/ADR-0017-one-capability-surface.md +34 -0
  350. package/docs/decisions/ADR-0018-builtin-feature-plugins.md +35 -0
  351. package/docs/decisions/ADR-0019-managed-git-worktrees.md +31 -0
  352. package/docs/decisions/ADR-0020-page-annotations.md +37 -0
  353. package/docs/decisions/ADR-0021-required-command-namespaces.md +40 -0
  354. package/docs/decisions/ADR-0022-plan-only-question-tools.md +34 -0
  355. package/docs/decisions/ADR-0023-contextual-side-question-runs.md +35 -0
  356. package/docs/decisions/ADR-0024-xagent-command.md +40 -0
  357. package/docs/decisions/ADR-0025-question-choice-sketches.md +49 -0
  358. package/docs/decisions/ADR-0026-contextual-question-wireframes.md +58 -0
  359. package/docs/decisions/ADR-0027-connected-sketch-display.md +60 -0
  360. package/docs/decisions/ADR-0028-xingular-agent-product-identity.md +56 -0
  361. package/docs/decisions/ADR-0029-normalized-question-wireframe-borders.md +33 -0
  362. package/docs/decisions/ADR-0030-agent-profiles.md +43 -0
  363. package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
  364. package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
  365. package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
  366. package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
  367. package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
  368. package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
  369. package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
  370. package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
  371. package/docs/doctor.md +10 -4
  372. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
  373. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
  374. package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
  375. package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
  376. package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
  377. package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
  378. package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
  379. package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
  380. package/docs/features/README.md +26 -0
  381. package/docs/features/interfaces/README.md +1 -1
  382. package/docs/future-work.md +48 -0
  383. package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
  384. package/docs/parity.md +134 -0
  385. package/docs/releases/1.0.0.md +2 -2
  386. package/docs/releases/1.1.1.md +28 -0
  387. package/docs/releases/1.2.0.md +49 -0
  388. package/docs/releases/4.0.0.md +47 -0
  389. package/docs/releases/README.md +37 -27
  390. package/docs/study/00-index.md +8 -0
  391. package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
  392. package/docs/study/autonomous-engineering-sources.md +277 -0
  393. package/docs/study/autonomy-sources.md +147 -0
  394. package/docs/study/proactive-autonomy.md +408 -0
  395. package/docs/study/tui-resource-browser.md +78 -0
  396. package/docs/terminal-tui.md +386 -113
  397. package/package.json +6 -5
@@ -13,6 +13,8 @@ import { readAudit } from "../accounts/store.js";
13
13
  import { log } from "../core/bus.js";
14
14
  import { HarnessError, invalid } from "../core/errors.js";
15
15
  import { newId, now } from "../core/fs.js";
16
+ import { formatBashCommandForApproval } from "../core/workspaceBashTool.js";
17
+ import { resolveWorkspaceRoot } from "../core/workspace.js";
16
18
  import { prependSearchPath, searchPath } from "../core/path.js";
17
19
  import { dispatch } from "../tools/dispatch.js";
18
20
  import { materialise } from "../variant/materialise.js";
@@ -21,8 +23,19 @@ import * as variantStore from "../variant/store.js";
21
23
  import { compile, copyToRun, record, render, saveForSession } from "../prompt/compile.js";
22
24
  import { isActiveStatus } from "../sessions/contracts.js";
23
25
  import { findRun, loadRun, saveRun, updateRun } from "../sessions/runStore.js";
26
+ import { combineWorkspaceScope, inspectRunWorkspaceWrite } from "../sessions/workspaceBoundary.js";
27
+ import { CHECKLIST_WRITE } from "../tools/checklist.js";
28
+ import { DEFAULT_TOOL_NAMES } from "../tools/definitions.js";
29
+ import { harnessTools } from "../tools/harnessTools.js";
30
+ import { HARNESS_PLUGINS } from "../plugins/builtin.js";
31
+ import * as attachments from "../plugins/attachments.js";
32
+ import { FIRST_PARTY_PLUGINS } from "../plugins/firstParty.js";
33
+ import { createProfile, listProfiles, profileDirs, profileSection, removeProfile, resolveProfile } from "../agents/profiles.js";
24
34
  import { SESSION_NAME } from "../tools/sessionName.js";
25
35
  import { contributionId } from "../plugins/identity.js";
36
+ import { setupScript } from "../worktrees/setup.js";
37
+ import { inspectStep } from "../annotations/inspect.js";
38
+ import * as pageNotes from "../annotations/store.js";
26
39
  import { APPS_COMMAND, WIKI_COMMAND } from "./cli.js";
27
40
  import { SOCKET_ENV, TOKEN_ENV } from "./client.js";
28
41
  import { TerminalController } from "./controller.js";
@@ -30,6 +43,7 @@ import { terminalCommandFiles } from "./launch.js";
30
43
  import { SessionSocket, socketPath } from "./socket.js";
31
44
  import { harnessLogInfo, readHarnessLogChunk } from "./logs.js";
32
45
  import { listTerminalTrajectoryRuns, readTerminalTrajectory } from "./trajectory.js";
46
+ import { BrowserControl } from "./browserControl.js";
33
47
  /** Identity of the exact resource decision that this terminal was prepared with. */
34
48
  function resourceIdentity(loading) {
35
49
  if (!loading)
@@ -63,8 +77,27 @@ export class Terminals {
63
77
  worldReadOnly = new Set();
64
78
  socket;
65
79
  assets;
66
- /** A session's `/accounts` switch, taken once its Pi process has ended. */
80
+ /** A session's `/accounts` or `/worktree` switch, taken once its Pi process has ended. */
67
81
  relaunches = new Map();
82
+ /** The managed worktree each live session holds. */
83
+ worktreeLeases = new Map();
84
+ /** Each child's profile name and colour, looked up once per child run. */
85
+ childProfiles = new Map();
86
+ subagentProfile(record) {
87
+ if (!record.profile)
88
+ return null;
89
+ if (!this.childProfiles.has(record.id)) {
90
+ if (this.childProfiles.size > 500)
91
+ this.childProfiles.clear();
92
+ let color = 'blue';
93
+ try {
94
+ color = resolveProfile(this.options.env.root, record.workingDirectory, record.profile)?.color ?? color;
95
+ }
96
+ catch { }
97
+ this.childProfiles.set(record.id, { name: record.profile, color });
98
+ }
99
+ return this.childProfiles.get(record.id) ?? null;
100
+ }
68
101
  constructor(options) {
69
102
  this.options = options;
70
103
  this.assets = options.assets ?? defaultAssets();
@@ -83,6 +116,12 @@ export class Terminals {
83
116
  const owner = this.sessions.get(parent.sessionId);
84
117
  if (owner?.controller.activeRun?.runId === parent.id) {
85
118
  owner.controller.observe(event, true);
119
+ const settings = options.subagents?.runtimeSettings(record.id);
120
+ owner.controller.observeSubagent(parent.id, record, event, this.subagentProfile(record), {
121
+ accountLabel: owner.account.label,
122
+ model: settings?.model ?? null,
123
+ effort: settings?.effort ?? null
124
+ });
86
125
  break;
87
126
  }
88
127
  parentId = parent.parentRunId;
@@ -127,23 +166,48 @@ export class Terminals {
127
166
  }
128
167
  return bin;
129
168
  }
130
- /** The broker's tools, then the world tools whose plugins are on for this session. */
169
+ /**
170
+ * The harness's own History, Wiki and skill tools, then the broker's, then the world tools whose
171
+ * plugins are on for this session: the same tools a structured session has (INV-PAR-1).
172
+ */
131
173
  async tools(session, listing) {
132
174
  const brokered = this.options.apps.brokerTools(session);
133
175
  const live = this.sessions.get(session.sessionId);
134
- const { world } = this.options;
135
- if (!world || !live)
176
+ if (!live)
136
177
  return brokered;
178
+ const own = await this.ownTools(live, listing);
179
+ const { world } = this.options;
180
+ if (!world)
181
+ return [...own, ...brokered];
137
182
  const context = { run: live.controller.activeRun, caller: live.caller, attended: true, subagent: false, sessionKey: session.sessionId, cwd: live.input.cwd };
138
183
  if (!listing)
139
- return [...brokered, ...world.tools(context, (_plugin, tool) => this.worldNames.get(session.sessionId)?.has(tool) ?? false)];
184
+ return [...own, ...brokered, ...world.tools(context, (_plugin, tool) => this.worldNames.get(session.sessionId)?.has(tool) ?? false)];
140
185
  const on = await this.worldOn(live);
141
186
  const tools = world.tools(context, (plugin, tool) => on(plugin, tool));
142
187
  this.worldNames.set(session.sessionId, new Set(tools.map((tool) => tool.name)));
143
188
  for (const tool of tools)
144
189
  if (tool.readOnly)
145
190
  this.worldReadOnly.add(tool.name);
146
- return [...brokered, ...tools];
191
+ return [...own, ...brokered, ...tools];
192
+ }
193
+ ownNames = new Map();
194
+ async ownTools(live, listing) {
195
+ const sessionId = live.caller.sessionId;
196
+ const all = harnessTools(this.options.env, live.caller, { variantDir: this.variantDirs.get(sessionId) ?? null })
197
+ .filter((tool) => DEFAULT_TOOL_NAMES.includes(tool.name));
198
+ if (!listing)
199
+ return all.filter((tool) => this.ownNames.get(sessionId)?.has(tool.name) ?? false);
200
+ const on = await this.worldOn(live);
201
+ const owner = (tool) => HARNESS_PLUGINS.find((plugin) => plugin.tools?.some((entry) => entry.name === tool))?.id ?? null;
202
+ const tools = all.filter((tool) => {
203
+ const plugin = owner(tool.name);
204
+ return !plugin || on(plugin, tool.name);
205
+ });
206
+ this.ownNames.set(sessionId, new Set(tools.map((tool) => tool.name)));
207
+ for (const tool of tools)
208
+ if (tool.readOnly)
209
+ this.worldReadOnly.add(tool.name);
210
+ return tools;
147
211
  }
148
212
  /** The world tools a new session will have, named in its instructions. */
149
213
  async worldToolNames(input, account) {
@@ -155,6 +219,16 @@ export class Terminals {
155
219
  const on = plugins ? new Set(plugins.flatMap((plugin) => plugin.contributions.filter((item) => item.state === 'on').map((item) => item.id))) : null;
156
220
  return world.plugins.flatMap((plugin) => (plugin.tools ?? []).map((tool) => tool.name).filter((name) => !on || on.has(contributionId(plugin.id, 'tool', name))));
157
221
  }
222
+ /** The harness's own History, Wiki and skill tools a new session will have. */
223
+ async ownToolNames(input, account) {
224
+ const list = this.options.pluginControl?.list;
225
+ const plugins = list ? await list({ cwd: input.cwd, accountId: account.id, projectId: input.projectId ?? null }) : null;
226
+ const on = plugins ? new Set(plugins.flatMap((plugin) => plugin.contributions.filter((item) => item.state === 'on').map((item) => item.id))) : null;
227
+ return DEFAULT_TOOL_NAMES.filter((name) => {
228
+ const plugin = HARNESS_PLUGINS.find((entry) => entry.tools?.some((tool) => tool.name === name));
229
+ return !on || !plugin || on.has(contributionId(plugin.id, 'tool', name));
230
+ });
231
+ }
158
232
  /** Whether a world tool's plugin is switched on for a session's folder, account and project. */
159
233
  async worldOn(live) {
160
234
  const list = this.options.pluginControl?.list;
@@ -177,6 +251,17 @@ export class Terminals {
177
251
  const { Type } = await import('typebox');
178
252
  return { tools: (await this.tools(session, true)).map((tool) => ({ name: tool.name, label: tool.label, description: tool.description, parameters: tool.parameters(Type) })) };
179
253
  }
254
+ case 'firstParty.list': {
255
+ // The harness's own extensions switched on for this session, with their settings.
256
+ const live = this.sessions.get(session.sessionId);
257
+ const list = this.options.pluginControl?.list;
258
+ if (!live || !list)
259
+ return { plugins: [] };
260
+ const plugins = await list({ cwd: live.input.cwd, accountId: live.account.id, projectId: live.input.projectId ?? null });
261
+ const on = new Set(plugins.flatMap((plugin) => plugin.contributions.filter((item) => item.state === 'on').map((item) => item.id)));
262
+ const settings = attachments.load(this.options.env.root).settings;
263
+ return { plugins: FIRST_PARTY_PLUGINS.filter((plugin) => plugin.code?.some((code) => on.has(`${plugin.id}/code/${code.name}`))).map((plugin) => ({ id: plugin.id, settings: settings[plugin.id] ?? {} })) };
264
+ }
180
265
  case 'tools.call': {
181
266
  const tool = (await this.tools(session, false)).find((candidate) => candidate.name === params.name);
182
267
  if (!tool)
@@ -235,6 +320,14 @@ export class Terminals {
235
320
  const settings = params.settings && typeof params.settings === 'object' && !Array.isArray(params.settings) ? params.settings : {};
236
321
  return api.configure({ ...scope, pluginId: text('pluginId'), settings });
237
322
  }
323
+ case 'plugins.install':
324
+ if (!api.install)
325
+ throw invalid('Package installation is unavailable in this host.');
326
+ return api.install({ ...scope, source: text('source') });
327
+ case 'plugins.remove':
328
+ if (!api.remove)
329
+ throw invalid('Package removal is unavailable in this host.');
330
+ return api.remove({ ...scope, pluginId: text('pluginId') });
238
331
  default:
239
332
  throw new HarnessError('unknown_method', `Unknown call ${call}.`);
240
333
  }
@@ -324,6 +417,232 @@ export class Terminals {
324
417
  throw new HarnessError('unknown_method', `Unknown call ${call}.`);
325
418
  }
326
419
  }
420
+ /** `/agents`: the agent profiles this session's folder can use (PROP-0029). */
421
+ agentsCall(live, call, params) {
422
+ const root = this.options.env.root;
423
+ const cwd = live.input.cwd;
424
+ const name = () => {
425
+ if (typeof params.name !== 'string' || !params.name.trim())
426
+ throw invalid('A profile name is required.');
427
+ return params.name.trim();
428
+ };
429
+ const scope = () => {
430
+ if (params.scope !== 'global' && params.scope !== 'project')
431
+ throw invalid('scope must be global or project.');
432
+ return params.scope;
433
+ };
434
+ switch (call.slice('terminal.agents.'.length)) {
435
+ case 'list': return { ...listProfiles(root, cwd, this.variantDirs.get(live.input.sessionId) ?? null), dirs: profileDirs(root, cwd) };
436
+ case 'create': return createProfile(root, cwd, { name: name(), scope: scope(), from: typeof params.from === 'string' && params.from ? params.from : null });
437
+ case 'remove':
438
+ removeProfile(root, cwd, { name: name(), scope: scope() });
439
+ return {};
440
+ default: throw invalid('Unknown profile operation.');
441
+ }
442
+ }
443
+ /** `/worktree`: the managed worktrees of the repository this session works in. */
444
+ async worktreesCall(live, call, params) {
445
+ const worktrees = this.options.worktrees;
446
+ if (!worktrees)
447
+ throw invalid('Worktrees are unavailable in this host.');
448
+ const sessionId = live.input.sessionId;
449
+ const cwd = live.input.cwd;
450
+ const current = this.worktreeLeases.get(sessionId) ?? null;
451
+ const id = () => {
452
+ if (typeof params.id !== 'string' || !params.id)
453
+ throw invalid('A worktree id is required.');
454
+ return params.id;
455
+ };
456
+ const action = call.slice('terminal.worktrees.'.length);
457
+ switch (action) {
458
+ case 'list': {
459
+ let repo;
460
+ try {
461
+ repo = await worktrees.repositoryOf(cwd);
462
+ }
463
+ catch (error) {
464
+ return { repo: null, current, worktrees: [], removed: [], reason: error instanceof Error ? error.message : String(error) };
465
+ }
466
+ const statuses = await Promise.all((await worktrees.list(repo)).map((record) => worktrees.status(record.id)));
467
+ return { repo, current, worktrees: statuses, removed: worktrees.removed(repo), reason: null };
468
+ }
469
+ case 'status': return worktrees.status(id());
470
+ case 'create': return worktrees.create({
471
+ repo: cwd,
472
+ ...(typeof params.name === 'string' && params.name ? { name: params.name } : {}),
473
+ ...(typeof params.branch === 'string' && params.branch ? { branch: params.branch } : {}),
474
+ kind: params.kind === 'permanent' ? 'permanent' : 'managed'
475
+ });
476
+ case 'branch':
477
+ if (typeof params.name !== 'string' || !params.name)
478
+ throw invalid('A branch name is required.');
479
+ return worktrees.branch(id(), params.name);
480
+ case 'handoff': {
481
+ const direction = params.direction === 'toSource' || params.direction === 'toWorktree' ? params.direction : null;
482
+ if (!direction)
483
+ throw invalid('direction is toSource or toWorktree.');
484
+ return worktrees.handoff(id(), direction, { move: params.move !== false });
485
+ }
486
+ case 'remove':
487
+ if (id() === current)
488
+ throw new HarnessError('worktree_in_use', 'This session works in that worktree. Switch to another folder first.');
489
+ return worktrees.remove(id(), { force: params.force === true });
490
+ case 'restore': return worktrees.restore(id());
491
+ case 'prune': return worktrees.prune({ repo: cwd, ...(typeof params.keep === 'number' ? { keep: params.keep } : {}) });
492
+ case 'trustSetup': {
493
+ const repo = await worktrees.repositoryOf(cwd);
494
+ const script = setupScript(repo);
495
+ if (!script)
496
+ throw invalid('This repository has no .xharness/worktree-setup.');
497
+ await worktrees.trustSetup(repo, script.hash);
498
+ return { hash: script.hash };
499
+ }
500
+ case 'switch': {
501
+ // A new conversation in the chosen folder: Pi keeps one history per working folder.
502
+ const target = params.main === true ? null : id();
503
+ const folder = target ? worktrees.pathOf(target) : await worktrees.repositoryOf(cwd);
504
+ if (!folder)
505
+ throw new HarnessError('worktree_missing', 'That worktree has no folder.');
506
+ this.relaunches.set(sessionId, { accountId: live.account.id, nativeSessionId: null, cwd: folder, worktreeId: target });
507
+ return { cwd: folder, worktreeId: target };
508
+ }
509
+ default:
510
+ throw new HarnessError('unknown_method', `Unknown call ${call}.`);
511
+ }
512
+ }
513
+ /** `/usage:analysis` and `/usage:limit`: usage for this session's own account, project or session. */
514
+ async usageCall(live, call, params) {
515
+ const usage = this.options.usage;
516
+ const limits = this.options.usageLimits;
517
+ if (!usage || !limits)
518
+ throw invalid('Usage is unavailable in this host.');
519
+ if (call === 'terminal.usage.summary') {
520
+ const scope = params.scope === 'session' || params.scope === 'project' || params.scope === 'all' ? params.scope : 'account';
521
+ if (scope === 'project' && !live.input.projectId)
522
+ throw invalid('This session has no project to report usage for.');
523
+ const period = params.period === 'today' || params.period === '30d' || params.period === 'all' ? params.period : '7d';
524
+ return usage.summary({
525
+ scope,
526
+ sessionId: live.input.sessionId,
527
+ accountId: live.account.id,
528
+ projectId: live.input.projectId ?? null,
529
+ period,
530
+ timezoneOffsetMinutes: typeof params.timezoneOffsetMinutes === 'number' ? params.timezoneOffsetMinutes : 0
531
+ });
532
+ }
533
+ if (call === 'terminal.usage.limits') {
534
+ return params.refresh === true ? limits.refresh(live.account.id, { force: true }) : limits.read(live.account.id);
535
+ }
536
+ if (call === 'terminal.usage.headers') {
537
+ if (typeof params.provider !== 'string' || !params.provider)
538
+ throw invalid('A provider is required.');
539
+ if (typeof params.status !== 'number' || !Number.isFinite(params.status))
540
+ throw invalid('A response status is required.');
541
+ const headers = params.headers && typeof params.headers === 'object' && !Array.isArray(params.headers)
542
+ ? Object.fromEntries(Object.entries(params.headers).filter((entry) => typeof entry[1] === 'string'))
543
+ : {};
544
+ if (!Object.keys(headers).length)
545
+ return {};
546
+ limits.recordHeaders(live.account.id, params.provider, { status: params.status, headers });
547
+ return {};
548
+ }
549
+ throw invalid('Unknown usage operation.');
550
+ }
551
+ /** `/annotate:notes` and `/annotate:`: Web annotations and their visible capture browser. */
552
+ async annotationsCall(live, call, params) {
553
+ const root = this.options.env.root;
554
+ const sessionId = live.input.sessionId;
555
+ const capture = this.options.noteCapture;
556
+ const id = () => {
557
+ if (typeof params.id !== 'string' || !params.id)
558
+ throw invalid('A note id is required.');
559
+ return params.id;
560
+ };
561
+ const version = () => {
562
+ if (typeof params.expectedVersion !== 'number')
563
+ throw invalid('The note version you last read is required.');
564
+ return params.expectedVersion;
565
+ };
566
+ const needCapture = () => {
567
+ if (!capture)
568
+ throw invalid('This host has no capture browser.');
569
+ return capture;
570
+ };
571
+ const needNotes = () => {
572
+ if (!this.options.pageNotes)
573
+ throw invalid('This host cannot send page notes.');
574
+ return this.options.pageNotes;
575
+ };
576
+ const action = call.slice('terminal.annotations.'.length);
577
+ switch (action) {
578
+ case 'list': {
579
+ const result = pageNotes.list(root, {
580
+ query: typeof params.query === 'string' ? params.query : null,
581
+ url: typeof params.url === 'string' ? params.url : null,
582
+ status: params.status === 'open' || params.status === 'resolved' ? params.status : null,
583
+ limit: 200
584
+ });
585
+ return { ...result, sessionId };
586
+ }
587
+ case 'read': return pageNotes.read(root, id());
588
+ case 'create': {
589
+ const note = pageNotes.create(root, { source: params.source, target: params.target, body: params.body, tags: params.tags });
590
+ return params.share === true ? pageNotes.share(root, note.id, { sessionId }) : note;
591
+ }
592
+ case 'update': return pageNotes.update(root, id(), version(), (params.patch ?? {}));
593
+ case 'delete':
594
+ pageNotes.remove(root, id(), version());
595
+ return {};
596
+ // A terminal shares with, and withdraws from, its own session only.
597
+ case 'share': return pageNotes.share(root, id(), { sessionId });
598
+ case 'unshare': return pageNotes.unshare(root, id(), { sessionId });
599
+ case 'open': return needCapture().open(sessionId, typeof params.url === 'string' && params.url ? params.url : null);
600
+ case 'identity': return needCapture().identity(sessionId);
601
+ case 'begin':
602
+ if (params.kind !== 'text' && params.kind !== 'element')
603
+ throw invalid('kind is text or element.');
604
+ return needCapture().begin(sessionId, params.kind);
605
+ case 'capture':
606
+ if (typeof params.captureId !== 'string')
607
+ throw invalid('A capture id is required.');
608
+ return needCapture().capture(sessionId, params.captureId);
609
+ case 'cancel':
610
+ await needCapture().cancel(sessionId, typeof params.captureId === 'string' ? params.captureId : undefined);
611
+ return {};
612
+ case 'reveal': {
613
+ const note = pageNotes.read(root, id());
614
+ return needCapture().reveal(sessionId, note.source.url, note.target);
615
+ }
616
+ // ---- The inspector and the queue (PROP-0018); a terminal queues and sends to its own session.
617
+ case 'target': return { ...needCapture().describe(sessionId), windows: await needCapture().windows(sessionId).catch(() => []) };
618
+ case 'attach': {
619
+ const kind = params.kind;
620
+ if (kind !== 'web' && kind !== 'cdp' && kind !== 'bridge')
621
+ throw invalid('kind is web, cdp or bridge.');
622
+ const spec = kind === 'cdp'
623
+ ? { kind, endpoint: typeof params.endpoint === 'string' ? params.endpoint : '', name: typeof params.name === 'string' ? params.name : null }
624
+ : kind === 'bridge' ? { kind, name: typeof params.name === 'string' ? params.name : null } : { kind };
625
+ return needCapture().attach(sessionId, spec);
626
+ }
627
+ case 'windows': return needCapture().windows(sessionId);
628
+ case 'selectWindow':
629
+ if (typeof params.window !== 'string')
630
+ throw invalid('A window id is required.');
631
+ await needCapture().selectWindow(sessionId, params.window);
632
+ return {};
633
+ case 'inspect': return needCapture().inspect(sessionId);
634
+ case 'stopInspect':
635
+ await needCapture().stopInspect(sessionId);
636
+ return {};
637
+ case 'next': return inspectStep(needCapture(), needNotes(), sessionId, sessionId, 'terminal');
638
+ case 'queue': return needNotes().queue(id(), version(), sessionId);
639
+ case 'dequeue': return needNotes().dequeue(id(), version());
640
+ case 'confirm': return needNotes().confirm(id(), version());
641
+ case 'send': return needNotes().send(sessionId, { ids: Array.isArray(params.ids) ? params.ids.filter((entry) => typeof entry === 'string') : null });
642
+ default:
643
+ throw new HarnessError('unknown_method', `Unknown call ${call}.`);
644
+ }
645
+ }
327
646
  /** The account a session asked to switch to. Taken once, after its Pi process has ended and before `dispose`. */
328
647
  takeRelaunch(sessionId) {
329
648
  const request = this.relaunches.get(sessionId) ?? null;
@@ -356,6 +675,7 @@ export class Terminals {
356
675
  sessionId: input.sessionId,
357
676
  nativeSessionId: live.controller.nativeSessionId,
358
677
  workingDirectory: input.cwd,
678
+ workspaceRoot: resolveWorkspaceRoot(input.cwd),
359
679
  worktreeId: input.run?.worktreeId ?? null,
360
680
  goal: text,
361
681
  attachmentCount: 0,
@@ -364,7 +684,9 @@ export class Terminals {
364
684
  status: 'queued',
365
685
  exitCode: null,
366
686
  handoffId: null,
367
- accountId: live.account.id
687
+ accountId: live.account.id,
688
+ variantScope: live.variant?.scope ?? null,
689
+ variantVersion: live.variant?.fingerprint ?? null
368
690
  };
369
691
  }
370
692
  /** The run a native turn reports into: the one reserved by `send`, or a new one for what the person typed. */
@@ -390,55 +712,151 @@ export class Terminals {
390
712
  const record = loadRun(this.options.env.root, run.runId);
391
713
  if (!isActiveStatus(record.status))
392
714
  throw invalid(`The run is no longer active (its status is ${record.status}).`);
393
- const call = { run: record, caller: { ...live.caller, runId: run.runId }, tool, args };
715
+ const name = tool.toLowerCase();
716
+ const workspaceRoot = record.workspaceRoot ?? resolveWorkspaceRoot(record.workingDirectory ?? live.input.cwd);
717
+ if (live.input.readOnly && ['bash', 'edit', 'write'].includes(name))
718
+ return { allowed: false, reason: 'This session is read-only.', workspaceRoot };
719
+ let inspection;
720
+ try {
721
+ inspection = inspectRunWorkspaceWrite(record, tool, args);
722
+ }
723
+ catch (error) {
724
+ return { allowed: false, reason: error instanceof Error ? error.message : 'The active project workspace could not be verified.', workspaceRoot };
725
+ }
726
+ const call = { run: record, caller: { ...live.caller, runId: run.runId }, tool, args: inspection.input };
394
727
  const { policy } = this.options;
395
728
  if (policy.scope) {
396
- const scope = await policy.scope(call);
729
+ const scope = combineWorkspaceScope(await policy.scope(call), inspection);
397
730
  if (!scope.allowed && (scope.automatic || !scope.requiresApproval))
398
- return { allowed: false, reason: scope.reason ?? 'Outside what this session may do.' };
731
+ return { allowed: false, reason: scope.reason ?? 'Outside what this session may do.', workspaceRoot };
399
732
  try {
400
733
  await policy.authorize?.(call);
401
734
  }
402
735
  catch (error) {
403
- return { allowed: false, reason: error instanceof Error ? error.message : 'Tool unavailable in the settings.' };
736
+ return { allowed: false, reason: error instanceof Error ? error.message : 'Tool unavailable in the settings.', workspaceRoot };
404
737
  }
405
738
  const requiresApproval = !!scope.requiresApproval || (scope.allowed && !!scope.humanOnly);
406
739
  return {
407
740
  allowed: scope.allowed,
408
741
  requiresApproval,
409
742
  reason: scope.reason ?? null,
743
+ workspaceRoot,
410
744
  rememberable: scope.allowed && !scope.humanOnly,
411
745
  ...(scope.humanOnly ? { standing: scope.allowGlobalApproval ? 'switch' : 'none' } : {})
412
746
  };
413
747
  }
414
- // With no application policy: reads run, a read-only session refuses the rest, and anything else asks.
415
- const name = tool.toLowerCase();
416
- if (name === SESSION_NAME || name === SPAWN_SUBAGENT || name === WAIT_SUBAGENTS || READ_ONLY_TOOLS.has(name) || this.worldReadOnly.has(name) || this.options.apps.toolNames.includes(name))
417
- return { allowed: true };
418
- if (live.input.readOnly)
419
- return { allowed: false, reason: 'This session is read-only.' };
420
- return { allowed: false, requiresApproval: true, rememberable: true, reason: 'This native action requires approval.' };
748
+ if (inspection.outside)
749
+ return { allowed: false, requiresApproval: true, rememberable: false, standing: 'switch', reason: inspection.reason, workspaceRoot };
750
+ if (name === SESSION_NAME || name === CHECKLIST_WRITE || name === SPAWN_SUBAGENT || name === WAIT_SUBAGENTS || READ_ONLY_TOOLS.has(name) || this.worldReadOnly.has(name) || this.options.apps.toolNames.includes(name) || ['bash', 'edit', 'write'].includes(name)) {
751
+ return { allowed: true, workspaceRoot };
752
+ }
753
+ return { allowed: false, requiresApproval: true, rememberable: true, reason: 'This native action requires approval.', workspaceRoot };
754
+ }
755
+ async workspaceEscape(live, run, tool, args, reason) {
756
+ if (tool !== 'bash' || live.input.readOnly)
757
+ return false;
758
+ const record = loadRun(this.options.env.root, run.runId);
759
+ if (!isActiveStatus(record.status) || (record.trigger != null && record.trigger !== 'user'))
760
+ return false;
761
+ const call = { run: record, caller: { ...live.caller, runId: run.runId }, tool, args };
762
+ const scope = (await this.options.policy.scope?.(call)) ?? { allowed: true };
763
+ if (scope.automatic || (!scope.allowed && !scope.requiresApproval))
764
+ return false;
765
+ try {
766
+ await this.options.policy.authorize?.(call);
767
+ }
768
+ catch {
769
+ return false;
770
+ }
771
+ const operationArgs = (args && typeof args === 'object' && !Array.isArray(args) ? args : {});
772
+ const command = typeof operationArgs.command === 'string' ? operationArgs.command : '';
773
+ if (operationArgs.workspaceWriteScope !== 'outside' || !command)
774
+ return false;
775
+ const workspaceRoot = record.workspaceRoot ?? resolveWorkspaceRoot(record.workingDirectory ?? live.input.cwd);
776
+ const decision = await run.requestApproval({
777
+ target: 'workspace-write',
778
+ action: `${reason} Workspace: ${workspaceRoot}. This exact command will run once with broader filesystem access.\n\nCommand:\n${formatBashCommandForApproval(command)}`,
779
+ operation: { tool, args: operationArgs },
780
+ rememberable: false,
781
+ standing: 'none'
782
+ });
783
+ if (decision !== 'approved')
784
+ return false;
785
+ const currentRecord = loadRun(this.options.env.root, run.runId);
786
+ if (!isActiveStatus(currentRecord.status) || (currentRecord.trigger != null && currentRecord.trigger !== 'user'))
787
+ return false;
788
+ const currentCall = { run: currentRecord, caller: { ...live.caller, runId: run.runId }, tool, args: operationArgs };
789
+ const currentScope = (await this.options.policy.scope?.(currentCall)) ?? { allowed: true };
790
+ if (currentScope.automatic || (!currentScope.allowed && !currentScope.requiresApproval))
791
+ return false;
792
+ try {
793
+ await this.options.policy.authorize?.(currentCall);
794
+ return true;
795
+ }
796
+ catch {
797
+ return false;
798
+ }
799
+ }
800
+ async workspaceBoundaryBypass(live, run, tool, args) {
801
+ if (tool !== 'bash' || live.input.readOnly || !this.options.approvals.isAuto('pi'))
802
+ return false;
803
+ const record = loadRun(this.options.env.root, run.runId);
804
+ if (!isActiveStatus(record.status) || (record.trigger != null && record.trigger !== 'user'))
805
+ return false;
806
+ const operationArgs = (args && typeof args === 'object' && !Array.isArray(args) ? args : {});
807
+ if (typeof operationArgs.command !== 'string' || !operationArgs.command)
808
+ return false;
809
+ const call = { run: record, caller: { ...live.caller, runId: run.runId }, tool, args: operationArgs };
810
+ try {
811
+ const scope = (await this.options.policy.scope?.(call)) ?? { allowed: true };
812
+ if (scope.automatic || (!scope.allowed && !scope.requiresApproval) || (scope.humanOnly && !scope.allowGlobalApproval))
813
+ return false;
814
+ await this.options.policy.authorize?.(call);
815
+ return true;
816
+ }
817
+ catch {
818
+ return false;
819
+ }
421
820
  }
422
821
  async subagentsCall(live, call, params) {
822
+ if (call === 'terminal.subagents.monitor')
823
+ return { children: live.controller.subagentSnapshot() };
423
824
  const service = this.options.subagents;
424
- if (!service || live.input.subagents === null) {
825
+ if ((!service || live.input.subagents === null) && call !== 'terminal.subagents.register') {
425
826
  if (call === 'terminal.subagents.tools')
426
827
  return { tools: [] };
427
828
  throw invalid('Subagents are disabled for this terminal.');
428
829
  }
429
- if (call === 'terminal.subagents.tools')
430
- return { tools: subagentTools(service, '').map(tool => ({ name: tool.name, label: tool.label, description: tool.description, parameters: tool.parameters(Type) })) };
830
+ if (call === 'terminal.subagents.tools') {
831
+ const variantDir = this.variantDirs.get(live.input.sessionId) ?? null;
832
+ return {
833
+ tools: subagentTools(service, '').map(tool => ({ name: tool.name, label: tool.label, description: tool.description, parameters: tool.parameters(Type) })),
834
+ profiles: {
835
+ normal: profileSection(this.options.env.root, live.input.cwd, variantDir).body,
836
+ plan: profileSection(this.options.env.root, live.input.cwd, variantDir, true).body
837
+ }
838
+ };
839
+ }
431
840
  const run = live.controller.activeRun;
432
841
  if (!run)
433
842
  throw new HarnessError('run_not_active', 'This terminal has no active parent run.');
843
+ if (call === 'terminal.subagents.planSafe') {
844
+ const reason = service.planSpawnRefusal(run.runId, params.input);
845
+ return { allowed: reason === null, ...(reason ? { reason } : {}) };
846
+ }
434
847
  if (call === 'terminal.subagents.register') {
435
848
  if (!Array.isArray(params.tools) || params.tools.some((name) => typeof name !== 'string'))
436
849
  throw invalid('Native tool names are required.');
850
+ if (params.plan !== undefined && typeof params.plan !== 'boolean')
851
+ throw invalid('plan must be a boolean.');
437
852
  const readOnly = live.input.readOnly === true;
438
- const readTools = new Set([...READ_ONLY_TOOLS, SESSION_NAME, SPAWN_SUBAGENT, WAIT_SUBAGENTS, 'ask_user', 'mcp_list_tools', 'mcp_call_tool', 'history_search', 'history_read', 'wiki_search', 'wiki_read', 'wiki_list', 'memory_facts', 'skill_load']);
439
- const tools = params.tools.filter((name) => !readOnly || readTools.has(name));
440
- const capabilities = this.options.resolveCapabilities ? await this.options.resolveCapabilities(live.caller, tools, live.input.cwd) : { version: 1, tools, connectors: [], contributions: [] };
441
- freezeRun(this.options.env.root, run.runId, live.caller, capabilities);
853
+ const readTools = new Set([...READ_ONLY_TOOLS, SESSION_NAME, CHECKLIST_WRITE, SPAWN_SUBAGENT, WAIT_SUBAGENTS, 'ask_user', 'mcp_list_tools', 'mcp_call_tool', 'history_search', 'history_read', 'wiki_search', 'wiki_read', 'wiki_list', 'memory_facts', 'skill_load']);
854
+ const tools = [...new Set(params.tools)].filter(name => (!readOnly || readTools.has(name)) && (params.plan === true || (name !== 'ask_user' && name !== 'ask_user_question')));
855
+ const caller = { ...live.caller, runId: run.runId };
856
+ const capabilities = this.options.resolveCapabilities ? await this.options.resolveCapabilities(caller, tools, live.input.cwd) : { version: 1, tools, connectors: [], contributions: [] };
857
+ freezeRun(this.options.env.root, run.runId, caller, capabilities);
858
+ if (!service || live.input.subagents === null)
859
+ return { runId: run.runId, connectors: capabilities.connectors };
442
860
  service.register({
443
861
  run: { id: run.runId, sessionId: live.input.sessionId, mode: live.input.run?.mode ?? 'code', projectId: live.input.projectId, worktreeId: live.input.run?.worktreeId },
444
862
  cwd: live.input.cwd, sessionDir: this.historyDir(live.input), text: '', caller: live.caller,
@@ -446,10 +864,12 @@ export class Terminals {
446
864
  effort: typeof params.effort === 'string' ? params.effort : null,
447
865
  launch: live.input.launch, tools: { allow: capabilities.tools, gated: capabilities.tools.filter(name => !readTools.has(name)) },
448
866
  connections: live.input.connections === false ? false : { readOnly }, subagents: live.input.subagents,
867
+ plan: params.plan === true,
868
+ variant: live.variant ? { dir: live.variant.dir, scope: live.variant.scope } : null,
449
869
  capabilities
450
870
  }, live.account.id, capabilities);
451
871
  service.restore(run.runId, params.history ?? { owners: [], receipts: [] });
452
- return { runId: run.runId };
872
+ return { runId: run.runId, connectors: capabilities.connectors };
453
873
  }
454
874
  if (call === 'terminal.subagents.verify') {
455
875
  await service.verify(run.runId, params.history);
@@ -519,6 +939,43 @@ export class Terminals {
519
939
  }
520
940
  throw new HarnessError('unknown_method', `Unknown call ${call}.`);
521
941
  }
942
+ async sideQuestionCall(live, call, params) {
943
+ const service = this.options.sideQuestions;
944
+ if (!service)
945
+ throw new HarnessError('unknown_method', `Unknown call ${call}.`);
946
+ if (call === 'terminal.sideQuestions.start') {
947
+ if (typeof params.question !== 'string')
948
+ throw invalid('A side question is required.');
949
+ const context = params.context && typeof params.context === 'object' && !Array.isArray(params.context)
950
+ ? params.context
951
+ : undefined;
952
+ const latest = listTerminalTrajectoryRuns(this.options.env.root, live.input.sessionId, live.controller.nativeSessionId)
953
+ .find(run => findRun(this.options.env.root, run.id)?.trigger !== 'side-question');
954
+ return service.start({
955
+ sessionId: live.input.sessionId,
956
+ parentRunId: live.controller.activeRun?.runId ?? latest?.id ?? null,
957
+ parentSessionId: live.input.sessionId,
958
+ accountId: live.account.id,
959
+ model: typeof params.model === 'string' ? params.model : live.input.model ?? null,
960
+ effort: typeof params.effort === 'string' ? params.effort : undefined,
961
+ cwd: live.input.cwd,
962
+ caller: live.caller,
963
+ question: params.question,
964
+ context
965
+ });
966
+ }
967
+ if (typeof params.runId !== 'string')
968
+ throw invalid('A side-question run id is required.');
969
+ if (call === 'terminal.sideQuestions.read') {
970
+ const afterSequence = params.afterSequence === undefined ? -1 : params.afterSequence;
971
+ if (typeof afterSequence !== 'number')
972
+ throw invalid('A side-question event sequence is required.');
973
+ return service.read(params.runId, live.input.sessionId, afterSequence);
974
+ }
975
+ if (call === 'terminal.sideQuestions.interrupt')
976
+ return service.interrupt(params.runId, live.input.sessionId);
977
+ throw new HarnessError('unknown_method', `Unknown call ${call}.`);
978
+ }
522
979
  /** The only terminal frontend: the Bun-run OpenTUI application. */
523
980
  frontendCommand() {
524
981
  return {
@@ -538,32 +995,55 @@ export class Terminals {
538
995
  if (!input?.sessionId || !input.cwd || !input.sessionFolder)
539
996
  throw invalid('A terminal session needs an id, a working folder, and its own folder.');
540
997
  await this.dispose(input.sessionId);
541
- const command = this.frontendCommand();
542
- // A pinned account is used as it is; it is never exchanged for another one.
543
- const account = input.accountId ? accounts.find(input.accountId) : accounts.resolve(null, null);
544
- const launch = launchEnv(accounts.dir, account);
545
- const loading = (await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir, account), install: true, caller: { projectId: input.projectId ?? null } })) ?? null;
546
- await this.socket.listen();
547
- await accounts.terminalBegin(account);
548
998
  const sessionId = input.sessionId;
549
- const caller = { sessionId, projectId: input.projectId ?? null, actor: 'Code', surface: 'cli' };
999
+ let acquiredWorktreeId = null;
1000
+ let account = null;
1001
+ let accountStarted = false;
550
1002
  let token = null;
551
1003
  try {
1004
+ const worktreeId = input.run?.worktreeId ?? null;
1005
+ const worktreePath = worktreeId ? this.options.worktrees?.pathOf(worktreeId) ?? null : null;
1006
+ if (worktreeId && worktreePath) {
1007
+ await this.options.worktrees.acquire(worktreeId, sessionId);
1008
+ acquiredWorktreeId = worktreeId;
1009
+ this.worktreeLeases.set(sessionId, worktreeId);
1010
+ input.cwd = worktreePath;
1011
+ }
1012
+ const command = this.frontendCommand();
1013
+ // A pinned account is used as it is; it is never exchanged for another one.
1014
+ const preparedAccount = input.accountId ? accounts.find(input.accountId) : accounts.resolve(null, null);
1015
+ account = preparedAccount;
1016
+ const launch = launchEnv(accounts.dir, preparedAccount);
1017
+ const loading = (await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir, preparedAccount), install: true, caller: { projectId: input.projectId ?? null } })) ?? null;
1018
+ await this.socket.listen();
1019
+ await accounts.terminalBegin(preparedAccount);
1020
+ accountStarted = true;
1021
+ const caller = { sessionId, projectId: input.projectId ?? null, actor: 'Code', surface: 'cli' };
552
1022
  const broker = input.connections === false ? null : apps.grant({ sessionId, runId: null, caller, readOnly: !!input.readOnly, depth: 0 });
553
1023
  // The session keeps these instructions; each of its runs records a copy, and a memory
554
1024
  // block that changes later is sent along as a note.
555
1025
  const variant = input.variant ? materialise(root, input.variant, { trust: this.options.trust ?? null }) : null;
1026
+ const browser = new BrowserControl({ root, cwd: input.cwd, pinned: variant,
1027
+ writable: Boolean(input.variant && !('dir' in input.variant)), apps, hub: this.options.hub,
1028
+ plugins: () => this.options.pluginControl?.list({ cwd: input.cwd, accountId: preparedAccount.id, projectId: input.projectId ?? null }) ?? Promise.resolve([]) });
556
1029
  if (variant)
557
1030
  this.variantDirs.set(sessionId, variant.dir);
558
- const worldTools = await this.worldToolNames(input, account);
559
- const launchTools = [...new Set([...(input.launch?.tools ?? []), SESSION_NAME, ...worldTools])];
560
- const pack = compile(env, caller, { ...input.launch, tools: launchTools }, variant);
1031
+ const worldTools = await this.worldToolNames(input, preparedAccount);
1032
+ const ownTools = await this.ownToolNames(input, preparedAccount);
1033
+ const launchTools = [...new Set([...(input.launch?.tools ?? []), CHECKLIST_WRITE, SESSION_NAME, ...ownTools, ...(broker ? apps.toolNames : []), ...worldTools])];
1034
+ // The profiles replace the variant's own subagent list, which they include.
1035
+ const profiles = this.options.subagents && input.subagents !== null ? profileSection(root, input.cwd, variant?.dir ?? null) : null;
1036
+ const launchSections = profiles ? [...(input.launch?.sections ?? []).filter((section) => section.id !== profiles.id), profiles] : input.launch?.sections;
1037
+ // A terminal is a person's own session: the same standard sections a host's session has.
1038
+ const standard = input.launch?.standard === null ? null : input.launch?.standard ?? { cwd: input.cwd };
1039
+ const pack = compile(env, caller, { ...input.launch, standard, sections: launchSections, tools: launchTools }, variant && profiles ? { ...variant, sections: variant.sections.filter((section) => section.id !== profiles.id) } : variant);
561
1040
  record(root, pack);
562
1041
  mkdirSync(this.historyDir(input), { recursive: true });
563
1042
  saveForSession(input.sessionFolder, pack);
564
1043
  const live = {
565
1044
  input,
566
- account,
1045
+ variant,
1046
+ account: preparedAccount,
567
1047
  broker,
568
1048
  caller,
569
1049
  token: '',
@@ -572,6 +1052,12 @@ export class Terminals {
572
1052
  nativeSessionId: input.nativeSessionId ?? null,
573
1053
  begin: ({ reservedRunId, text }) => this.begin(live, reservedRunId, text),
574
1054
  policy: (run, tool, args) => this.decide(live, run, tool, args),
1055
+ workspaceRoot: run => {
1056
+ const record = loadRun(this.options.env.root, run.runId);
1057
+ return record.workspaceRoot ?? resolveWorkspaceRoot(record.workingDirectory ?? live.input.cwd);
1058
+ },
1059
+ workspaceEscape: (run, tool, args, reason) => this.workspaceEscape(live, run, tool, args, reason),
1060
+ workspaceBoundaryBypass: (run, tool, args) => this.workspaceBoundaryBypass(live, run, tool, args),
575
1061
  allowToolGlobally: target => { this.options.approvals.allowTool('pi', target); },
576
1062
  native: (nativeSessionId) => bus.emit({ type: 'terminal.native', sessionId, nativeSessionId }),
577
1063
  telemetry: (event) => bus.emit({ type: 'terminal.telemetry', sessionId, event }),
@@ -586,6 +1072,46 @@ export class Terminals {
586
1072
  caller: () => ({ ...caller, runId: live.controller.activeRun?.runId ?? null }),
587
1073
  broker,
588
1074
  terminal: { accept: (event, data) => live.controller.accept(event, data), disconnected: () => live.controller.disconnected(), control: async (call, params) => {
1075
+ if (call === 'terminal.browser.config') {
1076
+ const world = this.options.world;
1077
+ if (!world)
1078
+ throw invalid('Browser settings are unavailable in this host.');
1079
+ return { allowPrivate: world.browserConfig().allowPrivate };
1080
+ }
1081
+ if (call === 'terminal.browser.configure') {
1082
+ const world = this.options.world;
1083
+ if (!world)
1084
+ throw invalid('Browser settings are unavailable in this host.');
1085
+ if (typeof params.allowPrivate !== 'boolean')
1086
+ throw invalid('allowPrivate must be a boolean.');
1087
+ return { allowPrivate: world.configureBrowser({ allowPrivate: params.allowPrivate }).allowPrivate };
1088
+ }
1089
+ if (call === 'terminal.browser.snapshot')
1090
+ return browser.snapshot(params.refresh === true);
1091
+ if (call === 'terminal.browser.read') {
1092
+ if (typeof params.id !== 'string')
1093
+ throw invalid('A resource id is required.');
1094
+ return browser.read(params.id, params.latest === true);
1095
+ }
1096
+ if (call === 'terminal.browser.importSkill') {
1097
+ if (typeof params.path !== 'string' || !params.path.trim())
1098
+ throw invalid('A skill folder is required.');
1099
+ browser.importSkill(params.path);
1100
+ bus.emit({ type: 'plugins.changed' });
1101
+ return {};
1102
+ }
1103
+ if (call.startsWith('terminal.browser.connections.'))
1104
+ return browser.connection(call.slice('terminal.browser.connections.'.length), params);
1105
+ if (call === 'terminal.browser.hubPreview') {
1106
+ if (!this.options.hub || typeof params.id !== 'string' || typeof params.version !== 'string')
1107
+ throw invalid('A catalog entry and version are required.');
1108
+ return this.options.hub.preview(params.id, params.version, typeof params.offset === 'number' ? params.offset : 0);
1109
+ }
1110
+ if (call === 'terminal.browser.hubDocument') {
1111
+ if (!this.options.hub || typeof params.id !== 'string' || typeof params.version !== 'string')
1112
+ throw invalid('An installed document and version are required.');
1113
+ return this.options.hub.readDocument(params.id, params.version);
1114
+ }
589
1115
  if (call.startsWith('terminal.doctor.')) {
590
1116
  const doctor = this.options.doctor;
591
1117
  if (!doctor)
@@ -616,8 +1142,16 @@ export class Terminals {
616
1142
  return doctor.cancel(id);
617
1143
  throw invalid('Unknown diagnostic operation.');
618
1144
  }
1145
+ if (call.startsWith('terminal.agents.'))
1146
+ return this.agentsCall(live, call, params);
619
1147
  if (call.startsWith('terminal.subagents.'))
620
1148
  return this.subagentsCall(live, call, params);
1149
+ if (call.startsWith('terminal.sideQuestions.'))
1150
+ return this.sideQuestionCall(live, call, params);
1151
+ if (call.startsWith('terminal.worktrees.'))
1152
+ return this.worktreesCall(live, call, params);
1153
+ if (call.startsWith('terminal.annotations.'))
1154
+ return this.annotationsCall(live, call, params);
621
1155
  if (call === 'terminal.logs.info')
622
1156
  return harnessLogInfo(this.options.stateDir);
623
1157
  if (call === 'terminal.logs.read') {
@@ -651,11 +1185,15 @@ export class Terminals {
651
1185
  : this.options.approvals.revokeTool('pi', params.tool);
652
1186
  return { globalTools: policy.globalTools.pi ?? [] };
653
1187
  }
1188
+ if (call.startsWith('terminal.usage.'))
1189
+ return this.usageCall(live, call, params);
654
1190
  if (call !== 'terminal.resources')
655
1191
  return live.controller.control(call, params);
656
- const current = await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir, account), install: false, caller: { projectId: input.projectId ?? null } }) ?? null;
657
- if (resourceIdentity(current) !== resourceIdentity(loading))
1192
+ const current = await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir, preparedAccount), install: false, caller: { projectId: input.projectId ?? null } }) ?? null;
1193
+ const currentVariant = variant ? materialise(root, { dir: variant.dir, scope: variant.scope }, { trust: this.options.trust ?? null }) : null;
1194
+ if (resourceIdentity(current) !== resourceIdentity(loading) || (variant && (currentVariant?.fingerprint !== variant.fingerprint || JSON.stringify(currentVariant.extensionPaths) !== JSON.stringify(variant.extensionPaths)))) {
658
1195
  throw new HarnessError('terminal_resources_changed', 'Trusted resources changed since this terminal was prepared. Review /plugins and reopen the terminal to load the new selection.');
1196
+ }
659
1197
  return {};
660
1198
  } }
661
1199
  });
@@ -664,16 +1202,17 @@ export class Terminals {
664
1202
  const args = [...command.argsPrefix, '--append-system-prompt', render(env, pack)];
665
1203
  if (input.model)
666
1204
  args.push('--model', input.model);
667
- if (loading) {
1205
+ {
668
1206
  // Pi's own discovery is off: the terminal loads what the harness found and nothing else.
669
1207
  args.push('--no-extensions', '--no-skills', '--no-prompt-templates', '--no-themes');
670
- for (const path of [...loading.extensionPaths, ...loading.builtins, ...(variant?.extensionPaths ?? [])])
1208
+ // The pi command's own extensions are not loaded: a host's session cannot have them (PROP-0039).
1209
+ for (const path of [...(loading?.extensionPaths ?? []), ...(variant?.extensionPaths ?? [])])
671
1210
  args.push('--extension', path);
672
- for (const path of [...(variant?.skillPaths ?? []), ...loading.skillPaths])
1211
+ for (const path of [...(variant?.skillPaths ?? []), ...(loading?.skillPaths ?? [])])
673
1212
  args.push('--skill', path);
674
- for (const path of loading.promptPaths)
1213
+ for (const path of loading?.promptPaths ?? [])
675
1214
  args.push('--prompt-template', path);
676
- for (const path of loading.themePaths)
1215
+ for (const path of loading?.themePaths ?? [])
677
1216
  args.push('--theme', path);
678
1217
  }
679
1218
  // Last, so its gate sees a tool call as every other extension left it.
@@ -691,7 +1230,7 @@ export class Terminals {
691
1230
  [SOCKET_ENV]: this.socket.path,
692
1231
  [TOKEN_ENV]: token
693
1232
  },
694
- accountId: account.id,
1233
+ accountId: preparedAccount.id,
695
1234
  nativeSessionId: input.nativeSessionId ?? null
696
1235
  };
697
1236
  }
@@ -699,8 +1238,15 @@ export class Terminals {
699
1238
  if (token)
700
1239
  this.socket.revoke(token);
701
1240
  this.sessions.delete(sessionId);
1241
+ this.variantDirs.delete(sessionId);
1242
+ this.worldNames.delete(sessionId);
1243
+ if (this.worktreeLeases.get(sessionId) === acquiredWorktreeId)
1244
+ this.worktreeLeases.delete(sessionId);
702
1245
  apps.revokeSession(sessionId);
703
- await accounts.terminalEnd(account).catch(() => undefined);
1246
+ if (accountStarted && account)
1247
+ await accounts.terminalEnd(account).catch(() => undefined);
1248
+ if (acquiredWorktreeId)
1249
+ await this.options.worktrees?.release(acquiredWorktreeId, sessionId).catch(() => undefined);
704
1250
  throw error;
705
1251
  }
706
1252
  }
@@ -754,6 +1300,11 @@ export class Terminals {
754
1300
  }
755
1301
  this.sessions.delete(sessionId);
756
1302
  this.worldNames.delete(sessionId);
1303
+ const worktreeId = this.worktreeLeases.get(sessionId);
1304
+ this.worktreeLeases.delete(sessionId);
1305
+ if (worktreeId)
1306
+ await this.options.worktrees?.release(worktreeId, sessionId).catch(() => undefined);
1307
+ void this.options.noteCapture?.release(sessionId).catch(() => undefined);
757
1308
  void this.options.world?.release(sessionId).catch(() => undefined);
758
1309
  live.controller.dispose();
759
1310
  this.socket.revoke(live.token);