one-code-extension 0.1.4 → 0.3.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 (300) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +200 -172
  3. package/agents/explore.md +9 -6
  4. package/agents/general-purpose.md +1 -1
  5. package/agents/plan.md +6 -4
  6. package/docs/guide/README.md +52 -0
  7. package/docs/guide/bring-your-claude-code-setup.md +126 -0
  8. package/docs/guide/configuration.md +171 -0
  9. package/docs/guide/differences-from-claude-code.md +150 -0
  10. package/docs/guide/doctor.md +86 -0
  11. package/docs/guide/hooks.md +109 -0
  12. package/docs/guide/installation.md +124 -0
  13. package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
  14. package/docs/guide/providers-and-models.md +194 -0
  15. package/docs/guide/reference.md +224 -0
  16. package/docs/guide/sessions-and-context.md +190 -0
  17. package/docs/guide/skills-plugins-and-mcp.md +176 -0
  18. package/docs/guide/subagents-and-workflows.md +193 -0
  19. package/docs/guide/tasks-and-background-work.md +111 -0
  20. package/docs/guide/terminal-interface.md +155 -0
  21. package/docs/guide/tools.md +175 -0
  22. package/docs/guide/troubleshooting.md +175 -0
  23. package/extensions/ask-user/index.ts +65 -71
  24. package/extensions/ask-user/questions.ts +15 -28
  25. package/extensions/ask-user/widget.ts +533 -0
  26. package/extensions/auto-mode/actions.ts +9 -26
  27. package/extensions/auto-mode/classifier-prompt.ts +127 -0
  28. package/extensions/auto-mode/classifier.ts +305 -227
  29. package/extensions/auto-mode/config.ts +279 -77
  30. package/extensions/auto-mode/defaults.ts +17 -60
  31. package/extensions/auto-mode/instructions.ts +2 -1
  32. package/extensions/auto-mode/model-picker.ts +53 -31
  33. package/extensions/auto-mode/model-select.ts +153 -82
  34. package/extensions/auto-mode/paths.ts +13 -1
  35. package/extensions/auto-mode/pause.ts +8 -0
  36. package/extensions/auto-mode/prompt.ts +205 -238
  37. package/extensions/auto-mode/recoverability.ts +229 -0
  38. package/extensions/auto-mode/rules.ts +92 -75
  39. package/extensions/auto-mode/safety-floor.ts +43 -12
  40. package/extensions/auto-mode/sensitive.ts +11 -0
  41. package/extensions/auto-mode/setup-run.ts +245 -0
  42. package/extensions/auto-mode/setup.ts +235 -0
  43. package/extensions/auto-mode/shell-analysis.ts +332 -26
  44. package/extensions/auto-mode/transcript.ts +102 -0
  45. package/extensions/background/index.ts +256 -43
  46. package/extensions/background/monitor-batch.ts +58 -0
  47. package/extensions/background/registry.ts +9 -0
  48. package/extensions/background/wakeup.ts +45 -0
  49. package/extensions/bash/background.ts +47 -22
  50. package/extensions/bash/guards.ts +229 -0
  51. package/extensions/bash/index.ts +81 -19
  52. package/extensions/branding/index.ts +44 -18
  53. package/extensions/branding/startup.ts +23 -8
  54. package/extensions/claude-compat/index.ts +17 -2
  55. package/extensions/claude-context/index.ts +41 -12
  56. package/extensions/clear/index.ts +27 -0
  57. package/extensions/compaction/fit.ts +121 -0
  58. package/extensions/compaction/index.ts +176 -71
  59. package/extensions/compaction/prompt.ts +21 -1
  60. package/extensions/context-budget/budget.ts +61 -0
  61. package/extensions/context-budget/index.ts +66 -0
  62. package/extensions/context-management/index.ts +40 -16
  63. package/extensions/doctor/build.ts +226 -0
  64. package/extensions/doctor/cli.ts +99 -0
  65. package/extensions/doctor/compat.ts +532 -0
  66. package/extensions/doctor/dependencies.ts +186 -0
  67. package/extensions/doctor/fix-prompt.ts +125 -0
  68. package/extensions/doctor/index.ts +308 -0
  69. package/extensions/doctor/models.ts +231 -0
  70. package/extensions/doctor/presets.ts +208 -0
  71. package/extensions/doctor/report.ts +189 -0
  72. package/extensions/doctor/update-lookup.ts +46 -0
  73. package/extensions/doctor/viewer.ts +124 -0
  74. package/extensions/effort/index.ts +41 -4
  75. package/extensions/effort/slider.ts +33 -2
  76. package/extensions/file-tracker/index.ts +143 -20
  77. package/extensions/file-tracker/replay.ts +46 -0
  78. package/extensions/file-tracker/tracker.ts +51 -4
  79. package/extensions/footer/footer-line.ts +207 -0
  80. package/extensions/footer/index.ts +152 -0
  81. package/extensions/footer/pr.ts +53 -0
  82. package/extensions/hooks/executor.ts +57 -20
  83. package/extensions/hooks/formatter-notice.ts +76 -0
  84. package/extensions/hooks/index.ts +314 -73
  85. package/extensions/hooks/matcher.ts +35 -1
  86. package/extensions/hooks/plugin-hooks.ts +3 -3
  87. package/extensions/hooks/protocol.ts +58 -0
  88. package/extensions/hooks/subagent-bridge.ts +77 -0
  89. package/extensions/hooks/trust.ts +15 -4
  90. package/extensions/init/index.ts +26 -0
  91. package/extensions/init/prompt.ts +40 -0
  92. package/extensions/interrupted/index.ts +35 -0
  93. package/extensions/interrupted/line.ts +9 -0
  94. package/extensions/lib/abort.ts +15 -0
  95. package/extensions/lib/agent-loader.ts +183 -0
  96. package/extensions/lib/anthropic-payload.ts +15 -0
  97. package/extensions/lib/atomic-write.ts +56 -0
  98. package/extensions/lib/bridge-watch.ts +20 -0
  99. package/extensions/lib/cache-retention.ts +60 -0
  100. package/extensions/lib/capability-index.ts +466 -0
  101. package/extensions/lib/claude-context.ts +318 -27
  102. package/extensions/lib/claude-settings.ts +64 -0
  103. package/extensions/lib/consent-preview.ts +30 -0
  104. package/extensions/lib/deferred.ts +144 -13
  105. package/extensions/lib/favorites.ts +43 -0
  106. package/extensions/lib/fetch-timeout.ts +15 -0
  107. package/extensions/lib/frontmatter.ts +31 -0
  108. package/extensions/lib/git.ts +44 -2
  109. package/extensions/lib/hook-gate.ts +90 -0
  110. package/extensions/lib/interrupt.ts +94 -0
  111. package/extensions/lib/mcp-overrides.ts +61 -0
  112. package/extensions/lib/mcp-share.ts +25 -0
  113. package/extensions/lib/mcp-status.ts +34 -0
  114. package/extensions/lib/memory.ts +41 -6
  115. package/extensions/lib/model-facts.generated.json +2480 -0
  116. package/extensions/lib/model-facts.ts +162 -0
  117. package/extensions/lib/model-policy.ts +145 -189
  118. package/extensions/lib/model-tier.ts +480 -64
  119. package/extensions/lib/model-unusable.ts +37 -0
  120. package/extensions/lib/notifications.ts +320 -1
  121. package/extensions/lib/one-code-settings.ts +63 -0
  122. package/extensions/lib/original-command.ts +71 -0
  123. package/extensions/lib/package-version.ts +31 -0
  124. package/extensions/lib/paths.ts +39 -4
  125. package/extensions/lib/permission-gate.ts +192 -0
  126. package/extensions/lib/persisted-output.ts +18 -0
  127. package/extensions/lib/pi-ai-estimate.ts +160 -0
  128. package/extensions/lib/pi-version.ts +1 -1
  129. package/extensions/lib/plan-mode-channels.ts +11 -0
  130. package/extensions/lib/plugin-overrides.ts +24 -0
  131. package/extensions/lib/plugin-root.ts +44 -0
  132. package/extensions/lib/plugins.ts +233 -54
  133. package/extensions/lib/prefix-warm-gate.ts +157 -0
  134. package/extensions/lib/process-tree.ts +56 -0
  135. package/extensions/lib/reminders.ts +321 -68
  136. package/extensions/lib/scratchpad.ts +11 -8
  137. package/extensions/lib/session-lifecycle.ts +28 -0
  138. package/extensions/lib/settings-channels.ts +14 -0
  139. package/extensions/lib/shell-tasks.ts +54 -0
  140. package/extensions/lib/skill-overrides.ts +86 -0
  141. package/extensions/lib/skill-scan.ts +110 -0
  142. package/extensions/lib/text-width.ts +267 -0
  143. package/extensions/lib/tui-render.ts +281 -54
  144. package/extensions/lib/usage-bus.ts +75 -0
  145. package/extensions/lib/usage-tracker.ts +67 -0
  146. package/extensions/lib/worktree-isolation.ts +170 -0
  147. package/extensions/lsp/client.ts +69 -12
  148. package/extensions/lsp/format.ts +12 -2
  149. package/extensions/lsp/index.ts +273 -84
  150. package/extensions/lsp/install-hints.ts +43 -0
  151. package/extensions/lsp/keep-alive.ts +19 -0
  152. package/extensions/lsp/plugin-servers.ts +331 -0
  153. package/extensions/lsp/watcher.ts +157 -0
  154. package/extensions/mcp/announce.ts +99 -0
  155. package/extensions/mcp/client.ts +181 -22
  156. package/extensions/mcp/config.ts +55 -24
  157. package/extensions/mcp/index.ts +586 -86
  158. package/extensions/mcp/oauth/browser.ts +26 -0
  159. package/extensions/mcp/oauth/callback.ts +104 -0
  160. package/extensions/mcp/oauth/flow.ts +68 -0
  161. package/extensions/mcp/oauth/provider.ts +147 -0
  162. package/extensions/mcp/oauth/store.ts +86 -0
  163. package/extensions/mcp/panel/keys.ts +34 -0
  164. package/extensions/mcp/panel/model.ts +62 -0
  165. package/extensions/mcp/panel/render.ts +141 -0
  166. package/extensions/mcp/panel/state.ts +108 -0
  167. package/extensions/mcp/schema.ts +83 -6
  168. package/extensions/mcp/trust.ts +340 -0
  169. package/extensions/memory/entries.ts +163 -0
  170. package/extensions/memory/index.ts +119 -2
  171. package/extensions/memory/open-external.ts +97 -0
  172. package/extensions/memory/panel.ts +124 -0
  173. package/extensions/model-default/index.ts +74 -0
  174. package/extensions/notebook/index.ts +36 -18
  175. package/extensions/notebook/notebook.ts +110 -17
  176. package/extensions/permissions/index.ts +1012 -247
  177. package/extensions/permissions/matcher.ts +488 -57
  178. package/extensions/permissions/modes.ts +55 -7
  179. package/extensions/permissions/project-trust.ts +81 -0
  180. package/extensions/permissions/protected-paths.ts +16 -3
  181. package/extensions/permissions/rule-prose.ts +79 -0
  182. package/extensions/permissions/session-grant.ts +91 -0
  183. package/extensions/permissions/settings.ts +112 -33
  184. package/extensions/permissions/subagent-gate.ts +57 -0
  185. package/extensions/plan-mode/index.ts +143 -33
  186. package/extensions/plan-mode/reminder.ts +16 -19
  187. package/extensions/plan-mode/viewer.ts +40 -31
  188. package/extensions/plugins/counts.ts +68 -0
  189. package/extensions/plugins/index.ts +496 -32
  190. package/extensions/plugins/install/install.ts +167 -0
  191. package/extensions/plugins/install/paths.ts +23 -0
  192. package/extensions/plugins/install/registry.ts +72 -0
  193. package/extensions/plugins/marketplace/git.ts +103 -0
  194. package/extensions/plugins/marketplace/official.ts +36 -0
  195. package/extensions/plugins/marketplace/parse.ts +94 -0
  196. package/extensions/plugins/marketplace/registry.ts +60 -0
  197. package/extensions/plugins/marketplace/sync.ts +146 -0
  198. package/extensions/plugins/marketplace/types.ts +128 -0
  199. package/extensions/plugins/panel/keys.ts +52 -0
  200. package/extensions/plugins/panel/render.ts +315 -0
  201. package/extensions/plugins/panel/rows.ts +230 -0
  202. package/extensions/plugins/panel/state.ts +333 -0
  203. package/extensions/recap/index.ts +215 -0
  204. package/extensions/recap/prompt.ts +44 -0
  205. package/extensions/recap/scheduler.ts +87 -0
  206. package/extensions/search-tools/index.ts +0 -0
  207. package/extensions/search-tools/tools.ts +7 -5
  208. package/extensions/skill/index.ts +360 -34
  209. package/extensions/skill/invoke.ts +185 -0
  210. package/extensions/skill/listing.ts +21 -0
  211. package/extensions/skill/panel/keys.ts +41 -0
  212. package/extensions/skill/panel/render.ts +91 -0
  213. package/extensions/skill/panel/state.ts +115 -0
  214. package/extensions/spinner/index.ts +110 -0
  215. package/extensions/spinner/line.ts +51 -0
  216. package/extensions/spinner/verbs.ts +201 -0
  217. package/extensions/subagents/agents.ts +88 -44
  218. package/extensions/subagents/default-model.ts +62 -32
  219. package/extensions/subagents/delegation-steer.ts +23 -0
  220. package/extensions/subagents/hand-back-review.ts +67 -0
  221. package/extensions/subagents/index.ts +1545 -346
  222. package/extensions/subagents/live-runs.ts +293 -0
  223. package/extensions/subagents/model-select.ts +252 -82
  224. package/extensions/subagents/outcome.ts +90 -0
  225. package/extensions/subagents/panel-keys.ts +41 -0
  226. package/extensions/subagents/panel-render.ts +357 -0
  227. package/extensions/subagents/panel-widget.ts +330 -0
  228. package/extensions/subagents/pending-claim.ts +45 -0
  229. package/extensions/subagents/prose.ts +105 -0
  230. package/extensions/subagents/runner.ts +674 -0
  231. package/extensions/subagents/runs.ts +68 -1
  232. package/extensions/subagents/send-to-main-tool.ts +55 -0
  233. package/extensions/subagents/session-turns.ts +138 -0
  234. package/extensions/subagents/shell-panel.ts +198 -0
  235. package/extensions/subagents/usage.ts +16 -4
  236. package/extensions/subagents/worktree.ts +13 -0
  237. package/extensions/system-prompt/environment.ts +5 -3
  238. package/extensions/system-prompt/git-status.ts +93 -0
  239. package/extensions/system-prompt/index.ts +23 -1
  240. package/extensions/system-prompt/template.ts +34 -4
  241. package/extensions/system-prompt/tiers/common.ts +31 -6
  242. package/extensions/system-prompt/tiers/low.ts +13 -3
  243. package/extensions/system-prompt/tiers/mid.ts +21 -8
  244. package/extensions/system-reminder/index.ts +53 -9
  245. package/extensions/tasks/index.ts +65 -16
  246. package/extensions/tasks/store.ts +76 -3
  247. package/extensions/tool-search/announce.ts +42 -0
  248. package/extensions/tool-search/index.ts +136 -13
  249. package/extensions/turn-duration/index.ts +60 -0
  250. package/extensions/turn-duration/line.ts +22 -0
  251. package/extensions/turn-duration/span.ts +41 -0
  252. package/extensions/turn-duration/verbs.ts +24 -0
  253. package/extensions/web/backends.ts +421 -0
  254. package/extensions/web/index.ts +178 -21
  255. package/extensions/web-fetch/index.ts +74 -18
  256. package/extensions/web-fetch/summarize.ts +1 -1
  257. package/extensions/workflow/agent-session.ts +182 -112
  258. package/extensions/workflow/globals.ts +129 -106
  259. package/extensions/workflow/index.ts +343 -28
  260. package/extensions/workflow/records.ts +118 -0
  261. package/extensions/workflow/run-manager.ts +80 -31
  262. package/extensions/workflow/saved-workflows.ts +2 -1
  263. package/extensions/workflow/script-worker.mjs +110 -0
  264. package/extensions/workflow/types.ts +74 -8
  265. package/extensions/workflow/viewer.ts +562 -0
  266. package/extensions/workflow/vm-runtime.ts +118 -22
  267. package/extensions/workflow/widget.ts +126 -18
  268. package/extensions/worktree/guards.ts +240 -0
  269. package/extensions/worktree/index.ts +30 -5
  270. package/extensions/worktree/rewrite.ts +32 -16
  271. package/package.json +20 -10
  272. package/skills/code-review/SKILL.md +165 -0
  273. package/skills/fewer-permission-prompts/SKILL.md +61 -0
  274. package/skills/security-review/SKILL.md +178 -0
  275. package/skills/simplify/SKILL.md +70 -0
  276. package/themes/{one-code-light.json → onecode-light.json} +6 -1
  277. package/themes/{one-code.json → onecode.json} +6 -1
  278. package/types/app.d.ts +8 -1
  279. package/types/community-packages.d.ts +45 -2
  280. package/docs/decisions/auto-mode.md +0 -526
  281. package/docs/decisions/branding.md +0 -127
  282. package/docs/decisions/code-review-remediation.md +0 -106
  283. package/docs/decisions/compaction.md +0 -140
  284. package/docs/decisions/distribution.md +0 -168
  285. package/docs/decisions/hooks.md +0 -64
  286. package/docs/decisions/lsp.md +0 -58
  287. package/docs/decisions/mcp.md +0 -104
  288. package/docs/decisions/memory-state.md +0 -170
  289. package/docs/decisions/model-policy.md +0 -126
  290. package/docs/decisions/modes.md +0 -178
  291. package/docs/decisions/skills-plugins.md +0 -56
  292. package/docs/decisions/subagents-workflows.md +0 -225
  293. package/docs/decisions/system-prompt.md +0 -98
  294. package/docs/decisions/tools.md +0 -407
  295. package/docs/decisions/tui.md +0 -217
  296. package/docs/decisions.md +0 -148
  297. package/extensions/subagents/child.ts +0 -414
  298. package/extensions/subagents/rpc-turns.ts +0 -123
  299. package/extensions/todo/index.ts +0 -133
  300. package/extensions/workflow/permission-gate.ts +0 -71
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Worktree isolation for in-process agent runs (`isolation: "worktree"` on the
3
+ * Agent tool and workflow `agent()` calls): a registry of the live isolation
4
+ * worktrees, and the inline-extension guard that applies the same git-isolation
5
+ * bash guard an `enter_worktree` session gets (extensions/worktree/guards.ts).
6
+ *
7
+ * The registry exists because the association can only resolve at call time:
8
+ * these worktrees live in tmpdirs (not statically recognizable paths), and the
9
+ * child loaders are cached and shared across runs with different cwds.
10
+ * createWorktree/cleanupWorktree (subagents/worktree.ts) register and release
11
+ * 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.
14
+ *
15
+ * Module state is deliberate and safe despite jiti isolation: registration
16
+ * (worktree creation) and lookup (the guard factory) always happen inside the
17
+ * SAME extension's import graph — the subagents extension, or the workflow
18
+ * extension via its own module copy — so the state never needs to cross
19
+ * extensions.
20
+ */
21
+
22
+ import { homedir } from "node:os";
23
+ import { isAbsolute, join, relative } from "node:path";
24
+ import type { InlineExtension } from "@earendil-works/pi-coding-agent";
25
+ import { isWithin, isWritingTool, pathArgument, resolveForContainment, toAbsolute } from "../auto-mode/paths.ts";
26
+ import { analyzeShellCommand } from "../auto-mode/shell-analysis.ts";
27
+ import { worktreeBashGuardReason } from "../worktree/guards.ts";
28
+
29
+ export interface WorktreeIsolation {
30
+ /** The isolated worktree the run works in. */
31
+ worktreePath: string;
32
+ /** Root of the shared checkout the worktree belongs to. */
33
+ sharedRoot: string;
34
+ /**
35
+ * Both roots in resolved form (macOS /var → /private/var, symlinked
36
+ * checkouts), computed once at registration so the per-call write guard
37
+ * resolves only the write's own target, not two static roots per tool call.
38
+ */
39
+ resolvedWorktreePath: string;
40
+ resolvedSharedRoot: string;
41
+ }
42
+
43
+ /** Build the registry entry for a worktree, resolving both roots once. Exported for tests. */
44
+ export function describeIsolation(worktreePath: string, sharedRoot: string): WorktreeIsolation {
45
+ return {
46
+ worktreePath,
47
+ sharedRoot,
48
+ resolvedWorktreePath: resolveForContainment(worktreePath) ?? worktreePath,
49
+ resolvedSharedRoot: resolveForContainment(sharedRoot) ?? sharedRoot,
50
+ };
51
+ }
52
+
53
+ /** worktreePath → its isolation record, for every live isolation worktree. */
54
+ const active = new Map<string, WorktreeIsolation>();
55
+
56
+ export function registerWorktreeIsolation(worktreePath: string, sharedRoot: string): void {
57
+ active.set(worktreePath, describeIsolation(worktreePath, sharedRoot));
58
+ }
59
+
60
+ export function releaseWorktreeIsolation(worktreePath: string): void {
61
+ active.delete(worktreePath);
62
+ }
63
+
64
+ /** The registered isolation worktree `cwd` lives in, if any. */
65
+ export function worktreeIsolationFor(cwd: string): WorktreeIsolation | undefined {
66
+ for (const isolation of active.values()) {
67
+ if (isWithin(isolation.worktreePath, cwd)) return isolation;
68
+ }
69
+ return undefined;
70
+ }
71
+
72
+ /**
73
+ * Where a write from an isolated run must not land: inside the shared checkout,
74
+ * outside the worktree. Returns the corresponding worktree path when `target`
75
+ * is such a path, else undefined (inside the worktree, or outside the
76
+ * repository altogether — the permission gate still judges those). Shared by
77
+ * the file-tool guard and the bash write-target guard below. Pure.
78
+ */
79
+ export function sharedCheckoutWriteTarget(target: string, cwd: string, isolation: WorktreeIsolation, home = homedir()): string | undefined {
80
+ const { worktreePath, sharedRoot, resolvedWorktreePath, resolvedSharedRoot } = isolation;
81
+ 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.
86
+ const resolved = resolveForContainment(absolute) ?? absolute;
87
+ if (isWithin(resolvedWorktreePath, resolved) || !isWithin(resolvedSharedRoot, resolved)) return undefined;
88
+ // The tail keeps the model's spelling when the path was written under the
89
+ // shared root as given; the resolved forms are case-folded on darwin, which
90
+ // would send the model to `a.ts` for a file named `A.ts` on a case-sensitive volume.
91
+ const spelled = relative(sharedRoot, absolute);
92
+ const tail = spelled && !spelled.startsWith("..") && !isAbsolute(spelled) ? spelled : relative(resolvedSharedRoot, resolved);
93
+ return join(worktreePath, tail);
94
+ }
95
+
96
+ const refusal = (isolation: WorktreeIsolation, what: string, mapped: string): string =>
97
+ `This agent is isolated in the worktree ${isolation.worktreePath}, but ${what} is in the shared checkout ${isolation.sharedRoot}. ` +
98
+ `Refusing the write — an isolated agent's changes belong in its own worktree. Write the corresponding path there instead: ${mapped}`;
99
+
100
+ /**
101
+ * Refuse a file-tool write (`edit`/`write`/`notebook_edit`) from an isolated
102
+ * run that lands in the shared checkout: the model was told to work in the
103
+ * worktree, but a fork inherits a transcript whose every path points at the
104
+ * main tree, and a named agent's task may quote such paths too. Pure; exported
105
+ * for tests.
106
+ */
107
+ export function worktreeWriteGuardReason(input: {
108
+ toolName: string;
109
+ target: string | undefined;
110
+ cwd: string;
111
+ isolation: WorktreeIsolation;
112
+ home?: string;
113
+ }): string | undefined {
114
+ if (!isWritingTool(input.toolName) || !input.target) return undefined;
115
+ const mapped = sharedCheckoutWriteTarget(input.target, input.cwd, input.isolation, input.home);
116
+ return mapped ? refusal(input.isolation, input.target, mapped) : undefined;
117
+ }
118
+
119
+ /**
120
+ * The same rule for a shell command: every path the command may write
121
+ * (redirections, `sed -i`/`cp`/`mv`/`tee` positionals, output flags — the
122
+ * write-target model auto-mode's shell analysis already maintains) is checked
123
+ * like a file-tool target. Reads of the shared checkout stay allowed: the fork
124
+ * may need to look at what it inherited. Interpreters (`sed -i`, `perl`,
125
+ * `python`) are not modelled as writers there, so a write hidden in one reaches
126
+ * the permission gate and, in auto mode, the classifier instead — steering, not
127
+ * a sandbox (same contract as extensions/worktree/guards.ts). Pure; exported for tests.
128
+ */
129
+ export function worktreeBashWriteGuardReason(input: { command: string; cwd: string; isolation: WorktreeIsolation; home?: string }): string | undefined {
130
+ const home = input.home ?? homedir();
131
+ const evidence = analyzeShellCommand({ command: input.command, cwd: input.cwd, home });
132
+ for (const write of evidence.writes) {
133
+ const mapped = sharedCheckoutWriteTarget(write.token, input.cwd, input.isolation, home);
134
+ if (mapped) return refusal(input.isolation, `this command writes to ${write.token}, which`, mapped);
135
+ }
136
+ return undefined;
137
+ }
138
+
139
+ /**
140
+ * The git-isolation guard for worktree-isolated child sessions, as its own
141
+ * inline extension so it loads AHEAD of the permission gate — the same
142
+ * guard-before-permissions layering the main session gets from extension load
143
+ * order (worktree before permissions, findings §3). buildAgentLoader places it
144
+ * first in `extensionFactories`; keep it there. Three checks: bash git operations
145
+ * must target the worktree (extensions/worktree/guards.ts), and writes — by file
146
+ * tool or by shell command — must land in it.
147
+ */
148
+ export function worktreeGuardFactory(cwd: string): InlineExtension {
149
+ return {
150
+ name: "agent-worktree-guard",
151
+ hidden: true,
152
+ factory: (pi) => {
153
+ pi.on("tool_call", (event, ctx) => {
154
+ const runCwd = ctx?.cwd ?? cwd;
155
+ const isolation = worktreeIsolationFor(runCwd);
156
+ if (!isolation) return undefined;
157
+ const input = (event.input ?? {}) as Record<string, unknown>;
158
+ if (event.toolName === "bash") {
159
+ if (typeof input.command !== "string") return undefined;
160
+ const reason =
161
+ worktreeBashGuardReason({ command: input.command, worktreePath: isolation.worktreePath, sharedRoot: isolation.sharedRoot }) ??
162
+ worktreeBashWriteGuardReason({ command: input.command, cwd: runCwd, isolation });
163
+ return reason ? { block: true, reason } : undefined;
164
+ }
165
+ const reason = worktreeWriteGuardReason({ toolName: event.toolName, target: pathArgument(input), cwd: runCwd, isolation });
166
+ return reason ? { block: true, reason } : undefined;
167
+ });
168
+ },
169
+ };
170
+ }
@@ -20,15 +20,41 @@ import { readFileSync } from "node:fs";
20
20
  import { pathToFileURL } from "node:url";
