oh-my-opencode 5.0.0-beta.10 → 5.0.0-beta.11

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 (71) hide show
  1. package/dist/cli/index.js +24 -85
  2. package/dist/cli-node/index.js +24 -85
  3. package/dist/index.js +70 -92
  4. package/dist/tools/delegate-task/builtin-categories.d.ts +1 -0
  5. package/dist/tools/delegate-task/builtin-category-definition.d.ts +1 -0
  6. package/dist/tools/delegate-task/tool-description.d.ts +3 -1
  7. package/dist/tui.js +9 -70
  8. package/package.json +14 -14
  9. package/packages/lsp-core/src/lsp/client-wrapper-outside-cwd.test.ts +78 -0
  10. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +6 -6
  11. package/packages/lsp-core/src/lsp/client-wrapper.ts +5 -6
  12. package/packages/lsp-core/src/lsp/outside-context-workspace.ts +33 -0
  13. package/packages/lsp-core/src/lsp/workspace-markers.ts +1 -0
  14. package/packages/lsp-daemon/dist/cli.js +94 -59
  15. package/packages/lsp-daemon/dist/client.js +72 -37
  16. package/packages/lsp-daemon/dist/index.js +78 -43
  17. package/packages/lsp-tools-mcp/dist/cli.js +68 -33
  18. package/packages/lsp-tools-mcp/dist/mcp.js +68 -33
  19. package/packages/lsp-tools-mcp/dist/tools.js +68 -33
  20. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  21. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  22. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  23. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  25. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  26. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  27. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  29. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  30. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  31. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +83 -54
  32. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  33. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  34. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  35. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  36. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  37. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  38. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  39. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  40. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  41. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  42. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  43. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  44. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  45. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  46. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  47. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  48. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  49. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  50. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  51. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  52. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  53. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  54. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  55. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  56. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  57. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  58. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  59. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  60. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  61. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  62. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  63. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  64. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  65. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  66. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  67. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  68. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  69. package/packages/omo-codex/plugin/package-lock.json +13 -13
  70. package/packages/omo-codex/plugin/package.json +1 -1
  71. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
@@ -2,5 +2,6 @@ import type { CategoryConfig } from "../../config/schema";
2
2
  export declare const DEFAULT_CATEGORIES: Record<string, CategoryConfig>;
3
3
  export declare const CATEGORY_PROMPT_APPENDS: Record<string, string>;
4
4
  export declare const CATEGORY_DESCRIPTIONS: Record<string, string>;
5
+ export declare const CATEGORY_CALLER_GUIDANCE: Record<string, string | undefined>;
5
6
  export declare const CATEGORY_PROMPT_APPEND_RESOLVERS: Record<string, (model: string | undefined) => string>;
6
7
  export declare const BUILTIN_CATEGORY_REQUIRES_MODEL: Record<string, string>;
