one-code-extension 0.4.1 → 0.5.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 (419) 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 +32 -23
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +27 -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 +123 -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 +515 -0
  56. package/extensions/background/index.ts +514 -236
  57. package/extensions/background/loop-fire.ts +136 -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 +52 -0
  63. package/extensions/background/wakeup.ts +159 -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 +88 -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 +229 -71
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/claude-compat/index.ts +7 -11
  80. package/extensions/claude-context/index.ts +446 -58
  81. package/extensions/compaction/cut.ts +157 -0
  82. package/extensions/compaction/index.ts +216 -75
  83. package/extensions/compaction/kept-thinking.ts +62 -0
  84. package/extensions/compaction/prompt.ts +15 -12
  85. package/extensions/context-budget/index.ts +24 -5
  86. package/extensions/context-management/index.ts +25 -0
  87. package/extensions/doctor/build.ts +59 -8
  88. package/extensions/doctor/builtins.ts +84 -0
  89. package/extensions/doctor/cli.ts +7 -1
  90. package/extensions/doctor/compat.ts +88 -23
  91. package/extensions/doctor/dependencies.ts +41 -5
  92. package/extensions/doctor/fix-prompt.ts +9 -2
  93. package/extensions/doctor/index.ts +102 -58
  94. package/extensions/doctor/models.ts +102 -63
  95. package/extensions/doctor/presets.ts +42 -27
  96. package/extensions/doctor/report.ts +19 -2
  97. package/extensions/doctor/viewer.ts +49 -27
  98. package/extensions/effort/index.ts +72 -19
  99. package/extensions/effort/settings.ts +21 -0
  100. package/extensions/effort/slider.ts +24 -20
  101. package/extensions/exit/index.ts +32 -1
  102. package/extensions/file-tracker/index.ts +337 -26
  103. package/extensions/file-tracker/replay.ts +96 -10
  104. package/extensions/file-tracker/restore.ts +63 -0
  105. package/extensions/file-tracker/results.ts +66 -0
  106. package/extensions/file-tracker/shell-reads.ts +211 -0
  107. package/extensions/file-tracker/tracker.ts +32 -15
  108. package/extensions/footer/footer-line.ts +45 -24
  109. package/extensions/footer/index.ts +47 -2
  110. package/extensions/footer/pr.ts +34 -5
  111. package/extensions/hooks/compaction.ts +7 -0
  112. package/extensions/hooks/executor.ts +32 -30
  113. package/extensions/hooks/index.ts +198 -31
  114. package/extensions/hooks/matcher.ts +3 -0
  115. package/extensions/hooks/protocol.ts +41 -3
  116. package/extensions/hooks/settings.ts +50 -19
  117. package/extensions/hooks/subagent-bridge.ts +2 -0
  118. package/extensions/hooks/trust.ts +13 -5
  119. package/extensions/idle-compact/index.ts +119 -0
  120. package/extensions/idle-compact/policy.ts +143 -0
  121. package/extensions/init/index.ts +11 -10
  122. package/extensions/init/prompt.ts +25 -32
  123. package/extensions/interrupted/index.ts +13 -1
  124. package/extensions/kept-thinking/index.ts +27 -0
  125. package/extensions/lib/agent-cron.ts +130 -0
  126. package/extensions/lib/agent-loader.ts +15 -1
  127. package/extensions/lib/agent-view.ts +85 -0
  128. package/extensions/lib/anthropic-payload.ts +82 -1
  129. package/extensions/lib/anthropic-server-call.ts +230 -0
  130. package/extensions/lib/anthropic-server-tools.ts +485 -0
  131. package/extensions/lib/app-launch.mjs +94 -0
  132. package/extensions/lib/argument-hints.ts +190 -0
  133. package/extensions/lib/atomic-write.ts +6 -2
  134. package/extensions/lib/auto-memory.ts +75 -0
  135. package/extensions/lib/bash-parser.ts +84 -0
  136. package/extensions/lib/btw-fork.ts +93 -0
  137. package/extensions/lib/child-extensions.ts +42 -0
  138. package/extensions/lib/child-writes.ts +45 -0
  139. package/extensions/lib/claude-context.ts +515 -100
  140. package/extensions/lib/claude-external-includes.ts +68 -0
  141. package/extensions/lib/claude-id.ts +22 -0
  142. package/extensions/lib/claude-rules.ts +323 -0
  143. package/extensions/lib/claude-settings.ts +76 -4
  144. package/extensions/lib/compaction-boundary.ts +36 -0
  145. package/extensions/lib/compaction-keep.mjs +83 -0
  146. package/extensions/lib/config-mode.ts +112 -0
  147. package/extensions/lib/consent-dialogs.ts +63 -0
  148. package/extensions/lib/consent-preview.ts +5 -1
  149. package/extensions/lib/consent-stores.ts +48 -0
  150. package/extensions/lib/context-facts.ts +78 -0
  151. package/extensions/lib/context-stack.ts +81 -0
  152. package/extensions/lib/deferred.ts +219 -25
  153. package/extensions/lib/dock-rows.ts +60 -0
  154. package/extensions/lib/empty-tool-result.ts +27 -0
  155. package/extensions/lib/environment-block.ts +76 -0
  156. package/extensions/lib/fetch-timeout.ts +24 -6
  157. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  158. package/extensions/lib/git.ts +75 -13
  159. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  160. package/extensions/lib/grammars/README.md +2 -0
  161. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  162. package/extensions/lib/guide-docs.ts +45 -0
  163. package/extensions/lib/headless-output.ts +46 -0
  164. package/extensions/lib/hook-gate.ts +7 -7
  165. package/extensions/lib/interrupt.ts +20 -4
  166. package/extensions/lib/key-input.ts +455 -0
  167. package/extensions/lib/keys.ts +14 -1
  168. package/extensions/lib/local-command.ts +47 -13
  169. package/extensions/lib/mcp-overrides.ts +1 -1
  170. package/extensions/lib/mcp-share.ts +46 -0
  171. package/extensions/lib/memory-content.ts +146 -0
  172. package/extensions/lib/memory-index.ts +18 -0
  173. package/extensions/lib/memory.ts +134 -123
  174. package/extensions/lib/model-catalog/huggingface.json +174 -0
  175. package/extensions/lib/model-catalog/models-dev.json +18297 -0
  176. package/extensions/lib/model-catalog/openrouter.json +5252 -0
  177. package/extensions/lib/model-catalog-data.ts +513 -0
  178. package/extensions/lib/model-catalog.ts +462 -0
  179. package/extensions/lib/model-choice-warnings.ts +137 -0
  180. package/extensions/lib/model-pairings.ts +152 -0
  181. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  182. package/extensions/lib/model-policy.ts +112 -15
  183. package/extensions/lib/model-tier.ts +283 -355
  184. package/extensions/lib/model-unusable.ts +1 -1
  185. package/extensions/lib/newer-model.ts +100 -0
  186. package/extensions/lib/notices.ts +87 -0
  187. package/extensions/lib/notifications.ts +308 -64
  188. package/extensions/lib/one-code-settings.ts +74 -8
  189. package/extensions/lib/open-browser.ts +63 -0
  190. package/extensions/lib/openrouter-generation.ts +94 -0
  191. package/extensions/lib/operation-deadline.ts +30 -0
  192. package/extensions/lib/original-command.ts +4 -4
  193. package/extensions/lib/package-version.ts +1 -1
  194. package/extensions/lib/panel-focus.ts +134 -0
  195. package/extensions/lib/paths.ts +17 -2
  196. package/extensions/lib/permission-gate.ts +72 -6
  197. package/extensions/lib/persisted-output.ts +60 -8
  198. package/extensions/lib/pi-ai-estimate.ts +19 -3
  199. package/extensions/lib/pi-version.ts +21 -2
  200. package/extensions/lib/plan-mode-channels.ts +20 -5
  201. package/extensions/lib/plugins.ts +26 -17
  202. package/extensions/lib/powershell-parse-script.ts +208 -0
  203. package/extensions/lib/powershell-parser.ts +344 -0
  204. package/extensions/lib/process-tree.ts +131 -8
  205. package/extensions/lib/prompt-options.ts +82 -0
  206. package/extensions/lib/queued-delivery.ts +44 -0
  207. package/extensions/lib/reminders.ts +364 -58
  208. package/extensions/lib/replaced-builtins.mjs +199 -0
  209. package/extensions/lib/replay-call.ts +70 -0
  210. package/extensions/lib/request-replay.ts +439 -0
  211. package/extensions/lib/scratchpad.ts +81 -14
  212. package/extensions/lib/send-now-channel.ts +7 -0
  213. package/extensions/lib/session-model-tier.ts +51 -0
  214. package/extensions/lib/session-work.ts +64 -0
  215. package/extensions/lib/settings-channels.ts +4 -4
  216. package/extensions/lib/shell-exit-semantics.ts +170 -0
  217. package/extensions/lib/shell-quote.ts +11 -0
  218. package/extensions/lib/shell-result.ts +70 -0
  219. package/extensions/lib/shell-spawn.ts +70 -6
  220. package/extensions/lib/shell-tool.ts +75 -25
  221. package/extensions/lib/side-call-cache.ts +124 -0
  222. package/extensions/lib/side-call-run.ts +96 -0
  223. package/extensions/lib/side-call-usage.ts +38 -0
  224. package/extensions/lib/side-call.ts +13 -1
  225. package/extensions/lib/single-flight.ts +18 -0
  226. package/extensions/lib/skill-body.ts +42 -0
  227. package/extensions/lib/skill-scan.ts +63 -24
  228. package/extensions/lib/spill-file.ts +140 -0
  229. package/extensions/lib/subagent-handback.ts +113 -0
  230. package/extensions/lib/system-role.ts +156 -0
  231. package/extensions/lib/terminal-text.ts +114 -0
  232. package/extensions/lib/text-width.ts +47 -16
  233. package/extensions/lib/timer-ops.ts +20 -0
  234. package/extensions/lib/tool-additions.ts +149 -0
  235. package/extensions/lib/tool-call-corruption.ts +134 -0
  236. package/extensions/lib/tool-path.ts +66 -0
  237. package/extensions/lib/tool-variants.ts +69 -0
  238. package/extensions/lib/tui-render.ts +255 -27
  239. package/extensions/lib/turn-budget-layout.ts +143 -0
  240. package/extensions/lib/update-check.mjs +15 -2
  241. package/extensions/lib/usage-bus.ts +1 -1
  242. package/extensions/lib/user-denial.ts +38 -0
  243. package/extensions/lib/workspace-channel.ts +16 -0
  244. package/extensions/lib/worktree-channel.ts +31 -3
  245. package/extensions/lib/worktree-isolation.ts +40 -10
  246. package/extensions/lsp/client.ts +8 -9
  247. package/extensions/lsp/index.ts +70 -4
  248. package/extensions/lsp/keep-alive.ts +1 -1
  249. package/extensions/lsp/trust.ts +119 -0
  250. package/extensions/lsp/watcher.ts +1 -1
  251. package/extensions/mcp/announce.ts +87 -5
  252. package/extensions/mcp/client.ts +161 -48
  253. package/extensions/mcp/config.ts +132 -8
  254. package/extensions/mcp/discovery.ts +19 -0
  255. package/extensions/mcp/index.ts +209 -71
  256. package/extensions/mcp/oauth/browser.ts +38 -17
  257. package/extensions/mcp/oauth/flow.ts +25 -17
  258. package/extensions/mcp/oauth/provider.ts +14 -8
  259. package/extensions/mcp/oauth/store.ts +34 -16
  260. package/extensions/mcp/panel/keys.ts +12 -11
  261. package/extensions/mcp/panel/render.ts +9 -6
  262. package/extensions/mcp/schema.ts +104 -5
  263. package/extensions/mcp/trust.ts +151 -67
  264. package/extensions/memory/entries.ts +23 -14
  265. package/extensions/memory/index.ts +134 -28
  266. package/extensions/memory/open-external.ts +33 -18
  267. package/extensions/memory/panel.ts +135 -26
  268. package/extensions/newer-model/index.ts +31 -0
  269. package/extensions/notebook/description.ts +18 -0
  270. package/extensions/notebook/index.ts +6 -5
  271. package/extensions/notebook/notebook.ts +1 -1
  272. package/extensions/permissions/index.ts +1300 -381
  273. package/extensions/permissions/matcher.ts +425 -74
  274. package/extensions/permissions/modes.ts +6 -4
  275. package/extensions/permissions/panel/host.ts +130 -0
  276. package/extensions/permissions/panel/keys.ts +48 -0
  277. package/extensions/permissions/panel/render.ts +450 -0
  278. package/extensions/permissions/panel/state.ts +550 -0
  279. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  280. package/extensions/permissions/powershell-paths.ts +197 -0
  281. package/extensions/permissions/powershell-rules.ts +207 -280
  282. package/extensions/permissions/powershell-tree.ts +557 -0
  283. package/extensions/permissions/project-trust.ts +27 -11
  284. package/extensions/permissions/protected-paths.ts +14 -2
  285. package/extensions/permissions/session-grant.ts +6 -0
  286. package/extensions/permissions/settings.ts +166 -31
  287. package/extensions/permissions/subagent-gate.ts +25 -0
  288. package/extensions/permissions/workspace.ts +78 -0
  289. package/extensions/plan-mode/index.ts +156 -69
  290. package/extensions/plan-mode/reminder.ts +89 -21
  291. package/extensions/plan-mode/texts.ts +177 -0
  292. package/extensions/plan-mode/viewer.ts +92 -27
  293. package/extensions/plugins/counts.ts +3 -3
  294. package/extensions/plugins/index.ts +61 -46
  295. package/extensions/plugins/install/install.ts +16 -0
  296. package/extensions/plugins/marketplace/sync.ts +9 -3
  297. package/extensions/plugins/marketplace/types.ts +3 -1
  298. package/extensions/plugins/panel/keys.ts +21 -24
  299. package/extensions/plugins/panel/render.ts +12 -6
  300. package/extensions/plugins/shell-expand.ts +107 -0
  301. package/extensions/plugins/template.ts +32 -15
  302. package/extensions/powershell/child.ts +7 -0
  303. package/extensions/powershell/description.ts +38 -6
  304. package/extensions/powershell/guards.ts +1 -1
  305. package/extensions/powershell/index.ts +12 -7
  306. package/extensions/recap/index.ts +65 -60
  307. package/extensions/recap/prompt.ts +2 -2
  308. package/extensions/recap/scheduler.ts +3 -5
  309. package/extensions/search-tools/grep-merge.ts +39 -0
  310. package/extensions/search-tools/index.ts +0 -0
  311. package/extensions/send-now/chord.ts +62 -0
  312. package/extensions/send-now/index.ts +133 -0
  313. package/extensions/session-title/index.ts +3 -0
  314. package/extensions/session-title/title.ts +4 -1
  315. package/extensions/skill/code-review.ts +97 -0
  316. package/extensions/skill/index.ts +421 -101
  317. package/extensions/skill/invoke.ts +128 -18
  318. package/extensions/skill/listing.ts +133 -9
  319. package/extensions/skill/panel/keys.ts +15 -16
  320. package/extensions/skill/panel/render.ts +5 -2
  321. package/extensions/skill/panel/state.ts +12 -1
  322. package/extensions/subagents/agent-args.ts +39 -0
  323. package/extensions/subagents/agent-description.ts +132 -0
  324. package/extensions/subagents/agents.ts +116 -23
  325. package/extensions/subagents/announce.ts +69 -0
  326. package/extensions/subagents/default-model.ts +2 -3
  327. package/extensions/subagents/delegation-steer.ts +4 -4
  328. package/extensions/subagents/fork-cache.ts +76 -0
  329. package/extensions/subagents/guide-agent.ts +189 -0
  330. package/extensions/subagents/hand-back-review.ts +3 -0
  331. package/extensions/subagents/index.ts +1493 -824
  332. package/extensions/subagents/live-runs.ts +26 -6
  333. package/extensions/subagents/model-select.ts +38 -25
  334. package/extensions/subagents/outcome.ts +2 -2
  335. package/extensions/subagents/panel-keys.ts +73 -31
  336. package/extensions/subagents/panel-render.ts +120 -96
  337. package/extensions/subagents/panel-widget.ts +60 -89
  338. package/extensions/subagents/runner.ts +108 -80
  339. package/extensions/subagents/runs.ts +39 -5
  340. package/extensions/subagents/send-to-main-tool.ts +1 -1
  341. package/extensions/subagents/session-turns.ts +32 -8
  342. package/extensions/subagents/shell-panel.ts +36 -178
  343. package/extensions/subagents/tasks-dialog.ts +170 -0
  344. package/extensions/subagents/viewport-mirror.ts +95 -0
  345. package/extensions/subagents/worktree.ts +57 -13
  346. package/extensions/system-prompt/environment.ts +19 -19
  347. package/extensions/system-prompt/idle-turn.ts +65 -0
  348. package/extensions/system-prompt/index.ts +131 -47
  349. package/extensions/system-prompt/template.ts +24 -50
  350. package/extensions/system-prompt/tiers/common.ts +23 -64
  351. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  352. package/extensions/system-prompt/tiers/low.ts +67 -45
  353. package/extensions/system-prompt/tiers/mid.ts +96 -38
  354. package/extensions/system-reminder/index.ts +175 -13
  355. package/extensions/tasks/descriptions.ts +190 -0
  356. package/extensions/tasks/index.ts +120 -41
  357. package/extensions/tasks/store.ts +83 -16
  358. package/extensions/tool-call-corruption/child.ts +7 -0
  359. package/extensions/tool-call-corruption/index.ts +237 -0
  360. package/extensions/tool-search/announce.ts +16 -0
  361. package/extensions/tool-search/index.ts +189 -41
  362. package/extensions/tool-style/index.ts +68 -10
  363. package/extensions/tool-style/write-description.ts +40 -0
  364. package/extensions/tool-style/write-render.ts +237 -0
  365. package/extensions/turn-duration/index.ts +7 -3
  366. package/extensions/turn-duration/line.ts +47 -4
  367. package/extensions/web/backends.ts +6 -9
  368. package/extensions/web/description.ts +50 -0
  369. package/extensions/web/index.ts +118 -53
  370. package/extensions/web/native-call.ts +15 -0
  371. package/extensions/web/native-results.ts +41 -0
  372. package/extensions/web-fetch/description.ts +42 -0
  373. package/extensions/web-fetch/extract.ts +53 -6
  374. package/extensions/web-fetch/index.ts +125 -76
  375. package/extensions/web-fetch/page-content.ts +19 -0
  376. package/extensions/web-fetch/response.ts +49 -0
  377. package/extensions/web-fetch/summarize.ts +9 -10
  378. package/extensions/workflow/agent-session.ts +105 -18
  379. package/extensions/workflow/args.ts +24 -0
  380. package/extensions/workflow/description.ts +67 -0
  381. package/extensions/workflow/globals.ts +17 -3
  382. package/extensions/workflow/index.ts +145 -162
  383. package/extensions/workflow/records.ts +1 -0
  384. package/extensions/workflow/run-manager.ts +29 -6
  385. package/extensions/workflow/saved-workflows.ts +3 -2
  386. package/extensions/workflow/types.ts +22 -1
  387. package/extensions/workflow/viewer.ts +54 -24
  388. package/extensions/workflow/widget.ts +20 -0
  389. package/extensions/worktree/descriptions.ts +91 -0
  390. package/extensions/worktree/guards.ts +316 -54
  391. package/extensions/worktree/index.ts +121 -39
  392. package/extensions/worktree/powershell-guards.ts +167 -0
  393. package/extensions/worktree/rewrite.ts +14 -16
  394. package/package.json +18 -7
  395. package/skills/artifact-design/SKILL.md +85 -0
  396. package/skills/code-review/SKILL.md +20 -20
  397. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  398. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  399. package/skills/code-review/cells/high-8-inline.md +122 -0
  400. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  401. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  402. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  403. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  404. package/skills/code-review/cells/minimal-prompt.md +32 -0
  405. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  406. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  407. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  408. package/skills/loop/SKILL.md +11 -0
  409. package/skills/security-review/SKILL.md +2 -5
  410. package/skills/simplify/SKILL.md +7 -5
  411. package/skills/workflow-authoring/SKILL.md +143 -0
  412. package/themes/onecode-light.json +1 -1
  413. package/themes/onecode.json +1 -1
  414. package/types/app.d.ts +58 -0
  415. package/types/community-packages.d.ts +26 -1
  416. package/extensions/auto-mode/recoverability.ts +0 -229
  417. package/extensions/lib/capability-index.ts +0 -466
  418. package/extensions/lib/model-facts.generated.json +0 -2675
  419. package/extensions/lib/model-facts.ts +0 -162
