one-code-extension 0.4.1 → 0.6.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 (439) hide show
  1. package/README.md +16 -5
  2. package/agents/general-purpose.md +2 -2
  3. package/docs/guide/README.md +2 -1
  4. package/docs/guide/bring-your-claude-code-setup.md +60 -9
  5. package/docs/guide/configuration.md +48 -14
  6. package/docs/guide/differences-from-claude-code.md +40 -28
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +26 -4
  10. package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
  11. package/docs/guide/providers-and-models.md +137 -54
  12. package/docs/guide/reference.md +27 -15
  13. package/docs/guide/sessions-and-context.md +69 -16
  14. package/docs/guide/skills-plugins-and-mcp.md +57 -20
  15. package/docs/guide/subagents-and-workflows.md +95 -29
  16. package/docs/guide/tasks-and-background-work.md +101 -26
  17. package/docs/guide/terminal-interface.md +24 -12
  18. package/docs/guide/tools.md +46 -5
  19. package/docs/guide/troubleshooting.md +13 -9
  20. package/docs/guide/windows.md +58 -31
  21. package/extensions/artifacts/gallery.ts +227 -0
  22. package/extensions/artifacts/index.ts +288 -0
  23. package/extensions/artifacts/store.ts +570 -0
  24. package/extensions/artifacts/viewer.ts +199 -0
  25. package/extensions/ask-user/description.ts +67 -0
  26. package/extensions/ask-user/dialogs.ts +80 -0
  27. package/extensions/ask-user/index.ts +37 -33
  28. package/extensions/ask-user/questions.ts +32 -7
  29. package/extensions/ask-user/widget.ts +45 -25
  30. package/extensions/auto-mode/actions.ts +6 -0
  31. package/extensions/auto-mode/cache.ts +88 -0
  32. package/extensions/auto-mode/classifier-prompt.ts +5 -4
  33. package/extensions/auto-mode/classifier.ts +290 -65
  34. package/extensions/auto-mode/config.ts +72 -20
  35. package/extensions/auto-mode/decision-log.ts +2 -0
  36. package/extensions/auto-mode/defaults.ts +1 -1
  37. package/extensions/auto-mode/denials.ts +124 -0
  38. package/extensions/auto-mode/git-checkout-programs.ts +187 -0
  39. package/extensions/auto-mode/git-status-meta.ts +159 -0
  40. package/extensions/auto-mode/history.ts +189 -0
  41. package/extensions/auto-mode/instructions.ts +58 -52
  42. package/extensions/auto-mode/model-select.ts +159 -134
  43. package/extensions/auto-mode/paths.ts +57 -5
  44. package/extensions/auto-mode/pause.ts +57 -2
  45. package/extensions/auto-mode/prompt.ts +37 -21
  46. package/extensions/auto-mode/read-only-options.ts +724 -0
  47. package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
  48. package/extensions/auto-mode/rules.ts +18 -1
  49. package/extensions/auto-mode/safety-floor.ts +361 -18
  50. package/extensions/auto-mode/setup-run.ts +20 -14
  51. package/extensions/auto-mode/shell-analysis.ts +764 -490
  52. package/extensions/auto-mode/shell-parse.ts +885 -0
  53. package/extensions/auto-mode/system-note.ts +33 -0
  54. package/extensions/auto-mode/transcript.ts +80 -51
  55. package/extensions/background/cron.ts +642 -0
  56. package/extensions/background/index.ts +614 -248
  57. package/extensions/background/loop-fire.ts +173 -0
  58. package/extensions/background/loop-preamble.ts +59 -0
  59. package/extensions/background/loop-skill.ts +163 -0
  60. package/extensions/background/monitor-batch.ts +53 -0
  61. package/extensions/background/noop-fold.ts +128 -0
  62. package/extensions/background/switch-guard.ts +56 -0
  63. package/extensions/background/wakeup.ts +178 -55
  64. package/extensions/bash/background.ts +53 -17
  65. package/extensions/bash/child.ts +7 -0
  66. package/extensions/bash/description.ts +188 -0
  67. package/extensions/bash/guards.ts +15 -67
  68. package/extensions/bash/index.ts +66 -35
  69. package/extensions/branding/compaction-keep.ts +63 -0
  70. package/extensions/branding/declined.ts +22 -0
  71. package/extensions/branding/index.ts +133 -9
  72. package/extensions/branding/prompt-editor.ts +53 -6
  73. package/extensions/branding/queued-edit.ts +31 -0
  74. package/extensions/branding/replaced-builtins.ts +68 -0
  75. package/extensions/branding/startup.ts +33 -71
  76. package/extensions/btw/index.ts +250 -78
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/cache-marks/index.ts +42 -0
  80. package/extensions/claude-compat/index.ts +7 -11
  81. package/extensions/claude-context/index.ts +436 -63
  82. package/extensions/compaction/cut.ts +157 -0
  83. package/extensions/compaction/index.ts +219 -75
  84. package/extensions/compaction/kept-thinking.ts +62 -0
  85. package/extensions/compaction/prompt.ts +15 -12
  86. package/extensions/context-budget/budget.ts +16 -2
  87. package/extensions/context-budget/index.ts +35 -15
  88. package/extensions/context-management/index.ts +36 -28
  89. package/extensions/doctor/build.ts +59 -8
  90. package/extensions/doctor/builtins.ts +84 -0
  91. package/extensions/doctor/cli.ts +7 -1
  92. package/extensions/doctor/compat.ts +88 -23
  93. package/extensions/doctor/dependencies.ts +41 -5
  94. package/extensions/doctor/fix-prompt.ts +9 -2
  95. package/extensions/doctor/index.ts +102 -58
  96. package/extensions/doctor/models.ts +102 -63
  97. package/extensions/doctor/presets.ts +42 -27
  98. package/extensions/doctor/report.ts +19 -2
  99. package/extensions/doctor/viewer.ts +49 -27
  100. package/extensions/effort/index.ts +141 -25
  101. package/extensions/effort/settings.ts +21 -0
  102. package/extensions/effort/slider.ts +24 -20
  103. package/extensions/exit/index.ts +32 -1
  104. package/extensions/file-tracker/index.ts +420 -42
  105. package/extensions/file-tracker/replay.ts +137 -24
  106. package/extensions/file-tracker/restore.ts +66 -0
  107. package/extensions/file-tracker/results.ts +66 -0
  108. package/extensions/file-tracker/shell-reads.ts +211 -0
  109. package/extensions/file-tracker/tracker.ts +45 -15
  110. package/extensions/footer/footer-line.ts +45 -24
  111. package/extensions/footer/index.ts +47 -2
  112. package/extensions/footer/pr.ts +34 -5
  113. package/extensions/hooks/compaction.ts +7 -0
  114. package/extensions/hooks/executor.ts +32 -30
  115. package/extensions/hooks/index.ts +209 -58
  116. package/extensions/hooks/matcher.ts +3 -0
  117. package/extensions/hooks/prompt-context.ts +59 -0
  118. package/extensions/hooks/protocol.ts +41 -3
  119. package/extensions/hooks/settings.ts +50 -19
  120. package/extensions/hooks/subagent-bridge.ts +2 -0
  121. package/extensions/hooks/trust.ts +26 -12
  122. package/extensions/idle-compact/index.ts +119 -0
  123. package/extensions/idle-compact/policy.ts +143 -0
  124. package/extensions/init/index.ts +11 -10
  125. package/extensions/init/prompt.ts +25 -32
  126. package/extensions/interrupted/index.ts +13 -1
  127. package/extensions/kept-thinking/index.ts +27 -0
  128. package/extensions/ledger/index.ts +46 -0
  129. package/extensions/ledger/wire.ts +154 -0
  130. package/extensions/lib/agent-cron.ts +146 -0
  131. package/extensions/lib/agent-loader.ts +18 -3
  132. package/extensions/lib/agent-view.ts +85 -0
  133. package/extensions/lib/anthropic-auth.ts +98 -0
  134. package/extensions/lib/anthropic-payload.ts +82 -1
  135. package/extensions/lib/anthropic-server-call.ts +230 -0
  136. package/extensions/lib/anthropic-server-tools.ts +485 -0
  137. package/extensions/lib/app-launch.mjs +94 -0
  138. package/extensions/lib/argument-hints.ts +190 -0
  139. package/extensions/lib/atomic-write.ts +6 -2
  140. package/extensions/lib/auto-memory.ts +75 -0
  141. package/extensions/lib/bash-parser.ts +84 -0
  142. package/extensions/lib/btw-fork.ts +93 -0
  143. package/extensions/lib/child-extensions.ts +52 -0
  144. package/extensions/lib/child-writes.ts +45 -0
  145. package/extensions/lib/claude-context.ts +515 -100
  146. package/extensions/lib/claude-external-includes.ts +68 -0
  147. package/extensions/lib/claude-id.ts +22 -0
  148. package/extensions/lib/claude-rules.ts +323 -0
  149. package/extensions/lib/claude-settings.ts +76 -4
  150. package/extensions/lib/compaction-boundary.ts +36 -0
  151. package/extensions/lib/compaction-keep.mjs +83 -0
  152. package/extensions/lib/config-mode.ts +112 -0
  153. package/extensions/lib/consent-dialogs.ts +63 -0
  154. package/extensions/lib/consent-preview.ts +5 -1
  155. package/extensions/lib/consent-stores.ts +48 -0
  156. package/extensions/lib/context-facts.ts +97 -0
  157. package/extensions/lib/covered-messages.ts +23 -0
  158. package/extensions/lib/deferred.ts +359 -25
  159. package/extensions/lib/dock-rows.ts +60 -0
  160. package/extensions/lib/empty-tool-result.ts +27 -0
  161. package/extensions/lib/environment-block.ts +76 -0
  162. package/extensions/lib/fetch-timeout.ts +24 -6
  163. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  164. package/extensions/lib/git.ts +75 -13
  165. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  166. package/extensions/lib/grammars/README.md +2 -0
  167. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  168. package/extensions/lib/guide-docs.ts +45 -0
  169. package/extensions/lib/headless-output.ts +46 -0
  170. package/extensions/lib/hook-gate.ts +7 -7
  171. package/extensions/lib/interrupt.ts +20 -4
  172. package/extensions/lib/key-input.ts +455 -0
  173. package/extensions/lib/keys.ts +14 -1
  174. package/extensions/lib/ledger/fold-memo.ts +49 -0
  175. package/extensions/lib/ledger/fold.ts +265 -0
  176. package/extensions/lib/ledger/layout.ts +72 -0
  177. package/extensions/lib/ledger/plan.ts +102 -0
  178. package/extensions/lib/ledger/records.ts +276 -0
  179. package/extensions/lib/ledger/state.ts +47 -0
  180. package/extensions/lib/ledger/validate.ts +141 -0
  181. package/extensions/lib/ledger/wire.ts +632 -0
  182. package/extensions/lib/local-command.ts +71 -19
  183. package/extensions/lib/mcp-overrides.ts +1 -1
  184. package/extensions/lib/mcp-share.ts +46 -0
  185. package/extensions/lib/memory-content.ts +146 -0
  186. package/extensions/lib/memory-index.ts +18 -0
  187. package/extensions/lib/memory.ts +134 -123
  188. package/extensions/lib/model-catalog/huggingface.json +178 -0
  189. package/extensions/lib/model-catalog/models-dev.json +18533 -0
  190. package/extensions/lib/model-catalog/openrouter.json +5050 -0
  191. package/extensions/lib/model-catalog-data.ts +513 -0
  192. package/extensions/lib/model-catalog.ts +462 -0
  193. package/extensions/lib/model-choice-warnings.ts +137 -0
  194. package/extensions/lib/model-pairings.ts +152 -0
  195. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  196. package/extensions/lib/model-policy.ts +112 -15
  197. package/extensions/lib/model-tier.ts +283 -355
  198. package/extensions/lib/model-unusable.ts +1 -1
  199. package/extensions/lib/newer-model.ts +100 -0
  200. package/extensions/lib/notices.ts +87 -0
  201. package/extensions/lib/notifications.ts +357 -73
  202. package/extensions/lib/one-code-settings.ts +74 -8
  203. package/extensions/lib/open-browser.ts +63 -0
  204. package/extensions/lib/openrouter-generation.ts +94 -0
  205. package/extensions/lib/operation-deadline.ts +30 -0
  206. package/extensions/lib/original-command.ts +4 -4
  207. package/extensions/lib/package-version.ts +1 -1
  208. package/extensions/lib/panel-focus.ts +134 -0
  209. package/extensions/lib/paths.ts +17 -2
  210. package/extensions/lib/permission-gate.ts +72 -6
  211. package/extensions/lib/persisted-output.ts +60 -8
  212. package/extensions/lib/pi-ai-estimate.ts +19 -3
  213. package/extensions/lib/pi-version.ts +38 -2
  214. package/extensions/lib/plan-mode-channels.ts +42 -5
  215. package/extensions/lib/plugins.ts +26 -17
  216. package/extensions/lib/powershell-parse-script.ts +208 -0
  217. package/extensions/lib/powershell-parser.ts +344 -0
  218. package/extensions/lib/previous-mark.ts +101 -0
  219. package/extensions/lib/process-state.ts +48 -0
  220. package/extensions/lib/process-tree.ts +132 -8
  221. package/extensions/lib/prompt-options.ts +82 -0
  222. package/extensions/lib/queued-delivery.ts +44 -0
  223. package/extensions/lib/reminders.ts +370 -358
  224. package/extensions/lib/replaced-builtins.mjs +199 -0
  225. package/extensions/lib/replay-call.ts +70 -0
  226. package/extensions/lib/request-replay.ts +547 -0
  227. package/extensions/lib/scratchpad.ts +81 -14
  228. package/extensions/lib/send-now-channel.ts +7 -0
  229. package/extensions/lib/session-model-tier.ts +51 -0
  230. package/extensions/lib/session-work.ts +64 -0
  231. package/extensions/lib/settings-channels.ts +4 -4
  232. package/extensions/lib/shell-exit-semantics.ts +170 -0
  233. package/extensions/lib/shell-quote.ts +11 -0
  234. package/extensions/lib/shell-result.ts +70 -0
  235. package/extensions/lib/shell-spawn.ts +70 -6
  236. package/extensions/lib/shell-tool.ts +75 -25
  237. package/extensions/lib/side-call-cache.ts +124 -0
  238. package/extensions/lib/side-call-run.ts +96 -0
  239. package/extensions/lib/side-call-usage.ts +38 -0
  240. package/extensions/lib/side-call.ts +24 -1
  241. package/extensions/lib/single-flight.ts +18 -0
  242. package/extensions/lib/skill-body.ts +42 -0
  243. package/extensions/lib/skill-scan.ts +63 -24
  244. package/extensions/lib/spill-file.ts +140 -0
  245. package/extensions/lib/subagent-handback.ts +112 -0
  246. package/extensions/lib/system-role.ts +156 -0
  247. package/extensions/lib/terminal-text.ts +114 -0
  248. package/extensions/lib/text-width.ts +47 -16
  249. package/extensions/lib/timer-ops.ts +20 -0
  250. package/extensions/lib/tool-additions.ts +129 -0
  251. package/extensions/lib/tool-call-corruption.ts +134 -0
  252. package/extensions/lib/tool-path.ts +66 -0
  253. package/extensions/lib/tool-variants.ts +69 -0
  254. package/extensions/lib/tui-render.ts +255 -27
  255. package/extensions/lib/turn-budget-layout.ts +159 -0
  256. package/extensions/lib/update-check.mjs +15 -2
  257. package/extensions/lib/usage-bus.ts +1 -1
  258. package/extensions/lib/user-denial.ts +38 -0
  259. package/extensions/lib/workspace-channel.ts +16 -0
  260. package/extensions/lib/worktree-channel.ts +31 -3
  261. package/extensions/lib/worktree-isolation.ts +40 -10
  262. package/extensions/lsp/client.ts +8 -9
  263. package/extensions/lsp/index.ts +76 -12
  264. package/extensions/lsp/keep-alive.ts +1 -1
  265. package/extensions/lsp/trust.ts +119 -0
  266. package/extensions/lsp/watcher.ts +1 -1
  267. package/extensions/mcp/announce.ts +90 -43
  268. package/extensions/mcp/client.ts +161 -48
  269. package/extensions/mcp/config.ts +132 -8
  270. package/extensions/mcp/discovery.ts +19 -0
  271. package/extensions/mcp/index.ts +220 -121
  272. package/extensions/mcp/oauth/browser.ts +38 -17
  273. package/extensions/mcp/oauth/flow.ts +25 -17
  274. package/extensions/mcp/oauth/provider.ts +14 -8
  275. package/extensions/mcp/oauth/store.ts +34 -16
  276. package/extensions/mcp/panel/keys.ts +12 -11
  277. package/extensions/mcp/panel/render.ts +9 -6
  278. package/extensions/mcp/schema.ts +104 -5
  279. package/extensions/mcp/trust.ts +164 -73
  280. package/extensions/memory/entries.ts +23 -14
  281. package/extensions/memory/index.ts +134 -28
  282. package/extensions/memory/open-external.ts +33 -18
  283. package/extensions/memory/panel.ts +135 -26
  284. package/extensions/model-default/index.ts +86 -19
  285. package/extensions/newer-model/index.ts +31 -0
  286. package/extensions/notebook/description.ts +18 -0
  287. package/extensions/notebook/index.ts +6 -5
  288. package/extensions/notebook/notebook.ts +1 -1
  289. package/extensions/permissions/index.ts +1426 -398
  290. package/extensions/permissions/matcher.ts +425 -74
  291. package/extensions/permissions/modes.ts +11 -4
  292. package/extensions/permissions/panel/host.ts +130 -0
  293. package/extensions/permissions/panel/keys.ts +48 -0
  294. package/extensions/permissions/panel/render.ts +450 -0
  295. package/extensions/permissions/panel/state.ts +550 -0
  296. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  297. package/extensions/permissions/powershell-paths.ts +197 -0
  298. package/extensions/permissions/powershell-rules.ts +207 -280
  299. package/extensions/permissions/powershell-tree.ts +557 -0
  300. package/extensions/permissions/project-trust.ts +27 -11
  301. package/extensions/permissions/protected-paths.ts +14 -2
  302. package/extensions/permissions/session-grant.ts +6 -0
  303. package/extensions/permissions/settings.ts +166 -31
  304. package/extensions/permissions/subagent-gate.ts +25 -0
  305. package/extensions/permissions/workspace.ts +78 -0
  306. package/extensions/plan-mode/index.ts +276 -92
  307. package/extensions/plan-mode/reminder.ts +89 -21
  308. package/extensions/plan-mode/run-state.ts +26 -0
  309. package/extensions/plan-mode/texts.ts +177 -0
  310. package/extensions/plan-mode/viewer.ts +92 -27
  311. package/extensions/plugins/counts.ts +3 -3
  312. package/extensions/plugins/index.ts +64 -46
  313. package/extensions/plugins/install/install.ts +16 -0
  314. package/extensions/plugins/marketplace/sync.ts +9 -3
  315. package/extensions/plugins/marketplace/types.ts +3 -1
  316. package/extensions/plugins/panel/keys.ts +21 -24
  317. package/extensions/plugins/panel/render.ts +12 -6
  318. package/extensions/plugins/shell-expand.ts +107 -0
  319. package/extensions/plugins/template.ts +32 -15
  320. package/extensions/powershell/child.ts +7 -0
  321. package/extensions/powershell/description.ts +38 -6
  322. package/extensions/powershell/guards.ts +1 -1
  323. package/extensions/powershell/index.ts +14 -7
  324. package/extensions/recap/index.ts +73 -71
  325. package/extensions/recap/prompt.ts +2 -2
  326. package/extensions/recap/scheduler.ts +3 -5
  327. package/extensions/search-tools/grep-merge.ts +39 -0
  328. package/extensions/search-tools/index.ts +0 -0
  329. package/extensions/send-now/chord.ts +62 -0
  330. package/extensions/send-now/index.ts +133 -0
  331. package/extensions/session-title/index.ts +3 -0
  332. package/extensions/session-title/title.ts +4 -1
  333. package/extensions/skill/announce.ts +30 -0
  334. package/extensions/skill/code-review.ts +97 -0
  335. package/extensions/skill/index.ts +469 -132
  336. package/extensions/skill/invoke.ts +118 -75
  337. package/extensions/skill/listing.ts +133 -9
  338. package/extensions/skill/panel/keys.ts +15 -16
  339. package/extensions/skill/panel/render.ts +5 -2
  340. package/extensions/skill/panel/state.ts +12 -1
  341. package/extensions/subagents/agent-args.ts +39 -0
  342. package/extensions/subagents/agent-description.ts +132 -0
  343. package/extensions/subagents/agents.ts +146 -31
  344. package/extensions/subagents/default-model.ts +2 -3
  345. package/extensions/subagents/delegation-steer.ts +4 -4
  346. package/extensions/subagents/fork-cache.ts +82 -0
  347. package/extensions/subagents/guide-agent.ts +189 -0
  348. package/extensions/subagents/hand-back-review.ts +3 -0
  349. package/extensions/subagents/index.ts +1594 -852
  350. package/extensions/subagents/live-runs.ts +26 -6
  351. package/extensions/subagents/model-select.ts +38 -25
  352. package/extensions/subagents/outcome.ts +5 -2
  353. package/extensions/subagents/panel-keys.ts +73 -31
  354. package/extensions/subagents/panel-render.ts +120 -96
  355. package/extensions/subagents/panel-widget.ts +60 -89
  356. package/extensions/subagents/parent-mode.ts +50 -0
  357. package/extensions/subagents/pending-claim.ts +18 -1
  358. package/extensions/subagents/runner.ts +135 -80
  359. package/extensions/subagents/runs.ts +44 -5
  360. package/extensions/subagents/send-to-main-tool.ts +1 -1
  361. package/extensions/subagents/session-turns.ts +32 -8
  362. package/extensions/subagents/shell-panel.ts +36 -178
  363. package/extensions/subagents/tasks-dialog.ts +170 -0
  364. package/extensions/subagents/viewport-mirror.ts +95 -0
  365. package/extensions/subagents/worktree.ts +57 -13
  366. package/extensions/system-prompt/environment.ts +19 -19
  367. package/extensions/system-prompt/idle-turn.ts +65 -0
  368. package/extensions/system-prompt/index.ts +165 -47
  369. package/extensions/system-prompt/template.ts +34 -51
  370. package/extensions/system-prompt/tiers/common.ts +23 -64
  371. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  372. package/extensions/system-prompt/tiers/low.ts +67 -45
  373. package/extensions/system-prompt/tiers/mid.ts +96 -38
  374. package/extensions/system-reminder/index.ts +204 -70
  375. package/extensions/tasks/descriptions.ts +190 -0
  376. package/extensions/tasks/index.ts +120 -41
  377. package/extensions/tasks/store.ts +83 -16
  378. package/extensions/tool-call-corruption/child.ts +7 -0
  379. package/extensions/tool-call-corruption/index.ts +237 -0
  380. package/extensions/tool-search/index.ts +366 -144
  381. package/extensions/tool-style/index.ts +68 -10
  382. package/extensions/tool-style/write-description.ts +40 -0
  383. package/extensions/tool-style/write-render.ts +237 -0
  384. package/extensions/turn-duration/index.ts +7 -3
  385. package/extensions/turn-duration/line.ts +47 -4
  386. package/extensions/web/backends.ts +6 -9
  387. package/extensions/web/description.ts +50 -0
  388. package/extensions/web/index.ts +118 -53
  389. package/extensions/web/native-call.ts +15 -0
  390. package/extensions/web/native-results.ts +41 -0
  391. package/extensions/web-fetch/description.ts +42 -0
  392. package/extensions/web-fetch/extract.ts +53 -6
  393. package/extensions/web-fetch/index.ts +125 -76
  394. package/extensions/web-fetch/page-content.ts +19 -0
  395. package/extensions/web-fetch/response.ts +49 -0
  396. package/extensions/web-fetch/summarize.ts +9 -10
  397. package/extensions/workflow/agent-session.ts +108 -18
  398. package/extensions/workflow/args.ts +24 -0
  399. package/extensions/workflow/description.ts +67 -0
  400. package/extensions/workflow/globals.ts +17 -3
  401. package/extensions/workflow/index.ts +157 -162
  402. package/extensions/workflow/records.ts +1 -0
  403. package/extensions/workflow/run-manager.ts +33 -6
  404. package/extensions/workflow/saved-workflows.ts +3 -2
  405. package/extensions/workflow/types.ts +22 -1
  406. package/extensions/workflow/viewer.ts +54 -24
  407. package/extensions/workflow/widget.ts +20 -0
  408. package/extensions/worktree/descriptions.ts +91 -0
  409. package/extensions/worktree/guards.ts +316 -54
  410. package/extensions/worktree/index.ts +162 -44
  411. package/extensions/worktree/powershell-guards.ts +167 -0
  412. package/extensions/worktree/rewrite.ts +14 -16
  413. package/package.json +21 -7
  414. package/skills/artifact-design/SKILL.md +85 -0
  415. package/skills/code-review/SKILL.md +20 -20
  416. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  417. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  418. package/skills/code-review/cells/high-8-inline.md +122 -0
  419. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  420. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  421. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  422. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  423. package/skills/code-review/cells/minimal-prompt.md +32 -0
  424. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  425. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  426. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  427. package/skills/loop/SKILL.md +11 -0
  428. package/skills/security-review/SKILL.md +2 -5
  429. package/skills/simplify/SKILL.md +7 -5
  430. package/skills/workflow-authoring/SKILL.md +143 -0
  431. package/themes/onecode-light.json +1 -1
  432. package/themes/onecode.json +1 -1
  433. package/types/app.d.ts +58 -0
  434. package/types/community-packages.d.ts +26 -1
  435. package/extensions/auto-mode/recoverability.ts +0 -229
  436. package/extensions/lib/capability-index.ts +0 -466
  437. package/extensions/lib/model-facts.generated.json +0 -2675
  438. package/extensions/lib/model-facts.ts +0 -162
  439. package/extensions/tool-search/announce.ts +0 -42
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The result a tool call gets when the user rejects it (pure): Claude Code's
3
+ * denial text, shared by the permission prompt and plan-mode approval. With
4
+ * the user's typed reason it carries their words (decisions/tools.md, "Denial
5
+ * feedback carries the user's words") in Claude Code's "To tell you how to
6
+ * proceed, the user said:" form; without one it tells the model to stop and
7
+ * wait for the user.
8
+ */
9
+
10
+ const REJECTED =
11
+ "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file).";
12
+
13
+ const MEMORY_NOTE =
14
+ "Note: The user's next message may contain a correction or preference. Pay close attention — if they explain what went wrong or how they'd prefer you to work, consider saving that to memory for future sessions.";
15
+
16
+ const STOP = "STOP what you are doing and wait for the user to tell you how to proceed.";
17
+ const USER_SAID = "To tell you how to proceed, the user said:\n";
18
+
19
+ /** Claude Code's rejection text, with the user's reason when they typed one. */
20
+ export function userDenialText(feedback?: string): string {
21
+ const said = feedback?.trim();
22
+ const next = said ? `${USER_SAID}${said}` : STOP;
23
+ return `${REJECTED} ${next}\n\n${MEMORY_NOTE}`;
24
+ }
25
+
26
+ /**
27
+ * Whether a tool result is the user's rejection, for the transcript: `stop`
28
+ * when they rejected without a reason, `reason` when they gave one, undefined
29
+ * for anything else. The transcript shows a short line for either (Claude
30
+ * Code's); the model still reads the whole text.
31
+ */
32
+ export function userDenialKind(text: string): "stop" | "reason" | undefined {
33
+ if (!text.startsWith(`${REJECTED} `)) return undefined;
34
+ const next = text.slice(REJECTED.length + 1);
35
+ if (next.startsWith(STOP)) return "stop";
36
+ if (next.startsWith(USER_SAID)) return "reason";
37
+ return undefined;
38
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The workspace directories in force when a session starts, announced by the
3
+ * permissions extension over `pi.events` (jiti isolation, findings §3) so the
4
+ * system prompt can list them the way Claude Code's environment block does
5
+ * (` - Additional working directories:`). It is emitted once, at
6
+ * session_start: the system prompt stays byte-stable for the session, and a
7
+ * directory added later reaches the model through the /permissions or
8
+ * /add-dir command output instead.
9
+ */
10
+
11
+ export const WORKSPACE_CHANNEL = "one-code:workspace-dirs";
12
+
13
+ export interface WorkspaceAnnouncement {
14
+ /** Absolute, resolved paths, in the order the gate applies them. */
15
+ dirs: string[];
16
+ }
@@ -4,9 +4,11 @@
4
4
  * worktree extension emits on every state change — enter, exit, and the
5
5
  * session_start/session_tree restore; the footer shows the worktree's path
6
6
  * and branch in place of the process cwd while one is active (Claude Code's
7
- * footer does the same). The bus does not replay: a late subscriber sees the
8
- * next change, which is fine here — the footer subscribes at load, and the
9
- * first emit is the session_start restore.
7
+ * footer does the same), cron fires and monitors run there, and subagents
8
+ * and workflow agents are spawned there (`sessionWorkCwd`). The bus does not
9
+ * replay: a late subscriber sees the next change, which is fine here — every
10
+ * subscriber subscribes at load, and the first emit is the session_start
11
+ * restore.
10
12
  */
