@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.
Files changed (43) 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/index.d.ts +1 -0
  7. package/dist/index.js +1 -0
  8. package/dist/permission-policy.d.ts +7 -0
  9. package/dist/permission-policy.js +152 -0
  10. package/dist/util.d.ts +1 -1
  11. package/dist/util.js +16 -2
  12. package/dist/writers/claude-code.js +51 -20
  13. package/dist/writers/cline.js +2 -2
  14. package/dist/writers/copilot.js +61 -8
  15. package/dist/writers/cursor.js +48 -2
  16. package/dist/writers/kiro.js +54 -38
  17. package/dist/writers/opencode.js +26 -23
  18. package/dist/writers/roo-code.js +38 -2
  19. package/dist/writers/universal.js +41 -3
  20. package/dist/writers/windsurf.js +43 -1
  21. package/package.json +2 -2
  22. package/src/index.ts +1 -0
  23. package/src/permission-policy.ts +173 -0
  24. package/src/util.ts +20 -7
  25. package/src/writers/claude-code.spec.ts +163 -5
  26. package/src/writers/claude-code.ts +63 -20
  27. package/src/writers/cline.spec.ts +146 -3
  28. package/src/writers/cline.ts +2 -2
  29. package/src/writers/copilot.spec.ts +157 -1
  30. package/src/writers/copilot.ts +76 -9
  31. package/src/writers/cursor.spec.ts +104 -1
  32. package/src/writers/cursor.ts +65 -3
  33. package/src/writers/kiro.spec.ts +228 -0
  34. package/src/writers/kiro.ts +77 -40
  35. package/src/writers/opencode.spec.ts +258 -0
  36. package/src/writers/opencode.ts +40 -27
  37. package/src/writers/roo-code.spec.ts +129 -1
  38. package/src/writers/roo-code.ts +49 -2
  39. package/src/writers/universal.spec.ts +134 -0
  40. package/src/writers/universal.ts +57 -4
  41. package/src/writers/windsurf.spec.ts +111 -3
  42. package/src/writers/windsurf.ts +64 -2
  43. package/tsconfig.tsbuildinfo +1 -1
@@ -1,5 +1,6 @@
1
1
  import { join } from "node:path";
2
- import { writeMcpServers, writeAgentMd, writeGitHooks } from "../util.js";
2
+ import { writeAgentMd, writeGitHooks, writeMcpServers } from "../util.js";
3
+ import { getHarnessPermissionRules } from "../permission-policy.js";
3
4
  export const opencodeWriter = {
4
5
  id: "opencode",
5
6
  label: "OpenCode",
@@ -11,35 +12,37 @@ export const opencodeWriter = {
11
12
  transform: (s) => ({
12
13
  type: "local",
13
14
  command: [s.command, ...s.args],
14
- ...(Object.keys(s.env).length > 0 ? { env: s.env } : {})
15
+ ...(Object.keys(s.env).length > 0 ? { environment: s.env } : {})
15
16
  }),
16
17
  defaults: { $schema: "https://opencode.ai/config.json" }
17
18
  });
18
- const servers = config.mcp_servers;
19
- const extraFm = [
20
- "tools:",
21
- " read: true",
22
- " edit: approve",
23
- " bash: approve"
24
- ];
25
- if (servers.length > 0) {
26
- extraFm.push("mcp_servers:");
27
- for (const s of servers) {
28
- extraFm.push(` ${s.ref}:`);
29
- extraFm.push(` command: [${[s.command, ...s.args].map((a) => `"${a}"`).join(", ")}]`);
30
- if (Object.keys(s.env).length > 0) {
31
- extraFm.push(" env:");
32
- for (const [k, v] of Object.entries(s.env)) {
33
- extraFm.push(` ${k}: "${v}"`);
34
- }
35
- }
36
- }
37
- }
19
+ const permission = getHarnessPermissionRules(config);
38
20
  await writeAgentMd(config, {
39
21
  path: join(projectRoot, ".opencode", "agents", "ade.md"),
40
- extraFrontmatter: extraFm,
22
+ extraFrontmatter: permission
23
+ ? renderYamlMapping("permission", permission)
24
+ : undefined,
41
25
  fallbackBody: "ADE — Agentic Development Environment agent with project conventions and tools."
42
26
  });
