@codemcp/ade-harnesses 0.0.2 → 0.1.1
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-format.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +15 -12
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/permission-policy.d.ts +7 -0
- package/dist/permission-policy.js +152 -0
- package/dist/util.d.ts +1 -1
- package/dist/util.js +16 -2
- package/dist/writers/claude-code.js +51 -20
- package/dist/writers/cline.js +2 -2
- package/dist/writers/copilot.js +61 -8
- package/dist/writers/cursor.js +48 -2
- package/dist/writers/kiro.js +54 -38
- package/dist/writers/opencode.js +26 -23
- package/dist/writers/roo-code.js +38 -2
- package/dist/writers/universal.js +41 -3
- package/dist/writers/windsurf.js +43 -1
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/permission-policy.ts +173 -0
- package/src/util.ts +20 -7
- package/src/writers/claude-code.spec.ts +163 -5
- package/src/writers/claude-code.ts +63 -20
- package/src/writers/cline.spec.ts +146 -3
- package/src/writers/cline.ts +2 -2
- package/src/writers/copilot.spec.ts +157 -1
- package/src/writers/copilot.ts +76 -9
- package/src/writers/cursor.spec.ts +104 -1
- package/src/writers/cursor.ts +65 -3
- package/src/writers/kiro.spec.ts +228 -0
- package/src/writers/kiro.ts +77 -40
- package/src/writers/opencode.spec.ts +258 -0
- package/src/writers/opencode.ts +40 -27
- package/src/writers/roo-code.spec.ts +129 -1
- package/src/writers/roo-code.ts +49 -2
- package/src/writers/universal.spec.ts +134 -0
- package/src/writers/universal.ts +57 -4
- package/src/writers/windsurf.spec.ts +111 -3
- package/src/writers/windsurf.ts +64 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, mkdir, rm, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type {
|
|
6
|
+
AutonomyProfile,
|
|
7
|
+
LogicalConfig,
|
|
8
|
+
PermissionPolicy
|
|
9
|
+
} from "@codemcp/ade-core";
|
|
10
|
+
import { parse as parseYaml } from "yaml";
|
|
11
|
+
import { opencodeWriter } from "./opencode.js";
|
|
12
|
+
|
|
13
|
+
function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy {
|
|
14
|
+
switch (profile) {
|
|
15
|
+
case "rigid":
|
|
16
|
+
return {
|
|
17
|
+
profile,
|
|
18
|
+
capabilities: {
|
|
19
|
+
read: "ask",
|
|
20
|
+
edit_write: "ask",
|
|
21
|
+
search_list: "ask",
|
|
22
|
+
bash_safe: "ask",
|
|
23
|
+
bash_unsafe: "ask",
|
|
24
|
+
web: "ask",
|
|
25
|
+
task_agent: "ask"
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
case "sensible-defaults":
|
|
29
|
+
return {
|
|
30
|
+
profile,
|
|
31
|
+
capabilities: {
|
|
32
|
+
read: "allow",
|
|
33
|
+
edit_write: "allow",
|
|
34
|
+
search_list: "allow",
|
|
35
|
+
bash_safe: "allow",
|
|
36
|
+
bash_unsafe: "ask",
|
|
37
|
+
web: "ask",
|
|
38
|
+
task_agent: "allow"
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
case "max-autonomy":
|
|
42
|
+
return {
|
|
43
|
+
profile,
|
|
44
|
+
capabilities: {
|
|
45
|
+
read: "allow",
|
|
46
|
+
edit_write: "allow",
|
|
47
|
+
search_list: "allow",
|
|
48
|
+
bash_safe: "allow",
|
|
49
|
+
bash_unsafe: "allow",
|
|
50
|
+
web: "ask",
|
|
51
|
+
task_agent: "allow"
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("opencodeWriter", () => {
|
|
58
|
+
let dir: string;
|
|
59
|
+
|
|
60
|
+
beforeEach(async () => {
|
|
61
|
+
dir = await mkdtemp(join(tmpdir(), "ade-harness-opencode-"));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(async () => {
|
|
65
|
+
await rm(dir, { recursive: true, force: true });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("writes OpenCode permissions to the ADE agent frontmatter using the documented schema", async () => {
|
|
69
|
+
const rigidRoot = join(dir, "rigid");
|
|
70
|
+
const defaultsRoot = join(dir, "defaults");
|
|
71
|
+
const maxRoot = join(dir, "max");
|
|
72
|
+
|
|
73
|
+
const baseConfig = {
|
|
74
|
+
mcp_servers: [],
|
|
75
|
+
instructions: ["Follow project rules."],
|
|
76
|
+
cli_actions: [],
|
|
77
|
+
knowledge_sources: [],
|
|
78
|
+
skills: [],
|
|
79
|
+
git_hooks: [],
|
|
80
|
+
setup_notes: []
|
|
81
|
+
} satisfies LogicalConfig;
|
|
82
|
+
|
|
83
|
+
const rigidConfig = {
|
|
84
|
+
...baseConfig,
|
|
85
|
+
permission_policy: autonomyPolicy("rigid")
|
|
86
|
+
} as LogicalConfig;
|
|
87
|
+
|
|
88
|
+
const maxConfig = {
|
|
89
|
+
...baseConfig,
|
|
90
|
+
permission_policy: autonomyPolicy("max-autonomy")
|
|
91
|
+
} as LogicalConfig;
|
|
92
|
+
|
|
93
|
+
const defaultsConfig = {
|
|
94
|
+
...baseConfig,
|
|
95
|
+
permission_policy: autonomyPolicy("sensible-defaults")
|
|
96
|
+
} as LogicalConfig;
|
|
97
|
+
|
|
98
|
+
await opencodeWriter.install(rigidConfig, rigidRoot);
|
|
99
|
+
await opencodeWriter.install(defaultsConfig, defaultsRoot);
|
|
100
|
+
await opencodeWriter.install(maxConfig, maxRoot);
|
|
101
|
+
|
|
102
|
+
const rigidAgent = await readFile(
|
|
103
|
+
join(rigidRoot, ".opencode", "agents", "ade.md"),
|
|
104
|
+
"utf-8"
|
|
105
|
+
);
|
|
106
|
+
const defaultsAgent = await readFile(
|
|
107
|
+
join(defaultsRoot, ".opencode", "agents", "ade.md"),
|
|
108
|
+
"utf-8"
|
|
109
|
+
);
|
|
110
|
+
const maxAgent = await readFile(
|
|
111
|
+
join(maxRoot, ".opencode", "agents", "ade.md"),
|
|
112
|
+
"utf-8"
|
|
113
|
+
);
|
|
114
|
+
const rigidFrontmatter = parseFrontmatter(rigidAgent);
|
|
115
|
+
const defaultsFrontmatter = parseFrontmatter(defaultsAgent);
|
|
116
|
+
const maxFrontmatter = parseFrontmatter(maxAgent);
|
|
117
|
+
|
|
118
|
+
await expect(
|
|
119
|
+
readFile(join(rigidRoot, "opencode.json"), "utf-8")
|
|
120
|
+
).rejects.toThrow();
|
|
121
|
+
await expect(
|
|
122
|
+
readFile(join(defaultsRoot, "opencode.json"), "utf-8")
|
|
123
|
+
).rejects.toThrow();
|
|
124
|
+
await expect(
|
|
125
|
+
readFile(join(maxRoot, "opencode.json"), "utf-8")
|
|
126
|
+
).rejects.toThrow();
|
|
127
|
+
|
|
128
|
+
expect(rigidAgent).toContain("permission:");
|
|
129
|
+
expect(rigidAgent).toContain('"*": "ask"');
|
|
130
|
+
expect(rigidAgent).toContain('webfetch: "ask"');
|
|
131
|
+
expect(rigidAgent).toContain('websearch: "ask"');
|
|
132
|
+
expect(rigidAgent).toContain('codesearch: "ask"');
|
|
133
|
+
expect(rigidFrontmatter.permission).toMatchObject({
|
|
134
|
+
"*": "ask",
|
|
135
|
+
webfetch: "ask",
|
|
136
|
+
websearch: "ask",
|
|
137
|
+
codesearch: "ask"
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(defaultsAgent).toContain('edit: "allow"');
|
|
141
|
+
expect(defaultsAgent).toContain('glob: "allow"');
|
|
142
|
+
expect(defaultsAgent).toContain('grep: "allow"');
|
|
143
|
+
expect(defaultsAgent).toContain('list: "allow"');
|
|
144
|
+
expect(defaultsAgent).toContain('lsp: "allow"');
|
|
145
|
+
expect(defaultsAgent).toContain('task: "allow"');
|
|
146
|
+
expect(defaultsAgent).toContain('skill: "deny"');
|
|
147
|
+
expect(defaultsAgent).toContain('todoread: "deny"');
|
|
148
|
+
expect(defaultsAgent).toContain('todowrite: "deny"');
|
|
149
|
+
expect(defaultsAgent).toContain('webfetch: "ask"');
|
|
150
|
+
expect(defaultsAgent).toContain('websearch: "ask"');
|
|
151
|
+
expect(defaultsAgent).toContain('codesearch: "ask"');
|
|
152
|
+
expect(defaultsAgent).toContain('external_directory: "deny"');
|
|
153
|
+
expect(defaultsAgent).toContain('doom_loop: "deny"');
|
|
154
|
+
expect(defaultsAgent).toContain('"grep *": "allow"');
|
|
155
|
+
expect(defaultsAgent).toContain('"cp *": "ask"');
|
|
156
|
+
expect(defaultsAgent).toContain('"rm *": "deny"');
|
|
157
|
+
expect(defaultsFrontmatter.permission).toMatchObject({
|
|
158
|
+
edit: "allow",
|
|
159
|
+
glob: "allow",
|
|
160
|
+
grep: "allow",
|
|
161
|
+
list: "allow",
|
|
162
|
+
lsp: "allow",
|
|
163
|
+
task: "allow",
|
|
164
|
+
skill: "deny",
|
|
165
|
+
todoread: "deny",
|
|
166
|
+
todowrite: "deny",
|
|
167
|
+
webfetch: "ask",
|
|
168
|
+
websearch: "ask",
|
|
169
|
+
codesearch: "ask",
|
|
170
|
+
external_directory: "deny",
|
|
171
|
+
doom_loop: "deny"
|
|
172
|
+
});
|
|
173
|
+
const defaultsPermission = defaultsFrontmatter.permission as {
|
|
174
|
+
bash: Record<string, string>;
|
|
175
|
+
};
|
|
176
|
+
expect(defaultsPermission.bash["grep *"]).toBe("allow");
|
|
177
|
+
expect(defaultsPermission.bash["cp *"]).toBe("ask");
|
|
178
|
+
expect(defaultsPermission.bash["rm *"]).toBe("deny");
|
|
179
|
+
|
|
180
|
+
expect(maxAgent).toContain('"*": "allow"');
|
|
181
|
+
expect(maxAgent).toContain('webfetch: "ask"');
|
|
182
|
+
expect(maxAgent).toContain('websearch: "ask"');
|
|
183
|
+
expect(maxAgent).toContain('codesearch: "ask"');
|
|
184
|
+
expect(maxFrontmatter.permission).toMatchObject({
|
|
185
|
+
"*": "allow",
|
|
186
|
+
webfetch: "ask",
|
|
187
|
+
websearch: "ask",
|
|
188
|
+
codesearch: "ask"
|
|
189
|
+
});
|
|
190
|
+
expect(rigidAgent).not.toContain("tools:");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("keeps MCP servers in project config and writes documented environment fields", async () => {
|
|
194
|
+
const projectRoot = join(dir, "mcp");
|
|
195
|
+
const config = {
|
|
196
|
+
mcp_servers: [
|
|
197
|
+
{
|
|
198
|
+
ref: "workflows",
|
|
199
|
+
command: "npx",
|
|
200
|
+
args: ["@codemcp/workflows-server@latest"],
|
|
201
|
+
env: { FOO: "bar" },
|
|
202
|
+
allowedTools: ["whats_next"]
|
|
203
|
+
}
|
|
204
|
+
],
|
|
205
|
+
instructions: ["Follow project rules."],
|
|
206
|
+
cli_actions: [],
|
|
207
|
+
knowledge_sources: [],
|
|
208
|
+
skills: [],
|
|
209
|
+
git_hooks: [],
|
|
210
|
+
setup_notes: [],
|
|
211
|
+
permission_policy: autonomyPolicy("rigid")
|
|
212
|
+
} as LogicalConfig;
|
|
213
|
+
|
|
214
|
+
await mkdir(projectRoot, { recursive: true });
|
|
215
|
+
await writeFile(
|
|
216
|
+
join(projectRoot, "opencode.json"),
|
|
217
|
+
JSON.stringify(
|
|
218
|
+
{
|
|
219
|
+
$schema: "https://opencode.ai/config.json",
|
|
220
|
+
permission: { read: "allow" }
|
|
221
|
+
},
|
|
222
|
+
null,
|
|
223
|
+
2
|
|
224
|
+
) + "\n",
|
|
225
|
+
"utf-8"
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await opencodeWriter.install(config, projectRoot);
|
|
229
|
+
|
|
230
|
+
const projectJson = JSON.parse(
|
|
231
|
+
await readFile(join(projectRoot, "opencode.json"), "utf-8")
|
|
232
|
+
);
|
|
233
|
+
const agent = await readFile(
|
|
234
|
+
join(projectRoot, ".opencode", "agents", "ade.md"),
|
|
235
|
+
"utf-8"
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(projectJson.permission).toEqual({ read: "allow" });
|
|
239
|
+
expect(projectJson.mcp).toEqual({
|
|
240
|
+
workflows: {
|
|
241
|
+
type: "local",
|
|
242
|
+
command: ["npx", "@codemcp/workflows-server@latest"],
|
|
243
|
+
environment: { FOO: "bar" }
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
expect(agent).toContain("permission:");
|
|
247
|
+
expect(agent).not.toContain("mcp_servers:");
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
function parseFrontmatter(content: string) {
|
|
252
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
253
|
+
if (!match) {
|
|
254
|
+
throw new Error("Expected frontmatter in agent markdown");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return parseYaml(match[1]) as Record<string, unknown>;
|
|
258
|
+
}
|
package/src/writers/opencode.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import type { LogicalConfig } from "@codemcp/ade-core";
|
|
2
|
+
import type { LogicalConfig, PermissionRule } from "@codemcp/ade-core";
|
|
3
3
|
import type { HarnessWriter } from "../types.js";
|
|
4
|
-
import {
|
|
4
|
+
import { writeAgentMd, writeGitHooks, writeMcpServers } from "../util.js";
|
|
5
|
+
import { getHarnessPermissionRules } from "../permission-policy.js";
|
|
5
6
|
|
|
6
7
|
export const opencodeWriter: HarnessWriter = {
|
|
7
8
|
id: "opencode",
|
|
@@ -14,41 +15,53 @@ export const opencodeWriter: HarnessWriter = {
|
|
|
14
15
|
transform: (s) => ({
|
|
15
16
|
type: "local",
|
|
16
17
|
command: [s.command, ...s.args],
|
|
17
|
-
...(Object.keys(s.env).length > 0 ? {
|
|
18
|
+
...(Object.keys(s.env).length > 0 ? { environment: s.env } : {})
|
|
18
19
|
}),
|
|
19
20
|
defaults: { $schema: "https://opencode.ai/config.json" }
|
|
20
21
|
});
|
|
21
22
|
|
|
22
|
-
const
|
|
23
|
-
const extraFm: string[] = [
|
|
24
|
-
"tools:",
|
|
25
|
-
" read: true",
|
|
26
|
-
" edit: approve",
|
|
27
|
-
" bash: approve"
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
if (servers.length > 0) {
|
|
31
|
-
extraFm.push("mcp_servers:");
|
|
32
|
-
for (const s of servers) {
|
|
33
|
-
extraFm.push(` ${s.ref}:`);
|
|
34
|
-
extraFm.push(
|
|
35
|
-
` command: [${[s.command, ...s.args].map((a) => `"${a}"`).join(", ")}]`
|
|
36
|
-
);
|
|
37
|
-
if (Object.keys(s.env).length > 0) {
|
|
38
|
-
extraFm.push(" env:");
|
|
39
|
-
for (const [k, v] of Object.entries(s.env)) {
|
|
40
|
-
extraFm.push(` ${k}: "${v}"`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
23
|
+
const permission = getHarnessPermissionRules(config);
|
|
45
24
|
|
|
46
25
|
await writeAgentMd(config, {
|
|
47
26
|
path: join(projectRoot, ".opencode", "agents", "ade.md"),
|
|
48
|
-
extraFrontmatter:
|
|
27
|
+
extraFrontmatter: permission
|
|
28
|
+
? renderYamlMapping("permission", permission)
|
|
29
|
+
: undefined,
|
|
49
30
|
fallbackBody:
|
|
50
31
|
"ADE — Agentic Development Environment agent with project conventions and tools."
|
|
51
32
|
});
|
|
52
33
|
await writeGitHooks(config.git_hooks, projectRoot);
|
|
53
34
|
}
|
|
54
35
|
};
|
|
36
|
+
|
|
37
|
+
function renderYamlMapping(
|
|
38
|
+
key: string,
|
|
39
|
+
value: Record<string, PermissionRule>,
|
|
40
|
+
indent = 0
|
|
41
|
+
): string[] {
|
|
42
|
+
const prefix = " ".repeat(indent);
|
|
43
|
+
const lines = [`${prefix}${formatYamlKey(key)}:`];
|
|
44
|
+
|
|
45
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
46
|
+
if (
|
|
47
|
+
typeof childValue === "object" &&
|
|
48
|
+
childValue !== null &&
|
|
49
|
+
!Array.isArray(childValue)
|
|
50
|
+
) {
|
|
51
|
+
lines.push(...renderYamlMapping(childKey, childValue, indent + 2));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lines.push(
|
|
56
|
+
`${" ".repeat(indent + 2)}${formatYamlKey(childKey)}: ${JSON.stringify(childValue)}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return lines;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatYamlKey(value: string): string {
|
|
64
|
+
return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(value)
|
|
65
|
+
? value
|
|
66
|
+
: JSON.stringify(value);
|
|
67
|
+
}
|
|
@@ -2,9 +2,57 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import { mkdtemp, rm, readFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
AutonomyProfile,
|
|
7
|
+
LogicalConfig,
|
|
8
|
+
PermissionPolicy
|
|
9
|
+
} from "@codemcp/ade-core";
|
|
6
10
|
import { rooCodeWriter } from "./roo-code.js";
|
|
7
11
|
|
|
12
|
+
function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy {
|
|
13
|
+
switch (profile) {
|
|
14
|
+
case "rigid":
|
|
15
|
+
return {
|
|
16
|
+
profile,
|
|
17
|
+
capabilities: {
|
|
18
|
+
read: "ask",
|
|
19
|
+
edit_write: "ask",
|
|
20
|
+
search_list: "ask",
|
|
21
|
+
bash_safe: "ask",
|
|
22
|
+
bash_unsafe: "ask",
|
|
23
|
+
web: "ask",
|
|
24
|
+
task_agent: "ask"
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
case "sensible-defaults":
|
|
28
|
+
return {
|
|
29
|
+
profile,
|
|
30
|
+
capabilities: {
|
|
31
|
+
read: "allow",
|
|
32
|
+
edit_write: "allow",
|
|
33
|
+
search_list: "allow",
|
|
34
|
+
bash_safe: "allow",
|
|
35
|
+
bash_unsafe: "ask",
|
|
36
|
+
web: "ask",
|
|
37
|
+
task_agent: "allow"
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
case "max-autonomy":
|
|
41
|
+
return {
|
|
42
|
+
profile,
|
|
43
|
+
capabilities: {
|
|
44
|
+
read: "allow",
|
|
45
|
+
edit_write: "allow",
|
|
46
|
+
search_list: "allow",
|
|
47
|
+
bash_safe: "allow",
|
|
48
|
+
bash_unsafe: "allow",
|
|
49
|
+
web: "ask",
|
|
50
|
+
task_agent: "allow"
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
8
56
|
describe("rooCodeWriter", () => {
|
|
9
57
|
let dir: string;
|
|
10
58
|
|
|
@@ -19,6 +67,7 @@ describe("rooCodeWriter", () => {
|
|
|
19
67
|
it("has correct metadata", () => {
|
|
20
68
|
expect(rooCodeWriter.id).toBe("roo-code");
|
|
21
69
|
expect(rooCodeWriter.label).toBe("Roo Code");
|
|
70
|
+
expect(rooCodeWriter.description).toContain(".roomodes");
|
|
22
71
|
});
|
|
23
72
|
|
|
24
73
|
it("writes .roo/mcp.json with MCP servers", async () => {
|
|
@@ -66,4 +115,83 @@ describe("rooCodeWriter", () => {
|
|
|
66
115
|
const content = await readFile(join(dir, ".roorules"), "utf-8");
|
|
67
116
|
expect(content).toContain("Follow TDD.");
|
|
68
117
|
});
|
|
118
|
+
|
|
119
|
+
it("maps autonomy to Roo mode groups conservatively while forwarding MCP approvals separately", async () => {
|
|
120
|
+
const rigidRoot = join(dir, "rigid");
|
|
121
|
+
const defaultsRoot = join(dir, "defaults");
|
|
122
|
+
const maxRoot = join(dir, "max");
|
|
123
|
+
|
|
124
|
+
const baseConfig = {
|
|
125
|
+
mcp_servers: [
|
|
126
|
+
{
|
|
127
|
+
ref: "workflows",
|
|
128
|
+
command: "npx",
|
|
129
|
+
args: ["-y", "@codemcp/workflows"],
|
|
130
|
+
env: {},
|
|
131
|
+
allowedTools: ["whats_next"]
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
instructions: [],
|
|
135
|
+
cli_actions: [],
|
|
136
|
+
knowledge_sources: [],
|
|
137
|
+
skills: [],
|
|
138
|
+
git_hooks: [],
|
|
139
|
+
setup_notes: []
|
|
140
|
+
} satisfies LogicalConfig;
|
|
141
|
+
|
|
142
|
+
await rooCodeWriter.install(
|
|
143
|
+
{
|
|
144
|
+
...baseConfig,
|
|
145
|
+
permission_policy: autonomyPolicy("rigid")
|
|
146
|
+
},
|
|
147
|
+
rigidRoot
|
|
148
|
+
);
|
|
149
|
+
await rooCodeWriter.install(
|
|
150
|
+
{
|
|
151
|
+
...baseConfig,
|
|
152
|
+
permission_policy: autonomyPolicy("sensible-defaults")
|
|
153
|
+
},
|
|
154
|
+
defaultsRoot
|
|
155
|
+
);
|
|
156
|
+
await rooCodeWriter.install(
|
|
157
|
+
{
|
|
158
|
+
...baseConfig,
|
|
159
|
+
permission_policy: autonomyPolicy("max-autonomy")
|
|
160
|
+
},
|
|
161
|
+
maxRoot
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const rigidModes = JSON.parse(
|
|
165
|
+
await readFile(join(rigidRoot, ".roomodes"), "utf-8")
|
|
166
|
+
);
|
|
167
|
+
const defaultsModes = JSON.parse(
|
|
168
|
+
await readFile(join(defaultsRoot, ".roomodes"), "utf-8")
|
|
169
|
+
);
|
|
170
|
+
const maxModes = JSON.parse(
|
|
171
|
+
await readFile(join(maxRoot, ".roomodes"), "utf-8")
|
|
172
|
+
);
|
|
173
|
+
const rigidMcp = JSON.parse(
|
|
174
|
+
await readFile(join(rigidRoot, ".roo", "mcp.json"), "utf-8")
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(rigidModes.customModes.ade.groups).toEqual(["mcp"]);
|
|
178
|
+
expect(defaultsModes.customModes.ade.groups).toEqual([
|
|
179
|
+
"read",
|
|
180
|
+
"edit",
|
|
181
|
+
"mcp"
|
|
182
|
+
]);
|
|
183
|
+
expect(maxModes.customModes.ade.groups).toEqual([
|
|
184
|
+
"read",
|
|
185
|
+
"edit",
|
|
186
|
+
"command",
|
|
187
|
+
"mcp"
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
expect(defaultsModes.customModes.ade.groups).not.toContain("command");
|
|
191
|
+
expect(rigidModes.customModes.ade.groups).not.toContain("web");
|
|
192
|
+
expect(defaultsModes.customModes.ade.groups).not.toContain("web");
|
|
193
|
+
expect(maxModes.customModes.ade.groups).not.toContain("web");
|
|
194
|
+
|
|
195
|
+
expect(rigidMcp.mcpServers.workflows.alwaysAllow).toEqual(["whats_next"]);
|
|
196
|
+
});
|
|
69
197
|
});
|
package/src/writers/roo-code.ts
CHANGED
|
@@ -2,23 +2,70 @@ import { join } from "node:path";
|
|
|
2
2
|
import type { LogicalConfig } from "@codemcp/ade-core";
|
|
3
3
|
import type { HarnessWriter } from "../types.js";
|
|
4
4
|
import {
|
|
5
|
+
readJsonOrEmpty,
|
|
5
6
|
writeMcpServers,
|
|
6
7
|
alwaysAllowEntry,
|
|
7
8
|
writeRulesFile,
|
|
8
|
-
writeGitHooks
|
|
9
|
+
writeGitHooks,
|
|
10
|
+
writeJson
|
|
9
11
|
} from "../util.js";
|
|
12
|
+
import { allowsCapability, hasPermissionPolicy } from "../permission-policy.js";
|
|
10
13
|
|
|
11
14
|
export const rooCodeWriter: HarnessWriter = {
|
|
12
15
|
id: "roo-code",
|
|
13
16
|
label: "Roo Code",
|
|
14
|
-
description: "AI coding agent — .roo/mcp.json + .roorules",
|
|
17
|
+
description: "AI coding agent — .roo/mcp.json + .roomodes + .roorules",
|
|
15
18
|
async install(config: LogicalConfig, projectRoot: string) {
|
|
16
19
|
await writeMcpServers(config.mcp_servers, {
|
|
17
20
|
path: join(projectRoot, ".roo", "mcp.json"),
|
|
18
21
|
transform: alwaysAllowEntry
|
|
19
22
|
});
|
|
20
23
|
|
|
24
|
+
await writeRooModes(config, projectRoot);
|
|
21
25
|
await writeRulesFile(config.instructions, join(projectRoot, ".roorules"));
|
|
22
26
|
await writeGitHooks(config.git_hooks, projectRoot);
|
|
23
27
|
}
|
|
24
28
|
};
|
|
29
|
+
|
|
30
|
+
async function writeRooModes(
|
|
31
|
+
config: LogicalConfig,
|
|
32
|
+
projectRoot: string
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
if (!hasPermissionPolicy(config)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const roomodesPath = join(projectRoot, ".roomodes");
|
|
39
|
+
const existing = await readJsonOrEmpty(roomodesPath);
|
|
40
|
+
const existingCustomModes = asRecord(existing.customModes);
|
|
41
|
+
|
|
42
|
+
await writeJson(roomodesPath, {
|
|
43
|
+
...existing,
|
|
44
|
+
customModes: {
|
|
45
|
+
...existingCustomModes,
|
|
46
|
+
ade: {
|
|
47
|
+
slug: "ade",
|
|
48
|
+
name: "ADE",
|
|
49
|
+
roleDefinition:
|
|
50
|
+
"ADE — Agentic Development Environment mode generated by ADE.",
|
|
51
|
+
groups: getRooModeGroups(config),
|
|
52
|
+
source: "project"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
59
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
60
|
+
? (value as Record<string, unknown>)
|
|
61
|
+
: {};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getRooModeGroups(config: LogicalConfig): string[] {
|
|
65
|
+
return [
|
|
66
|
+
...(allowsCapability(config, "read") ? ["read"] : []),
|
|
67
|
+
...(allowsCapability(config, "edit_write") ? ["edit"] : []),
|
|
68
|
+
...(allowsCapability(config, "bash_unsafe") ? ["command"] : []),
|
|
69
|
+
...(config.mcp_servers.length > 0 ? ["mcp"] : [])
|
|
70
|
+
];
|
|
71
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, rm, readFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type {
|
|
6
|
+
AutonomyProfile,
|
|
7
|
+
LogicalConfig,
|
|
8
|
+
PermissionPolicy
|
|
9
|
+
} from "@codemcp/ade-core";
|
|
10
|
+
import { universalWriter } from "./universal.js";
|
|
11
|
+
|
|
12
|
+
function autonomyPolicy(profile: AutonomyProfile): PermissionPolicy {
|
|
13
|
+
switch (profile) {
|
|
14
|
+
case "rigid":
|
|
15
|
+
return {
|
|
16
|
+
profile,
|
|
17
|
+
capabilities: {
|
|
18
|
+
read: "ask",
|
|
19
|
+
edit_write: "ask",
|
|
20
|
+
search_list: "ask",
|
|
21
|
+
bash_safe: "ask",
|
|
22
|
+
bash_unsafe: "ask",
|
|
23
|
+
web: "ask",
|
|
24
|
+
task_agent: "ask"
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
case "sensible-defaults":
|
|
28
|
+
return {
|
|
29
|
+
profile,
|
|
30
|
+
capabilities: {
|
|
31
|
+
read: "allow",
|
|
32
|
+
edit_write: "allow",
|
|
33
|
+
search_list: "allow",
|
|
34
|
+
bash_safe: "allow",
|
|
35
|
+
bash_unsafe: "ask",
|
|
36
|
+
web: "ask",
|
|
37
|
+
task_agent: "allow"
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
case "max-autonomy":
|
|
41
|
+
return {
|
|
42
|
+
profile,
|
|
43
|
+
capabilities: {
|
|
44
|
+
read: "allow",
|
|
45
|
+
edit_write: "allow",
|
|
46
|
+
search_list: "allow",
|
|
47
|
+
bash_safe: "allow",
|
|
48
|
+
bash_unsafe: "allow",
|
|
49
|
+
web: "ask",
|
|
50
|
+
task_agent: "allow"
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("universalWriter", () => {
|
|
57
|
+
let dir: string;
|
|
58
|
+
|
|
59
|
+
beforeEach(async () => {
|
|
60
|
+
dir = await mkdtemp(join(tmpdir(), "ade-harness-universal-"));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(async () => {
|
|
64
|
+
await rm(dir, { recursive: true, force: true });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("has correct metadata", () => {
|
|
68
|
+
expect(universalWriter.id).toBe("universal");
|
|
69
|
+
expect(universalWriter.label).toBe("Universal (AGENTS.md + .mcp.json)");
|
|
70
|
+
expect(universalWriter.description).toContain("AGENTS.md");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("writes AGENTS.md instructions when provided", async () => {
|
|
74
|
+
const config: LogicalConfig = {
|
|
75
|
+
mcp_servers: [],
|
|
76
|
+
instructions: ["Follow the workflow.", "Keep changes focused."],
|
|
77
|
+
cli_actions: [],
|
|
78
|
+
knowledge_sources: [],
|
|
79
|
+
skills: [],
|
|
80
|
+
git_hooks: [],
|
|
81
|
+
setup_notes: []
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
await universalWriter.install(config, dir);
|
|
85
|
+
|
|
86
|
+
const content = await readFile(join(dir, "AGENTS.md"), "utf-8");
|
|
87
|
+
expect(content).toContain("# AGENTS");
|
|
88
|
+
expect(content).toContain("Follow the workflow.");
|
|
89
|
+
expect(content).toContain("Keep changes focused.");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("documents autonomy as guidance only because Universal has no enforceable permission schema", async () => {
|
|
93
|
+
const config: LogicalConfig = {
|
|
94
|
+
mcp_servers: [
|
|
95
|
+
{
|
|
96
|
+
ref: "workflows",
|
|
97
|
+
command: "npx",
|
|
98
|
+
args: ["-y", "@codemcp/workflows"],
|
|
99
|
+
env: {},
|
|
100
|
+
allowedTools: ["whats_next"]
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
instructions: [],
|
|
104
|
+
cli_actions: [],
|
|
105
|
+
knowledge_sources: [],
|
|
106
|
+
skills: [],
|
|
107
|
+
git_hooks: [],
|
|
108
|
+
setup_notes: [],
|
|
109
|
+
permission_policy: autonomyPolicy("sensible-defaults")
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
await universalWriter.install(config, dir);
|
|
113
|
+
|
|
114
|
+
const agents = await readFile(join(dir, "AGENTS.md"), "utf-8");
|
|
115
|
+
expect(agents).toContain("## Autonomy");
|
|
116
|
+
expect(agents).toContain("documentation-only guidance");
|
|
117
|
+
expect(agents).toContain("no enforceable harness-level permission schema");
|
|
118
|
+
expect(agents).toContain("Profile: `sensible-defaults`");
|
|
119
|
+
expect(agents).toContain("- `read`: allow");
|
|
120
|
+
expect(agents).toContain("- `bash_unsafe`: ask");
|
|
121
|
+
expect(agents).toContain("- `web`: ask");
|
|
122
|
+
expect(agents).toContain(
|
|
123
|
+
"MCP permissions are not re-modeled by autonomy here"
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const mcpRaw = await readFile(join(dir, ".mcp.json"), "utf-8");
|
|
127
|
+
const mcp = JSON.parse(mcpRaw);
|
|
128
|
+
expect(mcp.mcpServers.workflows).toEqual({
|
|
129
|
+
command: "npx",
|
|
130
|
+
args: ["-y", "@codemcp/workflows"]
|
|
131
|
+
});
|
|
132
|
+
expect(mcp.mcpServers.workflows).not.toHaveProperty("allowedTools");
|
|
133
|
+
});
|
|
134
|
+
});
|