11
13
 
12
14
  export const WORKTREE_CHANNEL = "one-code:worktree";
@@ -15,4 +17,30 @@ export const WORKTREE_CHANNEL = "one-code:worktree";
15
17
  export interface WorktreeLocation {
16
18
  path: string;
17
19
  branch?: string;
20
+ /** Root of the shared checkout the worktree belongs to, for the child sessions' git-isolation guard. */
21
+ sharedRoot?: string;
22
+ }
23
+
24
+ /**
25
+ * Where the session's work happens: the entered worktree while one is active,
26
+ * else the session's own cwd. A removed worktree must fail at that path, never
27
+ * silently redirect work into the original checkout. The worktree extension
28
+ * announces a missing path on restore and blocks calls until an active missing
29
+ * worktree is explicitly left. pi fixes a session's cwd at creation, so a child
30
+ * spawned from `ctx.cwd` would work in the checkout the user meant to protect.
31
+ */
32
+ export function sessionWorkCwd(entered: WorktreeLocation | null | undefined, sessionCwd: string): string {
33
+ return entered?.path || sessionCwd;
34
+ }
35
+
36
+ /**
37
+ * Subscribe once at load and get `sessionWorkCwd` for the latest announcement:
38
+ * `(ctx.cwd) => where this session's work happens now`.
39
+ */
40
+ export function followWorkCwd(events: { on(channel: string, handler: (data: unknown) => void): unknown }): (sessionCwd: string) => string {
41
+ let entered: WorktreeLocation | null = null;
42
+ events.on(WORKTREE_CHANNEL, (data) => {
43
+ entered = data as WorktreeLocation | null;
44
+ });
45
+ return (sessionCwd) => sessionWorkCwd(entered, sessionCwd);
18
46
  }
