agentboot 0.1.0 → 0.3.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 +9 -8
- package/agentboot.config.json +4 -1
- package/package.json +2 -2
- package/scripts/cli.ts +465 -18
- package/scripts/compile.ts +724 -75
- package/scripts/dev-sync.ts +1 -1
- package/scripts/lib/config.ts +259 -1
- package/scripts/lib/frontmatter.ts +3 -1
- package/scripts/validate.ts +12 -7
- package/website/docusaurus.config.ts +117 -0
- package/website/package-lock.json +18448 -0
- package/website/package.json +47 -0
- package/website/sidebars.ts +53 -0
- package/website/src/css/custom.css +23 -0
- package/website/src/pages/index.module.css +23 -0
- package/website/src/pages/index.tsx +125 -0
- package/website/static/.nojekyll +0 -0
- package/website/static/CNAME +1 -0
- package/website/static/img/favicon.ico +0 -0
- package/website/static/img/logo.svg +1 -0
- package/.github/ISSUE_TEMPLATE/persona-request.md +0 -62
- package/.github/ISSUE_TEMPLATE/quality-feedback.md +0 -67
- package/.github/workflows/cla.yml +0 -25
- package/.github/workflows/validate.yml +0 -49
- package/.idea/agentboot.iml +0 -9
- package/.idea/misc.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
- package/CLAUDE.md +0 -230
- package/CONTRIBUTING.md +0 -168
- package/PERSONAS.md +0 -156
- package/core/instructions/baseline.instructions.md +0 -133
- package/core/instructions/security.instructions.md +0 -186
- package/core/personas/code-reviewer/SKILL.md +0 -175
- package/core/personas/security-reviewer/SKILL.md +0 -233
- package/core/personas/test-data-expert/SKILL.md +0 -234
- package/core/personas/test-generator/SKILL.md +0 -262
- package/core/traits/audit-trail.md +0 -182
- package/core/traits/confidence-signaling.md +0 -172
- package/core/traits/critical-thinking.md +0 -129
- package/core/traits/schema-awareness.md +0 -132
- package/core/traits/source-citation.md +0 -174
- package/core/traits/structured-output.md +0 -199
- package/docs/ci-cd-automation.md +0 -548
- package/docs/claude-code-reference/README.md +0 -21
- package/docs/claude-code-reference/agentboot-coverage.md +0 -484
- package/docs/claude-code-reference/feature-inventory.md +0 -906
- package/docs/cli-commands-audit.md +0 -112
- package/docs/cli-design.md +0 -924
- package/docs/concepts.md +0 -1117
- package/docs/config-schema-audit.md +0 -121
- package/docs/configuration.md +0 -645
- package/docs/delivery-methods.md +0 -758
- package/docs/developer-onboarding.md +0 -342
- package/docs/extending.md +0 -448
- package/docs/getting-started.md +0 -298
- package/docs/knowledge-layer.md +0 -464
- package/docs/marketplace.md +0 -822
- package/docs/org-connection.md +0 -570
- package/docs/plans/architecture.md +0 -2429
- package/docs/plans/design.md +0 -2018
- package/docs/plans/prd.md +0 -1862
- package/docs/plans/stack-rank.md +0 -261
- package/docs/plans/technical-spec.md +0 -2755
- package/docs/privacy-and-safety.md +0 -807
- package/docs/prompt-optimization.md +0 -1071
- package/docs/test-plan.md +0 -972
- package/docs/third-party-ecosystem.md +0 -496
- package/domains/compliance-template/README.md +0 -173
- package/domains/compliance-template/traits/compliance-aware.md +0 -228
- package/examples/enterprise/agentboot.config.json +0 -184
- package/examples/minimal/agentboot.config.json +0 -46
- package/tests/REGRESSION-PLAN.md +0 -705
- package/tests/TEST-PLAN.md +0 -111
- package/tests/cli.test.ts +0 -705
- package/tests/pipeline.test.ts +0 -608
- package/tests/validate.test.ts +0 -278
- package/tsconfig.json +0 -62
package/tests/pipeline.test.ts
DELETED
|
@@ -1,608 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for the full build pipeline.
|
|
3
|
-
*
|
|
4
|
-
* Runs validate → compile → sync against the project and temp targets,
|
|
5
|
-
* then verifies the output structure and content.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
9
|
-
import { execSync } from "node:child_process";
|
|
10
|
-
import fs from "node:fs";
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import os from "node:os";
|
|
13
|
-
|
|
14
|
-
const ROOT = path.resolve(__dirname, "..");
|
|
15
|
-
const TSX = path.join(ROOT, "node_modules", ".bin", "tsx");
|
|
16
|
-
|
|
17
|
-
function run(script: string, cwd = ROOT): string {
|
|
18
|
-
return execSync(`${TSX} ${script}`, {
|
|
19
|
-
cwd,
|
|
20
|
-
env: { ...process.env, NODE_NO_WARNINGS: "1" },
|
|
21
|
-
timeout: 30_000,
|
|
22
|
-
}).toString();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Validate
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
describe("validate script", () => {
|
|
30
|
-
it("passes all 4 checks", () => {
|
|
31
|
-
const output = run("scripts/validate.ts");
|
|
32
|
-
expect(output).toContain("All 4 checks passed");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("detects missing persona", () => {
|
|
36
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-test-"));
|
|
37
|
-
const tempConfig = path.join(tempDir, "agentboot.config.json");
|
|
38
|
-
fs.writeFileSync(
|
|
39
|
-
tempConfig,
|
|
40
|
-
JSON.stringify({
|
|
41
|
-
org: "test",
|
|
42
|
-
personas: { enabled: ["nonexistent-persona"] },
|
|
43
|
-
traits: { enabled: [] },
|
|
44
|
-
validation: { secretPatterns: [] },
|
|
45
|
-
})
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
run(`scripts/validate.ts --config ${tempConfig}`);
|
|
50
|
-
expect.fail("Should have exited with error");
|
|
51
|
-
} catch (err: any) {
|
|
52
|
-
expect(err.stdout?.toString() ?? err.message).toContain("nonexistent-persona");
|
|
53
|
-
} finally {
|
|
54
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// Compile — platform-based dist/ structure
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
describe("compile script", () => {
|
|
64
|
-
beforeAll(() => {
|
|
65
|
-
const distPath = path.join(ROOT, "dist");
|
|
66
|
-
if (fs.existsSync(distPath)) {
|
|
67
|
-
fs.rmSync(distPath, { recursive: true });
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("compiles all 4 personas across 3 platforms", () => {
|
|
72
|
-
const output = run("scripts/compile.ts");
|
|
73
|
-
expect(output).toContain("Compiled 4 persona(s)");
|
|
74
|
-
expect(output).toContain("3 platform(s)");
|
|
75
|
-
expect(output).toContain("dist/skill/");
|
|
76
|
-
expect(output).toContain("dist/claude/");
|
|
77
|
-
expect(output).toContain("dist/copilot/");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("creates dist/{platform}/core/ structure", () => {
|
|
81
|
-
for (const platform of ["skill", "claude", "copilot"]) {
|
|
82
|
-
const platformCore = path.join(ROOT, "dist", platform, "core");
|
|
83
|
-
expect(fs.existsSync(platformCore), `dist/${platform}/core/ should exist`).toBe(true);
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("skill and copilot have all 4 persona directories", () => {
|
|
88
|
-
const personas = ["code-reviewer", "security-reviewer", "test-generator", "test-data-expert"];
|
|
89
|
-
for (const platform of ["skill", "copilot"]) {
|
|
90
|
-
for (const persona of personas) {
|
|
91
|
-
const personaDir = path.join(ROOT, "dist", platform, "core", persona);
|
|
92
|
-
expect(
|
|
93
|
-
fs.existsSync(personaDir),
|
|
94
|
-
`dist/${platform}/core/${persona}/ should exist`
|
|
95
|
-
).toBe(true);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("claude has all 4 personas as skill directories with SKILL.md", () => {
|
|
101
|
-
const skills = ["review-code", "review-security", "gen-tests", "gen-testdata"];
|
|
102
|
-
for (const skill of skills) {
|
|
103
|
-
const skillPath = path.join(ROOT, "dist", "claude", "core", "skills", skill, "SKILL.md");
|
|
104
|
-
expect(fs.existsSync(skillPath), `dist/claude/core/skills/${skill}/SKILL.md should exist`).toBe(true);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// --- skill platform ---
|
|
109
|
-
|
|
110
|
-
it("skill: generates SKILL.md with traits injected", () => {
|
|
111
|
-
const skillPath = path.join(ROOT, "dist", "skill", "core", "code-reviewer", "SKILL.md");
|
|
112
|
-
const content = fs.readFileSync(skillPath, "utf-8");
|
|
113
|
-
expect(content).toContain("AgentBoot compiled output");
|
|
114
|
-
expect(content).toContain("<!-- trait: critical-thinking -->");
|
|
115
|
-
expect(content).toContain("<!-- trait: structured-output -->");
|
|
116
|
-
expect(content).toContain("<!-- trait: source-citation -->");
|
|
117
|
-
expect(content).toContain("<!-- trait: confidence-signaling -->");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// --- claude platform ---
|
|
121
|
-
|
|
122
|
-
it("claude: generates skills/{name}/SKILL.md with CC-native frontmatter", () => {
|
|
123
|
-
const skillPath = path.join(ROOT, "dist", "claude", "core", "skills", "review-code", "SKILL.md");
|
|
124
|
-
expect(fs.existsSync(skillPath)).toBe(true);
|
|
125
|
-
const content = fs.readFileSync(skillPath, "utf-8");
|
|
126
|
-
expect(content).toMatch(/^---\ndescription:/);
|
|
127
|
-
expect(content).toContain("Code Reviewer");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// --- AB-17: agent output ---
|
|
131
|
-
|
|
132
|
-
it("claude: generates agent files for all 4 personas", () => {
|
|
133
|
-
const agents = [
|
|
134
|
-
{ file: "code-reviewer.md", name: "code-reviewer", desc: "Senior code reviewer" },
|
|
135
|
-
{ file: "security-reviewer.md", name: "security-reviewer", desc: "Adversarial security reviewer" },
|
|
136
|
-
{ file: "test-generator.md", name: "test-generator", desc: "Test-driven engineer" },
|
|
137
|
-
{ file: "test-data-expert.md", name: "test-data-expert", desc: "Data engineer" },
|
|
138
|
-
];
|
|
139
|
-
for (const agent of agents) {
|
|
140
|
-
const agentPath = path.join(ROOT, "dist", "claude", "core", "agents", agent.file);
|
|
141
|
-
expect(fs.existsSync(agentPath), `${agent.file} should exist`).toBe(true);
|
|
142
|
-
const content = fs.readFileSync(agentPath, "utf-8");
|
|
143
|
-
expect(content).toMatch(/^---\nname:/);
|
|
144
|
-
expect(content).toContain(`name: "${agent.name}"`);
|
|
145
|
-
// model and permissionMode are only included when explicitly set in persona config
|
|
146
|
-
expect(content).not.toContain("model: inherit");
|
|
147
|
-
expect(content).not.toContain("permissionMode: default");
|
|
148
|
-
// Verify body content (traits should be present)
|
|
149
|
-
expect(content.length).toBeGreaterThan(500);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// --- AB-19: CLAUDE.md with @imports ---
|
|
154
|
-
|
|
155
|
-
it("claude: generates CLAUDE.md with all @import directives", () => {
|
|
156
|
-
const claudeMdPath = path.join(ROOT, "dist", "claude", "core", "CLAUDE.md");
|
|
157
|
-
expect(fs.existsSync(claudeMdPath)).toBe(true);
|
|
158
|
-
const content = fs.readFileSync(claudeMdPath, "utf-8");
|
|
159
|
-
// All 6 traits
|
|
160
|
-
for (const trait of ["critical-thinking", "structured-output", "source-citation", "confidence-signaling", "audit-trail", "schema-awareness"]) {
|
|
161
|
-
expect(content).toContain(`@.claude/traits/${trait}.md`);
|
|
162
|
-
}
|
|
163
|
-
// Both instructions (exact match — no double .md.md extension)
|
|
164
|
-
expect(content).toMatch(/@\.claude\/rules\/baseline\.instructions\.md$/m);
|
|
165
|
-
expect(content).toMatch(/@\.claude\/rules\/security\.instructions\.md$/m);
|
|
166
|
-
expect(content).toContain("Auto-generated");
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// --- AB-19: trait files ---
|
|
170
|
-
|
|
171
|
-
it("claude: generates all 6 trait files", () => {
|
|
172
|
-
const expectedTraits = [
|
|
173
|
-
"audit-trail", "confidence-signaling", "critical-thinking",
|
|
174
|
-
"schema-awareness", "source-citation", "structured-output"
|
|
175
|
-
];
|
|
176
|
-
for (const trait of expectedTraits) {
|
|
177
|
-
const traitPath = path.join(ROOT, "dist", "claude", "core", "traits", `${trait}.md`);
|
|
178
|
-
expect(fs.existsSync(traitPath), `traits/${trait}.md should exist`).toBe(true);
|
|
179
|
-
const content = fs.readFileSync(traitPath, "utf-8");
|
|
180
|
-
expect(content.length).toBeGreaterThan(100);
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// --- AB-25: token budget ---
|
|
185
|
-
|
|
186
|
-
it("compile output contains per-persona token estimates", () => {
|
|
187
|
-
const output = run("scripts/compile.ts");
|
|
188
|
-
// Verify actual token numbers appear for each persona
|
|
189
|
-
expect(output).toMatch(/code-reviewer.*~?\d+ tokens/);
|
|
190
|
-
expect(output).toMatch(/security-reviewer.*~?\d+ tokens/);
|
|
191
|
-
expect(output).toMatch(/test-data-expert.*~?\d+ tokens/);
|
|
192
|
-
expect(output).toMatch(/test-generator.*~?\d+ tokens/);
|
|
193
|
-
// Verify the token estimate section header appears
|
|
194
|
-
expect(output).toContain("Token estimates");
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// --- copilot platform ---
|
|
198
|
-
|
|
199
|
-
it("copilot: generates copilot-instructions.md (HTML comments stripped)", () => {
|
|
200
|
-
const copilotPath = path.join(
|
|
201
|
-
ROOT, "dist", "copilot", "core", "code-reviewer", "copilot-instructions.md"
|
|
202
|
-
);
|
|
203
|
-
const content = fs.readFileSync(copilotPath, "utf-8");
|
|
204
|
-
expect(content).not.toContain("<!-- trait:");
|
|
205
|
-
expect(content).toContain("Code Reviewer");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// --- cross-platform checks ---
|
|
209
|
-
|
|
210
|
-
it("copies persona.config.json to skill and copilot platforms", () => {
|
|
211
|
-
for (const platform of ["skill", "copilot"]) {
|
|
212
|
-
const configPath = path.join(
|
|
213
|
-
ROOT, "dist", platform, "core", "code-reviewer", "persona.config.json"
|
|
214
|
-
);
|
|
215
|
-
expect(fs.existsSync(configPath), `${platform} should have persona.config.json`).toBe(true);
|
|
216
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
217
|
-
expect(config.name).toBe("Code Reviewer");
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("compiles instructions to every platform (rules/ for claude)", () => {
|
|
222
|
-
// skill and copilot use instructions/
|
|
223
|
-
for (const platform of ["skill", "copilot"]) {
|
|
224
|
-
const instrDir = path.join(ROOT, "dist", platform, "core", "instructions");
|
|
225
|
-
expect(fs.existsSync(instrDir), `${platform} should have instructions/`).toBe(true);
|
|
226
|
-
expect(fs.existsSync(path.join(instrDir, "baseline.instructions.md"))).toBe(true);
|
|
227
|
-
expect(fs.existsSync(path.join(instrDir, "security.instructions.md"))).toBe(true);
|
|
228
|
-
}
|
|
229
|
-
// claude uses rules/ (CC-native)
|
|
230
|
-
const rulesDir = path.join(ROOT, "dist", "claude", "core", "rules");
|
|
231
|
-
expect(fs.existsSync(rulesDir), "claude should have rules/").toBe(true);
|
|
232
|
-
expect(fs.existsSync(path.join(rulesDir, "baseline.instructions.md"))).toBe(true);
|
|
233
|
-
expect(fs.existsSync(path.join(rulesDir, "security.instructions.md"))).toBe(true);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it("generates PERSONAS.md in every platform", () => {
|
|
237
|
-
for (const platform of ["skill", "claude", "copilot"]) {
|
|
238
|
-
const indexPath = path.join(ROOT, "dist", platform, "core", "PERSONAS.md");
|
|
239
|
-
expect(fs.existsSync(indexPath), `${platform} should have PERSONAS.md`).toBe(true);
|
|
240
|
-
const content = fs.readFileSync(indexPath, "utf-8");
|
|
241
|
-
expect(content).toContain("code-reviewer");
|
|
242
|
-
expect(content).toContain("/review-code");
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it("injects correct traits per persona across platforms", () => {
|
|
247
|
-
// security-reviewer: audit-trail yes, confidence-signaling no
|
|
248
|
-
const secSkill = fs.readFileSync(
|
|
249
|
-
path.join(ROOT, "dist", "skill", "core", "security-reviewer", "SKILL.md"),
|
|
250
|
-
"utf-8"
|
|
251
|
-
);
|
|
252
|
-
expect(secSkill).toContain("<!-- trait: audit-trail -->");
|
|
253
|
-
expect(secSkill).not.toContain("<!-- trait: confidence-signaling -->");
|
|
254
|
-
|
|
255
|
-
// test-data-expert: schema-awareness yes, critical-thinking no
|
|
256
|
-
const tdSkill = fs.readFileSync(
|
|
257
|
-
path.join(ROOT, "dist", "skill", "core", "test-data-expert", "SKILL.md"),
|
|
258
|
-
"utf-8"
|
|
259
|
-
);
|
|
260
|
-
expect(tdSkill).toContain("<!-- trait: schema-awareness -->");
|
|
261
|
-
expect(tdSkill).not.toContain("<!-- trait: critical-thinking -->");
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it("platforms are self-contained and each has all personas", () => {
|
|
265
|
-
// skill and copilot use persona directories
|
|
266
|
-
const nonPersonaDirs = new Set(["instructions", "gotchas"]);
|
|
267
|
-
const skillPersonas = fs.readdirSync(path.join(ROOT, "dist", "skill", "core"))
|
|
268
|
-
.filter(f => !f.endsWith(".md") && !nonPersonaDirs.has(f)).sort();
|
|
269
|
-
const copilotPersonas = fs.readdirSync(path.join(ROOT, "dist", "copilot", "core"))
|
|
270
|
-
.filter(f => !f.endsWith(".md") && !nonPersonaDirs.has(f)).sort();
|
|
271
|
-
expect(skillPersonas).toEqual(copilotPersonas);
|
|
272
|
-
|
|
273
|
-
// claude uses skills/ directory with subdirectories
|
|
274
|
-
const claudeSkills = fs.readdirSync(path.join(ROOT, "dist", "claude", "core", "skills")).sort();
|
|
275
|
-
expect(claudeSkills).toEqual(["gen-testdata", "gen-tests", "review-code", "review-security"]);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
// --- AB-26: settings.json ---
|
|
279
|
-
|
|
280
|
-
it("claude: generates settings.json when hooks configured", () => {
|
|
281
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-settings-"));
|
|
282
|
-
const tempConfig = path.join(tempDir, "agentboot.config.json");
|
|
283
|
-
fs.writeFileSync(tempConfig, JSON.stringify({
|
|
284
|
-
org: "test",
|
|
285
|
-
personas: { enabled: ["code-reviewer"], outputFormats: ["claude"] },
|
|
286
|
-
traits: { enabled: ["critical-thinking", "structured-output", "source-citation", "confidence-signaling"] },
|
|
287
|
-
instructions: { enabled: [] },
|
|
288
|
-
claude: {
|
|
289
|
-
hooks: { PreToolUse: [{ matcher: "Bash", hooks: [{ type: "command", command: "./check.sh" }] }] },
|
|
290
|
-
permissions: { allow: ["Read"], deny: [] },
|
|
291
|
-
},
|
|
292
|
-
}));
|
|
293
|
-
try {
|
|
294
|
-
run(`scripts/compile.ts --config ${tempConfig}`);
|
|
295
|
-
const settingsPath = path.join(tempDir, "dist", "claude", "core", "settings.json");
|
|
296
|
-
expect(fs.existsSync(settingsPath), "settings.json should be generated").toBe(true);
|
|
297
|
-
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
298
|
-
expect(settings.hooks).toBeDefined();
|
|
299
|
-
expect(settings.permissions).toBeDefined();
|
|
300
|
-
expect(settings.permissions.allow).toContain("Read");
|
|
301
|
-
} finally {
|
|
302
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
// --- AB-27: .mcp.json ---
|
|
307
|
-
|
|
308
|
-
it("claude: generates .mcp.json when mcpServers configured", () => {
|
|
309
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-mcp-"));
|
|
310
|
-
const tempConfig = path.join(tempDir, "agentboot.config.json");
|
|
311
|
-
fs.writeFileSync(tempConfig, JSON.stringify({
|
|
312
|
-
org: "test",
|
|
313
|
-
personas: { enabled: ["code-reviewer"], outputFormats: ["claude"] },
|
|
314
|
-
traits: { enabled: ["critical-thinking", "structured-output", "source-citation", "confidence-signaling"] },
|
|
315
|
-
instructions: { enabled: [] },
|
|
316
|
-
claude: {
|
|
317
|
-
mcpServers: { "test-server": { type: "stdio", command: "npx", args: ["-y", "test-pkg"] } },
|
|
318
|
-
},
|
|
319
|
-
}));
|
|
320
|
-
try {
|
|
321
|
-
run(`scripts/compile.ts --config ${tempConfig}`);
|
|
322
|
-
const mcpPath = path.join(tempDir, "dist", "claude", "core", ".mcp.json");
|
|
323
|
-
expect(fs.existsSync(mcpPath), ".mcp.json should be generated").toBe(true);
|
|
324
|
-
const mcp = JSON.parse(fs.readFileSync(mcpPath, "utf-8"));
|
|
325
|
-
expect(mcp.mcpServers["test-server"]).toBeDefined();
|
|
326
|
-
expect(mcp.mcpServers["test-server"].command).toBe("npx");
|
|
327
|
-
} finally {
|
|
328
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
// ---------------------------------------------------------------------------
|
|
334
|
-
// Sync — reads from dist/{platform}/
|
|
335
|
-
// ---------------------------------------------------------------------------
|
|
336
|
-
|
|
337
|
-
describe("sync script", () => {
|
|
338
|
-
let syncTarget: string;
|
|
339
|
-
let originalRepos: string;
|
|
340
|
-
|
|
341
|
-
beforeAll(() => {
|
|
342
|
-
originalRepos = fs.readFileSync(path.join(ROOT, "repos.json"), "utf-8");
|
|
343
|
-
syncTarget = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-sync-"));
|
|
344
|
-
fs.writeFileSync(
|
|
345
|
-
path.join(ROOT, "repos.json"),
|
|
346
|
-
JSON.stringify([{ path: syncTarget, label: "test-repo", platform: "claude" }])
|
|
347
|
-
);
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
afterAll(() => {
|
|
351
|
-
fs.writeFileSync(path.join(ROOT, "repos.json"), originalRepos);
|
|
352
|
-
if (syncTarget) {
|
|
353
|
-
fs.rmSync(syncTarget, { recursive: true, force: true });
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it("syncs claude platform files to target repo", () => {
|
|
358
|
-
const output = run("scripts/sync.ts");
|
|
359
|
-
expect(output).toContain("Synced 1 repo");
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it("creates .claude/ directory in target", () => {
|
|
363
|
-
expect(fs.existsSync(path.join(syncTarget, ".claude"))).toBe(true);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it("writes all skill directories from claude platform", () => {
|
|
367
|
-
const skills = ["review-code", "review-security", "gen-tests", "gen-testdata"];
|
|
368
|
-
for (const skill of skills) {
|
|
369
|
-
const skillPath = path.join(syncTarget, ".claude", "skills", skill, "SKILL.md");
|
|
370
|
-
expect(fs.existsSync(skillPath), `skills/${skill}/SKILL.md should be synced`).toBe(true);
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
it("writes rules to target (CC-native)", () => {
|
|
375
|
-
expect(
|
|
376
|
-
fs.existsSync(path.join(syncTarget, ".claude", "rules", "baseline.instructions.md"))
|
|
377
|
-
).toBe(true);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it("writes PERSONAS.md to target", () => {
|
|
381
|
-
expect(fs.existsSync(path.join(syncTarget, ".claude", "PERSONAS.md"))).toBe(true);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
// --- AB-24: manifest ---
|
|
385
|
-
|
|
386
|
-
it("writes .agentboot-manifest.json to target", () => {
|
|
387
|
-
const manifestPath = path.join(syncTarget, ".claude", ".agentboot-manifest.json");
|
|
388
|
-
expect(fs.existsSync(manifestPath), ".agentboot-manifest.json should exist").toBe(true);
|
|
389
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
390
|
-
expect(manifest.managed_by).toBe("agentboot");
|
|
391
|
-
expect(manifest.version).toBeDefined();
|
|
392
|
-
expect(manifest.synced_at).toBeDefined();
|
|
393
|
-
expect(Array.isArray(manifest.files)).toBe(true);
|
|
394
|
-
expect(manifest.files.length).toBeGreaterThan(0);
|
|
395
|
-
// Each file should have a path and sha256 hash
|
|
396
|
-
for (const file of manifest.files) {
|
|
397
|
-
expect(file.path).toBeDefined();
|
|
398
|
-
expect(file.hash).toMatch(/^[a-f0-9]{64}$/);
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it("skips unchanged files on re-sync", () => {
|
|
403
|
-
const output = run("scripts/sync.ts");
|
|
404
|
-
expect(output).toContain("unchanged");
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it("supports dry-run mode", () => {
|
|
408
|
-
const cleanTarget = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-dry-"));
|
|
409
|
-
fs.writeFileSync(
|
|
410
|
-
path.join(ROOT, "repos.json"),
|
|
411
|
-
JSON.stringify([{ path: cleanTarget, label: "dry-run-test", platform: "claude" }])
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
try {
|
|
415
|
-
const output = run("scripts/sync.ts -- --dry-run");
|
|
416
|
-
expect(output).toContain("DRY RUN");
|
|
417
|
-
expect(fs.existsSync(path.join(cleanTarget, ".claude"))).toBe(false);
|
|
418
|
-
} finally {
|
|
419
|
-
fs.writeFileSync(
|
|
420
|
-
path.join(ROOT, "repos.json"),
|
|
421
|
-
JSON.stringify([{ path: syncTarget, label: "test-repo", platform: "claude" }])
|
|
422
|
-
);
|
|
423
|
-
fs.rmSync(cleanTarget, { recursive: true, force: true });
|
|
424
|
-
}
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
it("syncs copilot platform to a different target", () => {
|
|
428
|
-
const copilotTarget = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-copilot-"));
|
|
429
|
-
fs.writeFileSync(
|
|
430
|
-
path.join(ROOT, "repos.json"),
|
|
431
|
-
JSON.stringify([{ path: copilotTarget, label: "copilot-repo", platform: "copilot" }])
|
|
432
|
-
);
|
|
433
|
-
|
|
434
|
-
try {
|
|
435
|
-
const output = run("scripts/sync.ts");
|
|
436
|
-
expect(output).toContain("Synced 1 repo");
|
|
437
|
-
|
|
438
|
-
// Copilot platform should have merged copilot-instructions.md in .github/
|
|
439
|
-
expect(
|
|
440
|
-
fs.existsSync(path.join(copilotTarget, ".github", "copilot-instructions.md")),
|
|
441
|
-
"merged copilot-instructions.md should be synced to .github/"
|
|
442
|
-
).toBe(true);
|
|
443
|
-
// PERSONAS.md should still be written to the target dir
|
|
444
|
-
expect(
|
|
445
|
-
fs.existsSync(path.join(copilotTarget, ".claude", "PERSONAS.md")),
|
|
446
|
-
"PERSONAS.md should be synced"
|
|
447
|
-
).toBe(true);
|
|
448
|
-
// Copilot repos should NOT get individual persona skill files in .claude/
|
|
449
|
-
expect(
|
|
450
|
-
fs.existsSync(path.join(copilotTarget, ".claude", "skills")),
|
|
451
|
-
"copilot repos should not have .claude/skills/"
|
|
452
|
-
).toBe(false);
|
|
453
|
-
} finally {
|
|
454
|
-
fs.writeFileSync(
|
|
455
|
-
path.join(ROOT, "repos.json"),
|
|
456
|
-
JSON.stringify([{ path: syncTarget, label: "test-repo", platform: "claude" }])
|
|
457
|
-
);
|
|
458
|
-
fs.rmSync(copilotTarget, { recursive: true, force: true });
|
|
459
|
-
}
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
// --- AB-28: PR mode ---
|
|
463
|
-
|
|
464
|
-
it("PR mode handles missing remote gracefully", () => {
|
|
465
|
-
const prTarget = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-pr-"));
|
|
466
|
-
// Initialize a git repo so PR mode has something to work with
|
|
467
|
-
execSync("git init", { cwd: prTarget, stdio: "pipe" });
|
|
468
|
-
execSync("git commit --allow-empty -m 'init'", { cwd: prTarget, stdio: "pipe" });
|
|
469
|
-
|
|
470
|
-
fs.writeFileSync(
|
|
471
|
-
path.join(ROOT, "repos.json"),
|
|
472
|
-
JSON.stringify([{ path: prTarget, label: "pr-test", platform: "claude" }])
|
|
473
|
-
);
|
|
474
|
-
|
|
475
|
-
try {
|
|
476
|
-
// PR creation will fail (no remote) — sync reports the error but still completes.
|
|
477
|
-
// execSync throws on non-zero exit, so we catch and verify the output.
|
|
478
|
-
run("scripts/sync.ts -- --mode pr");
|
|
479
|
-
} catch (err: unknown) {
|
|
480
|
-
// Verify the error is from PR creation (expected), not a process crash
|
|
481
|
-
const output = (err as { stdout?: Buffer })?.stdout?.toString() ?? "";
|
|
482
|
-
expect(output).toContain("PR creation failed");
|
|
483
|
-
} finally {
|
|
484
|
-
fs.writeFileSync(
|
|
485
|
-
path.join(ROOT, "repos.json"),
|
|
486
|
-
JSON.stringify([{ path: syncTarget, label: "test-repo", platform: "claude" }])
|
|
487
|
-
);
|
|
488
|
-
fs.rmSync(prTarget, { recursive: true, force: true });
|
|
489
|
-
}
|
|
490
|
-
});
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
// ---------------------------------------------------------------------------
|
|
494
|
-
// Scope merging — team > group > core
|
|
495
|
-
// ---------------------------------------------------------------------------
|
|
496
|
-
|
|
497
|
-
describe("sync scope merging", () => {
|
|
498
|
-
let syncTarget: string;
|
|
499
|
-
let originalRepos: string;
|
|
500
|
-
|
|
501
|
-
beforeAll(() => {
|
|
502
|
-
originalRepos = fs.readFileSync(path.join(ROOT, "repos.json"), "utf-8");
|
|
503
|
-
syncTarget = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-merge-"));
|
|
504
|
-
|
|
505
|
-
// Create a group-level override in dist/claude/groups/platform/
|
|
506
|
-
const groupSkillDir = path.join(ROOT, "dist", "claude", "groups", "platform", "skills", "review-code");
|
|
507
|
-
fs.mkdirSync(groupSkillDir, { recursive: true });
|
|
508
|
-
fs.writeFileSync(
|
|
509
|
-
path.join(groupSkillDir, "SKILL.md"),
|
|
510
|
-
"---\ndescription: Group-level override\n---\n\n# Group Code Reviewer\n",
|
|
511
|
-
"utf-8"
|
|
512
|
-
);
|
|
513
|
-
|
|
514
|
-
// Create a team-level override in dist/claude/teams/platform/api/
|
|
515
|
-
const teamSkillDir = path.join(ROOT, "dist", "claude", "teams", "platform", "api", "skills", "review-code");
|
|
516
|
-
fs.mkdirSync(teamSkillDir, { recursive: true });
|
|
517
|
-
fs.writeFileSync(
|
|
518
|
-
path.join(teamSkillDir, "SKILL.md"),
|
|
519
|
-
"---\ndescription: Team-level override\n---\n\n# Team Code Reviewer\n",
|
|
520
|
-
"utf-8"
|
|
521
|
-
);
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
afterAll(() => {
|
|
525
|
-
fs.writeFileSync(path.join(ROOT, "repos.json"), originalRepos);
|
|
526
|
-
if (syncTarget) fs.rmSync(syncTarget, { recursive: true, force: true });
|
|
527
|
-
// Clean up test scope dirs
|
|
528
|
-
const groupDir = path.join(ROOT, "dist", "claude", "groups");
|
|
529
|
-
const teamDir = path.join(ROOT, "dist", "claude", "teams");
|
|
530
|
-
if (fs.existsSync(groupDir)) fs.rmSync(groupDir, { recursive: true });
|
|
531
|
-
if (fs.existsSync(teamDir)) fs.rmSync(teamDir, { recursive: true });
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
it("team overrides group which overrides core on filename conflict", () => {
|
|
535
|
-
// Sync with team scope: team > group > core
|
|
536
|
-
fs.writeFileSync(
|
|
537
|
-
path.join(ROOT, "repos.json"),
|
|
538
|
-
JSON.stringify([{
|
|
539
|
-
path: syncTarget,
|
|
540
|
-
label: "merge-test",
|
|
541
|
-
platform: "claude",
|
|
542
|
-
group: "platform",
|
|
543
|
-
team: "api",
|
|
544
|
-
}])
|
|
545
|
-
);
|
|
546
|
-
|
|
547
|
-
run("scripts/sync.ts");
|
|
548
|
-
|
|
549
|
-
const skillPath = path.join(syncTarget, ".claude", "skills", "review-code", "SKILL.md");
|
|
550
|
-
expect(fs.existsSync(skillPath), "skill should be synced").toBe(true);
|
|
551
|
-
const content = fs.readFileSync(skillPath, "utf-8");
|
|
552
|
-
// Team override should win
|
|
553
|
-
expect(content).toContain("Team Code Reviewer");
|
|
554
|
-
expect(content).not.toContain("Group Code Reviewer");
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
it("group overrides core when no team scope", () => {
|
|
558
|
-
const groupTarget = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-group-"));
|
|
559
|
-
fs.writeFileSync(
|
|
560
|
-
path.join(ROOT, "repos.json"),
|
|
561
|
-
JSON.stringify([{
|
|
562
|
-
path: groupTarget,
|
|
563
|
-
label: "group-test",
|
|
564
|
-
platform: "claude",
|
|
565
|
-
group: "platform",
|
|
566
|
-
}])
|
|
567
|
-
);
|
|
568
|
-
|
|
569
|
-
try {
|
|
570
|
-
run("scripts/sync.ts");
|
|
571
|
-
|
|
572
|
-
const skillPath = path.join(groupTarget, ".claude", "skills", "review-code", "SKILL.md");
|
|
573
|
-
expect(fs.existsSync(skillPath), "skill should be synced").toBe(true);
|
|
574
|
-
const content = fs.readFileSync(skillPath, "utf-8");
|
|
575
|
-
// Group override should win over core
|
|
576
|
-
expect(content).toContain("Group Code Reviewer");
|
|
577
|
-
} finally {
|
|
578
|
-
fs.writeFileSync(
|
|
579
|
-
path.join(ROOT, "repos.json"),
|
|
580
|
-
JSON.stringify([{
|
|
581
|
-
path: syncTarget,
|
|
582
|
-
label: "merge-test",
|
|
583
|
-
platform: "claude",
|
|
584
|
-
group: "platform",
|
|
585
|
-
team: "api",
|
|
586
|
-
}])
|
|
587
|
-
);
|
|
588
|
-
fs.rmSync(groupTarget, { recursive: true, force: true });
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
// ---------------------------------------------------------------------------
|
|
594
|
-
// Full pipeline
|
|
595
|
-
// ---------------------------------------------------------------------------
|
|
596
|
-
|
|
597
|
-
describe("full pipeline (validate → compile)", () => {
|
|
598
|
-
it("runs end-to-end without errors", () => {
|
|
599
|
-
const output = execSync(
|
|
600
|
-
`${TSX} scripts/validate.ts && ${TSX} scripts/compile.ts`,
|
|
601
|
-
{ cwd: ROOT, env: { ...process.env, NODE_NO_WARNINGS: "1" }, timeout: 30_000 }
|
|
602
|
-
).toString();
|
|
603
|
-
|
|
604
|
-
expect(output).toContain("All 4 checks passed");
|
|
605
|
-
expect(output).toContain("Compiled 4 persona(s)");
|
|
606
|
-
expect(output).toContain("3 platform(s)");
|
|
607
|
-
});
|
|
608
|
-
});
|