@codemcp/ade-harnesses 0.0.2 → 0.1.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 (37) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-format.log +1 -1
  3. package/.turbo/turbo-lint.log +1 -1
  4. package/.turbo/turbo-test.log +15 -12
  5. package/.turbo/turbo-typecheck.log +1 -1
  6. package/dist/permission-policy.d.ts +7 -0
  7. package/dist/permission-policy.js +152 -0
  8. package/dist/writers/claude-code.js +50 -18
  9. package/dist/writers/cline.js +2 -2
  10. package/dist/writers/copilot.js +61 -8
  11. package/dist/writers/cursor.js +48 -2
  12. package/dist/writers/kiro.js +54 -38
  13. package/dist/writers/opencode.js +26 -23
  14. package/dist/writers/roo-code.js +38 -2
  15. package/dist/writers/universal.js +41 -3
  16. package/dist/writers/windsurf.js +43 -1
  17. package/package.json +2 -2
  18. package/src/permission-policy.ts +173 -0
  19. package/src/writers/claude-code.spec.ts +160 -3
  20. package/src/writers/claude-code.ts +63 -18
  21. package/src/writers/cline.spec.ts +146 -3
  22. package/src/writers/cline.ts +2 -2
  23. package/src/writers/copilot.spec.ts +157 -1
  24. package/src/writers/copilot.ts +76 -9
  25. package/src/writers/cursor.spec.ts +104 -1
  26. package/src/writers/cursor.ts +65 -3
  27. package/src/writers/kiro.spec.ts +228 -0
  28. package/src/writers/kiro.ts +77 -40
  29. package/src/writers/opencode.spec.ts +258 -0
  30. package/src/writers/opencode.ts +40 -27
  31. package/src/writers/roo-code.spec.ts +129 -1
  32. package/src/writers/roo-code.ts +49 -2
  33. package/src/writers/universal.spec.ts +134 -0
  34. package/src/writers/universal.ts +57 -4
  35. package/src/writers/windsurf.spec.ts +111 -3
  36. package/src/writers/windsurf.ts +64 -2
  37. package/tsconfig.tsbuildinfo +1 -1
@@ -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 { LogicalConfig } from "@codemcp/ade-core";
5
+ import type {
6
+ AutonomyProfile,
7
+ LogicalConfig,
8
+ PermissionPolicy
9
+ } from "@codemcp/ade-core";
6
10
  import { clineWriter } from "./cline.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("clineWriter", () => {
9
57
  let dir: string;
10
58
 
@@ -21,7 +69,7 @@ describe("clineWriter", () => {
21
69
  expect(clineWriter.label).toBe("Cline");
22
70
  });
23
71
 
