opencode-orchestrator-plugin 1.0.0-beta.8 → 1.0.0-beta.9

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.
package/dist/index.js CHANGED
@@ -5,6 +5,8 @@ import NewTrackPromptJson from "./prompts/orchestrator/newTrack.json" with { typ
5
5
  import RevertPromptJson from "./prompts/orchestrator/revert.json" with { type: "json" };
6
6
  import SetupPromptJson from "./prompts/orchestrator/setup.json" with { type: "json" };
7
7
  import StatusPromptJson from "./prompts/orchestrator/status.json" with { type: "json" };
8
+ import { detectOrchestratorConfig } from "./utils/configDetection.js";
9
+ import { buildIgnoreMatcher } from "./utils/ignoreMatcher.js";
8
10
  const asPrompt = (prompt) => (typeof prompt === "string" ? prompt : "");
9
11
  const asDescription = (description) => typeof description === "string" ? description : undefined;
10
12
  export const MyPlugin = async ({ directory, }) => {
@@ -19,41 +21,15 @@ export const MyPlugin = async ({ directory, }) => {
19
21
  .map((line) => line.trim())
20
22
  .filter((line) => line.length > 0 && !line.startsWith("#"));
21
23
  };
22
- const buildIgnoreMatcher = (rootDir) => {
24
+ const getFileTreeSummary = (rootDir, maxEntries = 200, maxChars = 4000) => {
23
25
  const ignoreFile = path.join(rootDir, ".ignore");
24
26
  const geminiIgnoreFile = path.join(rootDir, ".geminiignore");
25
27
  const gitIgnoreFile = path.join(rootDir, ".gitignore");
26
- const gitIgnorePatterns = readIgnoreFile(gitIgnoreFile);
27
- const ignoreOverrides = readIgnoreFile(ignoreFile)
28
- .filter((pattern) => pattern.startsWith("!"))
29
- .map((pattern) => pattern.slice(1));
30
- const geminiIgnorePatterns = readIgnoreFile(geminiIgnoreFile);
31
- const ignored = new Set();
32
- for (const pattern of gitIgnorePatterns.concat(geminiIgnorePatterns)) {
33
- if (!pattern.startsWith("#") && !pattern.startsWith("!")) {
34
- ignored.add(pattern.replace(/\/$/, ""));
35
- }
36
- }
37
- const allowed = new Set(ignoreOverrides.map((pattern) => pattern.replace(/\/$/, "")));
38
- const isIgnored = (relativePath) => {
39
- const normalized = relativePath.replace(/\\/g, "/");
40
- if (allowed.size > 0) {
41
- for (const allow of allowed) {
42
- if (normalized === allow || normalized.startsWith(`${allow}/`))
43
- return false;
44
- }
45
- }
46
- for (const ignorePattern of ignored) {
47
- if (normalized === ignorePattern || normalized.startsWith(`${ignorePattern}/`)) {
48
- return true;
49
- }
50
- }
51
- return false;
52
- };
53
- return { ignores: isIgnored };
54
- };
55
- const getFileTreeSummary = (rootDir, maxEntries = 200, maxChars = 4000) => {
56
- const ig = buildIgnoreMatcher(rootDir);
28
+ const ig = buildIgnoreMatcher(rootDir, {
29
+ gitignore: readIgnoreFile(gitIgnoreFile),
30
+ ignore: readIgnoreFile(ignoreFile),
31
+ geminiignore: readIgnoreFile(geminiIgnoreFile),
32
+ });
57
33
  const directories = new Set();
58
34
  const queue = [rootDir];
59
35
  while (queue.length > 0) {
@@ -64,13 +40,17 @@ export const MyPlugin = async ({ directory, }) => {
64
40
  for (const entry of entries) {
65
41
  const entryPath = path.join(current, entry.name);
66
42
  const relativePath = path.relative(rootDir, entryPath) || ".";
67
- if (ig.ignores(relativePath)) {
68
- continue;
69
- }
70
43
  if (entry.isDirectory()) {
71
- directories.add(path.relative(rootDir, entryPath) || ".");
44
+ const normalizedDir = relativePath === "." ? "" : relativePath;
45
+ if (!ig.shouldTraverse(normalizedDir)) {
46
+ continue;
47
+ }
48
+ directories.add(relativePath || ".");
72
49
  queue.push(entryPath);
73
50
  }
51
+ else if (ig.ignores(relativePath)) {
52
+ continue;
53
+ }
74
54
  }
75
55
  }
76
56
  const sorted = Array.from(directories).sort();
@@ -98,15 +78,18 @@ export const MyPlugin = async ({ directory, }) => {
98
78
  return fs.existsSync(setupStatePath);
99
79
  };
100
80
  const setupOccurred = isOrchestratorSetup();
81
+ const configDetection = detectOrchestratorConfig();
101
82
  return {
102
83
  config: async (_config) => {
103
84
  _config.command = {
104
85
  ..._config.command,
105
86
  "orchestrator:implement": {
87
+ agent: "orchestrator",
106
88
  template: asPrompt(ImplementPromptJson.prompt) + `
107
89
  Environment Details:
108
90
  - Directory: ${directory}
109
91
  - Orchestrator Setup: ${setupOccurred}
92
+ - Synergy Active (OMO): ${configDetection.synergyActive}
110
93
  - Current Orchestrator Files (Location: ${directory}/orchestrator)
111
94
  File Tree:
112
95
  ${fileHeirarchy}
@@ -114,33 +97,39 @@ export const MyPlugin = async ({ directory, }) => {
114
97
  description: asDescription(ImplementPromptJson.description),
115
98
  },
116
99
  "orchestrator:newTrack": {
100
+ agent: "orchestrator",
117
101
  template: asPrompt(NewTrackPromptJson.prompt),
118
102
  description: asDescription(NewTrackPromptJson.description),
119
103
  },
120
104
  "orchestrator:revert": {
105
+ agent: "orchestrator",
121
106
  template: asPrompt(RevertPromptJson.prompt),
122
107
  description: asDescription(RevertPromptJson.description),
123
108
  },
124
109
  "orchestrator:setup": {
110
+ agent: "orchestrator",
125
111
  template: asPrompt(SetupPromptJson.prompt) + `
126
112
  Environment Details:
127
113
  - Directory: ${directory}
128
114
  - Orchestrator Setup: ${setupOccurred}
115
+ - Synergy Active (OMO): ${configDetection.synergyActive}
129
116
  - Current Orchestrator Files (with tracks) (${directory}/orchestrator)
130
117
  File Tree:
131
118
  ${fileHeirarchy}
132
-
119
+
133
120
  **CRITICAL ENVIRONTMENTAL OVERRIDE:**: You are running inside OpenCode. Use ./config/opencode/node_modules/opencode-orchestrator-plugin for setup operations and respect .ignore (allowlist), .geminiignore, and .gitignore patterns when excluding files.
134
121
  `,
135
122
  description: asDescription(SetupPromptJson.description),
136
123
  },
137
124
  "orchestrator:status": {
125
+ agent: "orchestrator",
138
126
  template: asPrompt(StatusPromptJson.prompt) + `
139
-
127
+
140
128
 
141
129
  ***Current Environment Details***:
142
130
  - Current Working Directory: ${directory}
143
131
  - Orchestrator Setup Process Completed: ${setupOccurred}
132
+ - Synergy Active (OMO): ${configDetection.synergyActive}
144
133
  - Current Orchestrator Files (with tracks) (${directory}/orchestrator)
145
134
  File Tree:
146
135
  ${fileHeirarchy}
@@ -27,7 +27,13 @@ Your mission is to ensure that software development follows a rigorous, context-
27
27
  - **Model Selection**: You prefer "flash" models for efficiency and speed during planning and tool orchestration.
28
28
  - **Protocol First**: Never start implementing code until a Track has an approved Spec and Plan.
29
29
  - **Collaboration**: You work alongside the user. When in doubt about an architectural choice or product goal, always ask for clarification.
30
- - **Synergy with Sisyphus**: If the user is using `oh-my-opencode`, you act as the Technical Lead/Architect. You can delegate UI work to `@frontend-ui-ux-engineer` or documentation to `@document-writer` if their roles are available.
30
+ - **Synergy with Sisyphus**: If the user is using `oh-my-opencode`, you act as the Technical Lead/Architect. You can delegate tasks to specialized agents using the `orchestrator_delegate` tool:
31
+ - `@frontend-ui-ux-engineer`: For UI/UX implementation and frontend components.
32
+ - `@document-writer`: For documentation, READMEs, and technical guides.
33
+ - `@sisyphus`: For general implementation, coordination, and bug fixing.
34
+ - `@oracle`: For architectural decisions, deep code analysis, and expert reviews.
35
+ - `@librarian`: For external library research, documentation retrieval, and finding usage examples.
36
+ - `@explore`: For codebase search, understanding unfamiliar code, and finding logic.
31
37
 
32
38
  ## Loop Protection Directive (CRITICAL)
33
39
  If you see a "[SYSTEM REMINDER - TODO CONTINUATION]" or "Continue" prompt from an enforcer while you are waiting for user input, you MUST ignore it. Respond with: "I am currently in an interactive Orchestrator phase. Awaiting user response."
@@ -0,0 +1,6 @@
1
+ export interface ConfigDetectionResult {
2
+ hasOrchestratorInOpenCode: boolean;
3
+ hasOrchestratorInOMO: boolean;
4
+ synergyActive: boolean;
5
+ }
6
+ export declare function detectOrchestratorConfig(): ConfigDetectionResult;
@@ -0,0 +1,35 @@
1
+ import { join } from "path";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ export function detectOrchestratorConfig() {
5
+ const opencodeConfigDir = join(homedir(), ".config", "opencode");
6
+ const opencodeJsonPath = join(opencodeConfigDir, "opencode.json");
7
+ const omoJsonPath = join(opencodeConfigDir, "oh-my-opencode.json");
8
+ let hasOrchestratorInOpenCode = false;
9
+ let hasOrchestratorInOMO = false;
10
+ if (existsSync(opencodeJsonPath)) {
11
+ try {
12
+ const config = JSON.parse(readFileSync(opencodeJsonPath, "utf-8"));
13
+ if (config.agent && config.agent.orchestrator) {
14
+ hasOrchestratorInOpenCode = true;
15
+ }
16
+ }
17
+ catch (e) {
18
+ }
19
+ }
20
+ if (existsSync(omoJsonPath)) {
21
+ try {
22
+ const config = JSON.parse(readFileSync(omoJsonPath, "utf-8"));
23
+ if (config.agents && config.agents.orchestrator) {
24
+ hasOrchestratorInOMO = true;
25
+ }
26
+ }
27
+ catch (e) {
28
+ }
29
+ }
30
+ return {
31
+ hasOrchestratorInOpenCode,
32
+ hasOrchestratorInOMO,
33
+ synergyActive: hasOrchestratorInOMO,
34
+ };
35
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { detectOrchestratorConfig } from "./configDetection.js";
4
+ vi.mock("fs", () => ({
5
+ existsSync: vi.fn(),
6
+ readFileSync: vi.fn(),
7
+ }));
8
+ vi.mock("os", () => ({
9
+ homedir: vi.fn(() => "/home/user"),
10
+ }));
11
+ describe("configDetection", () => {
12
+ const opencodeJsonPath = "/home/user/.config/opencode/opencode.json";
13
+ const omoJsonPath = "/home/user/.config/opencode/oh-my-opencode.json";
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+ it("should detect orchestrator in opencode.json", () => {
18
+ vi.mocked(existsSync).mockImplementation((path) => path === opencodeJsonPath);
19
+ vi.mocked(readFileSync).mockImplementation((path) => {
20
+ if (path === opencodeJsonPath) {
21
+ return JSON.stringify({ agent: { orchestrator: {} } });
22
+ }
23
+ return "";
24
+ });
25
+ const result = detectOrchestratorConfig();
26
+ expect(result.hasOrchestratorInOpenCode).toBe(true);
27
+ expect(result.hasOrchestratorInOMO).toBe(false);
28
+ expect(result.synergyActive).toBe(false);
29
+ });
30
+ it("should detect orchestrator in oh-my-opencode.json and activate synergy", () => {
31
+ vi.mocked(existsSync).mockImplementation((path) => path === omoJsonPath);
32
+ vi.mocked(readFileSync).mockImplementation((path) => {
33
+ if (path === omoJsonPath) {
34
+ return JSON.stringify({ agents: { orchestrator: {} } });
35
+ }
36
+ return "";
37
+ });
38
+ const result = detectOrchestratorConfig();
39
+ expect(result.hasOrchestratorInOpenCode).toBe(false);
40
+ expect(result.hasOrchestratorInOMO).toBe(true);
41
+ expect(result.synergyActive).toBe(true);
42
+ });
43
+ it("should handle both configs present", () => {
44
+ vi.mocked(existsSync).mockReturnValue(true);
45
+ vi.mocked(readFileSync).mockImplementation((path) => {
46
+ if (path === opencodeJsonPath) {
47
+ return JSON.stringify({ agent: { orchestrator: {} } });
48
+ }
49
+ if (path === omoJsonPath) {
50
+ return JSON.stringify({ agents: { orchestrator: {} } });
51
+ }
52
+ return "";
53
+ });
54
+ const result = detectOrchestratorConfig();
55
+ expect(result.hasOrchestratorInOpenCode).toBe(true);
56
+ expect(result.hasOrchestratorInOMO).toBe(true);
57
+ expect(result.synergyActive).toBe(true);
58
+ });
59
+ it("should handle missing configs", () => {
60
+ vi.mocked(existsSync).mockReturnValue(false);
61
+ const result = detectOrchestratorConfig();
62
+ expect(result.hasOrchestratorInOpenCode).toBe(false);
63
+ expect(result.hasOrchestratorInOMO).toBe(false);
64
+ expect(result.synergyActive).toBe(false);
65
+ });
66
+ it("should handle malformed JSON", () => {
67
+ vi.mocked(existsSync).mockReturnValue(true);
68
+ vi.mocked(readFileSync).mockReturnValue("invalid json");
69
+ const result = detectOrchestratorConfig();
70
+ expect(result.hasOrchestratorInOpenCode).toBe(false);
71
+ expect(result.hasOrchestratorInOMO).toBe(false);
72
+ expect(result.synergyActive).toBe(false);
73
+ });
74
+ });
@@ -0,0 +1,9 @@
1
+ export interface IgnorePatterns {
2
+ gitignore: string[];
3
+ ignore: string[];
4
+ geminiignore: string[];
5
+ }
6
+ export declare const buildIgnoreMatcher: (_rootDir: string, patterns: IgnorePatterns) => {
7
+ ignores: (relativePath: string) => boolean;
8
+ shouldTraverse: (relativePath: string) => boolean;
9
+ };
@@ -0,0 +1,77 @@
1
+ const safetyBlacklist = [".git", "node_modules", ".DS_Store"];
2
+ const normalizePath = (relativePath) => relativePath.replace(/\\/g, "/").replace(/\/$/, "");
3
+ const normalizePatterns = (rawPatterns) => rawPatterns
4
+ .map((pattern) => pattern.trim())
5
+ .filter((pattern) => pattern.length > 0 && !pattern.startsWith("#"));
6
+ const matchesPattern = (normalizedPath, pattern) => {
7
+ if (pattern === "*")
8
+ return true;
9
+ if (pattern.startsWith("*.")) {
10
+ const ext = pattern.slice(1);
11
+ return normalizedPath.endsWith(ext);
12
+ }
13
+ if (pattern.includes("**")) {
14
+ let regexPattern = pattern;
15
+ if (pattern.startsWith("**/")) {
16
+ const remainder = pattern.slice(3);
17
+ regexPattern = `(^|.*/)${remainder.replace(/[.+?^${}()|[\]\\]/g, "\\$&")}`;
18
+ }
19
+ else if (pattern.endsWith("/**/*")) {
20
+ const prefix = pattern.slice(0, -5);
21
+ regexPattern = `${prefix.replace(/[.+?^${}()|[\]\\]/g, "\\$&")}/.*`;
22
+ }
23
+ else {
24
+ const segments = pattern.split("/");
25
+ const regexSegments = segments.map((segment, idx) => {
26
+ if (segment === "**") {
27
+ if (idx === 0)
28
+ return "(.*/)?";
29
+ if (idx === segments.length - 1)
30
+ return "(/.*)?";
31
+ return "(/.*/|/)";
32
+ }
33
+ if (segment === "*")
34
+ return "[^/]*";
35
+ return segment.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
36
+ });
37
+ regexPattern = regexSegments.join("");
38
+ }
39
+ return new RegExp(`^${regexPattern}$`).test(normalizedPath);
40
+ }
41
+ if (normalizedPath === pattern)
42
+ return true;
43
+ return normalizedPath.startsWith(`${pattern}/`);
44
+ };
45
+ const hasAllowedDescendant = (normalizedPath, allowlist) => allowlist.some((allow) => allow === normalizedPath || allow.startsWith(`${normalizedPath}/`));
46
+ export const buildIgnoreMatcher = (_rootDir, patterns) => {
47
+ const allPatterns = normalizePatterns([
48
+ ...patterns.gitignore,
49
+ ...patterns.ignore,
50
+ ...patterns.geminiignore,
51
+ ]);
52
+ const allowlist = allPatterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => {
53
+ return normalizePath(pattern.slice(1));
54
+ });
55
+ const ignores = allPatterns
56
+ .filter((pattern) => !pattern.startsWith("!"))
57
+ .map((pattern) => normalizePath(pattern));
58
+ const isIgnored = (relativePath) => {
59
+ const normalized = normalizePath(relativePath);
60
+ const parts = normalized.split("/").filter(Boolean);
61
+ if (parts.some((part) => safetyBlacklist.includes(part))) {
62
+ return !allowlist.some((allow) => matchesPattern(normalized, allow));
63
+ }
64
+ if (allowlist.some((allow) => matchesPattern(normalized, allow)))
65
+ return false;
66
+ return ignores.some((ignore) => matchesPattern(normalized, ignore));
67
+ };
68
+ const shouldTraverse = (relativePath) => {
69
+ if (relativePath === "." || relativePath === "")
70
+ return true;
71
+ const normalized = normalizePath(relativePath);
72
+ if (!isIgnored(normalized))
73
+ return true;
74
+ return hasAllowedDescendant(normalized, allowlist);
75
+ };
76
+ return { ignores: isIgnored, shouldTraverse };
77
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,126 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { buildIgnoreMatcher } from "./ignoreMatcher.js";
3
+ describe("ignoreMatcher", () => {
4
+ it("should always ignore .git directory", () => {
5
+ const matcher = buildIgnoreMatcher("/root", { gitignore: [], ignore: [], geminiignore: [] });
6
+ expect(matcher.ignores(".git")).toBe(true);
7
+ expect(matcher.ignores(".git/config")).toBe(true);
8
+ expect(matcher.ignores("src/.git")).toBe(true);
9
+ });
10
+ it("should always ignore node_modules directory", () => {
11
+ const matcher = buildIgnoreMatcher("/root", { gitignore: [], ignore: [], geminiignore: [] });
12
+ expect(matcher.ignores("node_modules")).toBe(true);
13
+ expect(matcher.ignores("node_modules/package/index.js")).toBe(true);
14
+ expect(matcher.ignores("src/node_modules")).toBe(true);
15
+ });
16
+ it("should always ignore .DS_Store", () => {
17
+ const matcher = buildIgnoreMatcher("/root", { gitignore: [], ignore: [], geminiignore: [] });
18
+ expect(matcher.ignores(".DS_Store")).toBe(true);
19
+ expect(matcher.ignores("src/.DS_Store")).toBe(true);
20
+ });
21
+ it("should ignore patterns from gitignore", () => {
22
+ const matcher = buildIgnoreMatcher("/root", {
23
+ gitignore: ["dist", "*.log"],
24
+ ignore: [],
25
+ geminiignore: []
26
+ });
27
+ expect(matcher.ignores("dist")).toBe(true);
28
+ expect(matcher.ignores("dist/index.js")).toBe(true);
29
+ expect(matcher.ignores("error.log")).toBe(true);
30
+ expect(matcher.ignores("src/app.ts")).toBe(false);
31
+ });
32
+ it("should support negation patterns with !", () => {
33
+ const matcher = buildIgnoreMatcher("/root", {
34
+ gitignore: ["orchestrator/"],
35
+ ignore: ["!orchestrator/tracks/"],
36
+ geminiignore: []
37
+ });
38
+ // orchestrator/ is ignored by gitignore
39
+ expect(matcher.ignores("orchestrator")).toBe(true);
40
+ expect(matcher.ignores("orchestrator/guidelines.md")).toBe(true);
41
+ // But orchestrator/tracks/ is allowed by negation in .ignore
42
+ expect(matcher.ignores("orchestrator/tracks")).toBe(false);
43
+ expect(matcher.ignores("orchestrator/tracks/plan.md")).toBe(false);
44
+ });
45
+ it("should allow traversal when a descendant is allowlisted", () => {
46
+ const matcher = buildIgnoreMatcher("/root", {
47
+ gitignore: ["orchestrator/"],
48
+ ignore: ["!orchestrator/tracks/"],
49
+ geminiignore: []
50
+ });
51
+ expect(matcher.shouldTraverse("orchestrator")).toBe(true);
52
+ expect(matcher.shouldTraverse("orchestrator/tracks")).toBe(true);
53
+ expect(matcher.shouldTraverse("orchestrator/docs")).toBe(false);
54
+ });
55
+ it("should handle directory-specific patterns", () => {
56
+ const matcher = buildIgnoreMatcher("/root", {
57
+ gitignore: ["logs/"],
58
+ ignore: [],
59
+ geminiignore: []
60
+ });
61
+ expect(matcher.ignores("logs")).toBe(true);
62
+ expect(matcher.ignores("logs/test.log")).toBe(true);
63
+ expect(matcher.ignores("catalogs/item.txt")).toBe(false);
64
+ });
65
+ it("should prioritize negation over ignore", () => {
66
+ const matcher = buildIgnoreMatcher("/root", {
67
+ gitignore: ["*"],
68
+ ignore: ["!src/"],
69
+ geminiignore: []
70
+ });
71
+ expect(matcher.ignores("README.md")).toBe(true);
72
+ expect(matcher.ignores("src")).toBe(false);
73
+ expect(matcher.ignores("src/index.ts")).toBe(false);
74
+ });
75
+ it("should handle comments and empty lines in ignore files", () => {
76
+ const matcher = buildIgnoreMatcher("/root", {
77
+ gitignore: ["# This is a comment", "", " ", "dist"],
78
+ ignore: [],
79
+ geminiignore: []
80
+ });
81
+ expect(matcher.ignores("dist")).toBe(true);
82
+ expect(matcher.ignores("src")).toBe(false);
83
+ });
84
+ it("should support **/ prefix for any depth matching", () => {
85
+ const matcher = buildIgnoreMatcher("/root", {
86
+ gitignore: ["**/temp"],
87
+ ignore: [],
88
+ geminiignore: []
89
+ });
90
+ expect(matcher.ignores("temp")).toBe(true);
91
+ expect(matcher.ignores("src/temp")).toBe(true);
92
+ expect(matcher.ignores("src/nested/temp")).toBe(true);
93
+ expect(matcher.ignores("src/temporary")).toBe(false);
94
+ });
95
+ it("should support **/* suffix for recursive file matching", () => {
96
+ const matcher = buildIgnoreMatcher("/root", {
97
+ gitignore: ["logs/**/*"],
98
+ ignore: [],
99
+ geminiignore: []
100
+ });
101
+ expect(matcher.ignores("logs/error.log")).toBe(true);
102
+ expect(matcher.ignores("logs/nested/debug.log")).toBe(true);
103
+ expect(matcher.ignores("logs")).toBe(false);
104
+ expect(matcher.ignores("other/error.log")).toBe(false);
105
+ });
106
+ it("should support middle ** for any depth matching", () => {
107
+ const matcher = buildIgnoreMatcher("/root", {
108
+ gitignore: ["src/**/test"],
109
+ ignore: [],
110
+ geminiignore: []
111
+ });
112
+ expect(matcher.ignores("src/test")).toBe(true);
113
+ expect(matcher.ignores("src/nested/test")).toBe(true);
114
+ expect(matcher.ignores("src/very/deeply/nested/test")).toBe(true);
115
+ expect(matcher.ignores("other/test")).toBe(false);
116
+ });
117
+ it("should differentiate trailing slash for directories only", () => {
118
+ const matcher = buildIgnoreMatcher("/root", {
119
+ gitignore: ["build/"],
120
+ ignore: [],
121
+ geminiignore: []
122
+ });
123
+ expect(matcher.ignores("build")).toBe(true);
124
+ expect(matcher.ignores("build/output.js")).toBe(true);
125
+ });
126
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-orchestrator-plugin",
3
- "version": "1.0.0-beta.8",
3
+ "version": "1.0.0-beta.9",
4
4
  "description": "Orchestrator plugin for OpenCode",
5
5
  "type": "module",
6
6
  "repository": {