43
27
  await writeGitHooks(config.git_hooks, projectRoot);
44
28
  }
45
29
  };
30
+ function renderYamlMapping(key, value, indent = 0) {
31
+ const prefix = " ".repeat(indent);
32
+ const lines = [`${prefix}${formatYamlKey(key)}:`];
33
+ for (const [childKey, childValue] of Object.entries(value)) {
34
+ if (typeof childValue === "object" &&
35
+ childValue !== null &&
36
+ !Array.isArray(childValue)) {
37
+ lines.push(...renderYamlMapping(childKey, childValue, indent + 2));
38
+ continue;
39
+ }
40
+ lines.push(`${" ".repeat(indent + 2)}${formatYamlKey(childKey)}: ${JSON.stringify(childValue)}`);
41
+ }
42
+ return lines;
43
+ }
44
+ function formatYamlKey(value) {
45
+ return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(value)
46
+ ? value
47
+ : JSON.stringify(value);
48
+ }
@@ -1,15 +1,51 @@
1
1
  import { join } from "node:path";
2
- import { writeMcpServers, alwaysAllowEntry, writeRulesFile, writeGitHooks } from "../util.js";
2
+ import { readJsonOrEmpty, writeMcpServers, alwaysAllowEntry, writeRulesFile, writeGitHooks, writeJson } from "../util.js";
3
+ import { allowsCapability, hasPermissionPolicy } from "../permission-policy.js";
3
4
  export const rooCodeWriter = {
4
5
  id: "roo-code",
5
6
  label: "Roo Code",
6
- description: "AI coding agent — .roo/mcp.json + .roorules",
7
+ description: "AI coding agent — .roo/mcp.json + .roomodes + .roorules",
7
8
  async install(config, projectRoot) {
8
9
  await writeMcpServers(config.mcp_servers, {
9
10
  path: join(projectRoot, ".roo", "mcp.json"),
10
11
  transform: alwaysAllowEntry
11
12
  });
13
+ await writeRooModes(config, projectRoot);
12
14
  await writeRulesFile(config.instructions, join(projectRoot, ".roorules"));
13
15
  await writeGitHooks(config.git_hooks, projectRoot);
14
16
  }
15
17
  };
18
+ async function writeRooModes(config, projectRoot) {
19
+ if (!hasPermissionPolicy(config)) {
20
+ return;
21
+ }
22
+ const roomodesPath = join(projectRoot, ".roomodes");
23
+ const existing = await readJsonOrEmpty(roomodesPath);
24
+ const existingCustomModes = asRecord(existing.customModes);
25
+ await writeJson(roomodesPath, {
26
+ ...existing,
27
+ customModes: {
28
+ ...existingCustomModes,
29
+ ade: {
30
+ slug: "ade",
31
+ name: "ADE",
32
+ roleDefinition: "ADE — Agentic Development Environment mode generated by ADE.",
33
+ groups: getRooModeGroups(config),
34
+ source: "project"
35
+ }
36
+ }
37
+ });
38
+ }
39
+ function asRecord(value) {
40
+ return value !== null && typeof value === "object" && !Array.isArray(value)
41
+ ? value
42
+ : {};
43
+ }
44
+ function getRooModeGroups(config) {
45
+ return [
46
+ ...(allowsCapability(config, "read") ? ["read"] : []),
47
+ ...(allowsCapability(config, "edit_write") ? ["edit"] : []),
48
+ ...(allowsCapability(config, "bash_unsafe") ? ["command"] : []),
49
+ ...(config.mcp_servers.length > 0 ? ["mcp"] : [])
50
+ ];
51
+ }
@@ -1,16 +1,54 @@
1
1
  import { join } from "node:path";
