@codemcp/ade 0.4.0 → 0.6.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 (40) hide show
  1. package/.beads/issues.jsonl +14 -0
  2. package/.beads/last-touched +1 -1
  3. package/.vibe/beads-state-ade-main-iazal7.json +29 -0
  4. package/.vibe/development-plan-extensibility.md +169 -0
  5. package/ade.extensions.mjs +66 -0
  6. package/docs/adr/0002-extension-file-type-safety.md +97 -0
  7. package/docs/guide/extensions.md +187 -0
  8. package/package.json +3 -2
  9. package/packages/cli/dist/index.js +166 -32
  10. package/packages/cli/package.json +4 -2
  11. package/packages/cli/src/commands/extensions.integration.spec.ts +122 -0
  12. package/packages/cli/src/commands/install.spec.ts +21 -1
  13. package/packages/cli/src/commands/install.ts +10 -5
  14. package/packages/cli/src/commands/setup.ts +8 -4
  15. package/packages/cli/src/extensions.spec.ts +128 -0
  16. package/packages/cli/src/extensions.ts +71 -0
  17. package/packages/cli/src/index.ts +10 -5
  18. package/packages/core/package.json +3 -2
  19. package/packages/core/src/catalog/facets/process.ts +10 -1
  20. package/packages/core/src/catalog/index.ts +38 -1
  21. package/packages/core/src/extensions.spec.ts +169 -0
  22. package/packages/core/src/index.ts +3 -1
  23. package/packages/core/src/registry.ts +3 -2
  24. package/packages/core/src/resolver.spec.ts +29 -0
  25. package/packages/core/src/types.ts +71 -0
  26. package/packages/core/src/writers/mcp-server.spec.ts +62 -0
  27. package/packages/core/src/writers/mcp-server.ts +25 -0
  28. package/packages/core/src/writers/workflows.spec.ts +22 -0
  29. package/packages/core/src/writers/workflows.ts +5 -2
  30. package/packages/harnesses/package.json +1 -1
  31. package/packages/harnesses/src/index.spec.ts +48 -1
  32. package/packages/harnesses/src/index.ts +10 -0
  33. package/packages/harnesses/src/writers/copilot.spec.ts +2 -6
  34. package/packages/harnesses/src/writers/copilot.ts +2 -9
  35. package/packages/harnesses/src/writers/kiro.spec.ts +32 -0
  36. package/packages/harnesses/src/writers/kiro.ts +22 -5
  37. package/packages/harnesses/src/writers/opencode.spec.ts +66 -0
  38. package/packages/harnesses/src/writers/opencode.ts +30 -3
  39. package/pnpm-workspace.yaml +2 -0
  40. /package/docs/{adrs → adr}/0001-tui-framework-selection.md +0 -0
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mcpServerWriter } from "./mcp-server.js";
3
+ import type { ResolutionContext } from "../types.js";
4
+
5
+ describe("mcpServerWriter", () => {
6
+ const context: ResolutionContext = { resolved: {} };
7
+
8
+ it("has id 'mcp-server'", () => {
9
+ expect(mcpServerWriter.id).toBe("mcp-server");
10
+ });
11
+
12
+ it("returns mcp_servers with correct ref, command, args, and env", async () => {
13
+ const result = await mcpServerWriter.write(
14
+ {
15
+ ref: "my-server",
16
+ command: "npx",
17
+ args: ["my-mcp-package"],
18
+ env: { KEY: "value" }
19
+ },
20
+ context
21
+ );
22
+ expect(result).toEqual({
23
+ mcp_servers: [
24
+ {
25
+ ref: "my-server",
26
+ command: "npx",
27
+ args: ["my-mcp-package"],
28
+ env: { KEY: "value" }
29
+ }
30
+ ]
31
+ });
32
+ });
33
+
34
+ it("defaults env to an empty object when not specified", async () => {
35
+ const result = await mcpServerWriter.write(
36
+ { ref: "my-server", command: "npx", args: ["my-mcp-package"] },
37
+ context
38
+ );
39
+ expect(result.mcp_servers![0].env).toEqual({});
40
+ });
41
+
42
+ it("includes allowedTools when specified", async () => {
43
+ const result = await mcpServerWriter.write(
44
+ {
45
+ ref: "my-server",
46
+ command: "npx",
47
+ args: ["my-mcp-package"],
48
+ allowedTools: ["tool_a", "tool_b"]
49
+ },
50
+ context
51
+ );
52
+ expect(result.mcp_servers![0].allowedTools).toEqual(["tool_a", "tool_b"]);
53
+ });
54
+
55
+ it("omits allowedTools from entry when not specified", async () => {
56
+ const result = await mcpServerWriter.write(
57
+ { ref: "my-server", command: "npx", args: ["my-mcp-package"] },
58
+ context
59
+ );
60
+ expect(result.mcp_servers![0]).not.toHaveProperty("allowedTools");
61
+ });
62
+ });
@@ -0,0 +1,25 @@
1
+ import type { ProvisionWriterDef } from "../types.js";
2
+
3
+ export const mcpServerWriter: ProvisionWriterDef = {
4
+ id: "mcp-server",
5
+ async write(config) {
6
+ const { ref, command, args, env, allowedTools } = config as {
7
+ ref: string;
8
+ command: string;
9
+ args: string[];
10
+ env?: Record<string, string>;
11
+ allowedTools?: string[];
12
+ };
13
+ return {
14
+ mcp_servers: [
15
+ {
16
+ ref,
17
+ command,
18
+ args,
19
+ env: env ?? {},
20
+ ...(allowedTools !== undefined ? { allowedTools } : {})
21
+ }
22
+ ]
23
+ };
24
+ }
25
+ };
@@ -59,6 +59,28 @@ describe("workflowsWriter", () => {
59
59
  expect(result.mcp_servers![0].env).toEqual({});
60
60
  });
