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.
Files changed (78) hide show
  1. package/README.md +9 -8
  2. package/agentboot.config.json +4 -1
  3. package/package.json +2 -2
  4. package/scripts/cli.ts +465 -18
  5. package/scripts/compile.ts +724 -75
  6. package/scripts/dev-sync.ts +1 -1
  7. package/scripts/lib/config.ts +259 -1
  8. package/scripts/lib/frontmatter.ts +3 -1
  9. package/scripts/validate.ts +12 -7
  10. package/website/docusaurus.config.ts +117 -0
  11. package/website/package-lock.json +18448 -0
  12. package/website/package.json +47 -0
  13. package/website/sidebars.ts +53 -0
  14. package/website/src/css/custom.css +23 -0
  15. package/website/src/pages/index.module.css +23 -0
  16. package/website/src/pages/index.tsx +125 -0
  17. package/website/static/.nojekyll +0 -0
  18. package/website/static/CNAME +1 -0
  19. package/website/static/img/favicon.ico +0 -0
  20. package/website/static/img/logo.svg +1 -0
  21. package/.github/ISSUE_TEMPLATE/persona-request.md +0 -62
  22. package/.github/ISSUE_TEMPLATE/quality-feedback.md +0 -67
  23. package/.github/workflows/cla.yml +0 -25
  24. package/.github/workflows/validate.yml +0 -49
  25. package/.idea/agentboot.iml +0 -9
  26. package/.idea/misc.xml +0 -6
  27. package/.idea/modules.xml +0 -8
  28. package/.idea/vcs.xml +0 -6
  29. package/CLAUDE.md +0 -230
  30. package/CONTRIBUTING.md +0 -168
  31. package/PERSONAS.md +0 -156
  32. package/core/instructions/baseline.instructions.md +0 -133
  33. package/core/instructions/security.instructions.md +0 -186
  34. package/core/personas/code-reviewer/SKILL.md +0 -175
  35. package/core/personas/security-reviewer/SKILL.md +0 -233
  36. package/core/personas/test-data-expert/SKILL.md +0 -234
  37. package/core/personas/test-generator/SKILL.md +0 -262
  38. package/core/traits/audit-trail.md +0 -182
  39. package/core/traits/confidence-signaling.md +0 -172
  40. package/core/traits/critical-thinking.md +0 -129
  41. package/core/traits/schema-awareness.md +0 -132
  42. package/core/traits/source-citation.md +0 -174
  43. package/core/traits/structured-output.md +0 -199
  44. package/docs/ci-cd-automation.md +0 -548
  45. package/docs/claude-code-reference/README.md +0 -21
  46. package/docs/claude-code-reference/agentboot-coverage.md +0 -484
  47. package/docs/claude-code-reference/feature-inventory.md +0 -906
  48. package/docs/cli-commands-audit.md +0 -112
  49. package/docs/cli-design.md +0 -924
  50. package/docs/concepts.md +0 -1117
  51. package/docs/config-schema-audit.md +0 -121
  52. package/docs/configuration.md +0 -645
  53. package/docs/delivery-methods.md +0 -758
  54. package/docs/developer-onboarding.md +0 -342
  55. package/docs/extending.md +0 -448
  56. package/docs/getting-started.md +0 -298
  57. package/docs/knowledge-layer.md +0 -464
  58. package/docs/marketplace.md +0 -822
  59. package/docs/org-connection.md +0 -570
  60. package/docs/plans/architecture.md +0 -2429
  61. package/docs/plans/design.md +0 -2018
  62. package/docs/plans/prd.md +0 -1862
  63. package/docs/plans/stack-rank.md +0 -261
  64. package/docs/plans/technical-spec.md +0 -2755
  65. package/docs/privacy-and-safety.md +0 -807
  66. package/docs/prompt-optimization.md +0 -1071
  67. package/docs/test-plan.md +0 -972
  68. package/docs/third-party-ecosystem.md +0 -496
  69. package/domains/compliance-template/README.md +0 -173
  70. package/domains/compliance-template/traits/compliance-aware.md +0 -228
  71. package/examples/enterprise/agentboot.config.json +0 -184
  72. package/examples/minimal/agentboot.config.json +0 -46
  73. package/tests/REGRESSION-PLAN.md +0 -705
  74. package/tests/TEST-PLAN.md +0 -111
  75. package/tests/cli.test.ts +0 -705
  76. package/tests/pipeline.test.ts +0 -608
  77. package/tests/validate.test.ts +0 -278
  78. package/tsconfig.json +0 -62
@@ -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
- });