2
2
  import { writeFile } from "node:fs/promises";
3
3
  import { writeMcpServers, writeGitHooks } from "../util.js";
4
+ const CAPABILITY_ORDER = [
5
+ "read",
6
+ "edit_write",
7
+ "search_list",
8
+ "bash_safe",
9
+ "bash_unsafe",
10
+ "web",
11
+ "task_agent"
12
+ ];
13
+ function formatCapabilityGuidance(capability, decision) {
14
+ return `- \`${capability}\`: ${decision}`;
15
+ }
16
+ function renderAutonomyGuidance(config) {
17
+ const policy = config.permission_policy;
18
+ if (!policy) {
19
+ return undefined;
20
+ }
21
+ const capabilityLines = CAPABILITY_ORDER.map((capability) => formatCapabilityGuidance(capability, policy.capabilities[capability]));
22
+ return [
23
+ "## Autonomy",
24
+ "",
25
+ "Universal harness limitation: `AGENTS.md` + `.mcp.json` provide documentation and server registration only; there is no enforceable harness-level permission schema here.",
26
+ "",
27
+ "Treat this autonomy profile as documentation-only guidance for built-in/basic operations.",
28
+ "",
29
+ `Profile: \`${policy.profile}\``,
30
+ "",
31
+ "Built-in/basic capability guidance:",
32
+ ...capabilityLines,
33
+ "",
34
+ "MCP permissions are not re-modeled by autonomy here; any MCP approvals must come from provisioning-aware consuming harnesses rather than the Universal writer."
35
+ ].join("\n");
36
+ }
4
37
  export const universalWriter = {
5
38
  id: "universal",
6
39
  label: "Universal (AGENTS.md + .mcp.json)",
7
- description: "Cross-tool standard — AGENTS.md + .mcp.json (works with any agent)",
40
+ description: "Cross-tool standard — AGENTS.md + .mcp.json (portable instructions and MCP registration, not enforceable permissions)",
8
41
  async install(config, projectRoot) {
9
- if (config.instructions.length > 0) {
42
+ const autonomyGuidance = renderAutonomyGuidance(config);
43
+ const instructionSections = [...config.instructions];
44
+ if (autonomyGuidance) {
45
+ instructionSections.push(autonomyGuidance);
46
+ }
47
+ if (instructionSections.length > 0) {
10
48
  const lines = [
11
49
  "# AGENTS",
12
50
  "",
13
- ...config.instructions.flatMap((i) => [i, ""])
51
+ ...instructionSections.flatMap((instruction) => [instruction, ""])
14
52
  ];
15
53
  await writeFile(join(projectRoot, "AGENTS.md"), lines.join("\n"), "utf-8");
16
54
  }
@@ -1,5 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { writeMcpServers, alwaysAllowEntry, writeRulesFile, writeGitHooks } from "../util.js";
3
+ import { hasPermissionPolicy } from "../permission-policy.js";
3
4
  export const windsurfWriter = {
4
5
  id: "windsurf",
5
6
  label: "Windsurf",
@@ -9,7 +10,48 @@ export const windsurfWriter = {
9
10
  path: join(projectRoot, ".windsurf", "mcp.json"),
10
11
  transform: alwaysAllowEntry
11
12
  });
12
- await writeRulesFile(config.instructions, join(projectRoot, ".windsurfrules"));
13
+ await writeRulesFile(getWindsurfRules(config), join(projectRoot, ".windsurfrules"));
13
14
  await writeGitHooks(config.git_hooks, projectRoot);
14
15
  }
15
16
  };
17
+ function getWindsurfRules(config) {
18
+ if (!hasPermissionPolicy(config)) {
19
+ return config.instructions;
20
+ }
21
+ const { capabilities } = config.permission_policy;
22
+ const allow = listCapabilities(capabilities, "allow");
23
+ const ask = listCapabilities(capabilities, "ask");
24
+ const deny = listCapabilities(capabilities, "deny");
25
+ const autonomyGuidance = [
26
+ "Windsurf limitation: ADE could not verify a stable committed project-local permission schema for Windsurf built-in tools, so this autonomy policy is advisory only and should be applied conservatively.",
27
+ formatGuidance(allow, ask, deny)
28
+ ];
29
+ return [...autonomyGuidance, ...config.instructions];
30
+ }
31
+ function listCapabilities(capabilities, decision) {
32
+ return Object.entries(capabilities)
33
+ .filter(([, value]) => value === decision)
34
+ .map(([capability]) => CAPABILITY_LABELS[capability]);
35
+ }
36
+ function formatGuidance(allow, ask, deny) {
37
+ const lines = ["Autonomy guidance for Windsurf built-in capabilities:"];
38
+ if (allow.length > 0) {
39
+ lines.push(`- May proceed without extra approval: ${allow.join(", ")}.`);
40
+ }
41
+ if (ask.length > 0) {
42
+ lines.push(`- Ask before: ${ask.join(", ")}.`);
43
+ }
44
+ if (deny.length > 0) {
45
+ lines.push(`- Do not use unless the user explicitly overrides: ${deny.join(", ")}.`);
46
+ }
47
+ return lines.join("\n");
48
+ }
49
+ const CAPABILITY_LABELS = {
50
+ read: "read files",
51
+ edit_write: "edit and write files",
52
+ search_list: "search and list files",
53
+ bash_safe: "safe local shell commands",
54
+ bash_unsafe: "unsafe local shell commands",
55
+ web: "web and network access",
56
+ task_agent: "task or agent delegation"
57
+ };
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@codemcp/skills": "^2.3.0",
11
- "@codemcp/ade-core": "0.0.2"
11
+ "@codemcp/ade-core": "0.1.1"
12
12
  },