@@ -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) => {
@@ -47,6 +47,9 @@ import {
47
47
  import { findProjectRoot, serverForPath, typescriptPreflight } from "./servers.ts";
48
48
  import { computeDelta, DeliveredTracker, fingerprintDiagnostic, formatNewDiagnostics, markDelivered } from "./watcher.ts";
49
49
  import { registerLocalCommand } from "../lib/local-command.ts";
50
+ import { singleFlight } from "../lib/single-flight.ts";
51
+ import { findProjectRoot as findRepoRoot } from "../lib/git.ts";
52
+ import { createLspTrustGate, PROJECT_CODE_REASON, PROJECT_CODE_SERVERS } from "./trust.ts";
50
53
 
51
54
  /** Everything needed to spawn/reuse the server responsible for a path. */
52
55
  interface ResolvedTarget {
@@ -133,12 +136,28 @@ export default function lspExtension(pi: ExtensionAPI) {
133
136
  const MAX_RESPAWNS = 2;
134
137
  const respawns = new Map<string, number>();
135
138
 
136
- const clientFor = async (target: ResolvedTarget): Promise<LspClient | undefined> => {
139
+ /**
140
+ * One startup per server key at a time: overlapping calls (two edits, or a
141
+ * diagnostics wait racing an edit) share it, since both would otherwise
142
+ * pass the empty-map check across the trust and backoff awaits and spawn a
143
+ * second server whose predecessor shutdown can no longer reach.
144
+ */
145
+ const startOnce = singleFlight<LspClient | undefined>();
146
+ const clientFor = (target: ResolvedTarget, ctx: ExtensionContext): Promise<LspClient | undefined> => {
147
+ const running = clients.get(target.key);
148
+ if (running?.isRunning) return Promise.resolve(running);
149
+ return startOnce(target.key, () => startClient(target, ctx));
150
+ };
151
+
152
+ const startClient = async (target: ResolvedTarget, ctx: ExtensionContext): Promise<LspClient | undefined> => {
137
153
  const { key } = target;
138
154
  if (startFailures.has(key)) return undefined;
139
155
 
140
156
  const existing = clients.get(key);
141
157
  if (existing?.isRunning) return existing;
158
+ // A server that runs project code starts only in a trusted project
159
+ // (trust.ts); checked here so no caller can spawn one without it.
160
+ if (!(await projectCodeAllowed(target, ctx)) || ctx.signal?.aborted) return undefined;
142
161
  if (existing) {
143
162
  // Crashed mid-session. Respawn with a short backoff, up to MAX_RESPAWNS
144
163
  // times; after that record why so downstream reports the real cause (a
@@ -155,6 +174,8 @@ export default function lspExtension(pi: ExtensionAPI) {
155
174
  await new Promise((resolve) => setTimeout(resolve, 500 * count));
156
175
  }
157
176
 
177
+ if (ctx.signal?.aborted) return undefined;
178
+
158
179
  // Keyed on the command being spawned, not on where the config came from —
159
180
  // a plugin configuring typescript-language-server hits the same TS7 trap.
160
181
  if (target.command === "typescript-language-server") {
@@ -181,6 +202,33 @@ export default function lspExtension(pi: ExtensionAPI) {
181
202
  }
182
203
  };
183
204
 
205
+ /**
206
+ * Consent for a built-in server that runs the project's own code
207
+ * (trust.ts): trusted project → start; otherwise ask once per project in an
208
+ * interactive session, and with no UI or a "no" leave it off with a
209
+ * not-started failure the one-time notice and /lsp report.
210
+ */
211
+ const trustGate = createLspTrustGate({ projectRoot: (dir) => findRepoRoot(dir) ?? dir });
212
+ const NOT_TRUSTED = "not started:";
213
+ const projectCodeAllowed = async (target: ResolvedTarget, ctx: ExtensionContext): Promise<boolean> => {
214
+ if (target.plugin || !PROJECT_CODE_SERVERS.has(target.command)) return true;
215
+ const why = PROJECT_CODE_REASON[target.command] ?? "runs this project's code";
216
+ const confirm = ctx.hasUI
217
+ ? async (root: string) =>
218
+ (await ctx.ui.confirm(
219
+ `Start ${target.command} in this project?`,
220
+ `${target.command} ${why}. Allow it only for a project you trust.\n\nThe answer for ${root} is remembered; /lsp trust allows it later.`,
221
+ { signal: ctx.signal },
222
+ )) === true
223
+ : undefined;
224
+ if (await trustGate.allowed(target.root, confirm, ctx.signal)) return true;
225
+ if (ctx.signal?.aborted) return false;
226
+ if (!startFailures.has(target.key)) {
227
+ startFailures.set(target.key, `${NOT_TRUSTED} ${target.command} ${why}, and this project is not trusted (run /lsp trust to allow it)`);
228
+ }
229
+ return false;
230
+ };
231
+
184
232
  /** One-time notice per server key so a missing server doesn't degrade silently. */
185
233
  const reportFailureOnce = (target: ResolvedTarget, notify: (message: string) => void) => {
186
234
  const failure = startFailures.get(target.key);
@@ -270,7 +318,7 @@ export default function lspExtension(pi: ExtensionAPI) {
270
318
  // pending work, and everything the client holds is unref'd —
271
319
  // without a ref the loop drains and pi exits mid-tool (keep-alive.ts).
272
320
  await withKeepAlive(async () => {
273
- const client = await clientFor(target);
321
+ const client = await clientFor(target, ctx);
274
322
  if (client) {
275
323
  tracker.clear(pathToUri(path));
276
324
  forceDeltaScan();
@@ -321,10 +369,13 @@ export default function lspExtension(pi: ExtensionAPI) {
321
369
  // One keep-alive spans both awaits (same pattern as the tool_result hook).
322
370
  const fetched = target
323
371
  ? await withKeepAlive(async () => {
324
- const client = await clientFor(target);
372
+ const client = await clientFor(target, ctx);
325
373
  return client && { all: await client.getDiagnostics(path, target.languageId) };
326
374
  })
327
375
  : undefined;
376
+ if (ctx.signal?.aborted) {
377
+ return { content: [{ type: "text", text: "Diagnostics cancelled because the turn was stopped." }], details: { available: false }, isError: true };
378
+ }
328
379
  if (!target || !fetched) {
329
380
  const failure = target ? startFailures.get(target.key) : undefined;
330
381
  return {
@@ -363,8 +414,23 @@ export default function lspExtension(pi: ExtensionAPI) {
363
414
  });
364
415
 
365
416
  registerLocalCommand(pi, "lsp", {
366
- description: "Show language server status",
417
+ description: "Show language server status: /lsp [trust]",
418
+ argumentHint: "[trust]",
419
+ getArgumentCompletions: () => [
420
+ { value: "trust", label: "let servers that run this project's code start here (rust-analyzer, jdtls)" },
421
+ ],
367
422
  handler: async (args, ctx) => {
423
+ if (args.trim() === "trust") {
424
+ const root = trustGate.trust(ctx.cwd);
425
+ for (const [key, failure] of startFailures) {
426
+ if (failure.startsWith(NOT_TRUSTED)) {
427
+ startFailures.delete(key);
428
+ warned.delete(key);
429
+ }
430
+ }
431
+ ctx.ui.notify(`Trusted ${root}: rust-analyzer and jdtls may start here. They run this project's build code.`, "info");
432
+ return;
433
+ }
368
434
  const lines = [...clients.entries()].map(
369
435
  ([key, client]) => `${client.isRunning ? "running" : "stopped"} ${key} (${client.diagnosticsCount} diagnostics)`,
370
436
  );
@@ -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";
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { mcpFailuresReminder, mcpInstructionsReminder } from "./client.ts";
13
+ import { countNoun } from "../lib/tui-render.ts";
13
14
 
14
15
  export interface McpAnnounced {
15
16
  /** Servers whose instructions the model has been given, with the text it saw. */
@@ -38,13 +39,79 @@ export function emptyAnnounced(): McpAnnounced {
38
39
  return { instructed: new Map(), failed: new Map() };
39
40
  }
40
41
 
42
+ /**
43
+ * Store and compare the exact bounded values the reminder puts on the wire.
44
+ * A changing tail beyond either cap is not a model-visible capability change
45
+ * and must not become a false resume addendum.
46
+ */
47
+ function displayedInstructions(name: string, instructions: string): string {
48
+ const rendered = mcpInstructionsReminder([{ server: { name }, instructions }]);
49
+ const prefix = "# MCP Server Instructions\n\nThe following MCP servers have provided instructions for how to use their tools and resources:\n\n" + `## ${name}\n`;
50
+ return rendered?.startsWith(prefix) ? rendered.slice(prefix.length) : instructions;
51
+ }
52
+ function displayedFailure(error: string): string {
53
+ return error.split("\n")[0].slice(0, 300);
54
+ }
55
+
56
+ /** Maps are serialized as entry arrays so arbitrary server instructions/errors round-trip exactly. */
57
+ export interface McpAnnouncedBaseline { instructed: Array<[string, string]>; failed: Array<[string, string]> }
58
+ export function serializeMcpAnnounced(announced: McpAnnounced): McpAnnouncedBaseline {
59
+ return { instructed: [...announced.instructed], failed: [...announced.failed] };
60
+ }
61
+ /**
62
+ * Best-effort compatibility reader for stacks written before structured MCP
63
+ * baselines. With the configured server `names`, a section starts only at a
64
+ * `## <name>` heading, so a `## ` heading inside a server's instructions stays
65
+ * part of them instead of reading as a server of its own.
66
+ */
67
+ export function mcpAnnouncedFromReminders(instructions: string | undefined, failures: string | undefined, names?: Iterable<string>): McpAnnounced {
68
+ const announced = emptyAnnounced();
69
+ const known = names ? new Set(names) : undefined;
70
+ const instructionPrefix = "# MCP Server Instructions\n\nThe following MCP servers have provided instructions for how to use their tools and resources:\n\n";
71
+ if (instructions?.startsWith(instructionPrefix)) {
72
+ const sections: string[] = [];
73
+ for (const section of instructions.slice(instructionPrefix.length).split("\n\n## ")) {
74
+ const normalized = section.startsWith("## ") ? section.slice(3) : section;
75
+ const newline = normalized.indexOf("\n");
76
+ const heading = newline > 0 ? normalized.slice(0, newline) : normalized;
77
+ if (known && sections.length > 0 && !known.has(heading)) sections[sections.length - 1] += `\n\n## ${normalized}`;
78
+ else sections.push(normalized);
79
+ }
80
+ for (const section of sections) {
81
+ const newline = section.indexOf("\n");
82
+ if (newline > 0) announced.instructed.set(section.slice(0, newline), section.slice(newline + 1));
83
+ }
84
+ }
85
+ const failurePrefix = "The following MCP servers are configured but failed to connect — their tools (typically named mcp__<server>__*) are unavailable for this session:\n";
86
+ if (failures?.startsWith(failurePrefix)) {
87
+ for (const line of failures.slice(failurePrefix.length).split("\n")) {
88
+ if (!line.startsWith("- ")) break;
89
+ const colon = line.indexOf(": ", 2);
90
+ if (colon > 2) announced.failed.set(line.slice(2, colon), line.slice(colon + 2));
91
+ }
92
+ }
93
+ return announced;
94
+ }
95
+
96
+ export function mcpAnnouncedBaseline(value: unknown): McpAnnounced | undefined {
97
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
98
+ const input = value as Record<string, unknown>;
99
+ const pairs = (item: unknown): Array<[string, string]> | undefined =>
100
+ Array.isArray(item) && item.every((pair) => Array.isArray(pair) && pair.length === 2 && typeof pair[0] === "string" && typeof pair[1] === "string")
101
+ ? item as Array<[string, string]>
102
+ : undefined;
103
+ const instructed = pairs(input.instructed);
104
+ const failed = pairs(input.failed);
105
+ return instructed && failed ? { instructed: new Map(instructed), failed: new Map(failed) } : undefined;
106
+ }
107
+
41
108
  /** What message 1 says after it was (re)written from `snapshot`, before the first request. */
42
109
  export function announcedFrom(snapshot: McpSnapshot): McpAnnounced {
43
110
  return {
44
111
  instructed: new Map(
45
- snapshot.connected.filter((c) => c.instructions).map((c) => [c.name, c.instructions as string]),
112
+ snapshot.connected.filter((c) => c.instructions).map((c) => [c.name, displayedInstructions(c.name, c.instructions as string)]),
46
113
  ),
47
- failed: new Map(snapshot.failed.map((f) => [f.name, f.error])),
114
+ failed: new Map(snapshot.failed.map((f) => [f.name, displayedFailure(f.error)])),
48
115
  };
49
116
  }
50
117
 
@@ -53,9 +120,11 @@ export function mcpDelta(announced: McpAnnounced, snapshot: McpSnapshot): McpDel
53
120
  const failedNames = new Set(snapshot.failed.map((f) => f.name));
54
121
  return {
55
122
  newInstructions: snapshot.connected
56
- .filter((c) => c.instructions && announced.instructed.get(c.name) !== c.instructions)
57
- .map((c) => ({ name: c.name, instructions: c.instructions as string })),
58
- newFailures: snapshot.failed.filter((f) => announced.failed.get(f.name) !== f.error),
123
+ .filter((c) => c.instructions && announced.instructed.get(c.name) !== displayedInstructions(c.name, c.instructions as string))
124
+ .map((c) => ({ name: c.name, instructions: displayedInstructions(c.name, c.instructions as string) })),
125
+ newFailures: snapshot.failed
126
+ .filter((f) => announced.failed.get(f.name) !== displayedFailure(f.error))
127
+ .map((f) => ({ name: f.name, error: displayedFailure(f.error) })),
59
128
  recovered: [...announced.failed.keys()].filter((name) => connectedNames.has(name)),
60
129
  dropped: [...announced.instructed.keys()].filter((name) => !connectedNames.has(name) && !failedNames.has(name)),
61
130
  };
@@ -97,3 +166,16 @@ export function mcpChangeNotices(delta: McpDelta): string[] {
97
166
  }
98
167
  return notices;
99
168
  }
169
+
170
+ /**
171
+ * The user's startup notices, Claude Code's wording (`useMcpConnectivityStatus`):
172
+ * `2 MCP servers failed · /mcp` and `1 MCP server needs auth · /mcp`, with each
173
+ * server's error left to /mcp. Printing every server's raw error ("fetch
174
+ * failed" per remote server when offline) buried the prompt.
175
+ */
176
+ export function mcpStartupNotices(failedServers: number, needsAuth: number): string[] {
177
+ const out: string[] = [];
178
+ if (failedServers > 0) out.push(`${countNoun(failedServers, "MCP server")} failed · /mcp`);
179
+ if (needsAuth > 0) out.push(`${countNoun(needsAuth, "MCP server")} ${needsAuth === 1 ? "needs" : "need"} auth · /mcp`);
180
+ return out;
181
+ }