@@ -3,6 +3,7 @@ export type BuiltinCategoryDefinition = {
3
3
  name: string;
4
4
  config: CategoryConfig;
5
5
  description: string;
6
+ callerGuidance?: string;
6
7
  promptAppend: string;
7
8
  resolvePromptAppend?: (model: string | undefined) => string;
8
9
  requiresModel?: string;
@@ -6,4 +6,6 @@ export interface DelegateTaskPresentation {
6
6
  categoryExamples: string;
7
7
  description: string;
8
8
  }
9
- export declare function createDelegateTaskPresentation(options: DelegateTaskToolOptions): DelegateTaskPresentation;
9
+ type DelegateTaskPresentationOptions = Pick<DelegateTaskToolOptions, "availableCategories" | "availableSkills" | "userCategories">;
10
+ export declare function createDelegateTaskPresentation(options: DelegateTaskPresentationOptions): DelegateTaskPresentation;
11
+ export {};
package/dist/tui.js CHANGED
@@ -78184,24 +78184,14 @@ var init_agent_display_names = __esm(() => {
78184
78184
  // packages/omo-opencode/src/tools/delegate-task/anthropic-categories.ts
78185
78185
  var UNSPECIFIED_HIGH_CATEGORY_PROMPT_APPEND = `<Category_Context>
78186
78186
  You are working on tasks that don't fit specific categories but require substantial effort.
78187
-
78188
- <Selection_Gate>
78189
- BEFORE selecting this category, VERIFY ALL conditions:
78190
- 1. Task does NOT fit: quick (trivial), visual-engineering (UI), ultrabrain (deep logic), artistry (creative), writing (docs)
78191
- 2. Task requires substantial effort across multiple systems/modules
78192
- 3. Changes have broad impact or require careful coordination
78193
- 4. NOT just "complex" - must be genuinely unclassifiable AND high-effort
78194
-
78195
- If task fits ANY other category, DO NOT select unspecified-high.
78196
- If task is unclassifiable but moderate-effort, use unspecified-low instead.
78197
- </Selection_Gate>
78198
- </Category_Context>`, ANTHROPIC_CATEGORIES;
78187
+ </Category_Context>`, UNSPECIFIED_HIGH_CATEGORY_CALLER_GUIDANCE = `<Selection_Gate>Use only when no specialist category fits and substantial effort spans systems/modules with broad impact. Use unspecified-low for contained moderate work.</Selection_Gate>`, ANTHROPIC_CATEGORIES;
78199
78188
  var init_anthropic_categories = __esm(() => {
78200
78189
  ANTHROPIC_CATEGORIES = [
78201
78190
  {
78202
78191
  name: "unspecified-high",
78203
78192
  config: { model: "kimi-for-coding/k3", variant: "max" },
78204
78193
  description: "Tasks that don't fit other categories, high effort required",
78194
+ callerGuidance: UNSPECIFIED_HIGH_CATEGORY_CALLER_GUIDANCE,
78205
78195
  promptAppend: UNSPECIFIED_HIGH_CATEGORY_PROMPT_APPEND
78206
78196
  }
78207
78197
  ];
@@ -78458,64 +78448,10 @@ Approach:
78458
78448
  - Minimal viable implementation
78459
78449
  - Skip unnecessary abstractions
78460
78450
  - Direct and concise
78461
- </Category_Context>
78462
-
78463
- <Caller_Warning>
78464
- THIS CATEGORY USES A SMALLER/FASTER MODEL (gpt-5.6-luna-fast).
78465
-
78466
- The model executing this task is optimized for speed over depth. Your prompt MUST be:
78467
-
78468
- **EXHAUSTIVELY EXPLICIT** - Leave NOTHING to interpretation:
78469
- 1. MUST DO: List every required action as atomic, numbered steps
78470
- 2. MUST NOT DO: Explicitly forbid likely mistakes and deviations
78471
- 3. EXPECTED OUTPUT: Describe exact success criteria with concrete examples
78472
-
78473
- **WHY THIS MATTERS:**
78474
- - Smaller models benefit from explicit guardrails
78475
- - Vague instructions may lead to unpredictable results
78476
- - Implicit expectations may be missed
78477
- **PROMPT STRUCTURE (MANDATORY):**
78478
- \`\`\`
78479
- TASK: [One-sentence goal]
78480
-
78481
- MUST DO:
78482
- 1. [Specific action with exact details]
78483
- 2. [Another specific action]
78484
- ...
78485
-
78486
- MUST NOT DO:
78487
- - [Forbidden action + why]
78488
- - [Another forbidden action]
78489
- ...
78490
-
78491
- EXPECTED OUTPUT:
78492
- - [Exact deliverable description]
78493
- - [Success criteria / verification method]
78494
- \`\`\`
78495
-
78496
- If your prompt lacks this structure, REWRITE IT before delegating.
78497
- </Caller_Warning>`, UNSPECIFIED_LOW_CATEGORY_PROMPT_APPEND = `<Category_Context>
78451
+ </Category_Context>`, QUICK_CATEGORY_CALLER_GUIDANCE = `<Caller_Warning>Small/fast model: before delegating, write an explicit prompt with numbered must-do steps, forbidden deviations, and concrete success criteria.</Caller_Warning>`, UNSPECIFIED_LOW_CATEGORY_PROMPT_APPEND = `<Category_Context>
78498
78452
  You are working on tasks that don't fit specific categories but require moderate effort.
78499
-
78500
- <Selection_Gate>
78501
- BEFORE selecting this category, VERIFY ALL conditions:
78502
- 1. Task does NOT fit: quick (trivial), visual-engineering (UI), ultrabrain (deep logic), artistry (creative), writing (docs)
78503
- 2. Task requires more than trivial effort but is NOT system-wide
78504
- 3. Scope is contained within a few files/modules
78505
-
78506
- If task fits ANY other category, DO NOT select unspecified-low.
78507
- This is NOT a default choice - it's for genuinely unclassifiable moderate-effort work.
78508
- </Selection_Gate>
78509
- </Category_Context>
78510
-
78511
- <Caller_Warning>
78512
- THIS CATEGORY USES GROK 4.6 (xhigh).
78513
-
78514
- **PROVIDE CLEAR STRUCTURE:**
78515
- 1. MUST DO: Enumerate required actions explicitly
78516
- 2. MUST NOT DO: State forbidden actions to prevent scope creep
78517
- 3. EXPECTED OUTPUT: Define concrete success criteria
78518
- </Caller_Warning>`, OPENAI_CATEGORIES;
78453
+ </Category_Context>`, UNSPECIFIED_LOW_CATEGORY_CALLER_GUIDANCE = `<Selection_Gate>Use only when no specialist category fits, effort is moderate, and scope stays within a few files/modules. Prefer any matching specialist category.</Selection_Gate>
78454
+ <Caller_Warning>Provide explicit must-do steps, forbidden scope, and concrete success criteria.</Caller_Warning>`, OPENAI_CATEGORIES;
78519
78455
  var init_openai_categories = __esm(() => {
78520
78456
  init_types6();
78521
78457
  OPENAI_CATEGORIES = [
@@ -78537,12 +78473,14 @@ var init_openai_categories = __esm(() => {
78537
78473
  name: "quick",
78538
78474
  config: { model: "kimi-for-coding/kimi-for-coding-highspeed" },
78539
78475
  description: "Trivial tasks - single file changes, typo fixes, simple modifications",
78476
+ callerGuidance: QUICK_CATEGORY_CALLER_GUIDANCE,
78540
78477
  promptAppend: QUICK_CATEGORY_PROMPT_APPEND
78541
78478
  },
78542
78479
  {
78543
78480
  name: "unspecified-low",
78544
78481
  config: { model: "xai/grok-4.6", variant: "xhigh" },
78545
78482
  description: "Tasks that don't fit other categories, low effort required",
78483
+ callerGuidance: UNSPECIFIED_LOW_CATEGORY_CALLER_GUIDANCE,
78546
78484
  promptAppend: UNSPECIFIED_LOW_CATEGORY_PROMPT_APPEND
78547
78485
  }
78548
78486
  ];
@@ -78552,7 +78490,7 @@ var init_openai_categories = __esm(() => {
78552
78490
  function buildCategoryRecord(selector) {
78553
78491
  return Object.fromEntries(BUILTIN_CATEGORIES.map((definition2) => [definition2.name, selector(definition2)]));
78554
78492
  }
78555
- var BUILTIN_CATEGORIES, DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS, CATEGORY_DESCRIPTIONS, CATEGORY_PROMPT_APPEND_RESOLVERS, BUILTIN_CATEGORY_REQUIRES_MODEL;
78493
+ var BUILTIN_CATEGORIES, DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS, CATEGORY_DESCRIPTIONS, CATEGORY_CALLER_GUIDANCE, CATEGORY_PROMPT_APPEND_RESOLVERS, BUILTIN_CATEGORY_REQUIRES_MODEL;
78556
78494
  var init_builtin_categories = __esm(() => {
78557
78495
  init_anthropic_categories();
78558
78496
  init_google_categories();
@@ -78567,6 +78505,7 @@ var init_builtin_categories = __esm(() => {
78567
78505
  DEFAULT_CATEGORIES = buildCategoryRecord((definition2) => definition2.config);
78568
78506
  CATEGORY_PROMPT_APPENDS = buildCategoryRecord((definition2) => definition2.promptAppend);
78569
78507
  CATEGORY_DESCRIPTIONS = buildCategoryRecord((definition2) => definition2.description);
78508
+ CATEGORY_CALLER_GUIDANCE = buildCategoryRecord((definition2) => definition2.callerGuidance);
78570
78509
  CATEGORY_PROMPT_APPEND_RESOLVERS = Object.fromEntries(BUILTIN_CATEGORIES.filter((definition2) => definition2.resolvePromptAppend !== undefined).map((definition2) => [definition2.name, definition2.resolvePromptAppend]));
78571
78510
  BUILTIN_CATEGORY_REQUIRES_MODEL = Object.fromEntries(BUILTIN_CATEGORIES.filter((definition2) => definition2.requiresModel !== undefined).map((definition2) => [definition2.name, definition2.requiresModel]));
78572
78511
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode",
3
- "version": "5.0.0-beta.10",
3
+ "version": "5.0.0-beta.11",
4
4
  "description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
5
5
  "main": "./dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -181,7 +181,7 @@
181
181
  "zod": "^4.4.3"
182
182
  },
183
183
  "devDependencies": {
184
- "@code-yeongyu/senpi": "2026.8.18-2",
184
+ "@code-yeongyu/senpi": "2026.8.18-3",
185
185
  "@oh-my-opencode/agents-md-core": "workspace:*",
186
186
  "@oh-my-opencode/ast-grep-mcp": "workspace:*",
187
187
  "@oh-my-opencode/boulder-state": "workspace:*",
@@ -222,18 +222,18 @@
222
222
  "typescript": "^7.0.2"
223
223
  },
224
224
  "optionalDependencies": {
225
- "oh-my-opencode-darwin-arm64": "5.0.0-beta.10",
226
- "oh-my-opencode-darwin-x64": "5.0.0-beta.10",
227
- "oh-my-opencode-darwin-x64-baseline": "5.0.0-beta.10",
228
- "oh-my-opencode-linux-arm64": "5.0.0-beta.10",
229
- "oh-my-opencode-linux-arm64-musl": "5.0.0-beta.10",
230
- "oh-my-opencode-linux-x64": "5.0.0-beta.10",
231
- "oh-my-opencode-linux-x64-baseline": "5.0.0-beta.10",
232
- "oh-my-opencode-linux-x64-musl": "5.0.0-beta.10",
233
- "oh-my-opencode-linux-x64-musl-baseline": "5.0.0-beta.10",
234
- "oh-my-opencode-windows-arm64": "5.0.0-beta.10",
235
- "oh-my-opencode-windows-x64": "5.0.0-beta.10",
236
- "oh-my-opencode-windows-x64-baseline": "5.0.0-beta.10"
225
+ "oh-my-opencode-darwin-arm64": "5.0.0-beta.11",
226
+ "oh-my-opencode-darwin-x64": "5.0.0-beta.11",
227
+ "oh-my-opencode-darwin-x64-baseline": "5.0.0-beta.11",
228
+ "oh-my-opencode-linux-arm64": "5.0.0-beta.11",
229
+ "oh-my-opencode-linux-arm64-musl": "5.0.0-beta.11",
230
+ "oh-my-opencode-linux-x64": "5.0.0-beta.11",
231
+ "oh-my-opencode-linux-x64-baseline": "5.0.0-beta.11",
232
+ "oh-my-opencode-linux-x64-musl": "5.0.0-beta.11",
233
+ "oh-my-opencode-linux-x64-musl-baseline": "5.0.0-beta.11",
234
+ "oh-my-opencode-windows-arm64": "5.0.0-beta.11",
235
+ "oh-my-opencode-windows-x64": "5.0.0-beta.11",
236
+ "oh-my-opencode-windows-x64-baseline": "5.0.0-beta.11"
237
237
  },
238
238
  "overrides": {
239
239
  "@earendil-works/pi-agent-core": "0.84.2",
@@ -0,0 +1,78 @@
1
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it } from "bun:test";
5
+
6
+ import { createStandaloneMcpRequestContext, runWithRequestContext } from "../request-context.js";
7
+ import { findWorkspaceRoot, resolveReadablePathInsideContext, resolvePathInsideContext } from "./client-wrapper.js";
8
+ import { LspInvalidPathError } from "./errors.js";
9
+
10
+ const tempDirectories: string[] = [];
11
+
12
+ afterEach(() => {
13
+ for (const directory of tempDirectories.splice(0)) {
14
+ rmSync(directory, { recursive: true, force: true });
15
+ }
16
+ });
17
+
18
+ function tempRoot(prefix: string): string {
19
+ const root = mkdtempSync(join(tmpdir(), prefix));
20
+ tempDirectories.push(root);
21
+ return root;
22
+ }
23
+
24
+ describe("LSP read-only access outside request cwd", () => {
25
+ it("#given an absolute file outside cwd #when resolving a read path #then returns the canonical path", () => {
26
+ const root = tempRoot("lsp-outcwd-root-");
27
+ const outside = tempRoot("lsp-outcwd-outside-");
28
+ const outsideFile = join(outside, "probe.ts");
29
+ writeFileSync(outsideFile, "export const value = 1;\n");
30
+
31
+ const resolved = runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
32
+ resolveReadablePathInsideContext(outsideFile),
33
+ );
34
+
35
+ expect(resolved).toBe(realpathSync(outsideFile));
36
+ });
37
+
38
+ it("#given an outside file inside its own marked project #when inferring workspace #then uses that project root", () => {
39
+ const root = tempRoot("lsp-outcwd-marker-root-");
40
+ const outside = tempRoot("lsp-outcwd-marker-outside-");
41
+ mkdirSync(join(outside, ".git"), { recursive: true });
42
+ mkdirSync(join(outside, "src"), { recursive: true });
43
+ const outsideFile = join(outside, "src", "probe.ts");
44
+ writeFileSync(outsideFile, "export const value = 1;\n");
45
+
46
+ const workspace = runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
47
+ findWorkspaceRoot(outsideFile),
48
+ );
49
+
50
+ expect(workspace).toBe(realpathSync(outside));
51
+ });
52
+
53
+ it("#given an outside file without any marker #when inferring workspace #then falls back to its own directory", () => {
54
+ const root = tempRoot("lsp-outcwd-nomarker-root-");
55
+ const outside = tempRoot("lsp-outcwd-nomarker-outside-");
56
+ const outsideFile = join(outside, "probe.ts");
57
+ writeFileSync(outsideFile, "export const value = 1;\n");
58
+
59
+ const workspace = runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
60
+ findWorkspaceRoot(outsideFile),
61
+ );
62
+
63
+ expect(workspace).toBe(realpathSync(outside));
64
+ });
65
+
66
+ it("#given an outside file #when resolving a mutating path #then still rejects to keep writes confined", () => {
67
+ const root = tempRoot("lsp-outcwd-write-root-");
68
+ const outside = tempRoot("lsp-outcwd-write-outside-");
69
+ const outsideFile = join(outside, "probe.ts");
70
+ writeFileSync(outsideFile, "export const value = 1;\n");
71
+
72
+ expect(() =>
73
+ runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
74
+ resolvePathInsideContext(outsideFile),
75
+ ),
76
+ ).toThrow(LspInvalidPathError);
77
+ });
78
+ });
@@ -36,17 +36,17 @@ describe("LSP client path confinement", () => {
36
36
  expect(workspace).toBe(realpathSync(root));
37
37
  });
38
38
 
39
- it("#given an absolute file outside context cwd #when resolving #then rejects before workspace inference", () => {
39
+ it("#given an absolute file outside context cwd #when resolving #then infers the file's own project root", () => {
40
40
  const root = tempRoot("lsp-client-wrapper-cwd-");
41
41
  const outside = tempRoot("lsp-client-wrapper-outside-");
42
42
  mkdirSync(join(outside, ".git"), { recursive: true });
43
43
  writeFileSync(join(outside, "file.ts"), "export const outside = true;\n");
44
44
 
45
- expect(() =>
46
- runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
47
- findWorkspaceRoot(join(outside, "file.ts")),
48
- ),
49
- ).toThrow(LspInvalidPathError);
45
+ const workspace = runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
46
+ findWorkspaceRoot(join(outside, "file.ts")),
47
+ );
48
+
49
+ expect(workspace).toBe(realpathSync(outside));
50
50
  });
51
51
 
52
52
  it("#given a symlink inside cwd that points outside #when resolving read path #then keeps lexical path and cwd root", () => {
@@ -16,11 +16,11 @@ import {
16
16
  LspServerLookupError,
17
17
  } from "./errors.js";
18
18
  import { getLspManager, type LspManager } from "./manager.js";
19
+ import { findWorkspaceRootOutsideContext } from "./outside-context-workspace.js";
19
20
  import { loadInstallDecision } from "./server-install-state.js";
20
21
  import { findServerForExtension } from "./server-resolution.js";
21
22
  import type { ServerLookupResult } from "./types.js";
22
-
23
- const WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
23
+ import { WORKSPACE_MARKERS } from "./workspace-markers.js";
24
24
 
25
25
  export function isDirectoryPath(filePath: string): boolean {
26
26
  try {
@@ -39,6 +39,8 @@ export function findWorkspaceRoot(filePath: string): string {
39
39
  dir = dirname(dir);
40
40
  }
41
41
 
42
+ if (!isPathInside(cwd, abs)) return findWorkspaceRootOutsideContext(dir);
43
+
42
44
  const fallbackRoot = nearestExistingDirectoryInsideContext(dir, cwd) ?? cwd;
43
45
  while (isPathInside(cwd, dir)) {
44
46
  const canonicalDir = existingDirectoryInsideContext(dir, cwd);
@@ -64,10 +66,7 @@ export function resolveReadablePathInsideContext(filePath: string): string {
64
66
  const rebased = rebaseThroughCanonicalAncestor(abs, cwd);
65
67
  if (rebased !== undefined) return rebased;
66
68
 
67
- const canonical = canonicalizeExistingOrNearestAncestor(abs);
68
- if (isPathInside(cwd, canonical)) return canonical;
69
-
70
- throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
69
+ return canonicalizeExistingOrNearestAncestor(abs);
71
70
  }
72
71
 
73
72
  export function resolvePathInsideContext(filePath: string): string {
@@ -0,0 +1,33 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+
4
+ import { WORKSPACE_MARKERS } from "./workspace-markers.js";
5
+
6
+ /** Read-only tools accept out-of-cwd files, so cwd cannot bound this search; the fallback keeps the server scoped to the file's own directory. */
7
+ export function findWorkspaceRootOutsideContext(directory: string): string {
8
+ const marked = nearestMarkedAncestor(directory);
9
+ if (marked !== undefined) return marked;
10
+ return realpathSync(nearestExistingAncestor(directory));
11
+ }
12
+
13
+ function nearestMarkedAncestor(directory: string): string | undefined {
14
+ let current = directory;
15
+ for (;;) {
16
+ if (existsSync(current) && WORKSPACE_MARKERS.some((marker) => existsSync(join(current, marker)))) {
17
+ return realpathSync(current);
18
+ }
19
+ const parent = dirname(current);
20
+ if (parent === current) return undefined;
21
+ current = parent;
22
+ }
23
+ }
24
+
25
+ function nearestExistingAncestor(directory: string): string {
26
+ let current = directory;
27
+ while (!existsSync(current)) {
28
+ const parent = dirname(current);
29
+ if (parent === current) return current;
30
+ current = parent;
31
+ }
32
+ return current;
33
+ }
@@ -0,0 +1 @@
1
+ export const WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"] as const;