21
21
  import type { LspDiagnostic } from "./format.ts";
22
22
  import { createReaderState, encodeMessage, type JsonRpcMessage, readMessages } from "./protocol.ts";
23
- import type { ServerConfig } from "./servers.ts";
24
23
 
25
24
  const INITIALIZE_TIMEOUT_MS = 15_000;
26
25
  const DIAGNOSTICS_TIMEOUT_MS = 6_000;
27
26
 
27
+ /** What a client actually needs to spawn — the built-in table's ServerConfig satisfies it. */
28
+ export interface SpawnConfig {
29
+ command: string;
30
+ args: string[];
31
+ }
32
+
33
+ /** Per-server extras a plugin config can carry (built-in servers pass none). */
34
+ export interface LspClientOptions {
35
+ /** Merged over process.env at spawn. */
36
+ env?: Record<string, string>;
37
+ /** Sent in the `initialize` request. */
38
+ initializationOptions?: unknown;
39
+ /** Answers the server's workspace/configuration requests. */
40
+ settings?: unknown;
41
+ startupTimeoutMs?: number;
42
+ }
43
+
28
44
  export function pathToUri(path: string): string {
29
45
  return pathToFileURL(path).toString();
30
46
  }
31
47
 
48
+ /**
49
+ * The reply to a server's workspace/configuration request: one settings object
50
+ * per requested item (LSP 3.6 shape), `{}` when none are configured.
51
+ */
52
+ export function configurationResponse(settings: unknown, params: unknown): unknown[] {
53
+ const items = (params as { items?: unknown[] } | undefined)?.items;
54
+ const count = Array.isArray(items) && items.length > 0 ? items.length : 1;
55
+ return Array.from({ length: count }, () => settings ?? {});
56
+ }
57
+
32
58
  function unrefStream(stream: unknown): void {
33
59
  (stream as { unref?: () => void } | null | undefined)?.unref?.();
34
60
  }
