agentboot 0.1.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/.github/ISSUE_TEMPLATE/persona-request.md +62 -0
- package/.github/ISSUE_TEMPLATE/quality-feedback.md +67 -0
- package/.github/workflows/cla.yml +25 -0
- package/.github/workflows/validate.yml +49 -0
- package/.idea/agentboot.iml +9 -0
- package/.idea/misc.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/CLA.md +98 -0
- package/CLAUDE.md +230 -0
- package/CONTRIBUTING.md +168 -0
- package/LICENSE +191 -0
- package/NOTICE +4 -0
- package/PERSONAS.md +156 -0
- package/README.md +172 -0
- package/agentboot.config.json +207 -0
- package/bin/agentboot.js +17 -0
- package/core/gotchas/README.md +35 -0
- package/core/instructions/baseline.instructions.md +133 -0
- package/core/instructions/security.instructions.md +186 -0
- package/core/personas/code-reviewer/SKILL.md +175 -0
- package/core/personas/code-reviewer/persona.config.json +11 -0
- package/core/personas/security-reviewer/SKILL.md +233 -0
- package/core/personas/security-reviewer/persona.config.json +11 -0
- package/core/personas/test-data-expert/SKILL.md +234 -0
- package/core/personas/test-data-expert/persona.config.json +10 -0
- package/core/personas/test-generator/SKILL.md +262 -0
- package/core/personas/test-generator/persona.config.json +10 -0
- package/core/traits/audit-trail.md +182 -0
- package/core/traits/confidence-signaling.md +172 -0
- package/core/traits/critical-thinking.md +129 -0
- package/core/traits/schema-awareness.md +132 -0
- package/core/traits/source-citation.md +174 -0
- package/core/traits/structured-output.md +199 -0
- package/docs/ci-cd-automation.md +548 -0
- package/docs/claude-code-reference/README.md +21 -0
- package/docs/claude-code-reference/agentboot-coverage.md +484 -0
- package/docs/claude-code-reference/feature-inventory.md +906 -0
- package/docs/cli-commands-audit.md +112 -0
- package/docs/cli-design.md +924 -0
- package/docs/concepts.md +1117 -0
- package/docs/config-schema-audit.md +121 -0
- package/docs/configuration.md +645 -0
- package/docs/delivery-methods.md +758 -0
- package/docs/developer-onboarding.md +342 -0
- package/docs/extending.md +448 -0
- package/docs/getting-started.md +298 -0
- package/docs/knowledge-layer.md +464 -0
- package/docs/marketplace.md +822 -0
- package/docs/org-connection.md +570 -0
- package/docs/plans/architecture.md +2429 -0
- package/docs/plans/design.md +2018 -0
- package/docs/plans/prd.md +1862 -0
- package/docs/plans/stack-rank.md +261 -0
- package/docs/plans/technical-spec.md +2755 -0
- package/docs/privacy-and-safety.md +807 -0
- package/docs/prompt-optimization.md +1071 -0
- package/docs/test-plan.md +972 -0
- package/docs/third-party-ecosystem.md +496 -0
- package/domains/compliance-template/README.md +173 -0
- package/domains/compliance-template/traits/compliance-aware.md +228 -0
- package/examples/enterprise/agentboot.config.json +184 -0
- package/examples/minimal/agentboot.config.json +46 -0
- package/package.json +63 -0
- package/repos.json +1 -0
- package/scripts/cli.ts +1069 -0
- package/scripts/compile.ts +1000 -0
- package/scripts/dev-sync.ts +149 -0
- package/scripts/lib/config.ts +137 -0
- package/scripts/lib/frontmatter.ts +61 -0
- package/scripts/sync.ts +687 -0
- package/scripts/validate.ts +421 -0
- package/tests/REGRESSION-PLAN.md +705 -0
- package/tests/TEST-PLAN.md +111 -0
- package/tests/cli.test.ts +705 -0
- package/tests/pipeline.test.ts +608 -0
- package/tests/validate.test.ts +278 -0
- package/tsconfig.json +62 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentBoot compile script.
|
|
3
|
+
*
|
|
4
|
+
* Reads agentboot.config.json, traverses core/traits/ and core/personas/,
|
|
5
|
+
* composes each persona by inlining trait content, and writes output to
|
|
6
|
+
* dist/{platform}/ — one self-contained distribution per platform.
|
|
7
|
+
*
|
|
8
|
+
* Output structure:
|
|
9
|
+
* dist/skill/ — cross-platform SKILL.md (agentskills.io, traits inlined)
|
|
10
|
+
* dist/claude/ — Claude Code native (.claude/ format)
|
|
11
|
+
* dist/copilot/ — GitHub Copilot (.github/ format)
|
|
12
|
+
*
|
|
13
|
+
* Each platform folder contains the full scope hierarchy:
|
|
14
|
+
* dist/{platform}/core/
|
|
15
|
+
* dist/{platform}/groups/{group}/
|
|
16
|
+
* dist/{platform}/teams/{group}/{team}/
|
|
17
|
+
*
|
|
18
|
+
* Trait injection points in SKILL.md:
|
|
19
|
+
* <!-- traits:start -->
|
|
20
|
+
* (existing content is replaced on each build)
|
|
21
|
+
* <!-- traits:end -->
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* npm run build
|
|
25
|
+
* tsx scripts/compile.ts
|
|
26
|
+
* tsx scripts/compile.ts --config path/to/agentboot.config.json
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import fs from "node:fs";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
import chalk from "chalk";
|
|
33
|
+
import {
|
|
34
|
+
type AgentBootConfig,
|
|
35
|
+
type PersonaConfig,
|
|
36
|
+
resolveConfigPath,
|
|
37
|
+
loadConfig,
|
|
38
|
+
stripJsoncComments,
|
|
39
|
+
} from "./lib/config.js";
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Paths
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
46
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
47
|
+
|
|
48
|
+
interface TraitContent {
|
|
49
|
+
name: string;
|
|
50
|
+
content: string;
|
|
51
|
+
filePath: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface CompileResult {
|
|
55
|
+
persona: string;
|
|
56
|
+
platforms: string[];
|
|
57
|
+
traitsInjected: string[];
|
|
58
|
+
scope: "core" | "group" | "team";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Helpers
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
function fatal(msg: string): never {
|
|
66
|
+
console.error(chalk.red(`✗ FATAL: ${msg}`));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function log(msg: string): void {
|
|
71
|
+
console.log(msg);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ensureDir(dirPath: string): void {
|
|
75
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function provenanceHeader(sourceFile: string, config: AgentBootConfig): string {
|
|
79
|
+
const relSource = path.relative(ROOT, sourceFile);
|
|
80
|
+
const timestamp = new Date().toISOString();
|
|
81
|
+
const org = config.orgDisplayName ?? config.org;
|
|
82
|
+
return [
|
|
83
|
+
`<!-- ============================================================ -->`,
|
|
84
|
+
`<!-- AgentBoot compiled output — do not edit manually. -->`,
|
|
85
|
+
`<!-- Source: ${relSource.padEnd(44)} -->`,
|
|
86
|
+
`<!-- Compiled: ${timestamp.padEnd(44)} -->`,
|
|
87
|
+
`<!-- Org: ${org.padEnd(44)} -->`,
|
|
88
|
+
`<!-- ============================================================ -->`,
|
|
89
|
+
"",
|
|
90
|
+
].join("\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Trait loading
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
function loadTraits(
|
|
98
|
+
coreTraitsDir: string,
|
|
99
|
+
enabledTraits: string[] | undefined
|
|
100
|
+
): Map<string, TraitContent> {
|
|
101
|
+
const traits = new Map<string, TraitContent>();
|
|
102
|
+
|
|
103
|
+
if (!fs.existsSync(coreTraitsDir)) {
|
|
104
|
+
log(chalk.yellow(` ⚠ Traits directory not found: ${coreTraitsDir} — skipping trait injection`));
|
|
105
|
+
return traits;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const traitFiles = fs.readdirSync(coreTraitsDir).filter((f) => f.endsWith(".md"));
|
|
109
|
+
|
|
110
|
+
for (const file of traitFiles) {
|
|
111
|
+
const traitName = path.basename(file, ".md");
|
|
112
|
+
|
|
113
|
+
if (enabledTraits && !enabledTraits.includes(traitName)) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const filePath = path.join(coreTraitsDir, file);
|
|
118
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
119
|
+
|
|
120
|
+
traits.set(traitName, {
|
|
121
|
+
name: traitName,
|
|
122
|
+
content: content.trim(),
|
|
123
|
+
filePath,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return traits;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Persona config loading
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
function loadPersonaConfig(personaDir: string): PersonaConfig | null {
|
|
135
|
+
const configPath = path.join(personaDir, "persona.config.json");
|
|
136
|
+
if (!fs.existsSync(configPath)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(stripJsoncComments(raw)) as PersonaConfig;
|
|
142
|
+
} catch {
|
|
143
|
+
log(chalk.yellow(` ⚠ Failed to parse persona.config.json in ${personaDir}`));
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Trait injection
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
const TRAITS_START_MARKER = "<!-- traits:start -->";
|
|
153
|
+
const TRAITS_END_MARKER = "<!-- traits:end -->";
|
|
154
|
+
|
|
155
|
+
function injectTraits(
|
|
156
|
+
skillContent: string,
|
|
157
|
+
traitNames: string[],
|
|
158
|
+
traits: Map<string, TraitContent>,
|
|
159
|
+
personaName: string
|
|
160
|
+
): { result: string; injected: string[] } {
|
|
161
|
+
const injected: string[] = [];
|
|
162
|
+
const missing: string[] = [];
|
|
163
|
+
|
|
164
|
+
const blocks: string[] = [];
|
|
165
|
+
for (const traitName of traitNames) {
|
|
166
|
+
const trait = traits.get(traitName);
|
|
167
|
+
if (!trait) {
|
|
168
|
+
missing.push(traitName);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
injected.push(traitName);
|
|
172
|
+
blocks.push(
|
|
173
|
+
`<!-- trait: ${traitName} -->\n${trait.content}\n<!-- /trait: ${traitName} -->`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (missing.length > 0) {
|
|
178
|
+
log(
|
|
179
|
+
chalk.yellow(
|
|
180
|
+
` ⚠ [${personaName}] Traits not found (skipped): ${missing.join(", ")}`
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const injectedBlock =
|
|
186
|
+
blocks.length > 0
|
|
187
|
+
? `\n\n${blocks.join("\n\n")}\n\n`
|
|
188
|
+
: "\n\n<!-- no traits configured -->\n\n";
|
|
189
|
+
|
|
190
|
+
const startIdx = skillContent.indexOf(TRAITS_START_MARKER);
|
|
191
|
+
const endIdx = skillContent.indexOf(TRAITS_END_MARKER);
|
|
192
|
+
|
|
193
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
194
|
+
const before = skillContent.slice(0, startIdx + TRAITS_START_MARKER.length);
|
|
195
|
+
const after = skillContent.slice(endIdx);
|
|
196
|
+
return {
|
|
197
|
+
result: `${before}${injectedBlock}${after}`,
|
|
198
|
+
injected,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
result: `${skillContent.trimEnd()}\n\n${TRAITS_START_MARKER}${injectedBlock}${TRAITS_END_MARKER}\n`,
|
|
204
|
+
injected,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Platform-specific output builders
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
function buildSkillOutput(
|
|
213
|
+
personaName: string,
|
|
214
|
+
_personaConfig: PersonaConfig | null,
|
|
215
|
+
composedContent: string,
|
|
216
|
+
config: AgentBootConfig,
|
|
217
|
+
skillPath: string
|
|
218
|
+
): string {
|
|
219
|
+
const provenanceEnabled = config.output?.provenanceHeaders !== false;
|
|
220
|
+
return provenanceEnabled
|
|
221
|
+
? `${provenanceHeader(skillPath, config)}${composedContent}`
|
|
222
|
+
: composedContent;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Build CC-native skill file.
|
|
227
|
+
* CC expects: .claude/skills/{skill-name}.md with description frontmatter.
|
|
228
|
+
* The skill name comes from the invocation (e.g., "/review-code" → "review-code").
|
|
229
|
+
*/
|
|
230
|
+
function buildClaudeOutput(
|
|
231
|
+
personaName: string,
|
|
232
|
+
personaConfig: PersonaConfig | null,
|
|
233
|
+
composedContent: string,
|
|
234
|
+
_config: AgentBootConfig
|
|
235
|
+
): { content: string; skillName: string } {
|
|
236
|
+
const invocation = personaConfig?.invocation ?? `/${personaName}`;
|
|
237
|
+
const skillName = invocation.replace(/^\//, "");
|
|
238
|
+
const description = personaConfig?.description ?? personaName;
|
|
239
|
+
// Escape newlines and quotes in description to prevent YAML injection
|
|
240
|
+
const safeDescription = description.replace(/\n/g, " ").replace(/"/g, '\\"');
|
|
241
|
+
|
|
242
|
+
// AB-18: CC skill frontmatter with context:fork → delegates to agent
|
|
243
|
+
const frontmatterLines: string[] = [
|
|
244
|
+
"---",
|
|
245
|
+
`description: "${safeDescription}"`,
|
|
246
|
+
"context: fork",
|
|
247
|
+
`agent: "${personaName}"`,
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
// Optional: include model override if specified
|
|
251
|
+
if (personaConfig?.model) {
|
|
252
|
+
frontmatterLines.push(`model: "${personaConfig.model}"`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
frontmatterLines.push("---", "");
|
|
256
|
+
|
|
257
|
+
// Strip any existing frontmatter from composed content (it's SKILL.md format)
|
|
258
|
+
const withoutFrontmatter = composedContent.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
content: `${frontmatterLines.join("\n")}\n${withoutFrontmatter}`,
|
|
262
|
+
skillName,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function buildCopilotOutput(
|
|
267
|
+
personaName: string,
|
|
268
|
+
personaConfig: PersonaConfig | null,
|
|
269
|
+
composedContent: string,
|
|
270
|
+
config: AgentBootConfig,
|
|
271
|
+
skillPath: string
|
|
272
|
+
): string {
|
|
273
|
+
const header = `# ${personaConfig?.name ?? personaName} (AgentBoot)\n\n`;
|
|
274
|
+
const description = personaConfig?.description
|
|
275
|
+
? `${personaConfig.description}\n\n---\n\n`
|
|
276
|
+
: "";
|
|
277
|
+
// Strip HTML comments for Copilot output.
|
|
278
|
+
const stripped = composedContent.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
279
|
+
return `${provenanceHeader(skillPath, config)}${header}${description}${stripped}\n`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Persona compilation — writes to each platform's dist folder
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
function compilePersona(
|
|
287
|
+
personaName: string,
|
|
288
|
+
personaDir: string,
|
|
289
|
+
traits: Map<string, TraitContent>,
|
|
290
|
+
config: AgentBootConfig,
|
|
291
|
+
distPath: string,
|
|
292
|
+
scopePath: string,
|
|
293
|
+
groupName?: string,
|
|
294
|
+
teamName?: string
|
|
295
|
+
): CompileResult {
|
|
296
|
+
const skillPath = path.join(personaDir, "SKILL.md");
|
|
297
|
+
const scope: "core" | "group" | "team" = teamName ? "team" : groupName ? "group" : "core";
|
|
298
|
+
|
|
299
|
+
if (!fs.existsSync(skillPath)) {
|
|
300
|
+
log(chalk.yellow(` ⚠ [${personaName}] No SKILL.md found — skipping`));
|
|
301
|
+
return { persona: personaName, platforms: [], traitsInjected: [], scope };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const personaConfig = loadPersonaConfig(personaDir);
|
|
305
|
+
const skillContent = fs.readFileSync(skillPath, "utf-8");
|
|
306
|
+
|
|
307
|
+
// Determine which traits to inject.
|
|
308
|
+
let traitNames: string[] = personaConfig?.traits ?? [];
|
|
309
|
+
|
|
310
|
+
if (groupName && personaConfig?.groups?.[groupName]?.traits) {
|
|
311
|
+
traitNames = [...traitNames, ...(personaConfig.groups[groupName]!.traits ?? [])];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (teamName && personaConfig?.teams?.[teamName]?.traits) {
|
|
315
|
+
traitNames = [...traitNames, ...(personaConfig.teams[teamName]!.traits ?? [])];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
traitNames = [...new Set(traitNames)];
|
|
319
|
+
|
|
320
|
+
const { result: composed, injected } = injectTraits(
|
|
321
|
+
skillContent,
|
|
322
|
+
traitNames,
|
|
323
|
+
traits,
|
|
324
|
+
personaName
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const outputFormats = config.personas?.outputFormats ?? ["skill", "claude", "copilot"];
|
|
328
|
+
const platforms: string[] = [];
|
|
329
|
+
|
|
330
|
+
// Write to dist/{platform}/{scopePath}/{persona}/ (or skills/{name}/ for claude)
|
|
331
|
+
// e.g., dist/skill/core/code-reviewer/SKILL.md
|
|
332
|
+
// dist/claude/core/skills/review-code/SKILL.md
|
|
333
|
+
|
|
334
|
+
if (outputFormats.includes("skill")) {
|
|
335
|
+
const outDir = path.join(distPath, "skill", scopePath, personaName);
|
|
336
|
+
ensureDir(outDir);
|
|
337
|
+
const content = buildSkillOutput(personaName, personaConfig, composed, config, skillPath);
|
|
338
|
+
fs.writeFileSync(path.join(outDir, "SKILL.md"), content, "utf-8");
|
|
339
|
+
if (personaConfig) {
|
|
340
|
+
fs.writeFileSync(
|
|
341
|
+
path.join(outDir, "persona.config.json"),
|
|
342
|
+
JSON.stringify(personaConfig, null, 2) + "\n",
|
|
343
|
+
"utf-8"
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
platforms.push("skill");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (outputFormats.includes("claude")) {
|
|
350
|
+
const { content, skillName } = buildClaudeOutput(personaName, personaConfig, composed, config);
|
|
351
|
+
// CC-native: write to dist/claude/{scope}/skills/{skillName}/SKILL.md
|
|
352
|
+
const skillDir = path.join(distPath, "claude", scopePath, "skills", skillName);
|
|
353
|
+
ensureDir(skillDir);
|
|
354
|
+
fs.writeFileSync(path.join(skillDir, "SKILL.md"), content, "utf-8");
|
|
355
|
+
platforms.push("claude");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (outputFormats.includes("claude")) {
|
|
359
|
+
// AB-17: Write agent file to dist/claude/{scope}/agents/{personaName}.md
|
|
360
|
+
const agentDir = path.join(distPath, "claude", scopePath, "agents");
|
|
361
|
+
ensureDir(agentDir);
|
|
362
|
+
|
|
363
|
+
const model = personaConfig?.model; // undefined = omit from frontmatter
|
|
364
|
+
const permMode = personaConfig?.permissionMode;
|
|
365
|
+
const agentDescription = personaConfig?.description ?? personaName;
|
|
366
|
+
// Escape newlines and quotes in description to prevent YAML injection.
|
|
367
|
+
// JS '\\"' produces the string \" which is the correct YAML double-quote escape.
|
|
368
|
+
const safeDescription = agentDescription.replace(/\n/g, " ").replace(/"/g, '\\"');
|
|
369
|
+
const withoutFrontmatter = composed.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
370
|
+
const agentFrontmatter: string[] = [
|
|
371
|
+
"---",
|
|
372
|
+
`name: "${personaName}"`,
|
|
373
|
+
`description: "${safeDescription}"`,
|
|
374
|
+
];
|
|
375
|
+
if (model) agentFrontmatter.push(`model: "${model}"`);
|
|
376
|
+
if (permMode && permMode !== "default") agentFrontmatter.push(`permissionMode: "${permMode}"`);
|
|
377
|
+
agentFrontmatter.push("---");
|
|
378
|
+
const agentContent = [...agentFrontmatter, "", withoutFrontmatter].join("\n");
|
|
379
|
+
|
|
380
|
+
fs.writeFileSync(path.join(agentDir, `${personaName}.md`), agentContent, "utf-8");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (outputFormats.includes("copilot")) {
|
|
384
|
+
const outDir = path.join(distPath, "copilot", scopePath, personaName);
|
|
385
|
+
ensureDir(outDir);
|
|
386
|
+
const content = buildCopilotOutput(personaName, personaConfig, composed, config, skillPath);
|
|
387
|
+
fs.writeFileSync(path.join(outDir, "copilot-instructions.md"), content, "utf-8");
|
|
388
|
+
if (personaConfig) {
|
|
389
|
+
fs.writeFileSync(
|
|
390
|
+
path.join(outDir, "persona.config.json"),
|
|
391
|
+
JSON.stringify(personaConfig, null, 2) + "\n",
|
|
392
|
+
"utf-8"
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
platforms.push("copilot");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return { persona: personaName, platforms, traitsInjected: injected, scope };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// Always-on instructions compilation — writes to each platform
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
function compileInstructions(
|
|
406
|
+
instructionsDir: string,
|
|
407
|
+
enabledInstructions: string[] | undefined,
|
|
408
|
+
distPath: string,
|
|
409
|
+
scopePath: string,
|
|
410
|
+
config: AgentBootConfig,
|
|
411
|
+
outputFormats: string[]
|
|
412
|
+
): void {
|
|
413
|
+
if (!fs.existsSync(instructionsDir)) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const files = fs.readdirSync(instructionsDir).filter((f) => f.endsWith(".md"));
|
|
418
|
+
const provenanceEnabled = config.output?.provenanceHeaders !== false;
|
|
419
|
+
|
|
420
|
+
for (const platform of outputFormats) {
|
|
421
|
+
// CC uses "rules/" for always-on instructions; other platforms use "instructions/"
|
|
422
|
+
const dirName = platform === "claude" ? "rules" : "instructions";
|
|
423
|
+
const outDir = path.join(distPath, platform, scopePath, dirName);
|
|
424
|
+
ensureDir(outDir);
|
|
425
|
+
|
|
426
|
+
for (const file of files) {
|
|
427
|
+
const name = path.basename(file, ".md");
|
|
428
|
+
if (enabledInstructions && !enabledInstructions.includes(name)) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const srcPath = path.join(instructionsDir, file);
|
|
432
|
+
let content = fs.readFileSync(srcPath, "utf-8");
|
|
433
|
+
|
|
434
|
+
// Strip HTML comments for copilot output
|
|
435
|
+
if (platform === "copilot") {
|
|
436
|
+
content = content.replace(/<!--[\s\S]*?-->/g, "").trim() + "\n";
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
let finalContent: string;
|
|
440
|
+
if (!provenanceEnabled) {
|
|
441
|
+
finalContent = content;
|
|
442
|
+
} else if (platform === "claude") {
|
|
443
|
+
// For CC rules, frontmatter must be the first thing in the file.
|
|
444
|
+
// Insert provenance after the closing --- of frontmatter.
|
|
445
|
+
const fmMatch = content.match(/^(---\n[\s\S]*?\n---\n)/);
|
|
446
|
+
if (fmMatch) {
|
|
447
|
+
const afterFm = content.slice(fmMatch[1].length);
|
|
448
|
+
finalContent = `${fmMatch[1]}\n${provenanceHeader(srcPath, config)}${afterFm}`;
|
|
449
|
+
} else {
|
|
450
|
+
finalContent = `${provenanceHeader(srcPath, config)}${content}`;
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
finalContent = `${provenanceHeader(srcPath, config)}${content}`;
|
|
454
|
+
}
|
|
455
|
+
fs.writeFileSync(path.join(outDir, file), finalContent, "utf-8");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
// AB-52: Gotchas compilation — path-scoped knowledge rules
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
function compileGotchas(
|
|
465
|
+
gotchasDir: string,
|
|
466
|
+
distPath: string,
|
|
467
|
+
scopePath: string,
|
|
468
|
+
config: AgentBootConfig,
|
|
469
|
+
outputFormats: string[]
|
|
470
|
+
): void {
|
|
471
|
+
if (!fs.existsSync(gotchasDir)) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const gotchaFiles = fs.readdirSync(gotchasDir).filter(
|
|
476
|
+
(f) => f.endsWith(".md") && f !== "README.md"
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
if (gotchaFiles.length === 0) return;
|
|
480
|
+
|
|
481
|
+
log(chalk.gray(` Gotchas: ${gotchaFiles.length} rule(s)`));
|
|
482
|
+
|
|
483
|
+
for (const file of gotchaFiles) {
|
|
484
|
+
const content = fs.readFileSync(path.join(gotchasDir, file), "utf-8");
|
|
485
|
+
const provenanceEnabled = config.output?.provenanceHeaders !== false;
|
|
486
|
+
const header = provenanceEnabled
|
|
487
|
+
? provenanceHeader(path.join(gotchasDir, file), config)
|
|
488
|
+
: "";
|
|
489
|
+
|
|
490
|
+
// Write to claude rules (gotchas are path-scoped rules)
|
|
491
|
+
if (outputFormats.includes("claude")) {
|
|
492
|
+
const rulesDir = path.join(distPath, "claude", scopePath, "rules");
|
|
493
|
+
ensureDir(rulesDir);
|
|
494
|
+
fs.writeFileSync(path.join(rulesDir, file), `${header}${content}`, "utf-8");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Write to skill output as well
|
|
498
|
+
if (outputFormats.includes("skill")) {
|
|
499
|
+
const gotchaOutDir = path.join(distPath, "skill", scopePath, "gotchas");
|
|
500
|
+
ensureDir(gotchaOutDir);
|
|
501
|
+
fs.writeFileSync(path.join(gotchaOutDir, file), `${header}${content}`, "utf-8");
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
// PERSONAS.md index generation — writes to each platform
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
|
|
510
|
+
function generatePersonasIndex(
|
|
511
|
+
results: CompileResult[],
|
|
512
|
+
config: AgentBootConfig,
|
|
513
|
+
personasBaseDir: string,
|
|
514
|
+
distPath: string,
|
|
515
|
+
scopePath: string,
|
|
516
|
+
outputFormats: string[]
|
|
517
|
+
): void {
|
|
518
|
+
const org = config.orgDisplayName ?? config.org;
|
|
519
|
+
const lines: string[] = [
|
|
520
|
+
`<!-- AgentBoot generated — do not edit manually. Org: ${org} -->`,
|
|
521
|
+
"",
|
|
522
|
+
`# Available Personas`,
|
|
523
|
+
"",
|
|
524
|
+
`> Generated by AgentBoot for **${org}**. Run \`npm run build\` to refresh.`,
|
|
525
|
+
"",
|
|
526
|
+
"| Persona | Invocation | Description |",
|
|
527
|
+
"|---|---|---|",
|
|
528
|
+
];
|
|
529
|
+
|
|
530
|
+
for (const result of results.filter((r) => r.platforms.length > 0)) {
|
|
531
|
+
const personaConfigPath = path.join(personasBaseDir, result.persona, "persona.config.json");
|
|
532
|
+
let invocation = `/${result.persona}`;
|
|
533
|
+
let description = "";
|
|
534
|
+
|
|
535
|
+
if (fs.existsSync(personaConfigPath)) {
|
|
536
|
+
try {
|
|
537
|
+
const pc = JSON.parse(fs.readFileSync(personaConfigPath, "utf-8")) as PersonaConfig;
|
|
538
|
+
invocation = pc.invocation ?? invocation;
|
|
539
|
+
description = pc.description ?? "";
|
|
540
|
+
} catch {
|
|
541
|
+
// ignore
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
lines.push(`| **${result.persona}** | \`${invocation}\` | ${description} |`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
lines.push("", `*Last compiled: ${new Date().toISOString()}*`, "");
|
|
549
|
+
const content = lines.join("\n");
|
|
550
|
+
|
|
551
|
+
for (const platform of outputFormats) {
|
|
552
|
+
const outDir = path.join(distPath, platform, scopePath);
|
|
553
|
+
ensureDir(outDir);
|
|
554
|
+
fs.writeFileSync(path.join(outDir, "PERSONAS.md"), content, "utf-8");
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ---------------------------------------------------------------------------
|
|
559
|
+
// AB-19: CLAUDE.md with @import directives + trait files
|
|
560
|
+
// ---------------------------------------------------------------------------
|
|
561
|
+
|
|
562
|
+
function generateClaudeMd(
|
|
563
|
+
traitNames: string[],
|
|
564
|
+
traits: Map<string, TraitContent>,
|
|
565
|
+
instructionFileNames: string[],
|
|
566
|
+
config: AgentBootConfig,
|
|
567
|
+
distPath: string,
|
|
568
|
+
scopePath: string,
|
|
569
|
+
personaConfigs?: Map<string, PersonaConfig>
|
|
570
|
+
): void {
|
|
571
|
+
const org = config.orgDisplayName ?? config.org;
|
|
572
|
+
|
|
573
|
+
// Write trait files to dist/claude/{scopePath}/traits/
|
|
574
|
+
const traitsDir = path.join(distPath, "claude", scopePath, "traits");
|
|
575
|
+
ensureDir(traitsDir);
|
|
576
|
+
|
|
577
|
+
for (const traitName of traitNames) {
|
|
578
|
+
const trait = traits.get(traitName);
|
|
579
|
+
if (trait) {
|
|
580
|
+
fs.writeFileSync(path.join(traitsDir, `${traitName}.md`), trait.content, "utf-8");
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Build CLAUDE.md with @import directives
|
|
585
|
+
const lines: string[] = [
|
|
586
|
+
`# AgentBoot — ${org}`,
|
|
587
|
+
"",
|
|
588
|
+
"<!-- Auto-generated. Do not edit manually. -->",
|
|
589
|
+
"",
|
|
590
|
+
];
|
|
591
|
+
|
|
592
|
+
if (traitNames.length > 0) {
|
|
593
|
+
lines.push("## Traits", "");
|
|
594
|
+
for (const traitName of traitNames) {
|
|
595
|
+
if (traits.has(traitName)) {
|
|
596
|
+
lines.push(`@.claude/traits/${traitName}.md`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
lines.push("");
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (instructionFileNames.length > 0) {
|
|
603
|
+
lines.push("## Instructions", "");
|
|
604
|
+
for (const instrName of instructionFileNames) {
|
|
605
|
+
lines.push(`@.claude/rules/${instrName}.md`);
|
|
606
|
+
}
|
|
607
|
+
lines.push("");
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// AB-77: First-session welcome fragment
|
|
611
|
+
if (personaConfigs && personaConfigs.size > 0) {
|
|
612
|
+
lines.push("## Available Personas", "");
|
|
613
|
+
for (const [, pc] of personaConfigs) {
|
|
614
|
+
const cmd = pc.invocation ?? `/${pc.name}`;
|
|
615
|
+
lines.push(`- \`${cmd}\` — ${pc.description}`);
|
|
616
|
+
}
|
|
617
|
+
lines.push("");
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const claudeMdPath = path.join(distPath, "claude", scopePath, "CLAUDE.md");
|
|
621
|
+
fs.writeFileSync(claudeMdPath, lines.join("\n"), "utf-8");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ---------------------------------------------------------------------------
|
|
625
|
+
// AB-26: settings.json generation
|
|
626
|
+
// ---------------------------------------------------------------------------
|
|
627
|
+
|
|
628
|
+
function generateSettingsJson(
|
|
629
|
+
config: AgentBootConfig,
|
|
630
|
+
distPath: string,
|
|
631
|
+
scopePath: string
|
|
632
|
+
): void {
|
|
633
|
+
const hooks = config.claude?.hooks;
|
|
634
|
+
const permissions = config.claude?.permissions;
|
|
635
|
+
|
|
636
|
+
if (!hooks && !permissions) return;
|
|
637
|
+
|
|
638
|
+
// Validate hooks structure (must be an object with string keys)
|
|
639
|
+
if (hooks && typeof hooks !== "object") {
|
|
640
|
+
log(chalk.yellow(" ⚠ config.claude.hooks must be an object — skipping settings.json"));
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (permissions) {
|
|
644
|
+
if (permissions.allow && !Array.isArray(permissions.allow)) {
|
|
645
|
+
log(chalk.yellow(" ⚠ config.claude.permissions.allow must be an array — skipping"));
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (permissions.deny && !Array.isArray(permissions.deny)) {
|
|
649
|
+
log(chalk.yellow(" ⚠ config.claude.permissions.deny must be an array — skipping"));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
log(chalk.yellow(" ⚠ Generating settings.json with hooks/permissions — these will be synced to all target repos"));
|
|
655
|
+
|
|
656
|
+
const settings: Record<string, unknown> = {};
|
|
657
|
+
if (hooks) settings.hooks = hooks;
|
|
658
|
+
if (permissions) settings.permissions = permissions;
|
|
659
|
+
|
|
660
|
+
const settingsPath = path.join(distPath, "claude", scopePath, "settings.json");
|
|
661
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
// AB-27: .mcp.json generation
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
|
|
668
|
+
function generateMcpJson(
|
|
669
|
+
config: AgentBootConfig,
|
|
670
|
+
distPath: string,
|
|
671
|
+
scopePath: string
|
|
672
|
+
): void {
|
|
673
|
+
const mcpServers = config.claude?.mcpServers;
|
|
674
|
+
if (!mcpServers) return;
|
|
675
|
+
|
|
676
|
+
if (typeof mcpServers !== "object") {
|
|
677
|
+
log(chalk.yellow(" ⚠ config.claude.mcpServers must be an object — skipping .mcp.json"));
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
log(chalk.yellow(" ⚠ Generating .mcp.json with MCP servers — these will be synced to all target repos"));
|
|
682
|
+
|
|
683
|
+
const mcpJson = { mcpServers };
|
|
684
|
+
const mcpPath = path.join(distPath, "claude", scopePath, ".mcp.json");
|
|
685
|
+
fs.writeFileSync(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf-8");
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ---------------------------------------------------------------------------
|
|
689
|
+
// Main entry point
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
691
|
+
|
|
692
|
+
function main(): void {
|
|
693
|
+
const argv = process.argv.slice(2);
|
|
694
|
+
const configPath = resolveConfigPath(argv, ROOT);
|
|
695
|
+
|
|
696
|
+
log(chalk.bold("\nAgentBoot — compile"));
|
|
697
|
+
log(chalk.gray(`Config: ${configPath}\n`));
|
|
698
|
+
|
|
699
|
+
const config = loadConfig(configPath);
|
|
700
|
+
const configDir = path.dirname(configPath);
|
|
701
|
+
|
|
702
|
+
const distPath = path.resolve(
|
|
703
|
+
configDir,
|
|
704
|
+
config.output?.distPath ?? "./dist"
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
// Optional: fail on dirty dist.
|
|
708
|
+
if (config.output?.failOnDirtyDist && fs.existsSync(distPath)) {
|
|
709
|
+
const entries = fs.readdirSync(distPath);
|
|
710
|
+
if (entries.length > 0) {
|
|
711
|
+
fatal(
|
|
712
|
+
`dist/ is not empty and failOnDirtyDist is enabled. Run: rm -rf ${distPath}`
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
ensureDir(distPath);
|
|
718
|
+
|
|
719
|
+
const coreDir = path.join(ROOT, "core");
|
|
720
|
+
const corePersonasDir = path.join(coreDir, "personas");
|
|
721
|
+
const coreTraitsDir = path.join(coreDir, "traits");
|
|
722
|
+
const coreInstructionsDir = path.join(coreDir, "instructions");
|
|
723
|
+
|
|
724
|
+
const validFormats = ["skill", "claude", "copilot"];
|
|
725
|
+
const outputFormats = config.personas?.outputFormats ?? validFormats;
|
|
726
|
+
const unknownFormats = outputFormats.filter((f) => !validFormats.includes(f));
|
|
727
|
+
if (unknownFormats.length > 0) {
|
|
728
|
+
console.error(chalk.red(`Unknown output format(s): ${unknownFormats.join(", ")}. Valid: ${validFormats.join(", ")}`));
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Load traits.
|
|
733
|
+
const enabledTraits = config.traits?.enabled;
|
|
734
|
+
const traits = loadTraits(coreTraitsDir, enabledTraits);
|
|
735
|
+
|
|
736
|
+
log(chalk.cyan(`Traits loaded: ${traits.size}`));
|
|
737
|
+
for (const name of traits.keys()) {
|
|
738
|
+
log(chalk.gray(` + ${name}`));
|
|
739
|
+
}
|
|
740
|
+
log(chalk.cyan(`Output formats: ${outputFormats.join(", ")}`));
|
|
741
|
+
log("");
|
|
742
|
+
|
|
743
|
+
const enabledPersonas = config.personas?.enabled;
|
|
744
|
+
|
|
745
|
+
// Discover persona directories.
|
|
746
|
+
const personaDirs = new Map<string, string>();
|
|
747
|
+
|
|
748
|
+
if (fs.existsSync(corePersonasDir)) {
|
|
749
|
+
for (const entry of fs.readdirSync(corePersonasDir)) {
|
|
750
|
+
const dir = path.join(corePersonasDir, entry);
|
|
751
|
+
if (fs.statSync(dir).isDirectory()) {
|
|
752
|
+
personaDirs.set(entry, dir);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (config.personas?.customDir) {
|
|
758
|
+
const extendDir = path.resolve(configDir, config.personas.extend);
|
|
759
|
+
if (fs.existsSync(extendDir)) {
|
|
760
|
+
for (const entry of fs.readdirSync(extendDir)) {
|
|
761
|
+
const dir = path.join(extendDir, entry);
|
|
762
|
+
if (fs.statSync(dir).isDirectory()) {
|
|
763
|
+
if (personaDirs.has(entry)) {
|
|
764
|
+
log(chalk.yellow(` ⚠ Extension persona overrides core: ${entry}`));
|
|
765
|
+
}
|
|
766
|
+
personaDirs.set(entry, dir);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
log(chalk.yellow(` ⚠ Extension path not found: ${extendDir}`));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const allResults: CompileResult[] = [];
|
|
775
|
+
|
|
776
|
+
// ---------------------------------------------------------------------------
|
|
777
|
+
// 1. Compile core personas → dist/{platform}/core/{persona}/
|
|
778
|
+
// ---------------------------------------------------------------------------
|
|
779
|
+
|
|
780
|
+
log(chalk.cyan("Compiling core personas..."));
|
|
781
|
+
|
|
782
|
+
for (const [personaName, personaDir] of personaDirs) {
|
|
783
|
+
if (enabledPersonas && !enabledPersonas.includes(personaName)) {
|
|
784
|
+
log(chalk.gray(` - ${personaName} (disabled)`));
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const result = compilePersona(
|
|
789
|
+
personaName,
|
|
790
|
+
personaDir,
|
|
791
|
+
traits,
|
|
792
|
+
config,
|
|
793
|
+
distPath,
|
|
794
|
+
"core" // scopePath → dist/{platform}/core/{persona}/
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
allResults.push(result);
|
|
798
|
+
|
|
799
|
+
const traitsNote =
|
|
800
|
+
result.traitsInjected.length > 0
|
|
801
|
+
? chalk.gray(` [traits: ${result.traitsInjected.join(", ")}]`)
|
|
802
|
+
: chalk.gray(" [no traits]");
|
|
803
|
+
log(` ${chalk.green("✓")} ${personaName}${traitsNote}`);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Compile always-on instructions.
|
|
807
|
+
compileInstructions(
|
|
808
|
+
coreInstructionsDir,
|
|
809
|
+
config.instructions?.enabled,
|
|
810
|
+
distPath,
|
|
811
|
+
"core",
|
|
812
|
+
config,
|
|
813
|
+
outputFormats
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
// AB-52: Compile gotchas (path-scoped knowledge rules)
|
|
817
|
+
const coreGotchasDir = path.join(coreDir, "gotchas");
|
|
818
|
+
compileGotchas(coreGotchasDir, distPath, "core", config, outputFormats);
|
|
819
|
+
|
|
820
|
+
// AB-19/26/27: Claude-specific output (CLAUDE.md, settings.json, .mcp.json)
|
|
821
|
+
if (outputFormats.includes("claude")) {
|
|
822
|
+
// Collect instruction file names for @import directives
|
|
823
|
+
const instrFileNames: string[] = [];
|
|
824
|
+
if (fs.existsSync(coreInstructionsDir)) {
|
|
825
|
+
const instrFiles = fs.readdirSync(coreInstructionsDir).filter((f) => f.endsWith(".md"));
|
|
826
|
+
for (const file of instrFiles) {
|
|
827
|
+
const name = path.basename(file, ".md");
|
|
828
|
+
if (!config.instructions?.enabled || config.instructions.enabled.includes(name)) {
|
|
829
|
+
instrFileNames.push(name);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Collect persona configs for welcome fragment (AB-77)
|
|
835
|
+
const personaConfigs = new Map<string, PersonaConfig>();
|
|
836
|
+
for (const [personaName, personaDir] of personaDirs) {
|
|
837
|
+
if (enabledPersonas && !enabledPersonas.includes(personaName)) continue;
|
|
838
|
+
const pc = loadPersonaConfig(personaDir);
|
|
839
|
+
if (pc) personaConfigs.set(personaName, pc);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
generateClaudeMd(
|
|
843
|
+
[...traits.keys()],
|
|
844
|
+
traits,
|
|
845
|
+
instrFileNames,
|
|
846
|
+
config,
|
|
847
|
+
distPath,
|
|
848
|
+
"core",
|
|
849
|
+
personaConfigs
|
|
850
|
+
);
|
|
851
|
+
|
|
852
|
+
generateSettingsJson(config, distPath, "core");
|
|
853
|
+
generateMcpJson(config, distPath, "core");
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ---------------------------------------------------------------------------
|
|
857
|
+
// 2. Compile group-level overrides → dist/{platform}/groups/{group}/{persona}/
|
|
858
|
+
// ---------------------------------------------------------------------------
|
|
859
|
+
|
|
860
|
+
if (config.groups) {
|
|
861
|
+
log(chalk.cyan("\nCompiling group-level personas..."));
|
|
862
|
+
|
|
863
|
+
for (const groupName of Object.keys(config.groups)) {
|
|
864
|
+
const groupPersonasDir = path.join(ROOT, "groups", groupName, "personas");
|
|
865
|
+
|
|
866
|
+
if (!fs.existsSync(groupPersonasDir)) {
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const groupPersonaDirs = fs.readdirSync(groupPersonasDir).filter((entry) =>
|
|
871
|
+
fs.statSync(path.join(groupPersonasDir, entry)).isDirectory()
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
for (const personaName of groupPersonaDirs) {
|
|
875
|
+
if (enabledPersonas && !enabledPersonas.includes(personaName)) {
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const personaDir = path.join(groupPersonasDir, personaName);
|
|
880
|
+
const result = compilePersona(
|
|
881
|
+
personaName,
|
|
882
|
+
personaDir,
|
|
883
|
+
traits,
|
|
884
|
+
config,
|
|
885
|
+
distPath,
|
|
886
|
+
`groups/${groupName}`,
|
|
887
|
+
groupName
|
|
888
|
+
);
|
|
889
|
+
allResults.push(result);
|
|
890
|
+
log(` ${chalk.green("✓")} ${groupName}/${personaName}`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// ---------------------------------------------------------------------------
|
|
896
|
+
// 3. Compile team-level overrides → dist/{platform}/teams/{group}/{team}/{persona}/
|
|
897
|
+
// ---------------------------------------------------------------------------
|
|
898
|
+
|
|
899
|
+
if (config.groups) {
|
|
900
|
+
log(chalk.cyan("\nCompiling team-level personas..."));
|
|
901
|
+
let teamPersonasFound = false;
|
|
902
|
+
|
|
903
|
+
for (const groupName of Object.keys(config.groups)) {
|
|
904
|
+
const teams = config.groups[groupName]!.teams ?? [];
|
|
905
|
+
|
|
906
|
+
for (const teamName of teams) {
|
|
907
|
+
const teamPersonasDir = path.join(ROOT, "teams", groupName, teamName, "personas");
|
|
908
|
+
|
|
909
|
+
if (!fs.existsSync(teamPersonasDir)) {
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
teamPersonasFound = true;
|
|
914
|
+
|
|
915
|
+
const teamPersonaDirs = fs.readdirSync(teamPersonasDir).filter((entry) =>
|
|
916
|
+
fs.statSync(path.join(teamPersonasDir, entry)).isDirectory()
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
for (const personaName of teamPersonaDirs) {
|
|
920
|
+
if (enabledPersonas && !enabledPersonas.includes(personaName)) {
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const personaDir = path.join(teamPersonasDir, personaName);
|
|
925
|
+
const result = compilePersona(
|
|
926
|
+
personaName,
|
|
927
|
+
personaDir,
|
|
928
|
+
traits,
|
|
929
|
+
config,
|
|
930
|
+
distPath,
|
|
931
|
+
`teams/${groupName}/${teamName}`, // scopePath → dist/{platform}/teams/{group}/{team}/{persona}/
|
|
932
|
+
groupName,
|
|
933
|
+
teamName
|
|
934
|
+
);
|
|
935
|
+
allResults.push(result);
|
|
936
|
+
log(` ${chalk.green("✓")} ${groupName}/${teamName}/${personaName}`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (!teamPersonasFound) {
|
|
942
|
+
log(chalk.gray(" (no team-level overrides found)"));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// ---------------------------------------------------------------------------
|
|
947
|
+
// 4. Generate PERSONAS.md index in each platform
|
|
948
|
+
// ---------------------------------------------------------------------------
|
|
949
|
+
|
|
950
|
+
generatePersonasIndex(allResults, config, corePersonasDir, distPath, "core", outputFormats);
|
|
951
|
+
log(chalk.gray("\n → PERSONAS.md written to each platform"));
|
|
952
|
+
|
|
953
|
+
// ---------------------------------------------------------------------------
|
|
954
|
+
// 5. AB-25: Token budget estimation
|
|
955
|
+
// ---------------------------------------------------------------------------
|
|
956
|
+
|
|
957
|
+
const tokenBudget = config.output?.tokenBudget?.warnAt ?? 8000;
|
|
958
|
+
log(chalk.cyan("\nToken estimates:"));
|
|
959
|
+
|
|
960
|
+
for (const result of allResults.filter((r) => r.platforms.length > 0)) {
|
|
961
|
+
const skillPath = path.join(distPath, "skill", "core", result.persona, "SKILL.md");
|
|
962
|
+
if (fs.existsSync(skillPath)) {
|
|
963
|
+
const content = fs.readFileSync(skillPath, "utf-8");
|
|
964
|
+
const estimatedTokens = Math.ceil(content.length / 4);
|
|
965
|
+
|
|
966
|
+
if (estimatedTokens > tokenBudget) {
|
|
967
|
+
log(
|
|
968
|
+
chalk.yellow(
|
|
969
|
+
` ⚠ [${result.persona}] estimated ${estimatedTokens} tokens (budget: ${tokenBudget})`
|
|
970
|
+
)
|
|
971
|
+
);
|
|
972
|
+
} else {
|
|
973
|
+
log(chalk.gray(` ${result.persona}: ~${estimatedTokens} tokens`));
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// ---------------------------------------------------------------------------
|
|
979
|
+
// Summary
|
|
980
|
+
// ---------------------------------------------------------------------------
|
|
981
|
+
|
|
982
|
+
const successCount = allResults.filter((r) => r.platforms.length > 0).length;
|
|
983
|
+
const platformCount = allResults.reduce((acc, r) => acc + r.platforms.length, 0);
|
|
984
|
+
|
|
985
|
+
log(
|
|
986
|
+
chalk.bold(
|
|
987
|
+
`\n${chalk.green("✓")} Compiled ${successCount} persona(s) × ${outputFormats.length} platform(s) → ${path.relative(ROOT, distPath)}/`
|
|
988
|
+
)
|
|
989
|
+
);
|
|
990
|
+
for (const fmt of outputFormats) {
|
|
991
|
+
log(chalk.gray(` → dist/${fmt}/`));
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
try {
|
|
996
|
+
main();
|
|
997
|
+
} catch (err: unknown) {
|
|
998
|
+
console.error(chalk.red("Unexpected error:"), err);
|
|
999
|
+
process.exit(1);
|
|
1000
|
+
}
|