@@ -6,11 +6,14 @@
6
6
  *
7
7
  * The registry exists because the association can only resolve at call time:
8
8
  * these worktrees live in tmpdirs (not statically recognizable paths), and the
9
- * child loaders are cached and shared across runs with different cwds.
9
+ * guard judges each call's `ctx.cwd`, not the cwd its child's loader was built
10
+ * with (one loader per child session, lib/agent-loader.ts openChildSession).
10
11
  * createWorktree/cleanupWorktree (subagents/worktree.ts) register and release
11
12
  * entries; reconstructed run records re-register kept worktrees after a
12
- * process restart (subagents/index.ts); the guard resolves each bash call's
13
- * cwd against them.
13
+ * process restart (subagents/index.ts); the worktree the main session entered
14
+ * with `enter_worktree` is registered too (`followEnteredWorktree`), so a child
15
+ * spawned there is guarded like an isolated one; the guard resolves each bash
16
+ * call's cwd against them.
14
17
  *
15
18
  * Module state is deliberate and safe despite jiti isolation: registration
16
19
  * (worktree creation) and lookup (the guard factory) always happen inside the
@@ -22,9 +25,11 @@
22
25
  import { homedir } from "node:os";
23
26
  import { isAbsolute, join, relative } from "node:path";
24
27
  import type { InlineExtension } from "@earendil-works/pi-coding-agent";
