gentle-agent-extensions 1.0.1 → 1.2.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/README.md +177 -287
- package/package.json +17 -14
- package/resources/agents/architecture-council.md +158 -0
- package/resources/overlays/orchestrator.overlay.md +162 -0
- package/resources/routing/architecture-council.md +65 -0
- package/resources/skills/architecture-quest/SKILL.md +238 -0
- package/resources/skills/gentle-agent-extension/SKILL.md +329 -0
- package/src/extension.ts +544 -0
- package/.manifest.json +0 -15
- package/agents/.gitkeep +0 -0
- package/assets/.gitkeep +0 -0
- package/docs/conventions.md +0 -278
- package/extensions/gentle-agent-extensions.ts +0 -547
- package/mcp/.gitkeep +0 -31
- package/overlays/.gitkeep +0 -0
- package/routing/.gitkeep +0 -0
- package/scripts/doctor.sh +0 -509
- package/scripts/install.sh +0 -206
- package/scripts/sync.sh +0 -513
- package/skills/gentle-agent-extension/SKILL.md +0 -360
- package/tests/gentle-extensions.test.ts +0 -653
|
@@ -1,653 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync, readdirSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join, resolve, dirname } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import test, { before, after, describe } from "node:test";
|
|
7
|
-
|
|
8
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const PROJECT_ROOT = resolve(__dirname, "..");
|
|
10
|
-
const EXTENSION_PATH = join(PROJECT_ROOT, "extensions/gentle-agent-extensions.ts");
|
|
11
|
-
|
|
12
|
-
const testRoot = mkdtempSync(join(tmpdir(), "gentle-extensions-"));
|
|
13
|
-
after(() => rmSync(testRoot, { recursive: true, force: true }));
|
|
14
|
-
|
|
15
|
-
function createTestProject(overrides: { agents?: string[]; overlays?: string[]; skills?: string[] } = {}) {
|
|
16
|
-
const projectDir = mkdtempSync(join(testRoot, "project-"));
|
|
17
|
-
|
|
18
|
-
mkdirSync(join(projectDir, "agents"), { recursive: true });
|
|
19
|
-
mkdirSync(join(projectDir, "overlays"), { recursive: true });
|
|
20
|
-
mkdirSync(join(projectDir, "routing"), { recursive: true });
|
|
21
|
-
mkdirSync(join(projectDir, "skills"), { recursive: true });
|
|
22
|
-
mkdirSync(join(projectDir, "scripts"), { recursive: true });
|
|
23
|
-
mkdirSync(join(projectDir, "extensions"), { recursive: true });
|
|
24
|
-
|
|
25
|
-
// Copy extension
|
|
26
|
-
const extensionSource = readFileSync(EXTENSION_PATH, "utf8");
|
|
27
|
-
writeFileSync(join(projectDir, "extensions/gentle-agent-extensions.ts"), extensionSource);
|
|
28
|
-
|
|
29
|
-
// Default agents
|
|
30
|
-
if (overrides.agents) {
|
|
31
|
-
for (const agent of overrides.agents) {
|
|
32
|
-
writeFileSync(join(projectDir, `agents/${agent}.md`), `---\nname: ${agent}\ndescription: Test agent\n---\nTest instructions for ${agent}`);
|
|
33
|
-
}
|
|
34
|
-
} else {
|
|
35
|
-
writeFileSync(join(projectDir, "agents/test-agent.md"), `---\nname: test-agent\ndescription: A test agent\n---\nTest instructions`);
|
|
36
|
-
writeFileSync(join(projectDir, "agents/another-agent.md"), `---\nname: another-agent\ndescription: Another test agent\nmodel: anthropic/claude-opus-4\nthinking: high\nmode: background\ntools: [read, write]\n---\nMore instructions`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Default overlays
|
|
40
|
-
if (overrides.overlays) {
|
|
41
|
-
for (const overlay of overrides.overlays) {
|
|
42
|
-
writeFileSync(join(projectDir, `overlays/${overlay}.overlay.md`), `---\ntarget: ${overlay}\npriority: 100\nmode: append\n---\nOverlay instructions for ${overlay}`);
|
|
43
|
-
}
|
|
44
|
-
} else {
|
|
45
|
-
writeFileSync(join(projectDir, "overlays/orchestrator.overlay.md"), `---\ntarget: orchestrator\npriority: 100\nmode: append\n---\nOrchestrator overlay instructions`);
|
|
46
|
-
writeFileSync(join(projectDir, "overlays/test-agent.overlay.md"), `---\ntarget: test-agent\npriority: 50\nmode: append\n---\nTest agent overlay instructions`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Default skill
|
|
50
|
-
if (overrides.skills) {
|
|
51
|
-
for (const skill of overrides.skills) {
|
|
52
|
-
mkdirSync(join(projectDir, `skills/${skill}`), { recursive: true });
|
|
53
|
-
writeFileSync(join(projectDir, `skills/${skill}/SKILL.md`), `---\nname: ${skill}\ndescription: Test skill\n---\n\n## Activation Contract\nTest\n\n## Hard Rules\nTest\n\n## Decision Gates\nTest\n\n## Execution Steps\nTest\n\n## Output Contract\nTest\n\n## References\nTest`);
|
|
54
|
-
}
|
|
55
|
-
} else {
|
|
56
|
-
mkdirSync(join(projectDir, "skills/test-skill"), { recursive: true });
|
|
57
|
-
writeFileSync(join(projectDir, "skills/test-skill/SKILL.md"), `---\nname: test-skill\ndescription: Test skill\n---\n\n## Activation Contract\nTest\n\n## Hard Rules\nTest\n\n## Decision Gates\nTest\n\n## Execution Steps\nTest\n\n## Output Contract\nTest\n\n## References\nTest`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Manifest
|
|
61
|
-
writeFileSync(join(projectDir, ".manifest.json"), JSON.stringify({
|
|
62
|
-
version: "1.0",
|
|
63
|
-
managedPaths: { extensions: [], agents: [], routing: [], overlays: [], skills: [], scripts: [], assets: [] },
|
|
64
|
-
installProfiles: {}
|
|
65
|
-
}, null, 2));
|
|
66
|
-
|
|
67
|
-
// Scripts
|
|
68
|
-
writeFileSync(join(projectDir, "scripts/sync.sh"), "#!/bin/bash\necho 'sync stub'");
|
|
69
|
-
writeFileSync(join(projectDir, "scripts/doctor.sh"), "#!/bin/bash\necho 'doctor stub'");
|
|
70
|
-
|
|
71
|
-
return projectDir;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function createAgentHome() {
|
|
75
|
-
const agentHome = mkdtempSync(join(testRoot, "agent-home-"));
|
|
76
|
-
mkdirSync(join(agentHome, "agents"), { recursive: true });
|
|
77
|
-
mkdirSync(join(agentHome, "subagents"), { recursive: true });
|
|
78
|
-
mkdirSync(join(agentHome, "extensions"), { recursive: true });
|
|
79
|
-
return agentHome;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
describe("gentle-agent-extensions project structure", () => {
|
|
83
|
-
test("project directories exist", () => {
|
|
84
|
-
assert.ok(existsSync(join(PROJECT_ROOT, "agents")), "agents/ exists");
|
|
85
|
-
assert.ok(existsSync(join(PROJECT_ROOT, "overlays")), "overlays/ exists");
|
|
86
|
-
assert.ok(existsSync(join(PROJECT_ROOT, "routing")), "routing/ exists");
|
|
87
|
-
assert.ok(existsSync(join(PROJECT_ROOT, "skills")), "skills/ exists");
|
|
88
|
-
assert.ok(existsSync(join(PROJECT_ROOT, "scripts")), "scripts/ exists");
|
|
89
|
-
assert.ok(existsSync(join(PROJECT_ROOT, "extensions")), "extensions/ exists");
|
|
90
|
-
assert.ok(existsSync(join(PROJECT_ROOT, "tests")), "tests/ exists");
|
|
91
|
-
assert.ok(existsSync(join(PROJECT_ROOT, "docs")), "docs/ exists");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("extension file exists and is valid TypeScript", () => {
|
|
95
|
-
assert.ok(existsSync(EXTENSION_PATH), "extension file exists");
|
|
96
|
-
const content = readFileSync(EXTENSION_PATH, "utf8");
|
|
97
|
-
assert.ok(content.includes("gentleAgentExtensions"), "exports main function");
|
|
98
|
-
assert.ok(content.includes("before_agent_start"), "handles before_agent_start");
|
|
99
|
-
assert.ok(content.includes("appendSystemPromptOnce"), "uses appendSystemPromptOnce");
|
|
100
|
-
assert.ok(content.includes("session_start"), "handles session_start");
|
|
101
|
-
assert.ok(content.includes("gentle-extensions:sync"), "registers sync command");
|
|
102
|
-
assert.ok(content.includes("gentle-extensions:doctor"), "registers doctor command");
|
|
103
|
-
assert.ok(content.includes("gentle-extensions:status"), "registers status command");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("manifest file exists", () => {
|
|
107
|
-
assert.ok(existsSync(join(PROJECT_ROOT, ".manifest.json")), "manifest exists");
|
|
108
|
-
const manifest = JSON.parse(readFileSync(join(PROJECT_ROOT, ".manifest.json"), "utf8"));
|
|
109
|
-
assert.equal(manifest.version, "1.0");
|
|
110
|
-
assert.ok(manifest.managedPaths);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test("scripts are executable", () => {
|
|
114
|
-
assert.ok(existsSync(join(PROJECT_ROOT, "scripts/install.sh")), "install.sh exists");
|
|
115
|
-
assert.ok(existsSync(join(PROJECT_ROOT, "scripts/sync.sh")), "sync.sh exists");
|
|
116
|
-
assert.ok(existsSync(join(PROJECT_ROOT, "scripts/doctor.sh")), "doctor.sh exists");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("conventions documentation exists", () => {
|
|
120
|
-
assert.ok(existsSync(join(PROJECT_ROOT, "docs/conventions.md")), "conventions.md exists");
|
|
121
|
-
const content = readFileSync(join(PROJECT_ROOT, "docs/conventions.md"), "utf8");
|
|
122
|
-
assert.ok(content.includes("agents/"), "documents agents directory");
|
|
123
|
-
assert.ok(content.includes("overlays/"), "documents overlays directory");
|
|
124
|
-
assert.ok(content.includes("routing/"), "documents routing directory");
|
|
125
|
-
assert.ok(content.includes("skills/"), "documents skills directory");
|
|
126
|
-
assert.ok(content.includes("appendSystemPrompt"), "documents overlay mechanism");
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe("extension agent discovery", () => {
|
|
131
|
-
let projectDir: string;
|
|
132
|
-
let agentHome: string;
|
|
133
|
-
|
|
134
|
-
before(() => {
|
|
135
|
-
projectDir = createTestProject();
|
|
136
|
-
agentHome = createAgentHome();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("discovers agents from project agents/ directory", () => {
|
|
140
|
-
const agentsDir = join(projectDir, "agents");
|
|
141
|
-
const files = readdirSync(agentsDir);
|
|
142
|
-
const agentFiles = files.filter((f: string) => f.endsWith(".md"));
|
|
143
|
-
assert.ok(agentFiles.length >= 2, "at least 2 agent files");
|
|
144
|
-
assert.ok(agentFiles.includes("test-agent.md"), "has test-agent.md");
|
|
145
|
-
assert.ok(agentFiles.includes("another-agent.md"), "has another-agent.md");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("discovers overlays from project overlays/ directory", () => {
|
|
149
|
-
const overlaysDir = join(projectDir, "overlays");
|
|
150
|
-
const files = readdirSync(overlaysDir);
|
|
151
|
-
const overlayFiles = files.filter((f: string) => f.endsWith(".overlay.md"));
|
|
152
|
-
assert.ok(overlayFiles.length >= 2, "at least 2 overlay files");
|
|
153
|
-
assert.ok(overlayFiles.includes("orchestrator.overlay.md"), "has orchestrator overlay");
|
|
154
|
-
assert.ok(overlayFiles.includes("test-agent.overlay.md"), "has test-agent overlay");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("discovers skills from project skills/ directory", () => {
|
|
158
|
-
const skillsDir = join(projectDir, "skills");
|
|
159
|
-
const dirs = readdirSync(skillsDir, { withFileTypes: true })
|
|
160
|
-
.filter((d: any) => d.isDirectory())
|
|
161
|
-
.map((d: any) => d.name);
|
|
162
|
-
assert.ok(dirs.includes("test-skill"), "has test-skill directory");
|
|
163
|
-
assert.ok(existsSync(join(projectDir, "skills/test-skill/SKILL.md")), "skill has SKILL.md");
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe("overlay parsing", () => {
|
|
168
|
-
test("parses overlay frontmatter correctly", () => {
|
|
169
|
-
const overlayContent = `---\ntarget: test-agent\npriority: 75\nmode: append\n---\nOverlay body content`;
|
|
170
|
-
|
|
171
|
-
const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(overlayContent);
|
|
172
|
-
assert.ok(fmMatch, "frontmatter regex matches");
|
|
173
|
-
|
|
174
|
-
const [, fm, body] = fmMatch!;
|
|
175
|
-
const targetMatch = /target:\s*([^\n]+)/.exec(fm);
|
|
176
|
-
const priorityMatch = /priority:\s*(\d+)/.exec(fm);
|
|
177
|
-
const modeMatch = /mode:\s*(\w+)/.exec(fm);
|
|
178
|
-
|
|
179
|
-
assert.equal(targetMatch![1].trim(), "test-agent");
|
|
180
|
-
assert.equal(parseInt(priorityMatch![1], 10), 75);
|
|
181
|
-
assert.equal(modeMatch![1].trim(), "append");
|
|
182
|
-
assert.equal(body.trim(), "Overlay body content");
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("handles missing optional fields with defaults", () => {
|
|
186
|
-
const overlayContent = `---\ntarget: orchestrator\n---\nBody only`;
|
|
187
|
-
|
|
188
|
-
const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(overlayContent);
|
|
189
|
-
const [, fm] = fmMatch!;
|
|
190
|
-
|
|
191
|
-
const targetMatch = /target:\s*([^\n]+)/.exec(fm);
|
|
192
|
-
const priorityMatch = /priority:\s*(\d+)/.exec(fm);
|
|
193
|
-
const modeMatch = /mode:\s*(\w+)/.exec(fm);
|
|
194
|
-
|
|
195
|
-
assert.equal(targetMatch![1].trim(), "orchestrator");
|
|
196
|
-
assert.equal(priorityMatch, null);
|
|
197
|
-
assert.equal(modeMatch, null);
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
describe("appendSystemPromptOnce", () => {
|
|
202
|
-
test("appends text to empty options", () => {
|
|
203
|
-
const options: { appendSystemPrompt?: string } = {};
|
|
204
|
-
const text = "Test prompt";
|
|
205
|
-
|
|
206
|
-
const normalized = text.replace(/^\n+/, "");
|
|
207
|
-
const current = options.appendSystemPrompt ?? "";
|
|
208
|
-
if (!current.includes(normalized)) {
|
|
209
|
-
options.appendSystemPrompt = current ? `${current}\n\n${normalized}` : normalized;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
assert.equal(options.appendSystemPrompt, "Test prompt");
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("does not duplicate existing text", () => {
|
|
216
|
-
const options: { appendSystemPrompt?: string } = { appendSystemPrompt: "Existing\n\nTest prompt" };
|
|
217
|
-
const text = "Test prompt";
|
|
218
|
-
|
|
219
|
-
const normalized = text.replace(/^\n+/, "");
|
|
220
|
-
const current = options.appendSystemPrompt ?? "";
|
|
221
|
-
if (!current.includes(normalized)) {
|
|
222
|
-
options.appendSystemPrompt = current ? `${current}\n\n${normalized}` : normalized;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
assert.equal(options.appendSystemPrompt, "Existing\n\nTest prompt");
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
test("handles leading newlines", () => {
|
|
229
|
-
const options: { appendSystemPrompt?: string } = { appendSystemPrompt: "Existing" };
|
|
230
|
-
const text = "\n\nNew prompt";
|
|
231
|
-
|
|
232
|
-
const normalized = text.replace(/^\n+/, "");
|
|
233
|
-
const current = options.appendSystemPrompt ?? "";
|
|
234
|
-
if (!current.includes(normalized)) {
|
|
235
|
-
options.appendSystemPrompt = current ? `${current}\n\n${normalized}` : normalized;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
assert.equal(options.appendSystemPrompt, "Existing\n\nNew prompt");
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
test("handles null/undefined options", () => {
|
|
242
|
-
let options: { appendSystemPrompt?: string } | null = null;
|
|
243
|
-
const text = "Test";
|
|
244
|
-
|
|
245
|
-
const normalized = text.replace(/^\n+/, "");
|
|
246
|
-
if (options && normalized) {
|
|
247
|
-
const current = options.appendSystemPrompt ?? "";
|
|
248
|
-
if (!current.includes(normalized)) {
|
|
249
|
-
options.appendSystemPrompt = current ? `${current}\n\n${normalized}` : normalized;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
assert.equal(options, null);
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
describe("sync.sh script behavior", () => {
|
|
258
|
-
let projectDir: string;
|
|
259
|
-
let agentHome: string;
|
|
260
|
-
|
|
261
|
-
before(() => {
|
|
262
|
-
projectDir = createTestProject();
|
|
263
|
-
agentHome = createAgentHome();
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
test("sync.sh validates agent frontmatter", () => {
|
|
267
|
-
writeFileSync(join(projectDir, "agents/bad-agent.md"), "No frontmatter here");
|
|
268
|
-
|
|
269
|
-
const content = readFileSync(join(projectDir, "agents/bad-agent.md"), "utf8");
|
|
270
|
-
assert.ok(!content.startsWith("---"), "agent missing frontmatter delimiter");
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
test("sync.sh validates overlay targets", () => {
|
|
274
|
-
writeFileSync(join(projectDir, "overlays/unknown-target.overlay.md"), `---\ntarget: nonexistent-agent\n---\nOverlay`);
|
|
275
|
-
|
|
276
|
-
const content = readFileSync(join(projectDir, "overlays/unknown-target.overlay.md"), "utf8");
|
|
277
|
-
const targetMatch = /target:\s*([^\n]+)/.exec(content);
|
|
278
|
-
assert.equal(targetMatch![1].trim(), "nonexistent-agent");
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
test("sync.sh validates skill structure", () => {
|
|
282
|
-
mkdirSync(join(projectDir, "skills/bad-skill"), { recursive: true });
|
|
283
|
-
writeFileSync(join(projectDir, "skills/bad-skill/SKILL.md"), `# Bad Skill\n\nMissing sections`);
|
|
284
|
-
|
|
285
|
-
const content = readFileSync(join(projectDir, "skills/bad-skill/SKILL.md"), "utf8");
|
|
286
|
-
assert.ok(!content.includes("## Activation Contract"), "missing Activation Contract");
|
|
287
|
-
assert.ok(!content.includes("## Hard Rules"), "missing Hard Rules");
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
describe("skill registry integration", () => {
|
|
292
|
-
test("skill follows gentle-ai skill style guide", () => {
|
|
293
|
-
const skillPath = join(PROJECT_ROOT, "skills/gentle-agent-extension/SKILL.md");
|
|
294
|
-
assert.ok(existsSync(skillPath), "project skill exists");
|
|
295
|
-
|
|
296
|
-
const content = readFileSync(skillPath, "utf8");
|
|
297
|
-
|
|
298
|
-
// Check frontmatter
|
|
299
|
-
assert.ok(content.startsWith("---"), "has frontmatter");
|
|
300
|
-
assert.ok(content.includes("name: gentle-agent-extension"), "correct name");
|
|
301
|
-
assert.ok(content.includes("description:"), "has description");
|
|
302
|
-
assert.ok(content.includes("license: Apache-2.0"), "has license");
|
|
303
|
-
|
|
304
|
-
// Check required sections
|
|
305
|
-
const sections = [
|
|
306
|
-
"## Activation Contract",
|
|
307
|
-
"## Hard Rules",
|
|
308
|
-
"## Decision Gates",
|
|
309
|
-
"## Execution Steps",
|
|
310
|
-
"## Output Contract",
|
|
311
|
-
"## References"
|
|
312
|
-
];
|
|
313
|
-
|
|
314
|
-
for (const section of sections) {
|
|
315
|
-
assert.ok(content.includes(section), `has section: ${section}`);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Check procedures
|
|
319
|
-
assert.ok(content.includes("Procedure A: Add a New Agent"), "has Procedure A");
|
|
320
|
-
assert.ok(content.includes("Procedure B: Add an Overlay"), "has Procedure B");
|
|
321
|
-
|
|
322
|
-
// Check references to local files
|
|
323
|
-
assert.ok(content.includes("docs/conventions.md"), "references conventions");
|
|
324
|
-
assert.ok(content.includes("extensions/gentle-agent-extensions.ts"), "references extension");
|
|
325
|
-
assert.ok(content.includes("scripts/install.sh"), "references install script");
|
|
326
|
-
assert.ok(content.includes("scripts/sync.sh"), "references sync script");
|
|
327
|
-
assert.ok(content.includes("scripts/doctor.sh"), "references doctor script");
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
test("skill Activation Contract defines when to use", () => {
|
|
331
|
-
const skillPath = join(PROJECT_ROOT, "skills/gentle-agent-extension/SKILL.md");
|
|
332
|
-
const content = readFileSync(skillPath, "utf8");
|
|
333
|
-
|
|
334
|
-
assert.ok(content.includes("Use this skill when:"), "defines when to use");
|
|
335
|
-
assert.ok(content.includes("Do NOT use this skill for:"), "defines when not to use");
|
|
336
|
-
assert.ok(content.includes("Creating a new custom agent"), "lists agent creation");
|
|
337
|
-
assert.ok(content.includes("Adding an overlay"), "lists overlay creation");
|
|
338
|
-
assert.ok(content.includes("Defining routing rules"), "lists routing");
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
describe("manifest tracking", () => {
|
|
343
|
-
test("manifest tracks managed paths by category", () => {
|
|
344
|
-
const manifestPath = join(PROJECT_ROOT, ".manifest.json");
|
|
345
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
346
|
-
|
|
347
|
-
assert.ok(Array.isArray(manifest.managedPaths.extensions), "has extensions array");
|
|
348
|
-
assert.ok(Array.isArray(manifest.managedPaths.agents), "has agents array");
|
|
349
|
-
assert.ok(Array.isArray(manifest.managedPaths.routing), "has routing array");
|
|
350
|
-
assert.ok(Array.isArray(manifest.managedPaths.overlays), "has overlays array");
|
|
351
|
-
assert.ok(Array.isArray(manifest.managedPaths.skills), "has skills array");
|
|
352
|
-
assert.ok(Array.isArray(manifest.managedPaths.scripts), "has scripts array");
|
|
353
|
-
assert.ok(Array.isArray(manifest.managedPaths.assets), "has assets array");
|
|
354
|
-
});
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
describe("installation profiles", () => {
|
|
358
|
-
test("install.sh supports pi profile", () => {
|
|
359
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/install.sh"), "utf8");
|
|
360
|
-
assert.ok(script.includes("pi)"), "handles pi profile");
|
|
361
|
-
assert.ok(script.includes('AGENT_HOME="${HOME}/.pi/agent"'), "sets pi agent home");
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
test("install.sh supports gentle-shell profile", () => {
|
|
365
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/install.sh"), "utf8");
|
|
366
|
-
assert.ok(script.includes("gentle-shell)"), "handles gentle-shell profile");
|
|
367
|
-
assert.ok(script.includes('AGENT_HOME="${HOME}/.local/share/gentle-shell/agent"'), "sets gentle-shell agent home");
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
test("install.sh supports gentle-shell-linked profile", () => {
|
|
371
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/install.sh"), "utf8");
|
|
372
|
-
assert.ok(script.includes("gentle-shell-linked)"), "handles gentle-shell-linked profile");
|
|
373
|
-
assert.ok(script.includes('AGENT_HOME="${HOME}/.pi/agent"'), "sets linked agent home");
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
test("install.sh supports --dry-run", () => {
|
|
377
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/install.sh"), "utf8");
|
|
378
|
-
assert.ok(script.includes("--dry-run"), "has dry-run option");
|
|
379
|
-
assert.ok(script.includes("DRY_RUN=true"), "sets dry-run flag");
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
test("sync.sh supports --dry-run and --check", () => {
|
|
383
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/sync.sh"), "utf8");
|
|
384
|
-
assert.ok(script.includes("--dry-run"), "has dry-run option");
|
|
385
|
-
assert.ok(script.includes("--check"), "has check option");
|
|
386
|
-
assert.ok(script.includes("DRY_RUN=true"), "sets dry-run flag");
|
|
387
|
-
assert.ok(script.includes("CHECK_MODE=true"), "sets check flag");
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
test("doctor.sh supports --verbose", () => {
|
|
391
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/doctor.sh"), "utf8");
|
|
392
|
-
assert.ok(script.includes("--verbose"), "has verbose option");
|
|
393
|
-
assert.ok(script.includes("VERBOSE=true"), "sets verbose flag");
|
|
394
|
-
});
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
describe("idempotency guarantees", () => {
|
|
398
|
-
test("sync.sh is idempotent - no changes on repeated runs", () => {
|
|
399
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/sync.sh"), "utf8");
|
|
400
|
-
assert.ok(script.includes("cmp -s"), "uses cmp to check for changes");
|
|
401
|
-
assert.ok(script.includes("Unchanged:"), "reports unchanged files");
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
test("install.sh refuses to overwrite without --force", () => {
|
|
405
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/install.sh"), "utf8");
|
|
406
|
-
assert.ok(script.includes("FORCE=false"), "default no force");
|
|
407
|
-
assert.ok(script.includes("if [[ -f \"$EXTENSION_TARGET\" && \"$FORCE\" == \"false\""), "checks existing file");
|
|
408
|
-
});
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
describe("no official file modification", () => {
|
|
412
|
-
test("scripts only write to project-owned locations", () => {
|
|
413
|
-
const syncScript = readFileSync(join(PROJECT_ROOT, "scripts/sync.sh"), "utf8");
|
|
414
|
-
const installScript = readFileSync(join(PROJECT_ROOT, "scripts/install.sh"), "utf8");
|
|
415
|
-
|
|
416
|
-
assert.ok(syncScript.includes("PROJECT_AGENTS_DIR"), "uses project agent dir");
|
|
417
|
-
assert.ok(syncScript.includes(".pi/agents"), "targets project directory");
|
|
418
|
-
|
|
419
|
-
// install.sh uses EXTENSION_TARGET="${AGENT_HOME}/extensions/gentle-agent-extensions.ts"
|
|
420
|
-
assert.ok(installScript.includes('EXTENSION_TARGET="${AGENT_HOME}/extensions/gentle-agent-extensions.ts"'), "targets extensions dir");
|
|
421
|
-
|
|
422
|
-
assert.ok(!syncScript.includes("subagents.json"), "does not modify subagents.json");
|
|
423
|
-
assert.ok(!syncScript.includes(".atl/skill-registry.md"), "does not modify skill registry directly");
|
|
424
|
-
});
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
describe("doctor.sh checks", () => {
|
|
428
|
-
test("checks project structure", () => {
|
|
429
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/doctor.sh"), "utf8");
|
|
430
|
-
assert.ok(script.includes('for dir in "agents" "overlays" "routing" "skills"'), "checks all dirs");
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
test("checks manifest", () => {
|
|
434
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/doctor.sh"), "utf8");
|
|
435
|
-
assert.ok(script.includes("MANIFEST="), "checks manifest");
|
|
436
|
-
assert.ok(script.includes(".manifest.json"), "correct manifest name");
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
test("checks extension installation", () => {
|
|
440
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/doctor.sh"), "utf8");
|
|
441
|
-
assert.ok(script.includes("EXTENSION_TARGET"), "checks extension target");
|
|
442
|
-
assert.ok(script.includes("gentle-agent-extensions.ts"), "correct extension name");
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
test("checks Pi/Gentle Pi installation", () => {
|
|
446
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/doctor.sh"), "utf8");
|
|
447
|
-
assert.ok(script.includes("command -v pi"), "checks pi command");
|
|
448
|
-
assert.ok(script.includes("gentle-shell"), "checks gentle-shell");
|
|
449
|
-
assert.ok(script.includes("gentle-pi"), "checks gentle-pi package");
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
test("validates agent frontmatter", () => {
|
|
453
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/doctor.sh"), "utf8");
|
|
454
|
-
assert.ok(script.includes("grep -q \"^name:\""), "checks agent name");
|
|
455
|
-
assert.ok(script.includes("grep -q \"^description:\""), "checks agent description");
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
test("validates overlay structure", () => {
|
|
459
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/doctor.sh"), "utf8");
|
|
460
|
-
assert.ok(script.includes("grep -q \"^target:\""), "checks overlay target");
|
|
461
|
-
assert.ok(script.includes("grep -q \"^mode:\""), "checks overlay mode");
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
test("validates skill structure", () => {
|
|
465
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/doctor.sh"), "utf8");
|
|
466
|
-
assert.ok(script.includes("Activation Contract"), "checks skill sections");
|
|
467
|
-
assert.ok(script.includes("Hard Rules"), "checks Hard Rules");
|
|
468
|
-
assert.ok(script.includes("name:"), "checks skill frontmatter name");
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
test("checks for conflicts", () => {
|
|
472
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/doctor.sh"), "utf8");
|
|
473
|
-
assert.ok(script.includes("GLOBAL_AGENTS"), "tracks global agents");
|
|
474
|
-
assert.ok(script.includes("shadows global agent"), "detects shadowing");
|
|
475
|
-
assert.ok(script.includes("targets unknown agent"), "detects unknown overlay targets");
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
test("checks skill registry", () => {
|
|
479
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/doctor.sh"), "utf8");
|
|
480
|
-
assert.ok(script.includes(".atl/skill-registry.md"), "checks skill registry");
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
test("checks script permissions", () => {
|
|
484
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/doctor.sh"), "utf8");
|
|
485
|
-
assert.ok(script.includes("for script in install.sh sync.sh doctor.sh"), "checks all scripts");
|
|
486
|
-
assert.ok(script.includes("-x"), "checks executable bit");
|
|
487
|
-
});
|
|
488
|
-
});
|
|
489
|
-
describe("MCP server support", () => {
|
|
490
|
-
let projectDir: string;
|
|
491
|
-
let agentHome: string;
|
|
492
|
-
|
|
493
|
-
before(() => {
|
|
494
|
-
projectDir = createTestProject();
|
|
495
|
-
agentHome = createAgentHome();
|
|
496
|
-
|
|
497
|
-
// Add MCP directory and test MCP definitions
|
|
498
|
-
mkdirSync(join(projectDir, "mcp"), { recursive: true });
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
test("mcp/ directory exists", () => {
|
|
502
|
-
assert.ok(existsSync(join(PROJECT_ROOT, "mcp")), "mcp/ directory exists");
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
test("extension loads MCP server definitions", () => {
|
|
506
|
-
const extContent = readFileSync(EXTENSION_PATH, "utf8");
|
|
507
|
-
assert.ok(extContent.includes("MCP_DIR"), "defines MCP_DIR constant");
|
|
508
|
-
assert.ok(extContent.includes("loadMcpServers"), "has loadMcpServers function");
|
|
509
|
-
assert.ok(extContent.includes("registerMcpServer"), "uses pi.registerMcpServer API");
|
|
510
|
-
assert.ok(extContent.includes("buildMcpOrchestratorInstructions"), "builds MCP instructions for orchestrator");
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
test("MCP server definition schema validation", () => {
|
|
514
|
-
// Test valid stdio MCP
|
|
515
|
-
const validStdio = {
|
|
516
|
-
name: "test-stdio",
|
|
517
|
-
description: "Test stdio server",
|
|
518
|
-
transport: "stdio",
|
|
519
|
-
command: "npx",
|
|
520
|
-
args: ["-y", "@test/server"],
|
|
521
|
-
exposure: "codemode",
|
|
522
|
-
enabled: true
|
|
523
|
-
};
|
|
524
|
-
assert.ok(validStdio.name.match(/^[A-Za-z0-9_-]+$/), "name format valid");
|
|
525
|
-
assert.ok(["stdio", "http"].includes(validStdio.transport), "transport valid");
|
|
526
|
-
|
|
527
|
-
// Test valid HTTP MCP
|
|
528
|
-
const validHttp = {
|
|
529
|
-
name: "test-http",
|
|
530
|
-
description: "Test HTTP server",
|
|
531
|
-
transport: "http",
|
|
532
|
-
url: "https://example.com/mcp",
|
|
533
|
-
exposure: "deferred",
|
|
534
|
-
enabled: true
|
|
535
|
-
};
|
|
536
|
-
assert.ok(validHttp.name.match(/^[A-Za-z0-9_-]+$/), "name format valid");
|
|
537
|
-
assert.ok(["stdio", "http"].includes(validHttp.transport), "transport valid");
|
|
538
|
-
|
|
539
|
-
// Test invalid name
|
|
540
|
-
const invalidName = { ...validStdio, name: "test server" };
|
|
541
|
-
assert.ok(!invalidName.name.match(/^[A-Za-z0-9_-]+$/), "name with space invalid");
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
test("MCP server config has required fields", () => {
|
|
545
|
-
const requiredFields = ["name", "description", "transport"];
|
|
546
|
-
for (const field of requiredFields) {
|
|
547
|
-
assert.ok(true, `required field: ${field}`);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// stdio requires command
|
|
551
|
-
const stdioServer = { transport: "stdio" };
|
|
552
|
-
assert.ok(!stdioServer.command, "stdio missing command");
|
|
553
|
-
|
|
554
|
-
// http requires url
|
|
555
|
-
const httpServer = { transport: "http" };
|
|
556
|
-
assert.ok(!httpServer.url, "http missing url");
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
test("MCP server exposure values", () => {
|
|
560
|
-
const validExposures = ["codemode", "direct", "deferred", "hidden"];
|
|
561
|
-
for (const exp of validExposures) {
|
|
562
|
-
assert.ok(validExposures.includes(exp), `exposure ${exp} is valid`);
|
|
563
|
-
}
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
test("MCP server env var substitution syntax", () => {
|
|
567
|
-
const envWithSubstitution = { API_KEY: "${API_KEY}" };
|
|
568
|
-
const envHardcoded = { API_KEY: "sk-12345" };
|
|
569
|
-
|
|
570
|
-
assert.ok(envWithSubstitution.API_KEY.startsWith("${"), "uses substitution syntax");
|
|
571
|
-
assert.ok(!envHardcoded.API_KEY.startsWith("${"), "hardcoded secret detected");
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
test("sync.sh validates MCP definitions", () => {
|
|
575
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/sync.sh"), "utf8");
|
|
576
|
-
assert.ok(script.includes("MCP_SRC"), "defines MCP_SRC");
|
|
577
|
-
assert.ok(script.includes("Validating MCP servers"), "validates MCP servers");
|
|
578
|
-
assert.ok(script.includes("jq empty"), "validates JSON syntax");
|
|
579
|
-
assert.ok(script.includes(".name // empty"), "checks name field");
|
|
580
|
-
assert.ok(script.includes(".description // empty"), "checks description field");
|
|
581
|
-
assert.ok(script.includes(".transport // empty"), "checks transport field");
|
|
582
|
-
assert.ok(script.includes("stdio transport requires"), "validates stdio command");
|
|
583
|
-
assert.ok(script.includes("http transport requires"), "validates http url");
|
|
584
|
-
assert.ok(script.includes("MCP server name conflicts"), "checks mcp.json conflicts");
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
test("doctor.sh validates MCP definitions", () => {
|
|
588
|
-
const script = readFileSync(join(PROJECT_ROOT, "scripts/doctor.sh"), "utf8");
|
|
589
|
-
assert.ok(script.includes("Validating MCP servers"), "validates MCP servers");
|
|
590
|
-
assert.ok(script.includes("jq empty"), "validates JSON syntax");
|
|
591
|
-
assert.ok(script.includes("has name field"), "checks name field");
|
|
592
|
-
assert.ok(script.includes("has description"), "checks description field");
|
|
593
|
-
assert.ok(script.includes("transport="), "checks transport field");
|
|
594
|
-
assert.ok(script.includes("has command"), "checks stdio command");
|
|
595
|
-
assert.ok(script.includes("has url"), "checks http url");
|
|
596
|
-
assert.ok(script.includes("name format valid"), "validates name format");
|
|
597
|
-
assert.ok(script.includes("may contain hardcoded secret"), "checks for secrets");
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
test("skill includes MCP procedures", () => {
|
|
601
|
-
const skillPath = join(PROJECT_ROOT, "skills/gentle-agent-extension/SKILL.md");
|
|
602
|
-
const content = readFileSync(skillPath, "utf8");
|
|
603
|
-
|
|
604
|
-
assert.ok(content.includes("External tool/API integration via MCP"), "has MCP in decision gates");
|
|
605
|
-
assert.ok(content.includes("mcp/{name}.mcp.json"), "references MCP location");
|
|
606
|
-
assert.ok(content.includes("Procedure C: Add an MCP Server"), "has Procedure C");
|
|
607
|
-
assert.ok(content.includes("Procedure D: Add MCP Dependencies"), "has Procedure D");
|
|
608
|
-
assert.ok(content.includes("mcpDependencies"), "references mcpDependencies in routing");
|
|
609
|
-
assert.ok(content.includes("MCP definition in"), "output contract includes MCP");
|
|
610
|
-
assert.ok(content.includes("MCP server registered"), "output contract verifies MCP registration");
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
test("conventions.md documents MCP conventions", () => {
|
|
614
|
-
const convPath = join(PROJECT_ROOT, "docs/conventions.md");
|
|
615
|
-
const content = readFileSync(convPath, "utf8");
|
|
616
|
-
|
|
617
|
-
assert.ok(content.includes("mcp/" ), "documents mcp directory");
|
|
618
|
-
assert.ok(content.includes("MCP Server Definitions"), "has MCP section");
|
|
619
|
-
assert.ok(content.includes("*.mcp.json"), "documents file naming");
|
|
620
|
-
assert.ok(content.includes("transport"), "documents transport field");
|
|
621
|
-
assert.ok(content.includes("exposure"), "documents exposure field");
|
|
622
|
-
assert.ok(content.includes("agentDependencies"), "documents agentDependencies");
|
|
623
|
-
assert.ok(content.includes("mcpDependencies"), "documents mcpDependencies in routing");
|
|
624
|
-
assert.ok(content.includes("pi.registerMcpServer"), "documents registration API");
|
|
625
|
-
assert.ok(content.includes("managedPaths" ), "manifest includes mcp");
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
test("manifest tracks MCP files", () => {
|
|
629
|
-
const manifestPath = join(PROJECT_ROOT, ".manifest.json");
|
|
630
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
631
|
-
|
|
632
|
-
assert.ok(Array.isArray(manifest.managedPaths.mcp), "has mcp array in manifest");
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
test("extension status command shows MCP servers", () => {
|
|
636
|
-
const extContent = readFileSync(EXTENSION_PATH, "utf8");
|
|
637
|
-
assert.ok(extContent.includes("mcpServers"), "status includes MCP servers");
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
test("MCP server auth types", () => {
|
|
641
|
-
const validAuthTypes = ["oauth", "bearer", "none"];
|
|
642
|
-
for (const type of validAuthTypes) {
|
|
643
|
-
assert.ok(validAuthTypes.includes(type), `auth type ${type} is valid`);
|
|
644
|
-
}
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
test("MCP server capabilities", () => {
|
|
648
|
-
const validCapabilities = ["tools", "resources"];
|
|
649
|
-
for (const cap of validCapabilities) {
|
|
650
|
-
assert.ok(validCapabilities.includes(cap), `capability ${cap} is valid`);
|
|
651
|
-
}
|
|
652
|
-
});
|
|
653
|
-
});
|