@codemcp/ade 0.2.6 → 0.4.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 (35) hide show
  1. package/.agentskills/skills/conventional-commits/SKILL.md +36 -0
  2. package/.beads/issues.jsonl +10 -0
  3. package/.beads/last-touched +1 -1
  4. package/.kiro/agents/ade.json +9 -2
  5. package/.opencode/agents/ade.md +9 -18
  6. package/.vibe/beads-state-ade-better-in-cli-explanation-b8jcv3.json +24 -0
  7. package/.vibe/beads-state-ade-fix-no-git-k396xs.json +34 -0
  8. package/.vibe/development-plan-better-in-cli-explanation.md +64 -0
  9. package/.vibe/development-plan-fix-no-git.md +76 -0
  10. package/AGENTS.md +27 -0
  11. package/config.lock.yaml +33 -9
  12. package/config.yaml +3 -0
  13. package/package.json +1 -1
  14. package/packages/cli/dist/index.js +190 -50
  15. package/packages/cli/package.json +1 -1
  16. package/packages/cli/src/commands/conventions.integration.spec.ts +8 -1
  17. package/packages/cli/src/commands/install.integration.spec.ts +1 -0
  18. package/packages/cli/src/commands/install.ts +19 -1
  19. package/packages/cli/src/commands/knowledge.integration.spec.ts +1 -0
  20. package/packages/cli/src/commands/setup.integration.spec.ts +2 -0
  21. package/packages/cli/src/commands/setup.spec.ts +2 -1
  22. package/packages/cli/src/commands/setup.ts +61 -6
  23. package/packages/cli/src/index.ts +8 -1
  24. package/packages/core/package.json +1 -1
  25. package/packages/core/src/catalog/catalog.spec.ts +1 -1
  26. package/packages/core/src/catalog/facets/autonomy.ts +9 -9
  27. package/packages/core/src/catalog/facets/backpressure.ts +1 -1
  28. package/packages/core/src/catalog/facets/practices.ts +1 -2
  29. package/packages/core/src/catalog/facets/process.ts +1 -1
  30. package/packages/harnesses/package.json +2 -1
  31. package/packages/harnesses/src/util.spec.ts +97 -0
  32. package/packages/harnesses/src/util.ts +21 -4
  33. package/packages/harnesses/src/writers/opencode.spec.ts +4 -6
  34. package/packages/harnesses/src/writers/opencode.ts +23 -27
  35. package/skills-lock.json +6 -1
