@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
@@ -0,0 +1,143 @@
1
+ import { closeSync, fsyncSync, linkSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, isAbsolute, join } from 'node:path';
5
+ import { fileSecretStore } from "../accounts/secrets.js";
6
+ const fail = () => { throw new Error('Invalid interactive profile configuration.'); };
7
+ const object = (value) => {
8
+ if (!value || typeof value !== 'object' || Array.isArray(value))
9
+ return fail();
10
+ return value;
11
+ };
12
+ const keys = (value, expected) => {
13
+ if (Object.keys(value).length !== expected.length || expected.some(key => !Object.hasOwn(value, key)))
14
+ fail();
15
+ };
16
+ const absolute = (value) => {
17
+ if (typeof value !== 'string' || value.length > 4096 || value.includes('\0') || !isAbsolute(value))
18
+ return fail();
19
+ return value;
20
+ };
21
+ const identifier = (value) => {
22
+ if (typeof value !== 'string' || value.length > 128 || !/^[A-Za-z0-9_-]+$/.test(value))
23
+ return fail();
24
+ return value;
25
+ };
26
+ /** Strict, bounded, credential-free configuration. Unknown fields are errors, not ignored secrets. */
27
+ export function validateInteractiveProfile(value) {
28
+ const record = object(value);
29
+ keys(record, ['version', 'dataRoot', 'stateDir', 'defaultVariantScope', 'secretBackend']);
30
+ if (record.version !== 1)
31
+ return fail();
32
+ const backend = object(record.secretBackend);
33
+ let secretBackend;
34
+ if (backend.kind === 'file') {
35
+ keys(backend, ['kind', 'path']);
36
+ secretBackend = { kind: 'file', path: absolute(backend.path) };
37
+ }
38
+ else if (backend.kind === 'host') {
39
+ keys(backend, ['kind', 'id']);
40
+ secretBackend = { kind: 'host', id: identifier(backend.id) };
41
+ }
42
+ else
43
+ return fail();
44
+ return {
45
+ version: 1,
46
+ dataRoot: absolute(record.dataRoot),
47
+ stateDir: absolute(record.stateDir),
48
+ defaultVariantScope: identifier(record.defaultVariantScope),
49
+ secretBackend
50
+ };
51
+ }
52
+ export function interactiveProfilePath(home = homedir()) {
53
+ return join(absolute(home), '.xingular-harness', 'profile.json');
54
+ }
55
+ function selectedConfigPath(location = {}) {
56
+ return location.configPath === undefined ? interactiveProfilePath(location.home) : absolute(location.configPath);
57
+ }
58
+ /** Read only. Missing is legacy mode; malformed or inaccessible is never silently ignored. */
59
+ export function readInteractiveProfile(location = {}) {
60
+ const path = selectedConfigPath(location);
61
+ try {
62
+ const info = statSync(path);
63
+ if (!info.isFile() || info.size > 16_384)
64
+ return fail();
65
+ return validateInteractiveProfile(JSON.parse(readFileSync(path, 'utf8')));
66
+ }
67
+ catch (error) {
68
+ if (error.code === 'ENOENT')
69
+ return null;
70
+ // Do not include JSON contents, paths, or backend details in diagnostics.
71
+ throw new Error('Interactive profile could not be read or validated.');
72
+ }
73
+ }
74
+ function overrides(value) {
75
+ if (value === undefined)
76
+ return {};
77
+ const record = object(value);
78
+ if (Object.keys(record).some(key => !['dataRoot', 'stateDir', 'defaultVariantScope', 'secretBackend'].includes(key)))
79
+ return fail();
80
+ return record;
81
+ }
82
+ /** Explicit roots > selected profile > caller's legacy defaults. Never creates roots or merges data. */
83
+ export function resolveInteractiveProfile(options = {}) {
84
+ const home = absolute(options.home ?? homedir());
85
+ const root = join(home, '.xingular-harness');
86
+ const selected = readInteractiveProfile(options);
87
+ const legacy = overrides(options.legacyDefaults);
88
+ const explicit = overrides(options.overrides);
89
+ const dataRoot = explicit.dataRoot ?? selected?.dataRoot ?? legacy.dataRoot ?? join(root, 'data');
90
+ const stateDir = explicit.stateDir ?? selected?.stateDir ?? legacy.stateDir ?? join(root, 'state');
91
+ const defaultVariantScope = explicit.defaultVariantScope ?? selected?.defaultVariantScope ?? legacy.defaultVariantScope ?? 'default';
92
+ const secretBackend = explicit.secretBackend ?? selected?.secretBackend ?? legacy.secretBackend ?? {
93
+ kind: 'file',
94
+ path: join(stateDir, 'secrets.json')
95
+ };
96
+ return validateInteractiveProfile({ version: 1, dataRoot, stateDir, defaultVariantScope, secretBackend });
97
+ }
98
+ /**
99
+ * Explicit adoption only. Creates the profile file, never roots or credentials; it never replaces
100
+ * an existing profile or merges data. A pre-profile host must supply its selected backend in
101
+ * `legacyDefaults` so an absent profile cannot silently downgrade it to the file store.
102
+ */
103
+ export function adoptInteractiveProfile(profile, location = {}) {
104
+ const validated = validateInteractiveProfile(profile);
105
+ const path = selectedConfigPath(location);
106
+ const folder = dirname(path);
107
+ mkdirSync(folder, { recursive: true, mode: 0o700 });
108
+ const temporary = join(folder, `.profile-${randomUUID()}.tmp`);
109
+ try {
110
+ const fd = openSync(temporary, 'wx', 0o600);
111
+ try {
112
+ writeFileSync(fd, `${JSON.stringify(validated, null, 2)}\n`);
113
+ fsyncSync(fd);
114
+ }
115
+ finally {
116
+ closeSync(fd);
117
+ }
118
+ linkSync(temporary, path);
119
+ }
120
+ catch {
121
+ throw new Error('Interactive profile adoption failed; existing configuration was not replaced.');
122
+ }
123
+ finally {
124
+ rmSync(temporary, { force: true });
125
+ }
126
+ }
127
+ /**
128
+ * Every participating process must map this host id to the same credential store. The account
129
+ * lock serializes access but cannot unify distinct native Keychain identities; no file fallback is
130
+ * allowed when a selected host adapter is unavailable.
131
+ */
132
+ export async function interactiveProfileSecretStore(profile, host) {
133
+ const { secretBackend } = validateInteractiveProfile(profile);
134
+ if (secretBackend.kind === 'file')
135
+ return fileSecretStore(secretBackend.path);
136
+ if (!host)
137
+ throw new Error('The selected credential backend is unavailable; supply its host SecretStore adapter.');
138
+ const store = await host(secretBackend);
139
+ if (!store || !['get', 'set', 'delete'].every(key => typeof store[key] === 'function')) {
140
+ throw new Error('The selected credential backend did not provide a SecretStore.');
141
+ }
142
+ return store;
143
+ }
@@ -0,0 +1,13 @@
1
+ /** The nearest Git repository containing `cwd`, or its canonical directory when there is none. */
2
+ export declare function resolveWorkspaceRoot(cwd: string): string;
3
+ /**
4
+ * Resolves a write target relative to `cwd` (the workspace root by default), and returns its
5
+ * canonical path only when the target stays inside the canonical workspace root.
6
+ */
7
+ export declare function resolveWorkspaceWritePath(workspaceRoot: string, candidate: string, cwd?: string): string;
8
+ /** Canonicalizes an absolute, relative, or home-relative write target without imposing a root. */
9
+ export declare function resolveWritePath(candidate: string, cwd?: string): string;
10
+ /** Whether a canonical target is within the workspace's canonical filesystem root. */
11
+ export declare function isWithinWorkspace(workspaceRoot: string, canonicalCandidate: string): boolean;
12
+ /** Revalidates a root pinned by an active run; a replaced or redirected root fails closed. */
13
+ export declare function verifyWorkspaceRoot(workspaceRoot: string): string;
@@ -0,0 +1,88 @@
1
+ import { lstatSync, realpathSync, statSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { invalid } from "./errors.js";
5
+ /** The nearest Git repository containing `cwd`, or its canonical directory when there is none. */
6
+ export function resolveWorkspaceRoot(cwd) {
7
+ const canonicalCwd = realpathSync(cwd);
8
+ if (!statSync(canonicalCwd).isDirectory())
9
+ throw invalid('The working directory must be a directory.');
10
+ if (dirname(canonicalCwd) === canonicalCwd)
11
+ throw invalid('The filesystem root cannot be used as an active project workspace.');
12
+ let directory = canonicalCwd;
13
+ while (true) {
14
+ try {
15
+ const marker = lstatSync(resolve(directory, '.git'));
16
+ if (marker.isDirectory() || marker.isFile())
17
+ return directory;
18
+ }
19
+ catch (error) {
20
+ if (error.code !== 'ENOENT')
21
+ throw error;
22
+ }
23
+ const parent = dirname(directory);
24
+ if (parent === directory)
25
+ return canonicalCwd;
26
+ directory = parent;
27
+ }
28
+ }
29
+ /**
30
+ * Resolves a write target relative to `cwd` (the workspace root by default), and returns its
31
+ * canonical path only when the target stays inside the canonical workspace root.
32
+ */
33
+ export function resolveWorkspaceWritePath(workspaceRoot, candidate, cwd = workspaceRoot) {
34
+ const canonicalCandidate = resolveWritePath(candidate, cwd);
35
+ if (!isWithinWorkspace(workspaceRoot, canonicalCandidate))
36
+ throw invalid('Write target is outside the active project workspace.');
37
+ return canonicalCandidate;
38
+ }
39
+ /** Canonicalizes an absolute, relative, or home-relative write target without imposing a root. */
40
+ export function resolveWritePath(candidate, cwd = process.cwd()) {
41
+ const expanded = candidate === '~'
42
+ ? homedir()
43
+ : candidate.startsWith('~/') || candidate.startsWith('~\\')
44
+ ? join(homedir(), candidate.slice(2))
45
+ : candidate;
46
+ const absoluteCandidate = isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded);
47
+ return canonicalizeTarget(absoluteCandidate);
48
+ }
49
+ /** Whether a canonical target is within the workspace's canonical filesystem root. */
50
+ export function isWithinWorkspace(workspaceRoot, canonicalCandidate) {
51
+ const canonicalRoot = realpathSync(workspaceRoot);
52
+ if (!statSync(canonicalRoot).isDirectory())
53
+ throw invalid('The workspace root must be a directory.');
54
+ const fromRoot = relative(canonicalRoot, canonicalCandidate);
55
+ return fromRoot === '' || (fromRoot !== '..' && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot));
56
+ }
57
+ /** Revalidates a root pinned by an active run; a replaced or redirected root fails closed. */
58
+ export function verifyWorkspaceRoot(workspaceRoot) {
59
+ const canonicalRoot = realpathSync(workspaceRoot);
60
+ if (dirname(canonicalRoot) === canonicalRoot)
61
+ throw invalid('The filesystem root cannot be used as an active project workspace.');
62
+ if (canonicalRoot !== resolve(workspaceRoot) || !statSync(canonicalRoot).isDirectory()) {
63
+ throw invalid('The active project workspace changed after this run began.');
64
+ }
65
+ return canonicalRoot;
66
+ }
67
+ function canonicalizeTarget(path) {
68
+ const missing = [];
69
+ let current = path;
70
+ while (true) {
71
+ let exists = false;
72
+ try {
73
+ lstatSync(current);
74
+ exists = true;
75
+ }
76
+ catch (error) {
77
+ if (error.code !== 'ENOENT')
78
+ throw error;
79
+ }
80
+ if (exists)
81
+ return resolve(realpathSync(current), ...missing.reverse());
82
+ const parent = dirname(current);
83
+ if (parent === current)
84
+ throw invalid('Write target could not be resolved.');
85
+ missing.push(basename(current));
86
+ current = parent;
87
+ }
88
+ }
@@ -0,0 +1,4 @@
1
+ /** Quotes command text and escapes terminal control/format characters for approval details. */
2
+ export declare function formatBashCommandForApproval(command: string): string;
3
+ /** Adds the explicit pre-execution escape request to Pi's native Bash tool. */
4
+ export declare function withWorkspaceWriteScope<T>(tool: T): T;
@@ -0,0 +1,36 @@
1
+ import { Type } from 'typebox';
2
+ import { COMMAND_TIMEOUT_CEILING_SECONDS, COMMAND_TIMEOUT_MAX_SECONDS, DEFAULT_COMMAND_TIMEOUT_SECONDS, resolveCommandTimeout } from "./commandBudget.js";
3
+ import { runWithWorkspaceWriteScope } from "./workspaceSandbox.js";
4
+ /** Quotes command text and escapes terminal control/format characters for approval details. */
5
+ export function formatBashCommandForApproval(command) {
6
+ return JSON.stringify(command).replace(/[\u007f-\u009f\u200b-\u200f\u2028-\u202e\u2060-\u206f\ufeff]/gu, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`);
7
+ }
8
+ /** Adds the explicit pre-execution escape request to Pi's native Bash tool. */
9
+ export function withWorkspaceWriteScope(tool) {
10
+ const native = tool;
11
+ const execute = native.execute.bind(tool);
12
+ return {
13
+ ...tool,
14
+ description: `${native.description} Workspace writes are the default. Set workspaceWriteScope to "outside" to request approval before broader writes. Commands run for ${DEFAULT_COMMAND_TIMEOUT_SECONDS} seconds unless timeout says otherwise; above ${COMMAND_TIMEOUT_CEILING_SECONDS} seconds a reason is required, and ${COMMAND_TIMEOUT_MAX_SECONDS} seconds is the maximum.`,
15
+ parameters: Type.Object({
16
+ command: Type.String({ description: 'Shell command to execute' }),
17
+ timeout: Type.Optional(Type.Number({ description: `Timeout in seconds (default ${DEFAULT_COMMAND_TIMEOUT_SECONDS})` })),
18
+ reason: Type.Optional(Type.String({ description: `Why a command needs more than ${COMMAND_TIMEOUT_CEILING_SECONDS} seconds` })),
19
+ workspaceWriteScope: Type.Optional(Type.Union([
20
+ Type.Literal('workspace'),
21
+ Type.Literal('outside')
22
+ ], { description: 'Write scope for this command. Defaults to workspace; outside requests approval before running.' }))
23
+ }),
24
+ execute: (...args) => {
25
+ const input = args[1];
26
+ const scope = input?.workspaceWriteScope === 'outside' ? 'outside' : 'workspace';
27
+ const budget = resolveCommandTimeout(input?.timeout, input?.reason);
28
+ if (!budget.ok)
29
+ return { content: [{ type: 'text', text: budget.message }], details: { refused: true } };
30
+ // The native tool reads the timeout from its own arguments; give it the effective one.
31
+ const callArgs = [...args];
32
+ callArgs[1] = { ...(input ?? {}), timeout: budget.timeout };
33
+ return runWithWorkspaceWriteScope(scope, () => execute(...callArgs));
34
+ }
35
+ };
36
+ }
@@ -0,0 +1,31 @@
1
+ import type { BashOperations } from '@earendil-works/pi-coding-agent';
2
+ /** `read-only` (plan mode) permits no workspace writes, no network and no outside processes. */
3
+ export type WorkspaceWriteScope = 'workspace' | 'outside' | 'read-only';
4
+ export declare function runWithWorkspaceWriteScope<T>(scope: WorkspaceWriteScope, action: () => T): T;
5
+ export interface WorkspaceWriteScopeRequest {
6
+ command: string;
7
+ cwd: string;
8
+ workspaceRoot: string;
9
+ reason: string;
10
+ }
11
+ export interface WorkspaceBoundaryBypassRequest {
12
+ command: string;
13
+ cwd: string;
14
+ workspaceRoot: string;
15
+ }
16
+ export interface WorkspaceBashOptions {
17
+ /** The explicit Full Permission switch can bypass the workspace boundary before the command runs. */
18
+ bypassWorkspaceBoundary?: (request: WorkspaceBoundaryBypassRequest) => Promise<boolean>;
19
+ requestWorkspaceWriteScope?: (request: WorkspaceWriteScopeRequest) => Promise<boolean>;
20
+ /** Overrides the per-call scope; `undefined` falls back to it. */
21
+ workspaceWriteScope?: () => WorkspaceWriteScope | undefined;
22
+ /** Exposed for platform-specific tests; production uses the current operating system. */
23
+ platform?: NodeJS.Platform;
24
+ }
25
+ export interface WorkspaceSandboxCommand {
26
+ executable: string;
27
+ args: string[];
28
+ marker: string;
29
+ }
30
+ export declare function workspaceSandboxCommand(platform: NodeJS.Platform, workspaceRoot: string, cwd: string, command: string, additionalWriteRoots?: readonly string[], readOnlyTempRoot?: string | null): WorkspaceSandboxCommand;
31
+ export declare function createWorkspaceBashOperations(workspaceRoot: string | (() => string | null), options?: WorkspaceBashOptions): BashOperations;
@@ -0,0 +1,320 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { spawn } from 'node:child_process';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { accessSync, constants, existsSync, lstatSync, mkdtempSync, realpathSync, readdirSync, rmSync, statSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { isWithinWorkspace, verifyWorkspaceRoot } from "./workspace.js";
8
+ const MARKER_PREFIX = '__XINGULAR_WORKSPACE_SANDBOX_STARTED_';
9
+ const bashWriteScope = new AsyncLocalStorage();
10
+ export function runWithWorkspaceWriteScope(scope, action) {
11
+ return bashWriteScope.run(scope, action);
12
+ }
13
+ function systemExecutable(candidates) {
14
+ for (const candidate of candidates) {
15
+ try {
16
+ accessSync(candidate, constants.X_OK);
17
+ if (statSync(candidate).isFile())
18
+ return candidate;
19
+ }
20
+ catch {
21
+ // Keep looking only at known system locations.
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ function shellPath() {
27
+ const shell = ['/bin/bash', '/usr/bin/bash'].find((candidate) => existsSync(candidate));
28
+ if (!shell)
29
+ throw new Error('The workspace write-boundary sandbox requires Bash, but no Bash executable was found.');
30
+ return shell;
31
+ }
32
+ function sbplString(value) {
33
+ return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\n', '\\n')}"`;
34
+ }
35
+ // Read-only commands need no mach services; denying them all keeps a command from acting through
36
+ // system daemons (preferences, Apple Events, Launch Services), which the file rules do not cover.
37
+ function macReadOnlyProfile(tempRoot) {
38
+ return [
39
+ '(version 1)',
40
+ '(deny default)',
41
+ '(allow file-read*)',
42
+ `(allow file-write* (subpath ${sbplString(tempRoot)}))`,
43
+ '(allow file-write* (literal "/dev/null"))',
44
+ '(allow file-write* (literal "/dev/tty"))',
45
+ '(deny file-link)',
46
+ '(allow process-fork)',
47
+ '(allow process-exec)',
48
+ '(allow signal (target same-sandbox))',
49
+ '(allow sysctl-read)'
50
+ ].join('');
51
+ }
52
+ function macSandboxProfile(writeRoots) {
53
+ return [
54
+ '(version 1)',
55
+ '(deny default)',
56
+ '(allow file-read*)',
57
+ ...writeRoots.map((root) => `(allow file-write* (subpath ${sbplString(root)}))`),
58
+ '(allow file-write* (literal "/dev/null"))',
59
+ '(allow file-write* (literal "/dev/tty"))',
60
+ '(deny file-link)',
61
+ '(allow process*)',
62
+ '(allow network*)',
63
+ '(allow sysctl-read)',
64
+ '(allow mach-lookup)'
65
+ ].join('');
66
+ }
67
+ export function workspaceSandboxCommand(platform, workspaceRoot, cwd, command, additionalWriteRoots = [], readOnlyTempRoot = null) {
68
+ const shell = shellPath();
69
+ const marker = `${MARKER_PREFIX}${randomUUID()}__`;
70
+ const wrappedCommand = `printf '%s\\n' '${marker}'\n${command}`;
71
+ if (readOnlyTempRoot !== null)
72
+ return readOnlySandboxCommand(platform, realpathSync(readOnlyTempRoot), cwd, shell, wrappedCommand, marker);
73
+ const writeRoots = [...new Set([workspaceRoot, ...additionalWriteRoots].map((root) => realpathSync(root)))].sort((left, right) => left.length - right.length);
74
+ if (platform === 'darwin') {
75
+ const executable = systemExecutable(['/usr/bin/sandbox-exec']);
76
+ if (!executable)
77
+ throw new Error('Workspace write-boundary sandbox unavailable: sandbox-exec was not found. Bash was not run.');
78
+ return { executable, args: ['-p', macSandboxProfile(writeRoots), shell, '-c', wrappedCommand], marker };
79
+ }
80
+ if (platform === 'linux') {
81
+ const executable = systemExecutable(['/usr/bin/bwrap', '/bin/bwrap']);
82
+ if (!executable)
83
+ throw new Error('Workspace write-boundary sandbox unavailable: install bubblewrap (bwrap). Bash was not run.');
84
+ return {
85
+ executable,
86
+ marker,
87
+ args: [
88
+ '--die-with-parent', '--new-session', '--unshare-pid',
89
+ '--ro-bind', '/', '/',
90
+ '--dev', '/dev',
91
+ '--proc', '/proc',
92
+ ...writeRoots.flatMap((root) => ['--bind', root, root]),
93
+ '--chdir', cwd,
94
+ shell, '-c', wrappedCommand
95
+ ]
96
+ };
97
+ }
98
+ throw new Error(`Workspace write-boundary sandbox unavailable on ${platform}. Bash was not run.`);
99
+ }
100
+ function readOnlySandboxCommand(platform, tempRoot, cwd, shell, wrappedCommand, marker) {
101
+ if (platform === 'darwin') {
102
+ const executable = systemExecutable(['/usr/bin/sandbox-exec']);
103
+ if (!executable)
104
+ throw new Error('Read-only Bash sandbox unavailable: sandbox-exec was not found. Bash was not run.');
105
+ return { executable, args: ['-p', macReadOnlyProfile(tempRoot), shell, '-c', wrappedCommand], marker };
106
+ }
107
+ if (platform === 'linux') {
108
+ const executable = systemExecutable(['/usr/bin/bwrap', '/bin/bwrap']);
109
+ if (!executable)
110
+ throw new Error('Read-only Bash sandbox unavailable: install bubblewrap (bwrap). Bash was not run.');
111
+ return {
112
+ executable,
113
+ marker,
114
+ args: [
115
+ '--die-with-parent', '--new-session', '--unshare-pid', '--unshare-net', '--unshare-ipc',
116
+ '--ro-bind', '/', '/',
117
+ '--dev', '/dev',
118
+ '--proc', '/proc',
119
+ // No session bus or other host sockets under /run.
120
+ '--tmpfs', '/run',
121
+ '--bind', tempRoot, tempRoot,
122
+ '--chdir', cwd,
123
+ shell, '-c', wrappedCommand
124
+ ]
125
+ };
126
+ }
127
+ throw new Error(`Read-only Bash sandbox unavailable on ${platform}. Bash was not run.`);
128
+ }
129
+ function killProcess(child) {
130
+ if (child.pid && process.platform !== 'win32') {
131
+ try {
132
+ process.kill(-child.pid, 'SIGKILL');
133
+ return;
134
+ }
135
+ catch {
136
+ // Fall back to the direct child if it has already left its process group.
137
+ }
138
+ }
139
+ child.kill('SIGKILL');
140
+ }
141
+ function runProcess(spec, cwd, options, marker) {
142
+ if (options.signal?.aborted)
143
+ return Promise.reject(new Error('aborted'));
144
+ return new Promise((resolve, reject) => {
145
+ const child = spawn(spec.executable, spec.args, {
146
+ cwd,
147
+ detached: process.platform !== 'win32',
148
+ env: options.env ?? process.env,
149
+ stdio: ['ignore', 'pipe', 'pipe'],
150
+ windowsHide: true
151
+ });
152
+ let pending = '';
153
+ let started = marker === null;
154
+ let timedOut = false;
155
+ let settled = false;
156
+ let timeoutHandle;
157
+ const deliver = (chunk) => {
158
+ const text = chunk.toString('utf8');
159
+ if (!started && marker) {
160
+ pending += text;
161
+ const markerAt = pending.indexOf(marker);
162
+ if (markerAt === -1) {
163
+ // Preserve only a short prefix in case the shell emitted a startup warning.
164
+ if (pending.length > marker.length + 2048) {
165
+ options.onData(Buffer.from(pending.slice(0, pending.length - marker.length - 2048)));
166
+ pending = pending.slice(-(marker.length + 2048));
167
+ }
168
+ return;
169
+ }
170
+ const before = pending.slice(0, markerAt);
171
+ const after = pending.slice(markerAt + marker.length).replace(/^\r?\n/u, '');
172
+ if (before)
173
+ options.onData(Buffer.from(before));
174
+ if (after)
175
+ options.onData(Buffer.from(after));
176
+ pending = '';
177
+ started = true;
178
+ return;
179
+ }
180
+ options.onData(chunk);
181
+ };
182
+ const finish = (code) => {
183
+ if (settled)
184
+ return;
185
+ settled = true;
186
+ if (timeoutHandle)
187
+ clearTimeout(timeoutHandle);
188
+ options.signal?.removeEventListener('abort', onAbort);
189
+ if (!started && pending)
190
+ options.onData(Buffer.from(pending));
191
+ if (options.signal?.aborted)
192
+ reject(new Error('aborted'));
193
+ else if (timedOut)
194
+ reject(new Error(`timeout:${options.timeout}`));
195
+ else
196
+ resolve({ exitCode: code });
197
+ };
198
+ const onAbort = () => killProcess(child);
199
+ if (options.timeout !== undefined && options.timeout > 0) {
200
+ timeoutHandle = setTimeout(() => {
201
+ timedOut = true;
202
+ killProcess(child);
203
+ }, options.timeout * 1000);
204
+ }
205
+ options.signal?.addEventListener('abort', onAbort, { once: true });
206
+ child.stdout?.on('data', deliver);
207
+ child.stderr?.on('data', deliver);
208
+ child.once('error', (error) => {
209
+ if (settled)
210
+ return;
211
+ settled = true;
212
+ if (timeoutHandle)
213
+ clearTimeout(timeoutHandle);
214
+ options.signal?.removeEventListener('abort', onAbort);
215
+ reject(error);
216
+ });
217
+ child.once('close', finish);
218
+ });
219
+ }
220
+ function assertNoExternalHardlinks(workspaceRoot) {
221
+ const inodes = new Map();
222
+ const visit = (directory) => {
223
+ let entries;
224
+ try {
225
+ entries = readdirSync(directory, { withFileTypes: true });
226
+ }
227
+ catch (error) {
228
+ throw new Error(`Workspace hard-link inspection failed at ${directory}; Bash was not run: ${error instanceof Error ? error.message : String(error)}`);
229
+ }
230
+ for (const entry of entries) {
231
+ const path = `${directory}/${entry.name}`;
232
+ let stats;
233
+ try {
234
+ stats = lstatSync(path);
235
+ }
236
+ catch (error) {
237
+ throw new Error(`Workspace hard-link inspection failed at ${path}; Bash was not run: ${error instanceof Error ? error.message : String(error)}`);
238
+ }
239
+ if (stats.isSymbolicLink())
240
+ continue;
241
+ if (stats.isDirectory()) {
242
+ visit(path);
243
+ continue;
244
+ }
245
+ if (!stats.isFile())
246
+ continue;
247
+ const key = `${stats.dev}:${stats.ino}`;
248
+ const inode = inodes.get(key);
249
+ if (inode)
250
+ inode.internalLinks += 1;
251
+ else
252
+ inodes.set(key, { internalLinks: 1, totalLinks: stats.nlink, samplePath: path });
253
+ }
254
+ };
255
+ visit(workspaceRoot);
256
+ for (const inode of inodes.values()) {
257
+ if (inode.totalLinks > inode.internalLinks) {
258
+ throw new Error(`Workspace write-boundary sandbox cannot safely run Bash because ${inode.samplePath} has a hard-link alias outside the workspace; remove the alias or replace the file with a copy.`);
259
+ }
260
+ }
261
+ }
262
+ export function createWorkspaceBashOperations(workspaceRoot, options = {}) {
263
+ return {
264
+ async exec(command, cwd, execOptions) {
265
+ const rootValue = typeof workspaceRoot === 'function' ? workspaceRoot() : workspaceRoot;
266
+ if (!rootValue)
267
+ throw new Error('The active project workspace is not available. Bash was not run.');
268
+ const root = verifyWorkspaceRoot(rootValue);
269
+ const canonicalCwd = realpathSync(cwd);
270
+ if (!isWithinWorkspace(root, canonicalCwd))
271
+ throw new Error('The Bash working directory is outside the active project workspace.');
272
+ const scope = options.workspaceWriteScope?.() ?? bashWriteScope.getStore() ?? 'workspace';
273
+ // Nothing widens a read-only scope: no Full Permission bypass, no outside request.
274
+ if (scope === 'read-only') {
275
+ const platform = options.platform ?? process.platform;
276
+ const tempRoot = realpathSync(mkdtempSync(join(tmpdir(), 'harness-read-only-bash-')));
277
+ try {
278
+ const spec = workspaceSandboxCommand(platform, root, canonicalCwd, command, [], tempRoot);
279
+ const env = { ...(execOptions.env ?? process.env), TMPDIR: tempRoot };
280
+ const result = await runProcess(spec, canonicalCwd, { ...execOptions, env }, spec.marker);
281
+ return { exitCode: result.exitCode };
282
+ }
283
+ finally {
284
+ rmSync(tempRoot, { recursive: true, force: true });
285
+ }
286
+ }
287
+ let bypass = false;
288
+ try {
289
+ bypass = await options.bypassWorkspaceBoundary?.({ command, cwd: canonicalCwd, workspaceRoot: root }) ?? false;
290
+ }
291
+ catch {
292
+ bypass = false;
293
+ }
294
+ if (bypass) {
295
+ const result = await runProcess({ executable: shellPath(), args: ['-c', command], marker: '' }, canonicalCwd, execOptions, null);
296
+ return { exitCode: result.exitCode };
297
+ }
298
+ if (scope === 'outside') {
299
+ const reason = 'This command requests filesystem writes outside the active project workspace. It will not run until this one-call request is approved.';
300
+ let approved = false;
301
+ try {
302
+ approved = await options.requestWorkspaceWriteScope?.({ command, cwd: canonicalCwd, workspaceRoot: root, reason }) ?? false;
303
+ }
304
+ catch {
305
+ approved = false;
306
+ }
307
+ if (!approved)
308
+ throw new Error(`${reason} Bash was not run.`);
309
+ execOptions.onData(Buffer.from('\n[Outside-workspace writes approved for this command; running once.]\n'));
310
+ const result = await runProcess({ executable: shellPath(), args: ['-c', command], marker: '' }, canonicalCwd, execOptions, null);
311
+ return { exitCode: result.exitCode };
312
+ }
313
+ assertNoExternalHardlinks(root);
314
+ const platform = options.platform ?? process.platform;
315
+ const spec = workspaceSandboxCommand(platform, root, canonicalCwd, command);
316
+ const result = await runProcess(spec, canonicalCwd, execOptions, spec.marker);
317
+ return { exitCode: result.exitCode };
318
+ }
319
+ };
320
+ }
@@ -0,0 +1,22 @@
1
+ export interface WorkspaceWriteAuthorization {
2
+ workspaceRoot: string;
3
+ cwd: string;
4
+ path: string;
5
+ outside: boolean;
6
+ authorized: boolean;
7
+ expectedContentHash?: string;
8
+ signal?: AbortSignal;
9
+ }
10
+ declare function writeWorkspaceFile(path: string, content: string): Promise<void>;
11
+ export declare const workspaceWriteOperations: {
12
+ mkdir: (_directory: string) => Promise<void>;
13
+ writeFile: typeof writeWorkspaceFile;
14
+ };
15
+ export declare const workspaceEditOperations: {
16
+ access: (path: string) => Promise<void>;
17
+ readFile: (path: string) => Promise<Buffer>;
18
+ writeFile: typeof writeWorkspaceFile;
19
+ };
20
+ /** Binds one policy-approved path and abort signal to Pi's native write/edit operation. */
21
+ export declare function withWorkspaceWriteAuthorization<T>(tool: T, authorizations: Map<string, WorkspaceWriteAuthorization>): T;
22
+ export {};