13
13
  "devDependencies": {
14
14
  "@typescript-eslint/eslint-plugin": "^8.21.0",
@@ -19,7 +19,7 @@
19
19
  "rimraf": "^6.0.1",
20
20
  "typescript": "^5.7.3"
21
21
  },
22
- "version": "0.0.2",
22
+ "version": "0.1.1",
23
23
  "scripts": {
24
24
  "build": "tsc -p tsconfig.build.json",
25
25
  "clean:build": "rimraf ./dist",
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type { HarnessWriter } from "./types.js";
2
2
  export { installSkills } from "./skills-installer.js";
3
+ export { writeInlineSkills } from "./util.js";
3
4
 
4
5
  export { universalWriter } from "./writers/universal.js";
5
6
  export { claudeCodeWriter } from "./writers/claude-code.js";
@@ -0,0 +1,173 @@
1
+ import type {
2
+ AutonomyCapability,
3
+ LogicalConfig,
4
+ PermissionDecision,
5
+ PermissionRule
6
+ } from "@codemcp/ade-core";
7
+
8
+ const SENSIBLE_DEFAULTS_RULES: Record<string, PermissionRule> = {
9
+ read: {
10
+ "*": "allow",
11
+ "*.env": "deny",
12
+ "*.env.*": "deny",
13
+ "*.env.example": "allow"
14
+ },
15
+ edit: "allow",
16
+ glob: "allow",
17
+ grep: "allow",
18
+ list: "allow",
19
+ lsp: "allow",
20
+ task: "allow",
21
+ todoread: "deny",
22
+ todowrite: "deny",
23
+ skill: "deny",
24
+ webfetch: "ask",
25
+ websearch: "ask",
26
+ codesearch: "ask",
27
+ bash: {
28
+ "*": "deny",
29
+ "grep *": "allow",
30
+ "rg *": "allow",
31
+ "find *": "allow",
32
+ "fd *": "allow",
33
+ ls: "allow",
34
+ "ls *": "allow",
35
+ "cat *": "allow",
36
+ "head *": "allow",
37
+ "tail *": "allow",
38
+ "wc *": "allow",
39
+ "sort *": "allow",
40
+ "uniq *": "allow",
41
+ "diff *": "allow",
42
+ "echo *": "allow",
43
+ "printf *": "allow",
44
+ pwd: "allow",
45
+ "which *": "allow",
46
+ "type *": "allow",
47
+ whoami: "allow",
48
+ date: "allow",
49
+ "date *": "allow",
50
+ env: "allow",
51
+ "tree *": "allow",
52
+ "file *": "allow",
53
+ "stat *": "allow",
54
+ "readlink *": "allow",
55
+ "realpath *": "allow",
56
+ "dirname *": "allow",
57
+ "basename *": "allow",
58
+ "sed *": "allow",
59
+ "awk *": "allow",
60
+ "cut *": "allow",
61
+ "tr *": "allow",
62
+ "tee *": "allow",
63
+ "xargs *": "allow",
64
+ "jq *": "allow",
65
+ "yq *": "allow",
66
+ "mkdir *": "allow",
67
+ "touch *": "allow",
68
+ "cp *": "ask",
69
+ "mv *": "ask",
70
+ "ln *": "ask",
71
+ "npm *": "ask",
72
+ "node *": "ask",
73
+ "pip *": "ask",
74
+ "python *": "ask",
75
+ "python3 *": "ask",
76
+ "rm *": "deny",
77
+ "rmdir *": "deny",
78
+ "curl *": "deny",
79
+ "wget *": "deny",
80
+ "chmod *": "deny",
81
+ "chown *": "deny",
82
+ "sudo *": "deny",
83
+ "su *": "deny",
84
+ "sh *": "deny",
85
+ "bash *": "deny",
86
+ "zsh *": "deny",
87
+ "eval *": "deny",
88
+ "exec *": "deny",
89
+ "source *": "deny",
90
+ ". *": "deny",
91
+ "nohup *": "deny",
92
+ "dd *": "deny",
93
+ "mkfs *": "deny",
94
+ "mount *": "deny",
95
+ "umount *": "deny",
96
+ "kill *": "deny",
97
+ "killall *": "deny",
98
+ "pkill *": "deny",
99
+ "nc *": "deny",
100
+ "ncat *": "deny",
101
+ "ssh *": "deny",
102
+ "scp *": "deny",
103
+ "rsync *": "deny",
104
+ "docker *": "deny",
105
+ "kubectl *": "deny",
106
+ "systemctl *": "deny",
107
+ "service *": "deny",
108
+ "crontab *": "deny",
109
+ reboot: "deny",
110
+ "shutdown *": "deny",
111
+ "passwd *": "deny",
112
+ "useradd *": "deny",
113
+ "userdel *": "deny",
114
+ "iptables *": "deny"
115
+ },
116
+ external_directory: "deny",
117
+ doom_loop: "deny"
118
+ };
119
+
120
+ export function getAutonomyProfile(config: LogicalConfig) {
121
+ return config.permission_policy?.profile;
122
+ }
123
+
124
+ export function hasPermissionPolicy(config: LogicalConfig): boolean {
125
+ return config.permission_policy !== undefined;
126
+ }
127
+
128
+ export function getCapabilityDecision(
129
+ config: LogicalConfig,
130
+ capability: AutonomyCapability
131
+ ): PermissionDecision | undefined {
132
+ return config.permission_policy?.capabilities?.[capability];
133
+ }
134
+
135
+ export function allowsCapability(
136
+ config: LogicalConfig,
137
+ capability: AutonomyCapability
138
+ ): boolean {
139
+ return getCapabilityDecision(config, capability) === "allow";
140
+ }
141
+
142
+ export function keepsWebOnAsk(config: LogicalConfig): boolean {
143
+ return getCapabilityDecision(config, "web") === "ask";
144
+ }
145
+
146
+ export function getHarnessPermissionRules(
147
+ config: LogicalConfig
148
+ ): Record<string, PermissionRule> | undefined {
149
+ switch (config.permission_policy?.profile) {
150
+ case "rigid":
151
+ return {
152
+ "*": "ask",
153
+ webfetch: "ask",
154
+ websearch: "ask",
155
+ codesearch: "ask",
156
+ external_directory: "deny",
157
+ doom_loop: "deny"
158
+ };
159
+ case "sensible-defaults":
160
+ return SENSIBLE_DEFAULTS_RULES;
161
+ case "max-autonomy":
162
+ return {
163
+ "*": "allow",
164
+ webfetch: "ask",
165
+ websearch: "ask",
166
+ codesearch: "ask",
167
+ external_directory: "deny",
168
+ doom_loop: "deny"
169
+ };
170
+ default:
171
+ return undefined;
172
+ }
173
+ }
package/src/util.ts CHANGED
@@ -185,12 +185,14 @@ export async function writeGitHooks(
185
185
  export async function writeInlineSkills(
186
186
  config: LogicalConfig,
187
187
  projectRoot: string
188
- ): Promise<void> {
188
+ ): Promise<string[]> {
189
+ const modified: string[] = [];
190
+
189
191
  for (const skill of config.skills) {
190
192
  if (!("body" in skill)) continue;
191
193
 
192
194
  const skillDir = join(projectRoot, ".ade", "skills", skill.name);
193
- await mkdir(skillDir, { recursive: true });
195
+ const skillPath = join(skillDir, "SKILL.md");
194
196
 
195
197
  const frontmatter = [
196
198
  "---",
@@ -199,10 +201,21 @@ export async function writeInlineSkills(
199
201
  "---"
200
202
  ].join("\n");
201
203
 
202
- await writeFile(
203
- join(skillDir, "SKILL.md"),
204
- `${frontmatter}\n\n${skill.body}\n`,
205
- "utf-8"
206
- );
204
+ const expected = `${frontmatter}\n\n${skill.body}\n`;
205
+
206
+ try {
207
+ const existing = await readFile(skillPath, "utf-8");
208
+ if (existing !== expected) {
209
+ modified.push(skill.name);
210
+ continue;
211
+ }
212
+ } catch {
213
+ // File doesn't exist yet — fall through to write
214
+ }
215
+
216
+ await mkdir(skillDir, { recursive: true });
217
+ await writeFile(skillPath, expected, "utf-8");
207
218
  }
219
+
220
+ return modified;
208
221
  }
@@ -2,8 +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 { claudeCodeWriter } from "./claude-code.js";
11
+ import { writeInlineSkills } from "../util.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
+ }
7
56
 
8
57
  describe("claudeCodeWriter", () => {
9
58
  let dir: string;
@@ -80,7 +129,38 @@ describe("claudeCodeWriter", () => {
80
129
  });
81
130
  });
82
131
 
83
- it("writes .claude/settings.json with MCP tool permissions", async () => {
132
+ it("forwards explicit MCP tool permissions using Claude rule names", async () => {
133
+ const config: LogicalConfig = {
134
+ mcp_servers: [
135
+ {
136
+ ref: "workflows",
137
+ command: "npx",
138
+ args: ["-y", "@codemcp/workflows"],
139
+ env: {},
140
+ allowedTools: ["use_skill", "whats_next"]
141
+ }
142
+ ],
143
+ instructions: [],
144
+ cli_actions: [],
145
+ knowledge_sources: [],
146
+ skills: [],
147
+ git_hooks: [],
148
+ setup_notes: []
149
+ };
150
+
151
+ await claudeCodeWriter.install(config, dir);
152
+
153
+ const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
154
+ const settings = JSON.parse(raw);
155
+ expect(settings.permissions.allow).toEqual(
156
+ expect.arrayContaining([
157
+ "mcp__workflows__use_skill",
158
+ "mcp__workflows__whats_next"
159
+ ])
160
+ );
161
+ });
162
+
163
+ it("does not invent wildcard MCP permission rules", async () => {
84
164
  const config: LogicalConfig = {
85
165
  mcp_servers: [
86
166
  {
@@ -102,7 +182,85 @@ describe("claudeCodeWriter", () => {
102
182
 
103
183
  const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
104
184
  const settings = JSON.parse(raw);
105
- expect(settings.permissions.allow).toContain("MCP(workflows:*)");
185
+ expect(settings.permissions.allow ?? []).toEqual([]);
186
+ });
187
+
188
+ it("keeps web on ask for rigid autonomy without broad built-in allows", async () => {
189
+ const config: LogicalConfig = {
190
+ mcp_servers: [],
191
+ instructions: [],
192
+ cli_actions: [],
193
+ knowledge_sources: [],
194
+ skills: [],
195
+ git_hooks: [],
196
+ setup_notes: [],
197
+ permission_policy: autonomyPolicy("rigid")
198
+ };
199
+
200
+ await claudeCodeWriter.install(config, dir);
201
+
202
+ const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
203
+ const settings = JSON.parse(raw);
204
+ expect(settings.permissions.allow ?? []).toEqual([]);
205
+ expect(settings.permissions.ask).toEqual(
206
+ expect.arrayContaining(["WebFetch", "WebSearch"])
207
+ );
208
+ });
209
+
210
+ it("maps sensible-defaults to Claude built-in permission rules", async () => {
211
+ const config: LogicalConfig = {
212
+ mcp_servers: [],
213
+ instructions: [],
214
+ cli_actions: [],
215
+ knowledge_sources: [],
216
+ skills: [],
217
+ git_hooks: [],
218
+ setup_notes: [],
219
+ permission_policy: autonomyPolicy("sensible-defaults")
220
+ };
221
+
222
+ await claudeCodeWriter.install(config, dir);
223
+
224
+ const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
225
+ const settings = JSON.parse(raw);
226
+ expect(settings.permissions.allow).toEqual(
227
+ expect.arrayContaining(["Read", "Edit", "Glob", "Grep", "TodoWrite"])
228
+ );
229
+ expect(settings.permissions.allow).not.toContain("Bash");
230
+ expect(settings.permissions.ask).toEqual(
231
+ expect.arrayContaining(["WebFetch", "WebSearch"])
232
+ );
233
+ });
234
+
235
+ it("maps max-autonomy to broad Claude built-in permission rules while preserving web ask", async () => {
236
+ const config: LogicalConfig = {
237
+ mcp_servers: [],
238
+ instructions: [],
239
+ cli_actions: [],
240
+ knowledge_sources: [],
241
+ skills: [],
242
+ git_hooks: [],
243
+ setup_notes: [],
244
+ permission_policy: autonomyPolicy("max-autonomy")
245
+ };
246
+
247
+ await claudeCodeWriter.install(config, dir);
248
+
249
+ const raw = await readFile(join(dir, ".claude", "settings.json"), "utf-8");
250
+ const settings = JSON.parse(raw);
251
+ expect(settings.permissions.allow).toEqual(
252
+ expect.arrayContaining([
253
+ "Read",
254
+ "Edit",
255
+ "Bash",
256
+ "Glob",
257
+ "Grep",
258
+ "TodoWrite"
259
+ ])
260
+ );
261
+ expect(settings.permissions.ask).toEqual(
262
+ expect.arrayContaining(["WebFetch", "WebSearch"])
263
+ );
106
264
  });
107
265
 
108
266
  it("includes agentskills server from mcp_servers", async () => {
@@ -133,7 +291,7 @@ describe("claudeCodeWriter", () => {
133
291
  });
134
292
  });
135
293
 
136
- it("writes inline SKILL.md files", async () => {
294
+ it("writes inline SKILL.md files via writeInlineSkills", async () => {
137
295
  const config: LogicalConfig = {
138
296
  mcp_servers: [],
139
297
  instructions: [],
@@ -150,7 +308,7 @@ describe("claudeCodeWriter", () => {
150
308
  setup_notes: []
151
309
  };
152
310
 
153
- await claudeCodeWriter.install(config, dir);
311
+ await writeInlineSkills(config, dir);
154
312
 
155
313
  const skillMd = await readFile(
156
314
  join(dir, ".ade", "skills", "tanstack-architecture", "SKILL.md"),