@@ -48,11 +74,12 @@ export class LspClient {
48
74
  private open = new Map<string, OpenDocument>();
49
75
  private ready = false;
50
76
  private failure?: string;
77
+ private publishes = 0;
51
78
 
52
79
  constructor(
53
- readonly languageId: string,
54
- private readonly config: ServerConfig,
80
+ private readonly config: SpawnConfig,
55
81
  private readonly root: string,
82
+ private readonly options: LspClientOptions = {},
56
83
  ) {}
57
84
 
58
85
  get isRunning(): boolean {
@@ -75,7 +102,7 @@ export class LspClient {
75
102
  const child = spawn(this.config.command, this.config.args, {
76
103
  cwd: this.root,
77
104
  stdio: ["pipe", "pipe", "pipe"],
78
- env: process.env,
105
+ env: this.options.env ? { ...process.env, ...this.options.env } : process.env,
79
106
  });
80
107
  this.child = child;
81
108
 
@@ -98,6 +125,12 @@ export class LspClient {
98
125
  this.failure = `Could not start ${this.config.command}: ${error.message}`;
99
126
  this.rejectAll(new Error(this.failure));
100
127
  });
128
+ // A write to a dead server's stdin is an EPIPE on the stream; without a
129
+ // listener that is an unhandled 'error' event, i.e. a crash (review T10).
130
+ child.stdin?.on("error", (error) => {
131
+ this.failure ??= `${this.config.command} stopped accepting input: ${error.message}`;
132
+ this.ready = false;
133
+ });
101
134
  child.on("exit", (code, signal) => {
102
135
  this.ready = false;
103
136
  if (!this.failure && code !== 0) {
@@ -113,6 +146,7 @@ export class LspClient {
113
146
  processId: process.pid,
114
147
  rootUri: pathToUri(this.root),
115
148
  workspaceFolders: [{ uri: pathToUri(this.root), name: "workspace" }],
149
+ initializationOptions: this.options.initializationOptions,
116
150
  capabilities: {
117
151
  textDocument: {
118
152
  synchronization: { dynamicRegistration: false, didSave: false },
@@ -121,7 +155,7 @@ export class LspClient {
121
155
  workspace: { workspaceFolders: true, configuration: true },
122
156
  },
123
157
  },
124
- INITIALIZE_TIMEOUT_MS,
158
+ this.options.startupTimeoutMs ?? INITIALIZE_TIMEOUT_MS,
125
159
  );
126
160
 
127
161
  this.notify("initialized", {});
@@ -131,6 +165,7 @@ export class LspClient {
131
165
  private handle(message: JsonRpcMessage): void {
132
166
  if (message.method === "textDocument/publishDiagnostics") {
133
167
  const params = message.params as { uri: string; diagnostics: LspDiagnostic[] };
168
+ this.publishes++;
134
169
  this.diagnostics.set(params.uri, params.diagnostics ?? []);
135
170
  const waiting = this.waiters.get(params.uri);
136
171
  if (waiting) {
@@ -142,7 +177,10 @@ export class LspClient {
142
177
 
143
178
  // Requests from the server: answer the ones that block startup.
144
179
  if (message.id !== undefined && message.method) {
145
- const result = message.method === "workspace/configuration" ? [{}] : null;
180
+ const result =
181
+ message.method === "workspace/configuration"
182
+ ? configurationResponse(this.options.settings, message.params)
183
+ : null;
146
184
  this.send({ id: message.id, result });
147
185
  return;
148
186
  }
@@ -193,8 +231,12 @@ export class LspClient {
193
231
  this.waiters.clear();
194
232
  }
195
233
 
196
- /** Push the file's current on-disk content and return its uri. */
197
- syncFile(path: string): string {
234
+ /**
235
+ * Push the file's current on-disk content and return its uri. `languageId`
236
+ * is per-document: one plugin server process can map different extensions to
237
+ * different language ids.
238
+ */
239
+ syncFile(path: string, languageId: string): string {
198
240
  const uri = pathToUri(path);
199
241
  let text: string;
200
242
  try {
@@ -207,7 +249,7 @@ export class LspClient {
207
249
  if (!existing) {
208
250
  this.open.set(uri, { version: 1, text });
209
251
  this.notify("textDocument/didOpen", {
210
- textDocument: { uri, languageId: this.languageId, version: 1, text },
252
+ textDocument: { uri, languageId, version: 1, text },
211
253
  });
212
254
  } else if (existing.text !== text) {
213
255
  const version = existing.version + 1;
@@ -240,10 +282,10 @@ export class LspClient {
240
282
  * nothing when a file is unchanged, hence the timeout plus last-known
241
283
  * fallback.
242
284
  */
243
- async getDiagnostics(path: string, timeoutMs = DIAGNOSTICS_TIMEOUT_MS): Promise<LspDiagnostic[]> {
285
+ async getDiagnostics(path: string, languageId: string, timeoutMs = DIAGNOSTICS_TIMEOUT_MS): Promise<LspDiagnostic[]> {
244
286
  const uri = pathToUri(path);
245
287
  const hadContent = this.open.get(uri)?.text;
246
- const synced = this.syncFile(path);
288
+ const synced = this.syncFile(path, languageId);
247
289
  const changed = this.open.get(uri)?.text !== hadContent;
248
290
 
249
291
  if (changed || !this.diagnostics.has(synced)) {
@@ -252,17 +294,32 @@ export class LspClient {
252
294
  return this.diagnostics.get(synced) ?? [];
253
295
  }
254
296
 
297
+ /** Everything the server has published, per document uri (for the watcher). */
298
+ allDiagnostics(): Map<string, LspDiagnostic[]> {
299
+ return new Map(this.diagnostics);
300
+ }
301
+
302
+ /**
303
+ * Monotonic count of publishDiagnostics received — the watcher's cheap
304
+ * "anything new since last look?" check, so the per-tool-round delta scan
305
+ * only runs when a server actually republished.
306
+ */
307
+ get publishCount(): number {
308
+ return this.publishes;
309
+ }
310
+
255
311
  async stop(): Promise<void> {
256
312
  const child = this.child;
257
313
  if (!child) return;
258
- this.child = undefined;
259
314
  this.ready = false;
260
315
  try {
316
+ // Send while this.child is still set — send() writes through it.
261
317
  this.send({ id: this.nextId++, method: "shutdown", params: null });
262
318
  this.notify("exit", null);
263
319
  } catch {
264
320
  // Server may already be gone.
265
321
  }
322
+ this.child = undefined;
266
323
  await new Promise<void>((resolve) => {
267
324
  const timer = setTimeout(() => {
268
325
  child.kill("SIGKILL");
@@ -12,10 +12,20 @@ export interface LspDiagnostic {
12
12
 
13
13
  export type SeverityFilter = "error" | "warning" | "all";
14
14
 
15
- const SEVERITY_NAMES: Record<number, string> = { 1: "error", 2: "warning", 3: "info", 4: "hint" };
15
+ /** LSP severity metadata (1..4; missing severity is treated as error). */
16
+ export const SEVERITY: Record<number, { name: string; symbol: string }> = {
17
+ 1: { name: "error", symbol: "✘" },
18
+ 2: { name: "warning", symbol: "⚠" },
19
+ 3: { name: "info", symbol: "ℹ" },
20
+ 4: { name: "hint", symbol: "★" },
21
+ };
16
22
 
17
23
  export function severityName(severity: number | undefined): string {
18
- return SEVERITY_NAMES[severity ?? 1] ?? "error";
24
+ return SEVERITY[severity ?? 1]?.name ?? "error";
25
+ }
26
+
27
+ export function severitySymbol(severity: number | undefined): string {
28
+ return SEVERITY[severity ?? 1]?.symbol ?? "✘";
19
29
  }
20
30
 
21
31
  export function filterDiagnostics(diagnostics: LspDiagnostic[], filter: SeverityFilter): LspDiagnostic[] {