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/scripts/compile.ts
CHANGED
|
@@ -33,9 +33,13 @@ import chalk from "chalk";
|
|
|
33
33
|
import {
|
|
34
34
|
type AgentBootConfig,
|
|
35
35
|
type PersonaConfig,
|
|
36
|
+
type DomainManifest,
|
|
37
|
+
type PluginManifest,
|
|
36
38
|
resolveConfigPath,
|
|
37
39
|
loadConfig,
|
|
38
40
|
stripJsoncComments,
|
|
41
|
+
flattenNodes,
|
|
42
|
+
groupsToNodes,
|
|
39
43
|
} from "./lib/config.js";
|
|
40
44
|
|
|
41
45
|
// ---------------------------------------------------------------------------
|
|
@@ -210,7 +214,7 @@ function injectTraits(
|
|
|
210
214
|
// ---------------------------------------------------------------------------
|
|
211
215
|
|
|
212
216
|
function buildSkillOutput(
|
|
213
|
-
|
|
217
|
+
_personaName: string,
|
|
214
218
|
_personaConfig: PersonaConfig | null,
|
|
215
219
|
composedContent: string,
|
|
216
220
|
config: AgentBootConfig,
|
|
@@ -236,8 +240,12 @@ function buildClaudeOutput(
|
|
|
236
240
|
const invocation = personaConfig?.invocation ?? `/${personaName}`;
|
|
237
241
|
const skillName = invocation.replace(/^\//, "");
|
|
238
242
|
const description = personaConfig?.description ?? personaName;
|
|
239
|
-
// Escape
|
|
240
|
-
const safeDescription = description
|
|
243
|
+
// Escape for YAML double-quoted strings
|
|
244
|
+
const safeDescription = description
|
|
245
|
+
.replace(/\\/g, "\\\\")
|
|
246
|
+
.replace(/"/g, '\\"')
|
|
247
|
+
.replace(/\n/g, " ")
|
|
248
|
+
.replace(/---/g, "\\-\\-\\-");
|
|
241
249
|
|
|
242
250
|
// AB-18: CC skill frontmatter with context:fork → delegates to agent
|
|
243
251
|
const frontmatterLines: string[] = [
|
|
@@ -363,9 +371,12 @@ function compilePersona(
|
|
|
363
371
|
const model = personaConfig?.model; // undefined = omit from frontmatter
|
|
364
372
|
const permMode = personaConfig?.permissionMode;
|
|
365
373
|
const agentDescription = personaConfig?.description ?? personaName;
|
|
366
|
-
// Escape
|
|
367
|
-
|
|
368
|
-
|
|
374
|
+
// Escape for YAML double-quoted strings: backslashes, quotes, newlines, and --- sequences.
|
|
375
|
+
const safeDescription = agentDescription
|
|
376
|
+
.replace(/\\/g, "\\\\") // backslashes first (before other escapes add more)
|
|
377
|
+
.replace(/"/g, '\\"') // double quotes
|
|
378
|
+
.replace(/\n/g, " ") // newlines → spaces
|
|
379
|
+
.replace(/---/g, "\\-\\-\\-"); // prevent YAML document markers
|
|
369
380
|
const withoutFrontmatter = composed.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
370
381
|
const agentFrontmatter: string[] = [
|
|
371
382
|
"---",
|
|
@@ -444,7 +455,7 @@ function compileInstructions(
|
|
|
444
455
|
// Insert provenance after the closing --- of frontmatter.
|
|
445
456
|
const fmMatch = content.match(/^(---\n[\s\S]*?\n---\n)/);
|
|
446
457
|
if (fmMatch) {
|
|
447
|
-
const afterFm = content.slice(fmMatch[1]
|
|
458
|
+
const afterFm = content.slice(fmMatch[1]!.length);
|
|
448
459
|
finalContent = `${fmMatch[1]}\n${provenanceHeader(srcPath, config)}${afterFm}`;
|
|
449
460
|
} else {
|
|
450
461
|
finalContent = `${provenanceHeader(srcPath, config)}${content}`;
|
|
@@ -651,11 +662,27 @@ function generateSettingsJson(
|
|
|
651
662
|
}
|
|
652
663
|
}
|
|
653
664
|
|
|
654
|
-
|
|
665
|
+
// Security: hooks execute shell commands in target repos — warn prominently
|
|
666
|
+
if (hooks) {
|
|
667
|
+
log(chalk.red(" ⚠ CAUTION: settings.json contains hooks that execute shell commands in target repos."));
|
|
668
|
+
log(chalk.red(" Review claude.hooks in agentboot.config.json carefully before syncing."));
|
|
669
|
+
// Validate hook event names against known CC events
|
|
670
|
+
const validEvents = [
|
|
671
|
+
"PreToolUse", "PostToolUse", "Notification", "Stop",
|
|
672
|
+
"SubagentStop", "SubagentStart", "UserPromptSubmit", "SessionEnd",
|
|
673
|
+
];
|
|
674
|
+
for (const key of Object.keys(hooks)) {
|
|
675
|
+
if (!validEvents.includes(key)) {
|
|
676
|
+
log(chalk.yellow(` ⚠ Unknown hook event: "${key}" — may not be recognized by Claude Code`));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
} else {
|
|
680
|
+
log(chalk.yellow(" ⚠ Generating settings.json with permissions — these will be synced to all target repos"));
|
|
681
|
+
}
|
|
655
682
|
|
|
656
683
|
const settings: Record<string, unknown> = {};
|
|
657
|
-
if (hooks) settings
|
|
658
|
-
if (permissions) settings
|
|
684
|
+
if (hooks) settings["hooks"] = hooks;
|
|
685
|
+
if (permissions) settings["permissions"] = permissions;
|
|
659
686
|
|
|
660
687
|
const settingsPath = path.join(distPath, "claude", scopePath, "settings.json");
|
|
661
688
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
@@ -685,6 +712,603 @@ function generateMcpJson(
|
|
|
685
712
|
fs.writeFileSync(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf-8");
|
|
686
713
|
}
|
|
687
714
|
|
|
715
|
+
// ---------------------------------------------------------------------------
|
|
716
|
+
// AB-53: Domain layer loading
|
|
717
|
+
// ---------------------------------------------------------------------------
|
|
718
|
+
|
|
719
|
+
function loadDomainManifest(domainDir: string): DomainManifest | null {
|
|
720
|
+
const manifestPath = path.join(domainDir, "agentboot.domain.json");
|
|
721
|
+
if (!fs.existsSync(manifestPath)) {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
const raw = fs.readFileSync(manifestPath, "utf-8");
|
|
726
|
+
return JSON.parse(stripJsoncComments(raw)) as DomainManifest;
|
|
727
|
+
} catch {
|
|
728
|
+
log(chalk.yellow(` ⚠ Failed to parse agentboot.domain.json in ${domainDir}`));
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function compileDomains(
|
|
734
|
+
config: AgentBootConfig,
|
|
735
|
+
configDir: string,
|
|
736
|
+
distPath: string,
|
|
737
|
+
traits: Map<string, TraitContent>,
|
|
738
|
+
outputFormats: string[]
|
|
739
|
+
): CompileResult[] {
|
|
740
|
+
const domains = config.domains;
|
|
741
|
+
if (!domains || domains.length === 0) return [];
|
|
742
|
+
|
|
743
|
+
log(chalk.cyan("\nCompiling domain layers..."));
|
|
744
|
+
const results: CompileResult[] = [];
|
|
745
|
+
|
|
746
|
+
for (const domainRef of domains) {
|
|
747
|
+
const domainPath = typeof domainRef === "string"
|
|
748
|
+
? path.resolve(configDir, domainRef)
|
|
749
|
+
: path.resolve(configDir, domainRef.path ?? `./domains/${domainRef.name}`);
|
|
750
|
+
|
|
751
|
+
if (!fs.existsSync(domainPath)) {
|
|
752
|
+
log(chalk.yellow(` ⚠ Domain not found: ${domainPath}`));
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// S3 fix: path traversal protection — resolve symlinks then check boundary
|
|
757
|
+
const boundary = path.resolve(configDir);
|
|
758
|
+
const realDomainPath = fs.realpathSync(domainPath);
|
|
759
|
+
if (!realDomainPath.startsWith(boundary + path.sep) && realDomainPath !== boundary) {
|
|
760
|
+
log(chalk.red(` ✗ Domain path escapes project boundary: ${domainPath} → ${realDomainPath}`));
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const manifest = loadDomainManifest(domainPath);
|
|
765
|
+
const domainName = manifest?.name ?? path.basename(domainPath);
|
|
766
|
+
log(chalk.gray(` Domain: ${domainName}${manifest?.version ? ` v${manifest.version}` : ""}`));
|
|
767
|
+
|
|
768
|
+
// Load domain-specific traits
|
|
769
|
+
const domainTraitsDir = path.join(domainPath, "traits");
|
|
770
|
+
if (fs.existsSync(domainTraitsDir)) {
|
|
771
|
+
const domainTraits = loadTraits(domainTraitsDir, undefined);
|
|
772
|
+
for (const [name, trait] of domainTraits) {
|
|
773
|
+
if (traits.has(name)) {
|
|
774
|
+
log(chalk.yellow(` ⚠ Domain trait '${name}' shadows existing trait`));
|
|
775
|
+
}
|
|
776
|
+
traits.set(name, trait);
|
|
777
|
+
}
|
|
778
|
+
log(chalk.gray(` + ${domainTraits.size} trait(s)`));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Compile domain personas
|
|
782
|
+
const domainPersonasDir = path.join(domainPath, "personas");
|
|
783
|
+
if (fs.existsSync(domainPersonasDir)) {
|
|
784
|
+
const personaDirs = fs.readdirSync(domainPersonasDir).filter((entry) =>
|
|
785
|
+
fs.statSync(path.join(domainPersonasDir, entry)).isDirectory()
|
|
786
|
+
);
|
|
787
|
+
for (const personaName of personaDirs) {
|
|
788
|
+
const personaDir = path.join(domainPersonasDir, personaName);
|
|
789
|
+
const result = compilePersona(
|
|
790
|
+
personaName,
|
|
791
|
+
personaDir,
|
|
792
|
+
traits,
|
|
793
|
+
config,
|
|
794
|
+
distPath,
|
|
795
|
+
`domains/${domainName}`
|
|
796
|
+
);
|
|
797
|
+
results.push(result);
|
|
798
|
+
log(` ${chalk.green("✓")} ${personaName}`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Compile domain instructions
|
|
803
|
+
const domainInstructionsDir = path.join(domainPath, "instructions");
|
|
804
|
+
compileInstructions(
|
|
805
|
+
domainInstructionsDir,
|
|
806
|
+
undefined,
|
|
807
|
+
distPath,
|
|
808
|
+
`domains/${domainName}`,
|
|
809
|
+
config,
|
|
810
|
+
outputFormats
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return results;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ---------------------------------------------------------------------------
|
|
818
|
+
// AB-57: Plugin structure generation
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
|
|
821
|
+
function generatePluginOutput(
|
|
822
|
+
config: AgentBootConfig,
|
|
823
|
+
distPath: string,
|
|
824
|
+
allResults: CompileResult[],
|
|
825
|
+
personasBaseDir: string,
|
|
826
|
+
traits: Map<string, TraitContent>
|
|
827
|
+
): void {
|
|
828
|
+
const pluginDir = path.join(distPath, "plugin");
|
|
829
|
+
ensureDir(pluginDir);
|
|
830
|
+
|
|
831
|
+
const pkgPath = path.join(ROOT, "package.json");
|
|
832
|
+
const pkg = fs.existsSync(pkgPath)
|
|
833
|
+
? JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
|
834
|
+
: { version: "0.0.0" };
|
|
835
|
+
|
|
836
|
+
const personas: PluginManifest["personas"] = [];
|
|
837
|
+
const traitEntries: PluginManifest["traits"] = [];
|
|
838
|
+
const hookEntries: PluginManifest["hooks"] = [];
|
|
839
|
+
const ruleEntries: PluginManifest["rules"] = [];
|
|
840
|
+
|
|
841
|
+
// Copy agents and skills from claude output
|
|
842
|
+
const claudeCorePath = path.join(distPath, "claude", "core");
|
|
843
|
+
|
|
844
|
+
// Agents
|
|
845
|
+
const agentsDir = path.join(claudeCorePath, "agents");
|
|
846
|
+
const pluginAgentsDir = path.join(pluginDir, "agents");
|
|
847
|
+
if (fs.existsSync(agentsDir)) {
|
|
848
|
+
ensureDir(pluginAgentsDir);
|
|
849
|
+
for (const file of fs.readdirSync(agentsDir)) {
|
|
850
|
+
fs.copyFileSync(path.join(agentsDir, file), path.join(pluginAgentsDir, file));
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Skills
|
|
855
|
+
const skillsDir = path.join(claudeCorePath, "skills");
|
|
856
|
+
const pluginSkillsDir = path.join(pluginDir, "skills");
|
|
857
|
+
if (fs.existsSync(skillsDir)) {
|
|
858
|
+
ensureDir(pluginSkillsDir);
|
|
859
|
+
for (const skillFolder of fs.readdirSync(skillsDir)) {
|
|
860
|
+
const src = path.join(skillsDir, skillFolder);
|
|
861
|
+
if (fs.statSync(src).isDirectory()) {
|
|
862
|
+
const dest = path.join(pluginSkillsDir, skillFolder);
|
|
863
|
+
ensureDir(dest);
|
|
864
|
+
for (const file of fs.readdirSync(src)) {
|
|
865
|
+
fs.copyFileSync(path.join(src, file), path.join(dest, file));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Traits
|
|
872
|
+
const pluginTraitsDir = path.join(pluginDir, "traits");
|
|
873
|
+
ensureDir(pluginTraitsDir);
|
|
874
|
+
for (const [name, trait] of traits) {
|
|
875
|
+
fs.writeFileSync(path.join(pluginTraitsDir, `${name}.md`), trait.content, "utf-8");
|
|
876
|
+
traitEntries.push({ id: name, path: `traits/${name}.md` });
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Rules
|
|
880
|
+
const rulesDir = path.join(claudeCorePath, "rules");
|
|
881
|
+
const pluginRulesDir = path.join(pluginDir, "rules");
|
|
882
|
+
if (fs.existsSync(rulesDir)) {
|
|
883
|
+
ensureDir(pluginRulesDir);
|
|
884
|
+
for (const file of fs.readdirSync(rulesDir)) {
|
|
885
|
+
fs.copyFileSync(path.join(rulesDir, file), path.join(pluginRulesDir, file));
|
|
886
|
+
ruleEntries.push({ path: `rules/${file}` });
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Hooks directory (compliance hooks go here)
|
|
891
|
+
const pluginHooksDir = path.join(pluginDir, "hooks");
|
|
892
|
+
ensureDir(pluginHooksDir);
|
|
893
|
+
|
|
894
|
+
// Build persona entries
|
|
895
|
+
for (const result of allResults.filter((r) => r.platforms.length > 0 && r.scope === "core")) {
|
|
896
|
+
const personaConfigPath = path.join(personasBaseDir, result.persona, "persona.config.json");
|
|
897
|
+
let pc: PersonaConfig | null = null;
|
|
898
|
+
if (fs.existsSync(personaConfigPath)) {
|
|
899
|
+
try {
|
|
900
|
+
pc = JSON.parse(fs.readFileSync(personaConfigPath, "utf-8")) as PersonaConfig;
|
|
901
|
+
} catch { /* skip */ }
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const invocation = pc?.invocation ?? `/${result.persona}`;
|
|
905
|
+
const skillName = invocation.replace(/^\//, "");
|
|
906
|
+
|
|
907
|
+
personas.push({
|
|
908
|
+
id: result.persona,
|
|
909
|
+
name: pc?.name ?? result.persona,
|
|
910
|
+
description: pc?.description ?? "",
|
|
911
|
+
model: pc?.model,
|
|
912
|
+
agent_path: `agents/${result.persona}.md`,
|
|
913
|
+
skill_path: `skills/${skillName}/SKILL.md`,
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Generate plugin.json
|
|
918
|
+
const pluginManifest: PluginManifest = {
|
|
919
|
+
name: `@${config.org}/${config.org}-personas`,
|
|
920
|
+
version: pkg.version,
|
|
921
|
+
description: `Agentic personas for ${config.orgDisplayName ?? config.org}`,
|
|
922
|
+
author: config.orgDisplayName ?? config.org,
|
|
923
|
+
license: "Apache-2.0",
|
|
924
|
+
agentboot_version: pkg.version,
|
|
925
|
+
personas,
|
|
926
|
+
traits: traitEntries,
|
|
927
|
+
hooks: hookEntries.length > 0 ? hookEntries : undefined,
|
|
928
|
+
rules: ruleEntries.length > 0 ? ruleEntries : undefined,
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
fs.writeFileSync(
|
|
932
|
+
path.join(pluginDir, "plugin.json"),
|
|
933
|
+
JSON.stringify(pluginManifest, null, 2) + "\n",
|
|
934
|
+
"utf-8"
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
log(chalk.gray(` → Plugin output written to dist/plugin/`));
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// ---------------------------------------------------------------------------
|
|
941
|
+
// AB-59/60/63: Compliance & audit trail hook generation
|
|
942
|
+
// ---------------------------------------------------------------------------
|
|
943
|
+
|
|
944
|
+
function generateComplianceHooks(
|
|
945
|
+
config: AgentBootConfig,
|
|
946
|
+
distPath: string,
|
|
947
|
+
scopePath: string
|
|
948
|
+
): void {
|
|
949
|
+
const hooksDir = path.join(distPath, "claude", scopePath, "hooks");
|
|
950
|
+
ensureDir(hooksDir);
|
|
951
|
+
|
|
952
|
+
// AB-59: Input scanning hook (UserPromptSubmit)
|
|
953
|
+
// S4 fix: use printf instead of echo to avoid flag interpretation
|
|
954
|
+
// S5 fix: add set -uo pipefail and jq dependency check
|
|
955
|
+
// Note: -e intentionally omitted because grep -q returns 1 on no-match
|
|
956
|
+
const inputScanHook = `#!/bin/bash
|
|
957
|
+
# AgentBoot compliance hook — input scanning (AB-59)
|
|
958
|
+
# Event: UserPromptSubmit
|
|
959
|
+
# Generated by AgentBoot. Do not edit manually.
|
|
960
|
+
|
|
961
|
+
set -uo pipefail
|
|
962
|
+
command -v jq >/dev/null 2>&1 || { echo '{"decision":"block","reason":"AgentBoot: jq is required for input scanning"}'; exit 2; }
|
|
963
|
+
|
|
964
|
+
INPUT=$(cat)
|
|
965
|
+
PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt // empty') || { echo '{"decision":"block","reason":"AgentBoot: Failed to parse hook input"}'; exit 2; }
|
|
966
|
+
|
|
967
|
+
# Scan for potential credential leaks in prompts
|
|
968
|
+
PATTERNS=(
|
|
969
|
+
'password[[:space:]]*[:=]'
|
|
970
|
+
'api[_-]?key[[:space:]]*[:=]'
|
|
971
|
+
'secret[[:space:]]*[:=]'
|
|
972
|
+
'token[[:space:]]*[:=]'
|
|
973
|
+
'AKIA[A-Z0-9]{16}'
|
|
974
|
+
'sk-[a-zA-Z0-9]{20,}'
|
|
975
|
+
'ghp_[a-zA-Z0-9]{36}'
|
|
976
|
+
'xox[bp]-[a-zA-Z0-9-]+'
|
|
977
|
+
'sk_live_[a-zA-Z0-9]+'
|
|
978
|
+
'BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY'
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
for pattern in "\${PATTERNS[@]}"; do
|
|
982
|
+
if printf '%s' "$PROMPT" | grep -qiE "$pattern"; then
|
|
983
|
+
echo '{"decision":"block","reason":"AgentBoot: Potential credential detected in prompt. Remove secrets before proceeding."}'
|
|
984
|
+
exit 2
|
|
985
|
+
fi
|
|
986
|
+
done
|
|
987
|
+
|
|
988
|
+
exit 0
|
|
989
|
+
`;
|
|
990
|
+
|
|
991
|
+
// AB-60: Output scanning hook (Stop)
|
|
992
|
+
const outputScanHook = `#!/bin/bash
|
|
993
|
+
# AgentBoot compliance hook — output scanning (AB-60)
|
|
994
|
+
# Event: Stop
|
|
995
|
+
# Generated by AgentBoot. Do not edit manually.
|
|
996
|
+
|
|
997
|
+
set -uo pipefail
|
|
998
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
999
|
+
|
|
1000
|
+
INPUT=$(cat)
|
|
1001
|
+
RESPONSE=$(printf '%s' "$INPUT" | jq -r '.response // empty') || exit 0
|
|
1002
|
+
|
|
1003
|
+
# Scan for accidental credential exposure in output
|
|
1004
|
+
PATTERNS=(
|
|
1005
|
+
'AKIA[A-Z0-9]{16}'
|
|
1006
|
+
'sk-[a-zA-Z0-9]{20,}'
|
|
1007
|
+
'ghp_[a-zA-Z0-9]{36}'
|
|
1008
|
+
'eyJ[a-zA-Z0-9_-]{10,}\\.eyJ'
|
|
1009
|
+
'xox[bp]-[a-zA-Z0-9-]+'
|
|
1010
|
+
'sk_live_[a-zA-Z0-9]+'
|
|
1011
|
+
'BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY'
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
for pattern in "\${PATTERNS[@]}"; do
|
|
1015
|
+
if printf '%s' "$RESPONSE" | grep -qiE "$pattern"; then
|
|
1016
|
+
echo "AgentBoot WARNING: Potential credential in output — review before sharing" >&2
|
|
1017
|
+
fi
|
|
1018
|
+
done
|
|
1019
|
+
|
|
1020
|
+
exit 0
|
|
1021
|
+
`;
|
|
1022
|
+
|
|
1023
|
+
// AB-63: Audit trail hook (SubagentStart/Stop, PostToolUse, SessionEnd)
|
|
1024
|
+
// S1 fix: use jq for safe JSON construction (no shell interpolation)
|
|
1025
|
+
// S2 fix: validate and sanitize telemetry.logPath
|
|
1026
|
+
let rawLogPath = config.telemetry?.logPath ?? "$HOME/.agentboot/telemetry.ndjson";
|
|
1027
|
+
// Normalize ~ to $HOME (~ is not expanded inside bash variable defaults)
|
|
1028
|
+
rawLogPath = rawLogPath.replace(/^~\//, "$HOME/");
|
|
1029
|
+
// Always reject path traversal
|
|
1030
|
+
if (/\.\./.test(rawLogPath)) {
|
|
1031
|
+
log(chalk.red(` ✗ telemetry.logPath contains path traversal: ${rawLogPath}`));
|
|
1032
|
+
log(chalk.red(` Use a simple path like ~/.agentboot/telemetry.ndjson`));
|
|
1033
|
+
process.exit(1);
|
|
1034
|
+
}
|
|
1035
|
+
// Reject shell metacharacters, exempting only a leading $HOME
|
|
1036
|
+
const pathWithoutHome = rawLogPath.replace(/^\$HOME/, "");
|
|
1037
|
+
if (/[`$|;&\n]/.test(pathWithoutHome)) {
|
|
1038
|
+
log(chalk.red(` ✗ telemetry.logPath contains unsafe shell characters: ${rawLogPath}`));
|
|
1039
|
+
log(chalk.red(` Use a simple path like ~/.agentboot/telemetry.ndjson`));
|
|
1040
|
+
process.exit(1);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const includeDevId = config.telemetry?.includeDevId ?? false;
|
|
1044
|
+
|
|
1045
|
+
let devIdBlock = "";
|
|
1046
|
+
if (includeDevId === "hashed") {
|
|
1047
|
+
devIdBlock = `DEV_ID=$(git config user.email 2>/dev/null | shasum -a 256 | cut -d' ' -f1)`;
|
|
1048
|
+
} else if (includeDevId === "email") {
|
|
1049
|
+
log(chalk.yellow(` ⚠ telemetry.includeDevId is "email" — raw emails will be in telemetry logs. Consider "hashed" for privacy.`));
|
|
1050
|
+
devIdBlock = `DEV_ID=$(git config user.email 2>/dev/null || echo "unknown")`;
|
|
1051
|
+
} else {
|
|
1052
|
+
devIdBlock = `DEV_ID=""`;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const auditTrailHook = `#!/bin/bash
|
|
1056
|
+
# AgentBoot audit trail hook (AB-63)
|
|
1057
|
+
# Events: SubagentStart, SubagentStop, PostToolUse, SessionEnd
|
|
1058
|
+
# Generated by AgentBoot. Do not edit manually.
|
|
1059
|
+
|
|
1060
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
1061
|
+
|
|
1062
|
+
TELEMETRY_LOG="\${AGENTBOOT_TELEMETRY_LOG:-${rawLogPath}}"
|
|
1063
|
+
umask 077
|
|
1064
|
+
mkdir -p "$(dirname "$TELEMETRY_LOG")"
|
|
1065
|
+
|
|
1066
|
+
INPUT=$(cat)
|
|
1067
|
+
EVENT_NAME=$(printf '%s' "$INPUT" | jq -r '.hook_event_name // empty')
|
|
1068
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1069
|
+
${devIdBlock}
|
|
1070
|
+
|
|
1071
|
+
# Use jq for safe JSON construction — prevents shell injection via agent_type/tool_name
|
|
1072
|
+
case "$EVENT_NAME" in
|
|
1073
|
+
SubagentStart)
|
|
1074
|
+
printf '%s' "$INPUT" | jq -c --arg ts "$TIMESTAMP" --arg status "started" --arg dev "$DEV_ID" \\
|
|
1075
|
+
'{event:"persona_invocation",persona_id:.agent_type,timestamp:$ts,status:$status,dev_id:$dev}' >> "$TELEMETRY_LOG"
|
|
1076
|
+
;;
|
|
1077
|
+
SubagentStop)
|
|
1078
|
+
printf '%s' "$INPUT" | jq -c --arg ts "$TIMESTAMP" --arg status "completed" --arg dev "$DEV_ID" \\
|
|
1079
|
+
'{event:"persona_invocation",persona_id:.agent_type,timestamp:$ts,status:$status,dev_id:$dev}' >> "$TELEMETRY_LOG"
|
|
1080
|
+
;;
|
|
1081
|
+
PostToolUse)
|
|
1082
|
+
printf '%s' "$INPUT" | jq -c --arg ts "$TIMESTAMP" --arg dev "$DEV_ID" \\
|
|
1083
|
+
'{event:"hook_execution",persona_id:.agent_type,tool_name:.tool_name,timestamp:$ts,dev_id:$dev}' >> "$TELEMETRY_LOG"
|
|
1084
|
+
;;
|
|
1085
|
+
SessionEnd)
|
|
1086
|
+
jq -n -c --arg ts "$TIMESTAMP" --arg dev "$DEV_ID" \\
|
|
1087
|
+
'{event:"session_summary",timestamp:$ts,dev_id:$dev}' >> "$TELEMETRY_LOG"
|
|
1088
|
+
;;
|
|
1089
|
+
esac
|
|
1090
|
+
|
|
1091
|
+
exit 0
|
|
1092
|
+
`;
|
|
1093
|
+
|
|
1094
|
+
fs.writeFileSync(path.join(hooksDir, "agentboot-input-scan.sh"), inputScanHook, { mode: 0o755 });
|
|
1095
|
+
fs.writeFileSync(path.join(hooksDir, "agentboot-output-scan.sh"), outputScanHook, { mode: 0o755 });
|
|
1096
|
+
fs.writeFileSync(path.join(hooksDir, "agentboot-telemetry.sh"), auditTrailHook, { mode: 0o755 });
|
|
1097
|
+
|
|
1098
|
+
// Also generate the plugin hooks
|
|
1099
|
+
const pluginHooksDir = path.join(distPath, "plugin", "hooks");
|
|
1100
|
+
if (fs.existsSync(path.join(distPath, "plugin"))) {
|
|
1101
|
+
ensureDir(pluginHooksDir);
|
|
1102
|
+
fs.writeFileSync(path.join(pluginHooksDir, "agentboot-input-scan.sh"), inputScanHook, { mode: 0o755 });
|
|
1103
|
+
fs.writeFileSync(path.join(pluginHooksDir, "agentboot-output-scan.sh"), outputScanHook, { mode: 0o755 });
|
|
1104
|
+
fs.writeFileSync(path.join(pluginHooksDir, "agentboot-telemetry.sh"), auditTrailHook, { mode: 0o755 });
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
log(chalk.gray(` → Compliance hooks written (input-scan, output-scan, telemetry)`));
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// ---------------------------------------------------------------------------
|
|
1111
|
+
// AB-59/60/63: Generate settings.json hooks entries for compliance
|
|
1112
|
+
// ---------------------------------------------------------------------------
|
|
1113
|
+
|
|
1114
|
+
function generateComplianceSettingsJson(
|
|
1115
|
+
_config: AgentBootConfig,
|
|
1116
|
+
distPath: string,
|
|
1117
|
+
scopePath: string
|
|
1118
|
+
): void {
|
|
1119
|
+
// Read existing settings.json if any, merge compliance hooks into it
|
|
1120
|
+
const settingsPath = path.join(distPath, "claude", scopePath, "settings.json");
|
|
1121
|
+
let settings: Record<string, unknown> = {};
|
|
1122
|
+
if (fs.existsSync(settingsPath)) {
|
|
1123
|
+
try {
|
|
1124
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
1125
|
+
} catch { /* start fresh */ }
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const hooks = (settings["hooks"] ?? {}) as Record<string, unknown>;
|
|
1129
|
+
|
|
1130
|
+
// B1 fix: append compliance hooks instead of overwriting user-defined hooks
|
|
1131
|
+
const appendHook = (event: string, entry: unknown) => {
|
|
1132
|
+
hooks[event] = [
|
|
1133
|
+
...(Array.isArray(hooks[event]) ? hooks[event] as unknown[] : []),
|
|
1134
|
+
entry,
|
|
1135
|
+
];
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
// AB-59: Input scanning
|
|
1139
|
+
appendHook("UserPromptSubmit", {
|
|
1140
|
+
matcher: "",
|
|
1141
|
+
hooks: [{ type: "command", command: ".claude/hooks/agentboot-input-scan.sh", timeout: 5000 }],
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
// AB-60: Output scanning
|
|
1145
|
+
appendHook("Stop", {
|
|
1146
|
+
matcher: "",
|
|
1147
|
+
hooks: [{ type: "command", command: ".claude/hooks/agentboot-output-scan.sh", timeout: 5000, async: true }],
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
// AB-63: Audit trail
|
|
1151
|
+
appendHook("SubagentStart", {
|
|
1152
|
+
matcher: "",
|
|
1153
|
+
hooks: [{ type: "command", command: ".claude/hooks/agentboot-telemetry.sh", timeout: 3000, async: true }],
|
|
1154
|
+
});
|
|
1155
|
+
appendHook("SubagentStop", {
|
|
1156
|
+
matcher: "",
|
|
1157
|
+
hooks: [{ type: "command", command: ".claude/hooks/agentboot-telemetry.sh", timeout: 3000, async: true }],
|
|
1158
|
+
});
|
|
1159
|
+
appendHook("PostToolUse", {
|
|
1160
|
+
matcher: "Edit|Write|Bash",
|
|
1161
|
+
hooks: [{ type: "command", command: ".claude/hooks/agentboot-telemetry.sh", timeout: 3000, async: true }],
|
|
1162
|
+
});
|
|
1163
|
+
// B3 fix: register SessionEnd (matches the case in telemetry hook script)
|
|
1164
|
+
appendHook("SessionEnd", {
|
|
1165
|
+
matcher: "",
|
|
1166
|
+
hooks: [{ type: "command", command: ".claude/hooks/agentboot-telemetry.sh", timeout: 3000, async: true }],
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
settings["hooks"] = hooks;
|
|
1170
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// ---------------------------------------------------------------------------
|
|
1174
|
+
// AB-64: Telemetry NDJSON schema file
|
|
1175
|
+
// ---------------------------------------------------------------------------
|
|
1176
|
+
|
|
1177
|
+
function generateTelemetrySchema(distPath: string): void {
|
|
1178
|
+
const schema = {
|
|
1179
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1180
|
+
$id: "https://agentboot.dev/schema/telemetry-event/v1",
|
|
1181
|
+
title: "AgentBoot Telemetry Event",
|
|
1182
|
+
type: "object",
|
|
1183
|
+
required: ["event", "persona_id", "timestamp"],
|
|
1184
|
+
properties: {
|
|
1185
|
+
event: {
|
|
1186
|
+
type: "string",
|
|
1187
|
+
enum: ["persona_invocation", "persona_error", "hook_execution", "session_summary"],
|
|
1188
|
+
description: "Event type",
|
|
1189
|
+
},
|
|
1190
|
+
persona_id: { type: "string", description: "Persona identifier" },
|
|
1191
|
+
persona_version: { type: "string", description: "Persona version" },
|
|
1192
|
+
model: { type: "string", description: "Model used" },
|
|
1193
|
+
scope: { type: "string", description: "Scope path: 'org:group:team'" },
|
|
1194
|
+
input_tokens: { type: "integer" },
|
|
1195
|
+
output_tokens: { type: "integer" },
|
|
1196
|
+
thinking_tokens: { type: "integer" },
|
|
1197
|
+
tool_calls: { type: "integer" },
|
|
1198
|
+
duration_ms: { type: "integer" },
|
|
1199
|
+
cost_usd: { type: "number" },
|
|
1200
|
+
findings_count: {
|
|
1201
|
+
type: "object",
|
|
1202
|
+
properties: {
|
|
1203
|
+
CRITICAL: { type: "integer" },
|
|
1204
|
+
ERROR: { type: "integer" },
|
|
1205
|
+
WARN: { type: "integer" },
|
|
1206
|
+
INFO: { type: "integer" },
|
|
1207
|
+
},
|
|
1208
|
+
},
|
|
1209
|
+
suggestions: { type: "integer" },
|
|
1210
|
+
timestamp: { type: "string", format: "date-time" },
|
|
1211
|
+
session_id: { type: "string" },
|
|
1212
|
+
dev_id: { type: "string", description: "Developer identifier (hashed or email per config)" },
|
|
1213
|
+
status: { type: "string", enum: ["started", "completed", "error"] },
|
|
1214
|
+
tool_name: { type: "string", description: "Tool name for hook_execution events" },
|
|
1215
|
+
},
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
const schemaDir = path.join(distPath, "schema");
|
|
1219
|
+
ensureDir(schemaDir);
|
|
1220
|
+
fs.writeFileSync(
|
|
1221
|
+
path.join(schemaDir, "telemetry-event.v1.json"),
|
|
1222
|
+
JSON.stringify(schema, null, 2) + "\n",
|
|
1223
|
+
"utf-8"
|
|
1224
|
+
);
|
|
1225
|
+
log(chalk.gray(` → Telemetry schema written to dist/schema/`));
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// ---------------------------------------------------------------------------
|
|
1229
|
+
// AB-61: Managed settings artifact generation
|
|
1230
|
+
// ---------------------------------------------------------------------------
|
|
1231
|
+
|
|
1232
|
+
function generateManagedSettings(config: AgentBootConfig, distPath: string): void {
|
|
1233
|
+
const managed = config.managed;
|
|
1234
|
+
if (!managed?.enabled) return;
|
|
1235
|
+
|
|
1236
|
+
log(chalk.cyan("\nGenerating managed settings..."));
|
|
1237
|
+
|
|
1238
|
+
const managedDir = path.join(distPath, "managed");
|
|
1239
|
+
ensureDir(managedDir);
|
|
1240
|
+
|
|
1241
|
+
// Managed settings carry HARD guardrails only
|
|
1242
|
+
const managedSettings: Record<string, unknown> = {};
|
|
1243
|
+
|
|
1244
|
+
// Permissions: deny dangerous tools
|
|
1245
|
+
if (managed.guardrails?.denyTools && managed.guardrails.denyTools.length > 0) {
|
|
1246
|
+
managedSettings["permissions"] = {
|
|
1247
|
+
deny: managed.guardrails.denyTools,
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Force audit logging
|
|
1252
|
+
if (managed.guardrails?.requireAuditLog) {
|
|
1253
|
+
managedSettings["hooks"] = {
|
|
1254
|
+
SubagentStart: [
|
|
1255
|
+
{
|
|
1256
|
+
matcher: "",
|
|
1257
|
+
hooks: [{ type: "command", command: ".claude/hooks/agentboot-telemetry.sh", timeout: 3000, async: true }],
|
|
1258
|
+
},
|
|
1259
|
+
],
|
|
1260
|
+
SubagentStop: [
|
|
1261
|
+
{
|
|
1262
|
+
matcher: "",
|
|
1263
|
+
hooks: [{ type: "command", command: ".claude/hooks/agentboot-telemetry.sh", timeout: 3000, async: true }],
|
|
1264
|
+
},
|
|
1265
|
+
],
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
fs.writeFileSync(
|
|
1270
|
+
path.join(managedDir, "managed-settings.json"),
|
|
1271
|
+
JSON.stringify(managedSettings, null, 2) + "\n",
|
|
1272
|
+
"utf-8"
|
|
1273
|
+
);
|
|
1274
|
+
|
|
1275
|
+
// Managed CLAUDE.md (minimal, HARD guardrails only)
|
|
1276
|
+
const managedClaudeMd = [
|
|
1277
|
+
`# ${config.orgDisplayName ?? config.org} — Managed Configuration`,
|
|
1278
|
+
"",
|
|
1279
|
+
"<!-- Managed by IT. Do not modify. -->",
|
|
1280
|
+
"",
|
|
1281
|
+
"This configuration is enforced by your organization's IT policy.",
|
|
1282
|
+
"Contact your platform team for changes.",
|
|
1283
|
+
"",
|
|
1284
|
+
].join("\n");
|
|
1285
|
+
|
|
1286
|
+
fs.writeFileSync(path.join(managedDir, "CLAUDE.md"), managedClaudeMd, "utf-8");
|
|
1287
|
+
|
|
1288
|
+
// MCP config if needed
|
|
1289
|
+
if (config.claude?.mcpServers) {
|
|
1290
|
+
fs.writeFileSync(
|
|
1291
|
+
path.join(managedDir, "managed-mcp.json"),
|
|
1292
|
+
JSON.stringify({ mcpServers: config.claude.mcpServers }, null, 2) + "\n",
|
|
1293
|
+
"utf-8"
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Output path guidance
|
|
1298
|
+
const platformPaths: Record<string, string> = {
|
|
1299
|
+
jamf: "/Library/Application Support/Claude/",
|
|
1300
|
+
intune: "C:\\ProgramData\\Claude\\",
|
|
1301
|
+
jumpcloud: "/etc/claude-code/",
|
|
1302
|
+
kandji: "/Library/Application Support/Claude/",
|
|
1303
|
+
other: "./managed-output/",
|
|
1304
|
+
};
|
|
1305
|
+
const platform = managed.platform ?? "other";
|
|
1306
|
+
const targetPath = platformPaths[platform] ?? platformPaths["other"];
|
|
1307
|
+
|
|
1308
|
+
log(chalk.gray(` → Managed settings written to dist/managed/`));
|
|
1309
|
+
log(chalk.gray(` → Target MDM path: ${targetPath}`));
|
|
1310
|
+
}
|
|
1311
|
+
|
|
688
1312
|
// ---------------------------------------------------------------------------
|
|
689
1313
|
// Main entry point
|
|
690
1314
|
// ---------------------------------------------------------------------------
|
|
@@ -721,14 +1345,25 @@ function main(): void {
|
|
|
721
1345
|
const coreTraitsDir = path.join(coreDir, "traits");
|
|
722
1346
|
const coreInstructionsDir = path.join(coreDir, "instructions");
|
|
723
1347
|
|
|
724
|
-
const validFormats = ["skill", "claude", "copilot"];
|
|
725
|
-
const outputFormats = config.personas?.outputFormats ??
|
|
1348
|
+
const validFormats = ["skill", "claude", "copilot", "plugin"];
|
|
1349
|
+
const outputFormats = config.personas?.outputFormats ?? ["skill", "claude", "copilot"];
|
|
726
1350
|
const unknownFormats = outputFormats.filter((f) => !validFormats.includes(f));
|
|
727
1351
|
if (unknownFormats.length > 0) {
|
|
728
1352
|
console.error(chalk.red(`Unknown output format(s): ${unknownFormats.join(", ")}. Valid: ${validFormats.join(", ")}`));
|
|
729
1353
|
process.exit(1);
|
|
730
1354
|
}
|
|
731
1355
|
|
|
1356
|
+
// AB-88: Resolve N-tier scope tree
|
|
1357
|
+
// B13 fix: warn if both groups and nodes defined
|
|
1358
|
+
if (config.groups && config.nodes) {
|
|
1359
|
+
log(chalk.yellow(" ⚠ Both 'groups' and 'nodes' defined — 'nodes' takes precedence. Remove 'groups' to suppress this warning."));
|
|
1360
|
+
}
|
|
1361
|
+
const scopeNodes = config.nodes
|
|
1362
|
+
? config.nodes
|
|
1363
|
+
: config.groups
|
|
1364
|
+
? groupsToNodes(config.groups)
|
|
1365
|
+
: undefined;
|
|
1366
|
+
|
|
732
1367
|
// Load traits.
|
|
733
1368
|
const enabledTraits = config.traits?.enabled;
|
|
734
1369
|
const traits = loadTraits(coreTraitsDir, enabledTraits);
|
|
@@ -755,7 +1390,7 @@ function main(): void {
|
|
|
755
1390
|
}
|
|
756
1391
|
|
|
757
1392
|
if (config.personas?.customDir) {
|
|
758
|
-
const extendDir = path.resolve(configDir, config.personas.
|
|
1393
|
+
const extendDir = path.resolve(configDir, config.personas.customDir);
|
|
759
1394
|
if (fs.existsSync(extendDir)) {
|
|
760
1395
|
for (const entry of fs.readdirSync(extendDir)) {
|
|
761
1396
|
const dir = path.join(extendDir, entry);
|
|
@@ -854,104 +1489,119 @@ function main(): void {
|
|
|
854
1489
|
}
|
|
855
1490
|
|
|
856
1491
|
// ---------------------------------------------------------------------------
|
|
857
|
-
// 2. Compile
|
|
1492
|
+
// 2. Compile scope nodes (AB-88: N-tier replaces flat groups/teams)
|
|
1493
|
+
// Also provides backward compat with legacy groups/teams config.
|
|
858
1494
|
// ---------------------------------------------------------------------------
|
|
859
1495
|
|
|
860
|
-
if (
|
|
861
|
-
log(chalk.cyan("\nCompiling
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const
|
|
871
|
-
|
|
1496
|
+
if (scopeNodes) {
|
|
1497
|
+
log(chalk.cyan("\nCompiling scope nodes..."));
|
|
1498
|
+
const flatNodes = flattenNodes(scopeNodes);
|
|
1499
|
+
let nodePersonasFound = false;
|
|
1500
|
+
|
|
1501
|
+
for (const { path: nodePath } of flatNodes) {
|
|
1502
|
+
// Look for personas at nodes/{path}/personas/
|
|
1503
|
+
const nodePersonasDir = path.join(ROOT, "nodes", nodePath, "personas");
|
|
1504
|
+
|
|
1505
|
+
// Also check legacy paths: groups/{name}/personas/ and teams/{group}/{team}/personas/
|
|
1506
|
+
const parts = nodePath.split("/");
|
|
1507
|
+
const legacyGroupDir = parts.length === 1
|
|
1508
|
+
? path.join(ROOT, "groups", parts[0]!, "personas")
|
|
1509
|
+
: undefined;
|
|
1510
|
+
const legacyTeamDir = parts.length === 2
|
|
1511
|
+
? path.join(ROOT, "teams", parts[0]!, parts[1]!, "personas")
|
|
1512
|
+
: undefined;
|
|
1513
|
+
|
|
1514
|
+
const personasDir = fs.existsSync(nodePersonasDir)
|
|
1515
|
+
? nodePersonasDir
|
|
1516
|
+
: legacyGroupDir && fs.existsSync(legacyGroupDir)
|
|
1517
|
+
? legacyGroupDir
|
|
1518
|
+
: legacyTeamDir && fs.existsSync(legacyTeamDir)
|
|
1519
|
+
? legacyTeamDir
|
|
1520
|
+
: null;
|
|
1521
|
+
|
|
1522
|
+
if (!personasDir) continue;
|
|
1523
|
+
|
|
1524
|
+
nodePersonasFound = true;
|
|
1525
|
+
const nodePersonaDirs = fs.readdirSync(personasDir).filter((entry) =>
|
|
1526
|
+
fs.statSync(path.join(personasDir, entry)).isDirectory()
|
|
872
1527
|
);
|
|
873
1528
|
|
|
874
|
-
for (const personaName of
|
|
1529
|
+
for (const personaName of nodePersonaDirs) {
|
|
875
1530
|
if (enabledPersonas && !enabledPersonas.includes(personaName)) {
|
|
876
1531
|
continue;
|
|
877
1532
|
}
|
|
878
1533
|
|
|
879
|
-
const personaDir = path.join(
|
|
1534
|
+
const personaDir = path.join(personasDir, personaName);
|
|
1535
|
+
// Use first part as group name, second as team name for trait resolution
|
|
1536
|
+
const groupName = parts[0];
|
|
1537
|
+
const teamName = parts.length >= 2 ? parts[parts.length - 1] : undefined;
|
|
1538
|
+
|
|
880
1539
|
const result = compilePersona(
|
|
881
1540
|
personaName,
|
|
882
1541
|
personaDir,
|
|
883
1542
|
traits,
|
|
884
1543
|
config,
|
|
885
1544
|
distPath,
|
|
886
|
-
`
|
|
887
|
-
groupName
|
|
1545
|
+
`nodes/${nodePath}`,
|
|
1546
|
+
groupName,
|
|
1547
|
+
teamName
|
|
888
1548
|
);
|
|
889
1549
|
allResults.push(result);
|
|
890
|
-
log(` ${chalk.green("✓")} ${
|
|
1550
|
+
log(` ${chalk.green("✓")} ${nodePath}/${personaName}`);
|
|
891
1551
|
}
|
|
892
1552
|
}
|
|
1553
|
+
|
|
1554
|
+
if (!nodePersonasFound) {
|
|
1555
|
+
log(chalk.gray(" (no node-level overrides found)"));
|
|
1556
|
+
}
|
|
893
1557
|
}
|
|
894
1558
|
|
|
895
1559
|
// ---------------------------------------------------------------------------
|
|
896
|
-
//
|
|
1560
|
+
// 2b. AB-53: Compile domain layers
|
|
897
1561
|
// ---------------------------------------------------------------------------
|
|
898
1562
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
let teamPersonasFound = false;
|
|
1563
|
+
const domainResults = compileDomains(config, configDir, distPath, traits, outputFormats);
|
|
1564
|
+
allResults.push(...domainResults);
|
|
902
1565
|
|
|
903
|
-
|
|
904
|
-
|
|
1566
|
+
// ---------------------------------------------------------------------------
|
|
1567
|
+
// 4. Generate PERSONAS.md index in each platform
|
|
1568
|
+
// ---------------------------------------------------------------------------
|
|
905
1569
|
|
|
906
|
-
|
|
907
|
-
|
|
1570
|
+
generatePersonasIndex(allResults, config, corePersonasDir, distPath, "core", outputFormats);
|
|
1571
|
+
log(chalk.gray("\n → PERSONAS.md written to each platform"));
|
|
908
1572
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1573
|
+
// ---------------------------------------------------------------------------
|
|
1574
|
+
// 5. AB-57: Plugin output generation
|
|
1575
|
+
// ---------------------------------------------------------------------------
|
|
912
1576
|
|
|
913
|
-
|
|
1577
|
+
// B5 fix: Only generate plugin output when claude format is active (plugin is always derived from claude)
|
|
1578
|
+
if (outputFormats.includes("claude")) {
|
|
1579
|
+
generatePluginOutput(config, distPath, allResults, corePersonasDir, traits);
|
|
1580
|
+
}
|
|
914
1581
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1582
|
+
// ---------------------------------------------------------------------------
|
|
1583
|
+
// 6. AB-59/60/63: Compliance & audit trail hooks
|
|
1584
|
+
// ---------------------------------------------------------------------------
|
|
918
1585
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1586
|
+
if (outputFormats.includes("claude")) {
|
|
1587
|
+
generateComplianceHooks(config, distPath, "core");
|
|
1588
|
+
generateComplianceSettingsJson(config, distPath, "core");
|
|
1589
|
+
}
|
|
923
1590
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
-
}
|
|
1591
|
+
// ---------------------------------------------------------------------------
|
|
1592
|
+
// 7. AB-64: Telemetry NDJSON schema
|
|
1593
|
+
// ---------------------------------------------------------------------------
|
|
940
1594
|
|
|
941
|
-
|
|
942
|
-
log(chalk.gray(" (no team-level overrides found)"));
|
|
943
|
-
}
|
|
944
|
-
}
|
|
1595
|
+
generateTelemetrySchema(distPath);
|
|
945
1596
|
|
|
946
1597
|
// ---------------------------------------------------------------------------
|
|
947
|
-
//
|
|
1598
|
+
// 8. AB-61: Managed settings
|
|
948
1599
|
// ---------------------------------------------------------------------------
|
|
949
1600
|
|
|
950
|
-
|
|
951
|
-
log(chalk.gray("\n → PERSONAS.md written to each platform"));
|
|
1601
|
+
generateManagedSettings(config, distPath);
|
|
952
1602
|
|
|
953
1603
|
// ---------------------------------------------------------------------------
|
|
954
|
-
//
|
|
1604
|
+
// 9. AB-25: Token budget estimation
|
|
955
1605
|
// ---------------------------------------------------------------------------
|
|
956
1606
|
|
|
957
1607
|
const tokenBudget = config.output?.tokenBudget?.warnAt ?? 8000;
|
|
@@ -980,7 +1630,6 @@ function main(): void {
|
|
|
980
1630
|
// ---------------------------------------------------------------------------
|
|
981
1631
|
|
|
982
1632
|
const successCount = allResults.filter((r) => r.platforms.length > 0).length;
|
|
983
|
-
const platformCount = allResults.reduce((acc, r) => acc + r.platforms.length, 0);
|
|
984
1633
|
|
|
985
1634
|
log(
|
|
986
1635
|
chalk.bold(
|