61
61
 
62
+ it("includes allowedTools in the entry when specified", async () => {
63
+ const result = await workflowsWriter.write(
64
+ {
65
+ package: "@codemcp/workflows-server",
66
+ allowedTools: ["whats_next", "conduct_review"]
67
+ },
68
+ context
69
+ );
70
+ expect(result.mcp_servers![0].allowedTools).toEqual([
71
+ "whats_next",
72
+ "conduct_review"
73
+ ]);
74
+ });
75
+
76
+ it("omits allowedTools from entry when not specified", async () => {
77
+ const result = await workflowsWriter.write(
78
+ { package: "@codemcp/workflows-server" },
79
+ context
80
+ );
81
+ expect(result.mcp_servers![0]).not.toHaveProperty("allowedTools");
82
+ });
83
+
62
84
  it("only returns mcp_servers, not other LogicalConfig keys", async () => {
63
85
  const result = await workflowsWriter.write(
64
86
  { package: "@codemcp/workflows-server" },
@@ -6,11 +6,13 @@ export const workflowsWriter: ProvisionWriterDef = {
6
6
  const {
7
7
  package: pkg,
8
8
  ref,
9
- env
9
+ env,
10
+ allowedTools
10
11
  } = config as {
11
12
  package: string;
12
13
  ref?: string;
13
14
  env?: Record<string, string>;
15
+ allowedTools?: string[];
14
16
  };
15
17
  return {
16
18
  mcp_servers: [
@@ -18,7 +20,8 @@ export const workflowsWriter: ProvisionWriterDef = {
18
20
  ref: ref ?? pkg,
19
21
  command: "npx",
20
22
  args: [pkg],
21
- env: env ?? {}
23
+ env: env ?? {},
24
+ ...(allowedTools !== undefined ? { allowedTools } : {})
22
25
  }
23
26
  ]
24
27
  };
@@ -40,5 +40,5 @@
40
40
  "typescript": "catalog:",
41
41
  "vitest": "catalog:"
42
42
  },
43
- "version": "0.4.0"
43
+ "version": "0.6.0"
44
44
  }
@@ -1,5 +1,11 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { allHarnessWriters, getHarnessWriter, getHarnessIds } from "./index.js";
2
+ import {
3
+ allHarnessWriters,
4
+ getHarnessWriter,
5
+ getHarnessIds,
6
+ buildHarnessWriters
7
+ } from "./index.js";
8
+ import type { HarnessWriter } from "./types.js";
3
9
 
4
10
  describe("harness registry", () => {
5
11
  it("exports all harness writers", () => {
@@ -43,3 +49,44 @@ describe("harness registry", () => {
43
49
  }
44
50
  });
45
51
  });
52
+
53
+ describe("buildHarnessWriters", () => {
54
+ it("returns all built-in writers when no extensions provided", () => {
55
+ const writers = buildHarnessWriters({});
56
+ expect(writers).toHaveLength(allHarnessWriters.length);
57
+ expect(writers.map((w) => w.id)).toEqual(
58
+ allHarnessWriters.map((w) => w.id)
59
+ );
60
+ });
61
+
62
+ it("appends extension harness writers after built-ins", () => {
63
+ const customWriter: HarnessWriter = {
64
+ id: "sap-copilot",
65
+ label: "SAP Copilot",
66
+ description: "SAP internal Copilot harness",
67
+ install: async () => {}
68
+ };
69
+
70
+ const writers = buildHarnessWriters({ harnessWriters: [customWriter] });
71
+ expect(writers).toHaveLength(allHarnessWriters.length + 1);
72
+ expect(writers.map((w) => w.id)).toContain("sap-copilot");
73
+ // built-ins come first
74
+ expect(writers[0].id).toBe("universal");
75
+ expect(writers[writers.length - 1].id).toBe("sap-copilot");
76
+ });
77
+
78
+ it("does not mutate allHarnessWriters", () => {
79
+ const originalLength = allHarnessWriters.length;
80
+ buildHarnessWriters({
81
+ harnessWriters: [
82
+ {
83
+ id: "ephemeral",
84
+ label: "Ephemeral",
85
+ description: "Should not persist",
86
+ install: async () => {}
87
+ }
88
+ ]
89
+ });
90
+ expect(allHarnessWriters).toHaveLength(originalLength);
91
+ });
92
+ });
@@ -45,3 +45,13 @@ export function getHarnessWriter(id: string): HarnessWriter | undefined {
45
45
  export function getHarnessIds(): string[] {
46
46
  return allHarnessWriters.map((w) => w.id);
47
47
  }
48
+
49
+ /**
50
+ * Returns the full list of harness writers: built-ins first, then any
51
+ * additional writers contributed via extensions. Does not mutate allHarnessWriters.
52
+ */
53
+ export function buildHarnessWriters(extensions: {
54
+ harnessWriters?: HarnessWriter[];
55
+ }): HarnessWriter[] {
56
+ return [...allHarnessWriters, ...(extensions.harnessWriters ?? [])];
57
+ }
@@ -198,12 +198,8 @@ describe("copilotWriter", () => {
198
198
  expect(sensibleAgent).not.toContain(" - execute");
199
199
  expect(sensibleAgent).not.toContain(" - todo");
200
200
  expect(sensibleAgent).not.toContain(" - web");
201
- expect(sensibleAgent).toContain(" - workflows/whats_next");
202
- expect(sensibleAgent).toContain(" - workflows/proceed_to_phase");
203
- expect(sensibleAgent).not.toContain(" - workflows/*");
204
- expect(sensibleAgent).toContain(
205
- ' tools: ["whats_next","proceed_to_phase"]'
206
- );
201
+ expect(sensibleAgent).toContain(" - workflows/*");
202
+ expect(sensibleAgent).toContain(' tools: ["*"]');
207
203
 
208
204
  expect(maxAgent).toContain(" - read");
209
205
  expect(maxAgent).toContain(" - edit");
@@ -56,14 +56,7 @@ function getBuiltInTools(profile: AutonomyProfile | undefined): string[] {
56
56
  }
57
57
 
58
58
  function getForwardedMcpTools(servers: McpServerEntry[]): string[] {
59
- return servers.flatMap((server) => {
60
- const allowedTools = server.allowedTools ?? ["*"];
61
- if (allowedTools.includes("*")) {
62
- return [`${server.ref}/*`];
63
- }
64
-
65
- return allowedTools.map((tool) => `${server.ref}/${tool}`);
66
- });
59
+ return servers.map((server) => `${server.ref}/*`);
67
60
  }
68
61
 
69
62
  function renderCopilotAgentMcpServers(servers: McpServerEntry[]): string[] {
@@ -78,7 +71,7 @@ function renderCopilotAgentMcpServers(servers: McpServerEntry[]): string[] {
78
71
  lines.push(" type: stdio");
79
72
  lines.push(` command: ${JSON.stringify(server.command)}`);
80
73
  lines.push(` args: ${JSON.stringify(server.args)}`);
81
- lines.push(` tools: ${JSON.stringify(server.allowedTools ?? ["*"])}`);
74
+ lines.push(` tools: ${JSON.stringify(["*"])}`);
82
75
 
83
76
  if (Object.keys(server.env).length > 0) {
84
77
  lines.push(" env:");
@@ -186,4 +186,36 @@ describe("kiroWriter", () => {
186
186
  expect(rigidMcp.mcpServers.workflows.autoApprove).toEqual(["*"]);
187
187
  expect(maxMcp.mcpServers.workflows.autoApprove).toEqual(["*"]);
188
188
  });
189
+
190
+ it("uses wildcard in tools but restricted names in allowedTools when allowedTools is set", async () => {
191
+ const config: LogicalConfig = {
192
+ mcp_servers: [
193
+ {
194
+ ref: "workflows",
195
+ command: "npx",
196
+ args: ["-y", "@codemcp/workflows"],
197
+ env: {},
198
+ allowedTools: ["whats_next", "conduct_review"]
199
+ }
200
+ ],
201
+ instructions: [],
202
+ cli_actions: [],
203
+ knowledge_sources: [],
204
+ skills: [],
205
+ git_hooks: [],
206
+ setup_notes: []
207
+ };
208
+
209
+ await kiroWriter.install(config, dir);
210
+
211
+ const agent = JSON.parse(
212
+ await readFile(join(dir, ".kiro", "agents", "ade.json"), "utf-8")
213
+ );
214
+
215
+ expect(agent.tools).toContain("@workflows/*");
216
+ expect(agent.tools).not.toContain("@workflows/whats_next");
217
+ expect(agent.allowedTools).toContain("@workflows/whats_next");
218
+ expect(agent.allowedTools).toContain("@workflows/conduct_review");
219
+ expect(agent.allowedTools).not.toContain("@workflows/*");
220
+ });
189
221
  });
@@ -27,6 +27,10 @@ export const kiroWriter: HarnessWriter = {
27
27
  });
28
28
 
29
29
  const tools = getKiroTools(getAutonomyProfile(config), config.mcp_servers);
30
+ const allowedTools = getKiroAllowedTools(
31
+ getAutonomyProfile(config),
32
+ config.mcp_servers
33
+ );
30
34
  await writeJson(join(projectRoot, ".kiro", "agents", "ade.json"), {
31
35
  name: "ade",
32
36
  description:
@@ -36,7 +40,7 @@ export const kiroWriter: HarnessWriter = {
36
40
  "ADE — Agentic Development Environment agent.",
37
41
  mcpServers: getKiroAgentMcpServers(config.mcp_servers),
38
42
  tools,
39
- allowedTools: tools,
43
+ allowedTools,
40
44
  useLegacyMcpJson: true
41
45
  });
42
46
 
@@ -48,7 +52,7 @@ function getKiroTools(
48
52
  profile: AutonomyProfile | undefined,
49
53
  servers: McpServerEntry[]
50
54
  ): string[] {
51
- const mcpTools = getKiroForwardedMcpTools(servers);
55
+ const mcpTools = servers.map((server) => `@${server.ref}/*`);
52
56
 
53
57
  switch (profile) {
54
58
  case "rigid":
@@ -62,15 +66,28 @@ function getKiroTools(
62
66
  }
63
67
  }
64
68
 
65
- function getKiroForwardedMcpTools(servers: McpServerEntry[]): string[] {
66
- return servers.flatMap((server) => {
69
+ function getKiroAllowedTools(
70
+ profile: AutonomyProfile | undefined,
71
+ servers: McpServerEntry[]
72
+ ): string[] {
73
+ const mcpAllowedTools = servers.flatMap((server) => {
67
74
  const allowedTools = server.allowedTools ?? ["*"];
68
75
  if (allowedTools.includes("*")) {
69
76
  return [`@${server.ref}/*`];
70
77
  }
71
-
72
78
  return allowedTools.map((tool) => `@${server.ref}/${tool}`);
73
79
  });
80
+
81
+ switch (profile) {
82
+ case "rigid":
83
+ return ["read", "shell", "spec", ...mcpAllowedTools];
84
+ case "sensible-defaults":
85
+ return ["read", "write", "shell", "spec", ...mcpAllowedTools];
86
+ case "max-autonomy":
87
+ return ["read", "write", "shell(*)", "spec", ...mcpAllowedTools];
88
+ default:
89
+ return ["read", "write", "shell", "spec", ...mcpAllowedTools];
90
+ }
74
91
  }
75
92
 
76
93
  function getKiroAgentMcpServers(
@@ -148,6 +148,72 @@ describe("opencodeWriter", () => {
148
148
  expect(rigidAgent).not.toContain("tools:");
149
149
  });
150
150
 
151
+ it("writes allowed MCP tools into the permission block of the agent frontmatter", async () => {
152
+ const projectRoot = join(dir, "mcp-tools");
153
+ const config: LogicalConfig = {
154
+ mcp_servers: [
155
+ {
156
+ ref: "workflows",
157
+ command: "npx",
158
+ args: ["@codemcp/workflows-server@latest"],
159
+ env: {},
160
+ allowedTools: ["whats_next", "conduct_review"]
161
+ }
162
+ ],
163
+ instructions: ["Follow project rules."],
164
+ cli_actions: [],
165
+ knowledge_sources: [],
166
+ skills: [],
167
+ git_hooks: [],
168
+ setup_notes: []
169
+ };
170
+
171
+ await opencodeWriter.install(config, projectRoot);
172
+
173
+ const agent = await readFile(
174
+ join(projectRoot, ".opencode", "agents", "ade.md"),
175
+ "utf-8"
176
+ );
177
+ const frontmatter = parseFrontmatter(agent);
178
+ const permission = frontmatter.permission as Record<string, string>;
179
+
180
+ expect(permission["workflows*"]).toBe("ask");
181
+ expect(permission["workflows_whats_next"]).toBe("allow");
182
+ expect(permission["workflows_conduct_review"]).toBe("allow");
183
+ expect(agent).not.toContain("tools:");
184
+ });
185
+
186
+ it("writes wildcard MCP permission when allowedTools is not restricted", async () => {
187
+ const projectRoot = join(dir, "mcp-wildcard");
188
+ const config: LogicalConfig = {
189
+ mcp_servers: [
190
+ {
191
+ ref: "workflows",
192
+ command: "npx",
193
+ args: ["@codemcp/workflows-server@latest"],
194
+ env: {}
195
+ }
196
+ ],
197
+ instructions: ["Follow project rules."],
198
+ cli_actions: [],
199
+ knowledge_sources: [],
200
+ skills: [],
201
+ git_hooks: [],
202
+ setup_notes: []
203
+ };
204
+
205
+ await opencodeWriter.install(config, projectRoot);
206
+
207
+ const agent = await readFile(
208
+ join(projectRoot, ".opencode", "agents", "ade.md"),
209
+ "utf-8"
210
+ );
211
+ const frontmatter = parseFrontmatter(agent);
212
+ const permission = frontmatter.permission as Record<string, string>;
213
+
214
+ expect(permission["workflows*"]).toBe("allow");
215
+ });
216
+
151
217
  it("keeps MCP servers in project config and writes documented environment fields", async () => {
152
218
  const projectRoot = join(dir, "mcp");
153
219
  const config = {
@@ -1,5 +1,9 @@
1
1
  import { join } from "node:path";
2
- import type { AutonomyProfile, LogicalConfig } from "@codemcp/ade-core";
2
+ import type {
3
+ AutonomyProfile,
4
+ LogicalConfig,
5
+ McpServerEntry
6
+ } from "@codemcp/ade-core";
3
7
  import type { HarnessWriter } from "../types.js";
4
8
  import {
5
9
  writeAgentMd,
@@ -138,6 +142,24 @@ const MAX_AUTONOMY_RULES: Record<string, PermissionRule> = {
138
142
  doom_loop: "deny"
139
143
  };
140
144
 
145
+ function getMcpPermissions(
146
+ servers: McpServerEntry[]
147
+ ): Record<string, PermissionRule> | undefined {
148
+ const entries: [string, PermissionRule][] = servers.flatMap((server) => {
149
+ const allowedTools = server.allowedTools ?? ["*"];
150
+ if (allowedTools.includes("*")) {
151
+ return [[`${server.ref}*`, "allow"]] as [string, PermissionRule][];
152
+ }
153
+ return [
154
+ [`${server.ref}*`, "ask"] as [string, PermissionRule],
155
+ ...allowedTools.map(
156
+ (tool) => [`${server.ref}_${tool}`, "allow"] as [string, PermissionRule]
157
+ )
158
+ ];
159
+ });
160
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
161
+ }
162
+
141
163
  function getPermissionRules(
142
164
  profile: AutonomyProfile | undefined
143
165
  ): Record<string, PermissionRule> | undefined {
@@ -170,11 +192,16 @@ export const opencodeWriter: HarnessWriter = {
170
192
  });
171
193
 
172
194
  const permission = getPermissionRules(getAutonomyProfile(config));
195
+ const mcpPermissions = getMcpPermissions(config.mcp_servers);
196
+ const mergedPermission =
197
+ permission || mcpPermissions
198
+ ? { ...(mcpPermissions ?? {}), ...(permission ?? {}) }
199
+ : undefined;
173
200
 
174
201
  await writeAgentMd(config, {
175
202
  path: join(projectRoot, ".opencode", "agents", "ade.md"),
176
- extraFrontmatter: permission
177
- ? renderYamlMapping("permission", permission)
203
+ extraFrontmatter: mergedPermission
204
+ ? renderYamlMapping("permission", mergedPermission)
178
205
  : undefined,
179
206
  fallbackBody:
180
207
  "ADE — Agentic Development Environment agent with project conventions and tools."
@@ -17,3 +17,5 @@ catalog:
17
17
  rimraf: "^6.1.3"
18
18
  tsx: "^4.19.0"
19
19
  tsup: "^8.3.0"
20
+ # Runtime
21
+ zod: "4.3.6"