@@ -9,26 +9,26 @@ export const autonomyFacet: Facet = {
9
9
  multiSelect: false,
10
10
  options: [
11
11
  {
12
- id: "rigid",
13
- label: "Rigid",
12
+ id: "sensible-defaults",
13
+ label: "Sensible defaults",
14
14
  description:
15
- "Keep built-in capabilities approval-gated and require confirmation before acting",
15
+ "Allow a curated built-in capabilities set while keeping potentially risky actions approval-gated",
16
16
  recipe: [
17
17
  {
18
18
  writer: "permission-policy",
19
- config: { profile: "rigid" }
19
+ config: { profile: "sensible-defaults" }
20
20
  }
21
21
  ]
22
22
  },
23
23
  {
24
- id: "sensible-defaults",
25
- label: "Sensible defaults",
24
+ id: "rigid",
25
+ label: "Rigid",
26
26
  description:
27
- "Allow a curated low-risk built-in capability set while keeping web access approval-gated",
27
+ "Keep built-in capabilities approval-gated and require confirmation before acting",
28
28
  recipe: [
29
29
  {
30
30
  writer: "permission-policy",
31
- config: { profile: "sensible-defaults" }
31
+ config: { profile: "rigid" }
32
32
  }
33
33
  ]
34
34
  },
@@ -36,7 +36,7 @@ export const autonomyFacet: Facet = {
36
36
  id: "max-autonomy",
37
37
  label: "Max autonomy",
38
38
  description:
39
- "Allow broad local built-in autonomy while keeping web access approval-gated",
39
+ "Allow broad local built-in autonomy. Recommended for sandboxed environments only",
40
40
  recipe: [
41
41
  {
42
42
  writer: "permission-policy",
@@ -89,7 +89,7 @@ export const backpressureFacet: Facet = {
89
89
  id: "backpressure",
90
90
  label: "Backpressure",
91
91
  description:
92
- "Install git hooks that enforce quality gates silent on success, surface only relevant failures",
92
+ "Which automated quality gates to put in place. Translates to git hooks",
93
93
  required: false,
94
94
  multiSelect: true,
95
95
  dependsOn: ["architecture"],
@@ -3,8 +3,7 @@ import type { Facet } from "../../types.js";
3
3
  export const practicesFacet: Facet = {
4
4
  id: "practices",
5
5
  label: "Practices",
6
- description:
7
- "Composable development practices — mix and match regardless of stack",
6
+ description: "Development practices — mix and match regardless of stack",
8
7
  required: false,
9
8
  multiSelect: true,
10
9
  options: [
@@ -3,7 +3,7 @@ import type { Facet } from "../../types.js";
3
3
  export const processFacet: Facet = {
4
4
  id: "process",
5
5
  label: "Process",
6
- description: "How your AI agent receives and executes tasks",
6
+ description: "How will you guide your agent",
7
7
  required: true,
8
8
  options: [
9
9
  {
@@ -28,6 +28,7 @@
28
28
  "typecheck": "tsc --noEmit"
29
29
  },
30
30
  "dependencies": {
31
+ "@clack/prompts": "^1.1.0",
31
32
  "@codemcp/ade-core": "workspace:*",
32
33
  "@codemcp/skills": "^2.3.0"
33
34
  },
@@ -39,5 +40,5 @@
39
40
  "typescript": "catalog:",
40
41
  "vitest": "catalog:"
41
42
  },
42
- "version": "0.2.6"
43
+ "version": "0.4.0"
43
44
  }
@@ -0,0 +1,97 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { mkdtemp, rm, mkdir, readFile, stat } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import * as clack from "@clack/prompts";
6
+ import type { GitHook } from "@codemcp/ade-core";
7
+ import { writeGitHooks } from "./util.js";
8
+
9
+ describe("writeGitHooks", () => {
10
+ let dir: string;
11
+
12
+ beforeEach(async () => {
13
+ dir = await mkdtemp(join(tmpdir(), "ade-util-git-hooks-"));
14
+ vi.spyOn(clack.log, "warn").mockImplementation(() => undefined);
15
+ });
16
+
17
+ afterEach(async () => {
18
+ await rm(dir, { recursive: true, force: true });
19
+ vi.restoreAllMocks();
20
+ });
21
+
22
+ it("is a no-op when hooks is undefined", async () => {
23
+ await expect(writeGitHooks(undefined, dir)).resolves.toBeUndefined();
24
+ expect(clack.log.warn).not.toHaveBeenCalled();
25
+ });
26
+
27
+ it("is a no-op when hooks array is empty", async () => {
28
+ await expect(writeGitHooks([], dir)).resolves.toBeUndefined();
29
+ expect(clack.log.warn).not.toHaveBeenCalled();
30
+ });
31
+
32
+ it("warns and skips gracefully when .git directory does not exist", async () => {
33
+ const hooks: GitHook[] = [
34
+ { phase: "pre-commit", script: "#!/bin/sh\nnpx lint-staged" }
35
+ ];
36
+
37
+ await expect(writeGitHooks(hooks, dir)).resolves.toBeUndefined();
38
+
39
+ expect(clack.log.warn).toHaveBeenCalledOnce();
40
+ expect(clack.log.warn).toHaveBeenCalledWith(
41
+ expect.stringContaining("not a git repository")
42
+ );
43
+
44
+ // No .git/hooks directory should have been created
45
+ await expect(stat(join(dir, ".git"))).rejects.toThrow();
46
+ });
47
+
48
+ it("writes hook files when .git exists", async () => {
49
+ await mkdir(join(dir, ".git"), { recursive: true });
50
+
51
+ const script = "#!/bin/sh\nnpx lint-staged\n";
52
+ const hooks: GitHook[] = [{ phase: "pre-commit", script }];
53
+
54
+ await writeGitHooks(hooks, dir);
55
+
56
+ expect(clack.log.warn).not.toHaveBeenCalled();
57
+
58
+ const written = await readFile(
59
+ join(dir, ".git", "hooks", "pre-commit"),
60
+ "utf-8"
61
+ );
62
+ expect(written).toBe(script);
63
+ });
64
+
65
+ it("creates .git/hooks directory if it does not exist yet", async () => {
66
+ // .git exists but no hooks subdir
67
+ await mkdir(join(dir, ".git"), { recursive: true });
68
+
69
+ const hooks: GitHook[] = [{ phase: "pre-commit", script: "#!/bin/sh\n" }];
70
+ await writeGitHooks(hooks, dir);
71
+
72
+ const hookStat = await stat(join(dir, ".git", "hooks"));
73
+ expect(hookStat.isDirectory()).toBe(true);
74
+ });
75
+
76
+ it("writes multiple hooks", async () => {
77
+ await mkdir(join(dir, ".git"), { recursive: true });
78
+
79
+ const hooks: GitHook[] = [
80
+ { phase: "pre-commit", script: "#!/bin/sh\necho pre-commit\n" },
81
+ { phase: "pre-push", script: "#!/bin/sh\necho pre-push\n" }
82
+ ];
83
+
84
+ await writeGitHooks(hooks, dir);
85
+
86
+ const preCommit = await readFile(
87
+ join(dir, ".git", "hooks", "pre-commit"),
88
+ "utf-8"
89
+ );
90
+ const prePush = await readFile(
91
+ join(dir, ".git", "hooks", "pre-push"),
92
+ "utf-8"
93
+ );
94
+ expect(preCommit).toBe(hooks[0].script);
95
+ expect(prePush).toBe(hooks[1].script);
96
+ });
97
+ });
@@ -1,5 +1,6 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
+ import * as clack from "@clack/prompts";
3
4
  import type { GitHook, LogicalConfig, McpServerEntry } from "@codemcp/ade-core";
4
5
 
5
6
  // ---------------------------------------------------------------------------
@@ -169,15 +170,31 @@ export async function writeAgentMd(
169
170
  /**
170
171
  * Write git hook scripts to `.git/hooks/<phase>`.
171
172
  * Files are created with executable permissions (0o755).
172
- * No-op when the hooks array is empty.
173
+ * No-op when the hooks array is empty or undefined.
174
+ * Emits a warning and skips gracefully when the project root is not a git repository.
173
175
  */
174
176
  export async function writeGitHooks(
175
177
  hooks: GitHook[] | undefined,
176
178
  projectRoot: string
177
179
  ): Promise<void> {
178
- if (!hooks) return;
180
+ if (!hooks || hooks.length === 0) return;
181
+
182
+ const gitDir = join(projectRoot, ".git");
183
+ try {
184
+ await access(gitDir);
185
+ } catch {
186
+ clack.log.warn(
187
+ "Git hooks were configured but could not be installed: the project is not a git repository.\n" +
188
+ "Run `git init` and re-run setup to install the hooks."
189
+ );
190
+ return;
191
+ }
192
+
193
+ const hooksDir = join(gitDir, "hooks");
194
+ await mkdir(hooksDir, { recursive: true });
195
+
179
196
  for (const hook of hooks) {
180
- const hookPath = join(projectRoot, ".git", "hooks", hook.phase);
197
+ const hookPath = join(hooksDir, hook.phase);
181
198
  await writeFile(hookPath, hook.script, { mode: 0o755 });
182
199
  }
183
200
  }
@@ -102,17 +102,16 @@ describe("opencodeWriter", () => {
102
102
  expect(defaultsAgent).toContain('grep: "allow"');
103
103
  expect(defaultsAgent).toContain('list: "allow"');
104
104
  expect(defaultsAgent).toContain('lsp: "allow"');
105
- expect(defaultsAgent).toContain('task: "allow"');
105
+ expect(defaultsAgent).toContain('task: "deny"');
106
106
  expect(defaultsAgent).toContain('skill: "deny"');
107
107
  expect(defaultsAgent).toContain('todoread: "deny"');
108
108
  expect(defaultsAgent).toContain('todowrite: "deny"');
109
109
  expect(defaultsAgent).toContain('webfetch: "ask"');
110
110
  expect(defaultsAgent).toContain('websearch: "ask"');
111
111
  expect(defaultsAgent).toContain('codesearch: "ask"');
112
- expect(defaultsAgent).toContain('external_directory: "deny"');
112
+ expect(defaultsAgent).toContain('external_directory: "ask"');
113
113
  expect(defaultsAgent).toContain('doom_loop: "deny"');
114
114
  expect(defaultsAgent).toContain('"grep *": "allow"');
115
- expect(defaultsAgent).toContain('"cp *": "ask"');
116
115
  expect(defaultsAgent).toContain('"rm *": "deny"');
117
116
  expect(defaultsFrontmatter.permission).toMatchObject({
118
117
  edit: "allow",
@@ -120,21 +119,20 @@ describe("opencodeWriter", () => {
120
119
  grep: "allow",
121
120
  list: "allow",
122
121
  lsp: "allow",
123
- task: "allow",
122
+ task: "deny",
124
123
  skill: "deny",
125
124
  todoread: "deny",
126
125
  todowrite: "deny",
127
126
  webfetch: "ask",
128
127
  websearch: "ask",
129
128
  codesearch: "ask",
130
- external_directory: "deny",
129
+ external_directory: "ask",
131
130
  doom_loop: "deny"
132
131
  });
133
132
  const defaultsPermission = defaultsFrontmatter.permission as {
134
133
  bash: Record<string, string>;
135
134
  };
136
135
  expect(defaultsPermission.bash["grep *"]).toBe("allow");
137
- expect(defaultsPermission.bash["cp *"]).toBe("ask");
138
136
  expect(defaultsPermission.bash["rm *"]).toBe("deny");
139
137
 
140
138
  expect(maxAgent).toContain('"*": "allow"');
@@ -12,7 +12,26 @@ import { getAutonomyProfile } from "../permission-policy.js";
12
12
  type PermissionDecision = "ask" | "allow" | "deny";
13
13
  type PermissionRule = PermissionDecision | Record<string, PermissionDecision>;
14
14
 
15
+ const APPLICABLE_TO_ALL: Record<string, PermissionRule> = {
16
+ read: {
17
+ "*": "allow",
18
+ "*.env": "deny",
19
+ "*.env.*": "deny",
20
+ "*.env.example": "allow"
21
+ },
22
+ skill: "deny", //we're using an own skills-mcp
23
+ todoread: "deny", //no agent-proprieatry todo tools
24
+ todowrite: "deny",
25
+ task: "deny",
26
+ lsp: "allow",
27
+ glob: "allow",
28
+ grep: "allow",
29
+ list: "allow",
30
+ external_directory: "ask"
31
+ };
32
+
15
33
  const RIGID_RULES: Record<string, PermissionRule> = {
34
+ ...APPLICABLE_TO_ALL,
16
35
  "*": "ask",
17
36
  webfetch: "ask",
18
37
  websearch: "ask",
@@ -22,31 +41,17 @@ const RIGID_RULES: Record<string, PermissionRule> = {
22
41
  };
23
42
 
24
43
  const SENSIBLE_DEFAULTS_RULES: Record<string, PermissionRule> = {
25
- read: {
26
- "*": "allow",
27
- "*.env": "deny",
28
- "*.env.*": "deny",
29
- "*.env.example": "allow"
30
- },
44
+ ...APPLICABLE_TO_ALL,
31
45
  edit: "allow",
32
- glob: "allow",
33
- grep: "allow",
34
- list: "allow",
35
- lsp: "allow",
36
- task: "allow",
37
- todoread: "deny",
38
- todowrite: "deny",
39
- skill: "deny",
40
46
  webfetch: "ask",
41
47
  websearch: "ask",
42
48
  codesearch: "ask",
43
49
  bash: {
44
- "*": "deny",
50
+ "*": "ask",
45
51
  "grep *": "allow",
46
52
  "rg *": "allow",
47
53
  "find *": "allow",
48
54
  "fd *": "allow",
49
- ls: "allow",
50
55
  "ls *": "allow",
51
56
  "cat *": "allow",
52
57
  "head *": "allow",
@@ -81,14 +86,7 @@ const SENSIBLE_DEFAULTS_RULES: Record<string, PermissionRule> = {
81
86
  "yq *": "allow",
82
87
  "mkdir *": "allow",
83
88
  "touch *": "allow",
84
- "cp *": "ask",
85
- "mv *": "ask",
86
- "ln *": "ask",
87
- "npm *": "ask",
88
- "node *": "ask",
89
- "pip *": "ask",
90
- "python *": "ask",
91
- "python3 *": "ask",
89
+ "kill *": "ask",
92
90
  "rm *": "deny",
93
91
  "rmdir *": "deny",
94
92
  "curl *": "deny",
@@ -109,7 +107,6 @@ const SENSIBLE_DEFAULTS_RULES: Record<string, PermissionRule> = {
109
107
  "mkfs *": "deny",
110
108
  "mount *": "deny",
111
109
  "umount *": "deny",
112
- "kill *": "deny",
113
110
  "killall *": "deny",
114
111
  "pkill *": "deny",
115
112
  "nc *": "deny",
@@ -129,16 +126,15 @@ const SENSIBLE_DEFAULTS_RULES: Record<string, PermissionRule> = {
129
126
  "userdel *": "deny",
130
127
  "iptables *": "deny"
131
128
  },
132
- external_directory: "deny",
133
129
  doom_loop: "deny"
134
130
  };
135
131
 
136
132
  const MAX_AUTONOMY_RULES: Record<string, PermissionRule> = {
133
+ ...APPLICABLE_TO_ALL,
137
134
  "*": "allow",
138
135
  webfetch: "ask",
139
136
  websearch: "ask",
140
137
  codesearch: "ask",
141
- external_directory: "deny",
142
138
  doom_loop: "deny"
143
139
  };
144
140
 
package/skills-lock.json CHANGED
@@ -4,13 +4,18 @@
4
4
  "adr-nygard": {
5
5
  "source": "/Users/oliverjaegle/projects/privat/codemcp/ade/.ade/skills/adr-nygard",
6
6
  "sourceType": "local",
7
- "computedHash": "13cd33eb604e9e090057cce458469b2a1b609a6db3313a03df88d91776095b19"
7
+ "computedHash": "d62ee4c175a38f91d98a0536f863396ceb48c7de4275787520b07870705b4367"
8
8
  },
9
9
  "commit": {
10
10
  "source": "mrsimpson/skills-coding",
11
11
  "sourceType": "github",
12
12
  "computedHash": "fc628c7d577d2d9cf3cb0a917d3c5e2e35b460fdc62c353595b7472b6f1c6548"
13
13
  },
14
+ "conventional-commits": {
15
+ "source": "/Users/oliverjaegle/projects/privat/codemcp/ade/.ade/skills/conventional-commits",
16
+ "sourceType": "local",
17
+ "computedHash": "49dd439dbd856412264fa345eaa9bbf2526095cb16457ffc7fb66a9d2f4d5f9d"
18
+ },
14
19
  "tdd": {
15
20
  "source": "mrsimpson/skills-coding",
16
21
  "sourceType": "github",