@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.
- package/.agentskills/skills/conventional-commits/SKILL.md +36 -0
- package/.beads/issues.jsonl +10 -0
- package/.beads/last-touched +1 -1
- package/.kiro/agents/ade.json +9 -2
- package/.opencode/agents/ade.md +9 -18
- package/.vibe/beads-state-ade-better-in-cli-explanation-b8jcv3.json +24 -0
- package/.vibe/beads-state-ade-fix-no-git-k396xs.json +34 -0
- package/.vibe/development-plan-better-in-cli-explanation.md +64 -0
- package/.vibe/development-plan-fix-no-git.md +76 -0
- package/AGENTS.md +27 -0
- package/config.lock.yaml +33 -9
- package/config.yaml +3 -0
- package/package.json +1 -1
- package/packages/cli/dist/index.js +190 -50
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/conventions.integration.spec.ts +8 -1
- package/packages/cli/src/commands/install.integration.spec.ts +1 -0
- package/packages/cli/src/commands/install.ts +19 -1
- package/packages/cli/src/commands/knowledge.integration.spec.ts +1 -0
- package/packages/cli/src/commands/setup.integration.spec.ts +2 -0
- package/packages/cli/src/commands/setup.spec.ts +2 -1
- package/packages/cli/src/commands/setup.ts +61 -6
- package/packages/cli/src/index.ts +8 -1
- package/packages/core/package.json +1 -1
- package/packages/core/src/catalog/catalog.spec.ts +1 -1
- package/packages/core/src/catalog/facets/autonomy.ts +9 -9
- package/packages/core/src/catalog/facets/backpressure.ts +1 -1
- package/packages/core/src/catalog/facets/practices.ts +1 -2
- package/packages/core/src/catalog/facets/process.ts +1 -1
- package/packages/harnesses/package.json +2 -1
- package/packages/harnesses/src/util.spec.ts +97 -0
- package/packages/harnesses/src/util.ts +21 -4
- package/packages/harnesses/src/writers/opencode.spec.ts +4 -6
- package/packages/harnesses/src/writers/opencode.ts +23 -27
- 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: "
|
|
13
|
-
label: "
|
|
12
|
+
id: "sensible-defaults",
|
|
13
|
+
label: "Sensible defaults",
|
|
14
14
|
description:
|
|
15
|
-
"
|
|
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: "
|
|
19
|
+
config: { profile: "sensible-defaults" }
|
|
20
20
|
}
|
|
21
21
|
]
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
|
-
id: "
|
|
25
|
-
label: "
|
|
24
|
+
id: "rigid",
|
|
25
|
+
label: "Rigid",
|
|
26
26
|
description:
|
|
27
|
-
"
|
|
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: "
|
|
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
|
|
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
|
-
"
|
|
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: [
|
|
@@ -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.
|
|
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(
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
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
|
-
"*": "
|
|
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
|
-
"
|
|
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": "
|
|
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",
|