28
+ import { findProjectRoot, repositoryWorktrees } from "./git.ts";
29
+ import { WORKTREE_CHANNEL, type WorktreeLocation } from "./worktree-channel.ts";
25
30
  import { isWithin, isWritingTool, pathArgument, resolveForContainment, toAbsolute } from "../auto-mode/paths.ts";
26
31
  import { analyzeShellCommand } from "../auto-mode/shell-analysis.ts";
27
- import { worktreeBashGuardReason } from "../worktree/guards.ts";
32
+ import { namesGit, unlistedWorktreesReason, worktreeBashGuardReason } from "../worktree/guards.ts";
28
33
 
29
34
  export interface WorktreeIsolation {
30
35
  /** The isolated worktree the run works in. */
@@ -61,11 +66,35 @@ export function releaseWorktreeIsolation(worktreePath: string): void {
61
66
  active.delete(worktreePath);
62
67
  }
63
68
 
69
+ /**
70
+ * Follow the main session's `enter_worktree` state over `WORKTREE_CHANNEL`
71
+ * and return a getter for it. The entered worktree is registered in this
72
+ * extension's registry, so a child spawned there gets the git-isolation and
73
+ * write guards. The entry stays after the session exits the worktree: a child
74
+ * started there may still be running, and a registration for a directory no
75
+ * later child runs in is inert.
76
+ */
77
+ export function followEnteredWorktree(events: { on(channel: string, handler: (data: unknown) => void): unknown }): () => WorktreeLocation | undefined {
78
+ let entered: WorktreeLocation | undefined;
79
+ events.on(WORKTREE_CHANNEL, (data) => {
80
+ const location = data as WorktreeLocation | null | undefined;
81
+ entered = location?.path ? location : undefined;
82
+ if (entered) registerWorktreeIsolation(entered.path, entered.sharedRoot ?? findProjectRoot(entered.path) ?? entered.path);
83
+ });
84
+ return () => entered;
85
+ }
86
+
64
87
  /** The registered isolation worktree `cwd` lives in, if any. */
65
88
  export function worktreeIsolationFor(cwd: string): WorktreeIsolation | undefined {
66
89
  for (const isolation of active.values()) {
67
90
  if (isWithin(isolation.worktreePath, cwd)) return isolation;
68
91
  }
92
+ if (active.size === 0) return undefined;
93
+ const resolved = resolveForContainment(cwd);
94
+ if (resolved === undefined) return undefined;
95
+ for (const isolation of active.values()) {
96
+ if (isWithin(isolation.resolvedWorktreePath, resolved)) return isolation;
97
+ }
69
98
  return undefined;
70
99
  }
71
100
 
@@ -79,10 +108,8 @@ export function worktreeIsolationFor(cwd: string): WorktreeIsolation | undefined
79
108
  export function sharedCheckoutWriteTarget(target: string, cwd: string, isolation: WorktreeIsolation, home = homedir()): string | undefined {
80
109
  const { worktreePath, sharedRoot, resolvedWorktreePath, resolvedSharedRoot } = isolation;
81
110
  const absolute = toAbsolute(cwd, target, home);
82
- // Fast path: spelled under the worktree as registered — the common case for
83
- // every write the model does where it was told to (no syscall).
84
- if (isWithin(worktreePath, absolute)) return undefined;
85
- // Resolved so a write is judged by where it lands, not how it is spelled.
111
+ // Resolve even paths spelled under the worktree: a symlink there can point
112
+ // back into the shared checkout.
86
113
  const resolved = resolveForContainment(absolute) ?? absolute;
87
114
  if (isWithin(resolvedWorktreePath, resolved) || !isWithin(resolvedSharedRoot, resolved)) return undefined;
88
115
  // The tail keeps the model's spelling when the path was written under the
@@ -151,15 +178,18 @@ export function worktreeGuardFactory(cwd: string): InlineExtension {
151
178
  name: "agent-worktree-guard",
152
179
  hidden: true,
153
180
  factory: (pi) => {
154
- pi.on("tool_call", (event, ctx) => {
181
+ pi.on("tool_call", async (event, ctx) => {
155
182
  const runCwd = ctx?.cwd ?? cwd;
156
183
  const isolation = worktreeIsolationFor(runCwd);
157
184
  if (!isolation) return undefined;
158
185
  const input = (event.input ?? {}) as Record<string, unknown>;
159
186
  if (event.toolName === "bash") {
160
187
  if (typeof input.command !== "string") return undefined;
188
+ // Every worktree of the repository, as the main session's guard checks: a linked one may live outside the shared root.
189
+ const others = namesGit(input.command) ? await repositoryWorktrees(isolation.worktreePath) : [];
190
+ if (others === undefined) return { block: true, reason: unlistedWorktreesReason(isolation.worktreePath) };
161
191
  const reason =
162
- worktreeBashGuardReason({ command: input.command, worktreePath: isolation.worktreePath, sharedRoot: isolation.sharedRoot }) ??
192
+ worktreeBashGuardReason({ command: input.command, worktreePath: isolation.worktreePath, sharedRoot: isolation.sharedRoot, otherWorktrees: others }) ??
163
193
  worktreeBashWriteGuardReason({ command: input.command, cwd: runCwd, isolation });
164
194
  return reason ? { block: true, reason } : undefined;
165
195
  }
@@ -19,8 +19,9 @@ import { type ChildProcess, spawn } from "node:child_process";
19
19
  import { readFileSync } from "node:fs";
20
20
  import { pathToFileURL } from "node:url";
21
21
  import type { LspDiagnostic } from "./format.ts";
22
+ import { childProcessEnv } from "../lib/app-launch.mjs";
22
23
  import { commandLaunch } from "../lib/command-launch.ts";
23
- import { killProcessTree } from "../lib/process-tree.ts";
24
+ import { killProcessTree, unrefHandle } from "../lib/process-tree.ts";
24
25
  import { createReaderState, encodeMessage, type JsonRpcMessage, readMessages } from "./protocol.ts";
25
26
 
26
27
  const INITIALIZE_TIMEOUT_MS = 15_000;
@@ -57,10 +58,6 @@ export function configurationResponse(settings: unknown, params: unknown): unkno
57
58
  return Array.from({ length: count }, () => settings ?? {});
58
59
  }
59
60
 
60
- function unrefStream(stream: unknown): void {
61
- (stream as { unref?: () => void } | null | undefined)?.unref?.();
62
- }
63
-
64
61
  interface OpenDocument {
65
62
  version: number;
66
63
  text: string;
@@ -101,7 +98,9 @@ export class LspClient {
101
98
  async start(): Promise<void> {
102
99
  if (this.child) return;
103
100
 
104
- const env = this.options.env ? { ...process.env, ...this.options.env } : process.env;
101
+ // The user's own environment under the bundled app (lib/app-launch.mjs).
102
+ const base = childProcessEnv(process.env);
103
+ const env = this.options.env ? { ...base, ...this.options.env } : base;
105
104
  // On Windows an npm-installed server is a .cmd shim, which a bare spawn
106
105
  // cannot start (lib/command-launch.ts); failure messages keep naming the command.
107
106
  const launch = commandLaunch(this.config.command, this.config.args, env);
@@ -118,9 +117,9 @@ export class LspClient {
118
117
  // are sockets at runtime, so they carry unref() even though the stream
119
118
  // types don't declare it.
120
119
  child.unref();
121
- unrefStream(child.stdout);
122
- unrefStream(child.stderr);
123
- unrefStream(child.stdin);
120
+ unrefHandle(child.stdout);
121
+ unrefHandle(child.stderr);
122
+ unrefHandle(child.stdin);
124
123
 
125
124
  let stderr = "";
126
125
  child.stderr?.on("data", (chunk: Buffer) => {
@@ -5,9 +5,8 @@
5
5
  * 1. A session-wide watcher (watcher.ts): new diagnostics — cross-file, all
6
6
  * severities, deduplicated against what was already delivered — ride the
7
7
  * tool result of the round that surfaced them as a raw `<new-diagnostics>`
8
- * block (a reminder-queue one-shot, pinned to the trailing tool result
9
- * after the `<total_tokens>` line — the exact place a Claude Code session
10
- * shows them), or the message that opens the next run when they arrived
8
+ * block (a reminder-queue one-shot, delivered on the trailing tool result
9
+ * of the next request — the exact place a Claude Code session shows them), or the message that opens the next run when they arrived
11
10
  * while idle (`agent_start`, so a notification-opened run gets them too).
12
11
  * Until 2026-09-05 they went out as a custom steer message of their own
13
12
  * (a separate user turn on the wire). Editing a file clears its
@@ -47,6 +46,9 @@ import {
47
46
  import { findProjectRoot, serverForPath, typescriptPreflight } from "./servers.ts";
48
47
  import { computeDelta, DeliveredTracker, fingerprintDiagnostic, formatNewDiagnostics, markDelivered } from "./watcher.ts";
49
48
  import { registerLocalCommand } from "../lib/local-command.ts";
49
+ import { singleFlight } from "../lib/single-flight.ts";
50
+ import { findProjectRoot as findRepoRoot } from "../lib/git.ts";
51
+ import { createLspTrustGate, PROJECT_CODE_REASON, PROJECT_CODE_SERVERS } from "./trust.ts";
50
52
 
51
53
  /** Everything needed to spawn/reuse the server responsible for a path. */
52
54
  interface ResolvedTarget {
@@ -133,12 +135,28 @@ export default function lspExtension(pi: ExtensionAPI) {
133
135
  const MAX_RESPAWNS = 2;
134
136
  const respawns = new Map<string, number>();
135
137
 
136
- const clientFor = async (target: ResolvedTarget): Promise<LspClient | undefined> => {
138
+ /**
139
+ * One startup per server key at a time: overlapping calls (two edits, or a
140
+ * diagnostics wait racing an edit) share it, since both would otherwise
141
+ * pass the empty-map check across the trust and backoff awaits and spawn a
142
+ * second server whose predecessor shutdown can no longer reach.
143
+ */
144
+ const startOnce = singleFlight<LspClient | undefined>();
145
+ const clientFor = (target: ResolvedTarget, ctx: ExtensionContext): Promise<LspClient | undefined> => {
146
+ const running = clients.get(target.key);
147
+ if (running?.isRunning) return Promise.resolve(running);
148
+ return startOnce(target.key, () => startClient(target, ctx));
149
+ };
150
+
151
+ const startClient = async (target: ResolvedTarget, ctx: ExtensionContext): Promise<LspClient | undefined> => {
137
152
  const { key } = target;
138
153
  if (startFailures.has(key)) return undefined;
139
154
 
140
155
  const existing = clients.get(key);
141
156
  if (existing?.isRunning) return existing;
157
+ // A server that runs project code starts only in a trusted project
158
+ // (trust.ts); checked here so no caller can spawn one without it.
159
+ if (!(await projectCodeAllowed(target, ctx)) || ctx.signal?.aborted) return undefined;
142
160
  if (existing) {
143
161
  // Crashed mid-session. Respawn with a short backoff, up to MAX_RESPAWNS
144
162
  // times; after that record why so downstream reports the real cause (a
@@ -155,6 +173,8 @@ export default function lspExtension(pi: ExtensionAPI) {
155
173
  await new Promise((resolve) => setTimeout(resolve, 500 * count));
156
174
  }
157
175
 
176
+ if (ctx.signal?.aborted) return undefined;
177
+
158
178
  // Keyed on the command being spawned, not on where the config came from —
159
179
  // a plugin configuring typescript-language-server hits the same TS7 trap.
160
180
  if (target.command === "typescript-language-server") {
@@ -181,6 +201,33 @@ export default function lspExtension(pi: ExtensionAPI) {
181
201
  }
182
202
  };
183
203
 
204
+ /**
205
+ * Consent for a built-in server that runs the project's own code
206
+ * (trust.ts): trusted project → start; otherwise ask once per project in an
207
+ * interactive session, and with no UI or a "no" leave it off with a
208
+ * not-started failure the one-time notice and /lsp report.
209
+ */
210
+ const trustGate = createLspTrustGate({ projectRoot: (dir) => findRepoRoot(dir) ?? dir });
211
+ const NOT_TRUSTED = "not started:";
212
+ const projectCodeAllowed = async (target: ResolvedTarget, ctx: ExtensionContext): Promise<boolean> => {
213
+ if (target.plugin || !PROJECT_CODE_SERVERS.has(target.command)) return true;
214
+ const why = PROJECT_CODE_REASON[target.command] ?? "runs this project's code";
215
+ const confirm = ctx.hasUI
216
+ ? async (root: string) =>
217
+ (await ctx.ui.confirm(
218
+ `Start ${target.command} in this project?`,
219
+ `${target.command} ${why}. Allow it only for a project you trust.\n\nThe answer for ${root} is remembered; /lsp trust allows it later.`,
220
+ { signal: ctx.signal },
221
+ )) === true
222
+ : undefined;
223
+ if (await trustGate.allowed(target.root, confirm, ctx.signal)) return true;
224
+ if (ctx.signal?.aborted) return false;
225
+ if (!startFailures.has(target.key)) {
226
+ startFailures.set(target.key, `${NOT_TRUSTED} ${target.command} ${why}, and this project is not trusted (run /lsp trust to allow it)`);
227
+ }
228
+ return false;
229
+ };
230
+
184
231
  /** One-time notice per server key so a missing server doesn't degrade silently. */
185
232
  const reportFailureOnce = (target: ResolvedTarget, notify: (message: string) => void) => {
186
233
  const failure = startFailures.get(target.key);
@@ -241,11 +288,10 @@ export default function lspExtension(pi: ExtensionAPI) {
241
288
 
242
289
  /**
243
290
  * Queue the block where the model reads next. Raw (no `<system-reminder>`
244
- * frame, as in Claude Code) and last-append: from a `tool_result` hook this
245
- * extension runs after system-reminder's, so the one-shot is pending at the
246
- * next `context` and pinned to the trailing tool result (process memory,
247
- * same mechanism as the file-tracker's per-round report); from `agent_start`
248
- * it pins to the message that opened the run.
291
+ * frame, as in Claude Code) and last-append: persisted as a pending entry,
292
+ * it rides the trailing tool result of the next request (the same mechanism
293
+ * as the file-tracker's per-round report); from `agent_start` it rides the
294
+ * message that opened the run.
249
295
  */
250
296
  const queueNewDiagnostics = (text: string) => {
251
297
  pi.events.emit(REMINDER_CHANNEL, { text, placement: "last-append", raw: true } satisfies ReminderPayload);
@@ -270,7 +316,7 @@ export default function lspExtension(pi: ExtensionAPI) {
270
316
  // pending work, and everything the client holds is unref'd —
271
317
  // without a ref the loop drains and pi exits mid-tool (keep-alive.ts).
272
318
  await withKeepAlive(async () => {
273
- const client = await clientFor(target);
319
+ const client = await clientFor(target, ctx);
274
320
  if (client) {
275
321
  tracker.clear(pathToUri(path));
276
322
  forceDeltaScan();
@@ -321,10 +367,13 @@ export default function lspExtension(pi: ExtensionAPI) {
321
367
  // One keep-alive spans both awaits (same pattern as the tool_result hook).
322
368
  const fetched = target
323
369
  ? await withKeepAlive(async () => {
324
- const client = await clientFor(target);
370
+ const client = await clientFor(target, ctx);
325
371
  return client && { all: await client.getDiagnostics(path, target.languageId) };
326
372
  })
327
373
  : undefined;
374
+ if (ctx.signal?.aborted) {
375
+ return { content: [{ type: "text", text: "Diagnostics cancelled because the turn was stopped." }], details: { available: false }, isError: true };
376
+ }
328
377
  if (!target || !fetched) {
329
378
  const failure = target ? startFailures.get(target.key) : undefined;
330
379
  return {
@@ -363,8 +412,23 @@ export default function lspExtension(pi: ExtensionAPI) {
363
412
  });
364
413
 
365
414
  registerLocalCommand(pi, "lsp", {
366
- description: "Show language server status",
415
+ description: "Show language server status: /lsp [trust]",
416
+ argumentHint: "[trust]",
417
+ getArgumentCompletions: () => [
418
+ { value: "trust", label: "let servers that run this project's code start here (rust-analyzer, jdtls)" },
419
+ ],
367
420
  handler: async (args, ctx) => {
421
+ if (args.trim() === "trust") {
422
+ const root = trustGate.trust(ctx.cwd);
423
+ for (const [key, failure] of startFailures) {
424
+ if (failure.startsWith(NOT_TRUSTED)) {
425
+ startFailures.delete(key);
426
+ warned.delete(key);
427
+ }
428
+ }
429
+ ctx.ui.notify(`Trusted ${root}: rust-analyzer and jdtls may start here. They run this project's build code.`, "info");
430
+ return;
431
+ }
368
432
  const lines = [...clients.entries()].map(
369
433
  ([key, client]) => `${client.isRunning ? "running" : "stopped"} ${key} (${client.diagnosticsCount} diagnostics)`,
370
434
  );
@@ -7,7 +7,7 @@
7
7
  * process exits 0 mid-await, silently ending the run right after
8
8
  * tool_execution_start. Wrap each outermost LS await in this; keeping the ref
9
9
  * at the call sites (not inside the client) preserves drain-on-idle for
10
- * everything not actively awaited. docs/one-shot-lsp-event-loop-drain.md.
10
+ * everything not actively awaited. working-docs/one-shot-lsp-event-loop-drain.md.
11
11
  */
12
12
  export async function withKeepAlive<T>(work: () => Promise<T>): Promise<T> {
13
13
  const keepAlive = setInterval(() => {}, 1_000);
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Consent before a language server that runs the project's own code (pure fs).
3
+ *
4
+ * The LSP extension starts a server after any edit, outside the permission
5
+ * gate. Most servers only parse, but some execute the project as part of their
6
+ * job: rust-analyzer runs `build.rs` build scripts and proc macros (and cargo
7
+ * honours a `rust-toolchain.toml` that can name any toolchain binary), and
8
+ * jdtls imports Gradle and Maven builds by running them. In an untrusted
9
+ * checkout, an approved edit to one `.rs` file therefore also ran the
10
+ * repository's build script (SECURITY-REVIEW-2026-09-23 M4). Those servers now
11
+ * start only in a project the user has trusted: once per project root,
12
+ * persisted under `~/.onecode`, asked on first use in an interactive session
13
+ * or granted with `/lsp trust`. With no UI they stay off (fails closed, like
14
+ * project hooks and `.mcp.json` servers). Plugin-provided servers never ask:
15
+ * installing the plugin was the consent.
16
+ */
17
+
18
+ import { readJsonFile, writeJsonAtomic } from "../lib/atomic-write.ts";
19
+ import { consentStorePath } from "../lib/consent-stores.ts";
20
+ import { comparablePath, isPathAtOrUnder } from "../lib/paths.ts";
21
+ import { singleFlight } from "../lib/single-flight.ts";
22
+
23
+ /** Built-in server commands that execute project code. */
24
+ export const PROJECT_CODE_SERVERS: ReadonlySet<string> = new Set(["rust-analyzer", "jdtls"]);
25
+
26
+ /** What each of those runs, for the consent dialog and the not-started notice. */
27
+ export const PROJECT_CODE_REASON: Record<string, string> = {
28
+ "rust-analyzer": "runs this project's build scripts and proc macros",
29
+ jdtls: "runs this project's Gradle or Maven build to import it",
30
+ };
31
+
32
+ interface TrustStore {
33
+ version: 1;
34
+ roots: Record<string, { trustedAt: string }>;
35
+ }
36
+
37
+ export function lspTrustStorePath(): string {
38
+ return consentStorePath("lsp");
39
+ }
40
+
41
+ function readStore(storePath: string): TrustStore {
42
+ const store = readJsonFile<TrustStore>(storePath);
43
+ return store && typeof store === "object" && store.roots && typeof store.roots === "object" ? store : { version: 1, roots: {} };
44
+ }
45
+
46
+ /** Whether `serverRoot` lies at or under a trusted project root. */
47
+ export function isLspRootTrusted(serverRoot: string, storePath = lspTrustStorePath()): boolean {
48
+ const target = comparablePath(serverRoot);
49
+ return Object.keys(readStore(storePath).roots).some((root) => isPathAtOrUnder(target, comparablePath(root)));
50
+ }
51
+
52
+ /** Trust a project root (and everything under it) for project-code servers. */
53
+ export function persistLspTrust(projectRoot: string, storePath = lspTrustStorePath()): void {
54
+ const store = readStore(storePath);
55
+ store.roots[projectRoot] = { trustedAt: new Date().toISOString() };
56
+ try {
57
+ writeJsonAtomic(storePath, store, { mode: 0o600 });
58
+ } catch {
59
+ // Trust still holds for this session through the caller's own memory.
60
+ }
61
+ }
62
+
63
+ /**
64
+ * The per-session trust decision for project-code servers. Every answer is
65
+ * keyed by the project the SERVER runs in (the repository holding its root),
66
+ * never the session's cwd: trusting the current checkout must not start a
67
+ * server rooted in another one (PR #8 review). One question per project is in
68
+ * flight at a time; a "yes" is persisted, a "no" holds for the session.
69
+ */
70
+ export function createLspTrustGate(deps: {
71
+ /** The project a directory belongs to (its repository root, or itself). */
72
+ projectRoot: (dir: string) => string;
73
+ isTrusted?: (serverRoot: string) => boolean;
74
+ persist?: (projectRoot: string) => void;
75
+ }) {
76
+ const { projectRoot, isTrusted = (root: string) => isLspRootTrusted(root), persist = (root: string) => persistLspTrust(root) } = deps;
77
+ const sessionTrust = new Map<string, boolean>();
78
+ /** Server roots found trusted on disk this process, so the store is read once per root. */
79
+ const trustedServerRoots = new Set<string>();
80
+ /** Undefined: the caller that opened the dialog aborted, so nobody decided. */
81
+ const askOnce = singleFlight<boolean | undefined>();
82
+ const gate = {
83
+ /**
84
+ * Whether a server rooted at `serverRoot` may start. Without `confirm`
85
+ * (no UI), or after a "no" for its project, only stored trust counts.
86
+ */
87
+ async allowed(serverRoot: string, confirm?: (projectRoot: string) => Promise<boolean>, signal?: AbortSignal): Promise<boolean> {
88
+ if (signal?.aborted) return false;
89
+ const root = projectRoot(serverRoot);
90
+ if (sessionTrust.get(root) === true || trustedServerRoots.has(serverRoot)) return true;
91
+ // The project root is what a "yes" persists; a linked worktree outside
92
+ // its main checkout is not under it, so both are checked.
93
+ if (isTrusted(serverRoot) || isTrusted(root)) {
94
+ trustedServerRoots.add(serverRoot);
95
+ return true;
96
+ }
97
+ if (sessionTrust.get(root) !== undefined || !confirm) return false;
98
+ const decided = await askOnce(root, async () => {
99
+ const ok = await confirm(root);
100
+ // Aborting the turn neither grants trust nor remembers a refusal.
101
+ if (signal?.aborted) return undefined;
102
+ sessionTrust.set(root, ok);
103
+ if (ok) persist(root);
104
+ return ok;
105
+ });
106
+ if (signal?.aborted) return false;
107
+ // The opener aborted without an answer: a caller still live asks again.
108
+ return decided ?? gate.allowed(serverRoot, confirm, signal);
109
+ },
110
+ /** `/lsp trust`: trust the project `dir` belongs to, persisted. Returns that project root. */
111
+ trust(dir: string): string {
112
+ const root = projectRoot(dir);
113
+ persist(root);
114
+ sessionTrust.set(root, true);
115
+ return root;
116
+ },
117
+ };
118
+ return gate;
119
+ }
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * One deliberate divergence: file headers are cwd-relative paths, not
16
16
  * basenames — two files with the same basename would otherwise be
17
- * indistinguishable in one block (docs/decisions/lsp.md).
17
+ * indistinguishable in one block (working-docs/decisions/lsp.md).
18
18
  */
19
19
 
20
20
  import { relative } from "node:path";