24
- it("writes .cline/mcp.json with MCP servers", async () => {
72
+ it("writes cline_mcp_settings.json with MCP servers", async () => {
25
73
  const config: LogicalConfig = {
26
74
  mcp_servers: [
27
75
  {
@@ -41,7 +89,7 @@ describe("clineWriter", () => {
41
89
 
42
90
  await clineWriter.install(config, dir);
43
91
 
44
- const raw = await readFile(join(dir, ".cline", "mcp.json"), "utf-8");
92
+ const raw = await readFile(join(dir, "cline_mcp_settings.json"), "utf-8");
45
93
  const parsed = JSON.parse(raw);
46
94
  expect(parsed.mcpServers["workflows"]).toEqual({
47
95
  command: "npx",
@@ -50,6 +98,36 @@ describe("clineWriter", () => {
50
98
  });
51
99
  });
52
100
 
101
+ it("forwards explicit MCP approvals unchanged from provisioning", async () => {
102
+ const config: LogicalConfig = {
103
+ mcp_servers: [
104
+ {
105
+ ref: "workflows",
106
+ command: "npx",
107
+ args: ["-y", "@codemcp/workflows"],
108
+ env: {},
109
+ allowedTools: ["whats_next", "proceed_to_phase"]
110
+ }
111
+ ],
112
+ instructions: [],
113
+ cli_actions: [],
114
+ knowledge_sources: [],
115
+ skills: [],
116
+ git_hooks: [],
117
+ setup_notes: []
118
+ };
119
+
120
+ await clineWriter.install(config, dir);
121
+
122
+ const raw = await readFile(join(dir, "cline_mcp_settings.json"), "utf-8");
123
+ const parsed = JSON.parse(raw);
124
+ expect(parsed.mcpServers["workflows"]).toEqual({
125
+ command: "npx",
126
+ args: ["-y", "@codemcp/workflows"],
127
+ alwaysAllow: ["whats_next", "proceed_to_phase"]
128
+ });
129
+ });
130
+
53
131
  it("writes .clinerules with instructions", async () => {
54
132
  const config: LogicalConfig = {
55
133
  mcp_servers: [],
@@ -66,4 +144,69 @@ describe("clineWriter", () => {
66
144
  const content = await readFile(join(dir, ".clinerules"), "utf-8");
67
145
  expect(content).toContain("Follow TDD.");
68
146
  });
147
+
148
+ it("does not invent built-in auto-approval settings for autonomy profiles", async () => {
149
+ const rigidRoot = join(dir, "rigid");
150
+ const sensibleRoot = join(dir, "sensible");
151
+ const maxRoot = join(dir, "max");
152
+
153
+ const rigidConfig: LogicalConfig = {
154
+ mcp_servers: [
155
+ {
156
+ ref: "workflows",
157
+ command: "npx",
158
+ args: ["-y", "@codemcp/workflows"],
159
+ env: {}
160
+ }
161
+ ],
162
+ instructions: ["Use approvals for risky actions."],
163
+ cli_actions: [],
164
+ knowledge_sources: [],
165
+ skills: [],
166
+ git_hooks: [],
167
+ setup_notes: [],
168
+ permission_policy: autonomyPolicy("rigid")
169
+ };
170
+
171
+ const sensibleConfig: LogicalConfig = {
172
+ ...rigidConfig,
173
+ permission_policy: autonomyPolicy("sensible-defaults")
174
+ };
175
+
176
+ const maxConfig: LogicalConfig = {
177
+ ...rigidConfig,
178
+ permission_policy: autonomyPolicy("max-autonomy")
179
+ };
180
+
181
+ await clineWriter.install(rigidConfig, rigidRoot);
182
+ await clineWriter.install(sensibleConfig, sensibleRoot);
183
+ await clineWriter.install(maxConfig, maxRoot);
184
+
185
+ const rigidSettings = JSON.parse(
186
+ await readFile(join(rigidRoot, "cline_mcp_settings.json"), "utf-8")
187
+ );
188
+ const sensibleSettings = JSON.parse(
189
+ await readFile(join(sensibleRoot, "cline_mcp_settings.json"), "utf-8")
190
+ );
191
+ const maxSettings = JSON.parse(
192
+ await readFile(join(maxRoot, "cline_mcp_settings.json"), "utf-8")
193
+ );
194
+ const maxRules = await readFile(join(maxRoot, ".clinerules"), "utf-8");
195
+
196
+ expect(rigidSettings).toEqual(sensibleSettings);
197
+ expect(sensibleSettings).toEqual(maxSettings);
198
+ expect(maxSettings).toEqual({
199
+ mcpServers: {
200
+ workflows: {
201
+ command: "npx",
202
+ args: ["-y", "@codemcp/workflows"],
203
+ alwaysAllow: ["*"]
204
+ }
205
+ }
206
+ });
207
+ expect(maxRules).toContain("Use approvals for risky actions.");
208
+ expect(maxRules).not.toContain("browser_action");
209
+ expect(maxRules).not.toContain("execute_command");
210
+ expect(maxRules).not.toContain("web");
211
+ });
69
212
  });
@@ -11,10 +11,10 @@ import {
11
11
  export const clineWriter: HarnessWriter = {
12
12
  id: "cline",
13
13
  label: "Cline",
14
- description: "VS Code AI agent — .cline/mcp.json + .clinerules",
14
+ description: "VS Code AI agent — cline_mcp_settings.json + .clinerules",
15
15
  async install(config: LogicalConfig, projectRoot: string) {
16
16
  await writeMcpServers(config.mcp_servers, {
17
- path: join(projectRoot, ".cline", "mcp.json"),
17
+ path: join(projectRoot, "cline_mcp_settings.json"),
18
18
  transform: alwaysAllowEntry
19
19
  });
20
20
 
@@ -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 { LogicalConfig } from "@codemcp/ade-core";
5
+ import type {
6
+ AutonomyProfile,
7
+ LogicalConfig,
8
+ PermissionPolicy
9
+ } from "@codemcp/ade-core";
6
10
  import { copilotWriter } from "./copilot.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("copilotWriter", () => {
9
57
  let dir: string;
10
58
 
@@ -96,7 +144,115 @@ describe("copilotWriter", () => {
96
144
  expect(content).toContain("name: ade");
97
145
  expect(content).toContain("tools:");
98
146
  expect(content).toContain(" - workflows/*");
147
+ expect(content).toContain("mcp-servers:");
148
+ expect(content).toContain(" workflows:");
149
+ expect(content).toContain(" type: stdio");
150
+ expect(content).toContain(' command: "npx"');
151
+ expect(content).toContain(' args: ["-y","@codemcp/workflows"]');
152
+ expect(content).toContain(' tools: ["*"]');
153
+ expect(content).toContain(" - read");
99
154
  expect(content).toContain(" - edit");
155
+ expect(content).toContain(" - search");
156
+ expect(content).toContain(" - execute");
157
+ expect(content).toContain(" - agent");
158
+ expect(content).toContain(" - web");
159
+ expect(content).not.toContain("runCommands");
160
+ expect(content).not.toContain("runTasks");
161
+ expect(content).not.toContain("fetch");
162
+ expect(content).not.toContain("githubRepo");
100
163
  expect(content).toContain("Follow TDD.");
101
164
  });
165
+
166
+ it("derives the tools allowlist from autonomy while keeping web access approval-gated", async () => {
167
+ const rigidRoot = join(dir, "rigid");
168
+ const sensibleRoot = join(dir, "sensible");
169
+ const maxRoot = join(dir, "max");
170
+
171
+ const rigidConfig: LogicalConfig = {
172
+ mcp_servers: [
173
+ {
174
+ ref: "workflows",
175
+ command: "npx",
176
+ args: ["-y", "@codemcp/workflows"],
177
+ env: {}
178
+ }
179
+ ],
180
+ instructions: [],
181
+ cli_actions: [],
182
+ knowledge_sources: [],
183
+ skills: [],
184
+ git_hooks: [],
185
+ setup_notes: [],
186
+ permission_policy: autonomyPolicy("rigid")
187
+ };
188
+
189
+ const sensibleConfig: LogicalConfig = {
190
+ ...rigidConfig,
191
+ mcp_servers: [
192
+ {
193
+ ref: "workflows",
194
+ command: "npx",
195
+ args: ["-y", "@codemcp/workflows"],
196
+ env: {},
197
+ allowedTools: ["whats_next", "proceed_to_phase"]
198
+ }
199
+ ],
200
+ permission_policy: autonomyPolicy("sensible-defaults")
201
+ };
202
+
203
+ const maxConfig: LogicalConfig = {
204
+ ...rigidConfig,
205
+ permission_policy: autonomyPolicy("max-autonomy")
206
+ };
207
+
208
+ await copilotWriter.install(rigidConfig, rigidRoot);
209
+ await copilotWriter.install(sensibleConfig, sensibleRoot);
210
+ await copilotWriter.install(maxConfig, maxRoot);
211
+
212
+ const rigidAgent = await readFile(
213
+ join(rigidRoot, ".github", "agents", "ade.agent.md"),
214
+ "utf-8"
215
+ );
216
+ const sensibleAgent = await readFile(
217
+ join(sensibleRoot, ".github", "agents", "ade.agent.md"),
218
+ "utf-8"
219
+ );
220
+ const maxAgent = await readFile(
221
+ join(maxRoot, ".github", "agents", "ade.agent.md"),
222
+ "utf-8"
223
+ );
224
+
225
+ expect(rigidAgent).not.toContain(" - server/workflows/*");
226
+ expect(rigidAgent).toContain(" - workflows/*");
227
+ expect(rigidAgent).not.toContain(" - read");
228
+ expect(rigidAgent).not.toContain(" - edit");
229
+ expect(rigidAgent).not.toContain(" - search");
230
+ expect(rigidAgent).not.toContain(" - execute");
231
+ expect(rigidAgent).not.toContain(" - agent");
232
+ expect(rigidAgent).not.toContain(" - web");
233
+
234
+ expect(sensibleAgent).toContain(" - read");
235
+ expect(sensibleAgent).toContain(" - edit");
236
+ expect(sensibleAgent).toContain(" - search");
237
+ expect(sensibleAgent).toContain(" - agent");
238
+ expect(sensibleAgent).not.toContain(" - execute");
239
+ expect(sensibleAgent).not.toContain(" - todo");
240
+ expect(sensibleAgent).not.toContain(" - web");
241
+ expect(sensibleAgent).toContain(" - workflows/whats_next");
242
+ expect(sensibleAgent).toContain(" - workflows/proceed_to_phase");
243
+ expect(sensibleAgent).not.toContain(" - workflows/*");
244
+ expect(sensibleAgent).toContain(
245
+ ' tools: ["whats_next","proceed_to_phase"]'
246
+ );
247
+
248
+ expect(maxAgent).toContain(" - read");
249
+ expect(maxAgent).toContain(" - edit");
250
+ expect(maxAgent).toContain(" - search");
251
+ expect(maxAgent).toContain(" - execute");
252
+ expect(maxAgent).toContain(" - agent");
253
+ expect(maxAgent).toContain(" - todo");
254
+ expect(maxAgent).not.toContain(" - web");
255
+ expect(maxAgent).toContain(" - workflows/*");
256
+ expect(maxAgent).toContain("mcp-servers:");
257
+ });
102
258
  });
@@ -1,5 +1,5 @@
1
1
  import { join } from "node:path";
2
- import type { LogicalConfig } from "@codemcp/ade-core";
2
+ import type { LogicalConfig, McpServerEntry } from "@codemcp/ade-core";
3
3
  import type { HarnessWriter } from "../types.js";
4
4
  import {
5
5
  writeMcpServers,
@@ -7,6 +7,11 @@ import {
7
7
  writeAgentMd,
8
8
  writeGitHooks
9
9
  } from "../util.js";
10
+ import {
11
+ allowsCapability,
12
+ hasPermissionPolicy,
13
+ keepsWebOnAsk
14
+ } from "../permission-policy.js";
10
15
 
11
16
  export const copilotWriter: HarnessWriter = {
12
17
  id: "copilot",
@@ -20,19 +25,81 @@ export const copilotWriter: HarnessWriter = {
20
25
  });
21
26
 
22
27
  const tools = [
23
- "edit",
24
- "search",
25
- "runCommands",
26
- "runTasks",
27
- "fetch",
28
- "githubRepo",
29
- ...config.mcp_servers.map((s) => `${s.ref}/*`)
28
+ ...getBuiltInTools(config),
29
+ ...getForwardedMcpTools(config.mcp_servers)
30
30
  ];
31
31
 
32
32
  await writeAgentMd(config, {
33
33
  path: join(projectRoot, ".github", "agents", "ade.agent.md"),
34
- extraFrontmatter: ["tools:", ...tools.map((t) => ` - ${t}`)]
34
+ extraFrontmatter: [
35
+ "tools:",
36
+ ...tools.map((t) => ` - ${t}`),
37
+ ...renderCopilotAgentMcpServers(config.mcp_servers)
38
+ ]
35
39
  });
36
40
  await writeGitHooks(config.git_hooks, projectRoot);
37
41
  }
38
42
  };
43
+
44
+ function getBuiltInTools(config: LogicalConfig): string[] {
45
+ if (!hasPermissionPolicy(config)) {
46
+ return ["read", "edit", "search", "execute", "agent", "web"];
47
+ }
48
+
49
+ return [
50
+ ...(allowsCapability(config, "read") ? ["read"] : []),
51
+ ...(allowsCapability(config, "edit_write") ? ["edit"] : []),
52
+ ...(allowsCapability(config, "search_list") ? ["search"] : []),
53
+ ...(allowsCapability(config, "bash_unsafe") ? ["execute"] : []),
54
+ ...(allowsCapability(config, "task_agent") ? ["agent"] : []),
55
+ ...(allowsCapability(config, "task_agent") &&
56
+ allowsCapability(config, "bash_unsafe")
57
+ ? ["todo"]
58
+ : []),
59
+ ...(!keepsWebOnAsk(config) && allowsCapability(config, "web")
60
+ ? ["web"]
61
+ : [])
62
+ ];
63
+ }
64
+
65
+ function getForwardedMcpTools(servers: McpServerEntry[]): string[] {
66
+ return servers.flatMap((server) => {
67
+ const allowedTools = server.allowedTools ?? ["*"];
68
+ if (allowedTools.includes("*")) {
69
+ return [`${server.ref}/*`];
70
+ }
71
+
72
+ return allowedTools.map((tool) => `${server.ref}/${tool}`);
73
+ });
74
+ }
75
+
76
+ function renderCopilotAgentMcpServers(servers: McpServerEntry[]): string[] {
77
+ if (servers.length === 0) {
78
+ return [];
79
+ }
80
+
81
+ const lines = ["mcp-servers:"];
82
+
83
+ for (const server of servers) {
84
+ lines.push(` ${formatYamlKey(server.ref)}:`);
85
+ lines.push(" type: stdio");
86
+ lines.push(` command: ${JSON.stringify(server.command)}`);
87
+ lines.push(` args: ${JSON.stringify(server.args)}`);
88
+ lines.push(` tools: ${JSON.stringify(server.allowedTools ?? ["*"])}`);
89
+
90
+ if (Object.keys(server.env).length > 0) {
91
+ lines.push(" env:");
92
+ for (const [key, value] of Object.entries(server.env)) {
93
+ lines.push(` ${formatYamlKey(key)}: ${JSON.stringify(value)}`);
94
+ }
95
+ }
96
+ }
97
+
98
+ return lines;
99
+ }
100
+
101
+ function formatYamlKey(value: string): string {
102
+ return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(value)
103
+ ? value
104
+ : JSON.stringify(value);
105
+ }
@@ -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 { LogicalConfig } from "@codemcp/ade-core";
5
+ import type {
6
+ AutonomyProfile,
7
+ LogicalConfig,
8
+ PermissionPolicy
9
+ } from "@codemcp/ade-core";
6
10
  import { cursorWriter } from "./cursor.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("cursorWriter", () => {
9
57
  let dir: string;
10
58
 
@@ -71,6 +119,61 @@ describe("cursorWriter", () => {
71
119
  expect(content).toContain("Use conventional commits.");
72
120
  });
73
121
 
122
+ it("documents autonomy limits in Cursor rules without inventing built-in permission config", async () => {
123
+ const config: LogicalConfig = {
124
+ mcp_servers: [
125
+ {
126
+ ref: "workflows",
127
+ command: "npx",
128
+ args: ["-y", "@codemcp/workflows"],
129
+ env: {},
130
+ allowedTools: ["whats_next"]
131
+ }
132
+ ],
133
+ instructions: [],
134
+ cli_actions: [],
135
+ knowledge_sources: [],
136
+ skills: [],
137
+ git_hooks: [],
138
+ setup_notes: [],
139
+ permission_policy: autonomyPolicy("sensible-defaults")
140
+ };
141
+
142
+ await cursorWriter.install(config, dir);
143
+
144
+ const content = await readFile(
145
+ join(dir, ".cursor", "rules", "ade.mdc"),
146
+ "utf-8"
147
+ );
148
+ expect(content).toContain(
149
+ "Cursor autonomy note (documented, not enforced): sensible-defaults."
150
+ );
151
+ expect(content).toContain(
152
+ "Cursor has no verified committed project-local built-in ask/allow/deny config surface"
153
+ );
154
+ expect(content).toContain(
155
+ "Prefer handling these built-in capabilities without extra approval when Cursor permits it: read project files, edit and write project files, search and list project contents, run safe local shell commands, delegate or decompose work into agent tasks."
156
+ );
157
+ expect(content).toContain(
158
+ "Request approval before these capabilities: run high-impact shell commands, use web or network access."
159
+ );
160
+ expect(content).toContain(
161
+ "Web and network access must remain approval-gated."
162
+ );
163
+ expect(content).toContain(
164
+ "MCP server registration stays in .cursor/mcp.json; MCP tool approvals remain owned by provisioning"
165
+ );
166
+
167
+ const raw = await readFile(join(dir, ".cursor", "mcp.json"), "utf-8");
168
+ const parsed = JSON.parse(raw);
169
+ expect(parsed).not.toHaveProperty("permissions");
170
+ expect(parsed.mcpServers["workflows"]).toEqual({
171
+ command: "npx",
172
+ args: ["-y", "@codemcp/workflows"]
173
+ });
174
+ expect(parsed.mcpServers["workflows"]).not.toHaveProperty("allowedTools");
175
+ });
176
+
74
177
  it("includes agentskills server from mcp_servers", async () => {
75
178
  const config: LogicalConfig = {
76
179
  mcp_servers: [
@@ -1,8 +1,33 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import type { LogicalConfig } from "@codemcp/ade-core";
3
+ import type { AutonomyCapability, LogicalConfig } from "@codemcp/ade-core";
4
4
  import type { HarnessWriter } from "../types.js";
5
5
  import { writeMcpServers, writeGitHooks } from "../util.js";
6
+ import {
7
+ getAutonomyProfile,
8
+ getCapabilityDecision,
9
+ hasPermissionPolicy
10
+ } from "../permission-policy.js";
11
+
12
+ const CURSOR_CAPABILITY_ORDER: AutonomyCapability[] = [
13
+ "read",
14
+ "edit_write",
15
+ "search_list",
16
+ "bash_safe",
17
+ "bash_unsafe",
18
+ "web",
19
+ "task_agent"
20
+ ];
21
+
22
+ const CURSOR_CAPABILITY_LABELS: Record<AutonomyCapability, string> = {
23
+ read: "read project files",
24
+ edit_write: "edit and write project files",
25
+ search_list: "search and list project contents",
26
+ bash_safe: "run safe local shell commands",
27
+ bash_unsafe: "run high-impact shell commands",
28
+ web: "use web or network access",
29
+ task_agent: "delegate or decompose work into agent tasks"
30
+ };
6
31
 
7
32
  export const cursorWriter: HarnessWriter = {
8
33
  id: "cursor",
@@ -13,7 +38,9 @@ export const cursorWriter: HarnessWriter = {
13
38
  path: join(projectRoot, ".cursor", "mcp.json")
14
39
  });
15
40
 
16
- if (config.instructions.length > 0) {
41
+ const rulesBody = getCursorRulesBody(config);
42
+
43
+ if (rulesBody.length > 0) {
17
44
  const rulesDir = join(projectRoot, ".cursor", "rules");
18
45
  await mkdir(rulesDir, { recursive: true });
19
46
 
@@ -23,7 +50,7 @@ export const cursorWriter: HarnessWriter = {
23
50
  "globs: *",
24
51
  "---",
25
52
  "",
26
- ...config.instructions.flatMap((i) => [i, ""])
53
+ ...rulesBody.flatMap((line) => [line, ""])
27
54
  ].join("\n");
28
55
 
29
56
  await writeFile(join(rulesDir, "ade.mdc"), content, "utf-8");
@@ -31,3 +58,38 @@ export const cursorWriter: HarnessWriter = {
31
58
  await writeGitHooks(config.git_hooks, projectRoot);
32
59
  }
33
60
  };
61
+
62
+ function getCursorRulesBody(config: LogicalConfig): string[] {
63
+ return [...config.instructions, ...getCursorAutonomyNotes(config)];
64
+ }
65
+
66
+ function getCursorAutonomyNotes(config: LogicalConfig): string[] {
67
+ if (!hasPermissionPolicy(config)) {
68
+ return [];
69
+ }
70
+
71
+ const allowedCapabilities = CURSOR_CAPABILITY_ORDER.filter(
72
+ (capability) => getCapabilityDecision(config, capability) === "allow"
73
+ ).map((capability) => CURSOR_CAPABILITY_LABELS[capability]);
74
+
75
+ const approvalGatedCapabilities = CURSOR_CAPABILITY_ORDER.filter(
76
+ (capability) => getCapabilityDecision(config, capability) === "ask"
77
+ ).map((capability) => CURSOR_CAPABILITY_LABELS[capability]);
78
+
79
+ return [
80
+ `Cursor autonomy note (documented, not enforced): ${getAutonomyProfile(config) ?? "custom"}.`,
81
+ "Cursor has no verified committed project-local built-in ask/allow/deny config surface, so ADE documents autonomy intent here instead of writing unsupported permission config.",
82
+ ...(allowedCapabilities.length > 0
83
+ ? [
84
+ `Prefer handling these built-in capabilities without extra approval when Cursor permits it: ${allowedCapabilities.join(", ")}.`
85
+ ]
86
+ : []),
87
+ ...(approvalGatedCapabilities.length > 0
88
+ ? [
89
+ `Request approval before these capabilities: ${approvalGatedCapabilities.join(", ")}.`
90
+ ]
91
+ : []),
92
+ "Web and network access must remain approval-gated.",
93
+ "MCP server registration stays in .cursor/mcp.json; MCP tool approvals remain owned by provisioning and are not enforced or re-modeled in this rules file."
94
+ ];
95
+ }