agentboot 0.2.0 → 0.3.1
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 +1 -1
- package/bin/agentboot.js +9 -1
- package/package.json +1 -1
- package/scripts/cli.ts +423 -4
- package/scripts/compile.ts +695 -69
- package/scripts/lib/config.ts +243 -1
- package/scripts/lib/frontmatter.ts +3 -1
- 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/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,
|
|
@@ -451,7 +455,7 @@ function compileInstructions(
|
|
|
451
455
|
// Insert provenance after the closing --- of frontmatter.
|
|
452
456
|
const fmMatch = content.match(/^(---\n[\s\S]*?\n---\n)/);
|
|
453
457
|
if (fmMatch) {
|
|
454
|
-
const afterFm = content.slice(fmMatch[1]
|
|
458
|
+
const afterFm = content.slice(fmMatch[1]!.length);
|
|
455
459
|
finalContent = `${fmMatch[1]}\n${provenanceHeader(srcPath, config)}${afterFm}`;
|
|
456
460
|
} else {
|
|
457
461
|
finalContent = `${provenanceHeader(srcPath, config)}${content}`;
|
|
@@ -665,7 +669,7 @@ function generateSettingsJson(
|
|
|
665
669
|
// Validate hook event names against known CC events
|
|
666
670
|
const validEvents = [
|
|
667
671
|
"PreToolUse", "PostToolUse", "Notification", "Stop",
|
|
668
|
-
"SubagentStop", "SubagentStart",
|
|
672
|
+
"SubagentStop", "SubagentStart", "UserPromptSubmit", "SessionEnd",
|
|
669
673
|
];
|
|
670
674
|
for (const key of Object.keys(hooks)) {
|
|
671
675
|
if (!validEvents.includes(key)) {
|
|
@@ -677,8 +681,8 @@ function generateSettingsJson(
|
|
|
677
681
|
}
|
|
678
682
|
|
|
679
683
|
const settings: Record<string, unknown> = {};
|
|
680
|
-
if (hooks) settings
|
|
681
|
-
if (permissions) settings
|
|
684
|
+
if (hooks) settings["hooks"] = hooks;
|
|
685
|
+
if (permissions) settings["permissions"] = permissions;
|
|
682
686
|
|
|
683
687
|
const settingsPath = path.join(distPath, "claude", scopePath, "settings.json");
|
|
684
688
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
@@ -708,6 +712,603 @@ function generateMcpJson(
|
|
|
708
712
|
fs.writeFileSync(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf-8");
|
|
709
713
|
}
|
|
710
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
|
+
|
|
711
1312
|
// ---------------------------------------------------------------------------
|
|
712
1313
|
// Main entry point
|
|
713
1314
|
// ---------------------------------------------------------------------------
|
|
@@ -744,14 +1345,25 @@ function main(): void {
|
|
|
744
1345
|
const coreTraitsDir = path.join(coreDir, "traits");
|
|
745
1346
|
const coreInstructionsDir = path.join(coreDir, "instructions");
|
|
746
1347
|
|
|
747
|
-
const validFormats = ["skill", "claude", "copilot"];
|
|
748
|
-
const outputFormats = config.personas?.outputFormats ??
|
|
1348
|
+
const validFormats = ["skill", "claude", "copilot", "plugin"];
|
|
1349
|
+
const outputFormats = config.personas?.outputFormats ?? ["skill", "claude", "copilot"];
|
|
749
1350
|
const unknownFormats = outputFormats.filter((f) => !validFormats.includes(f));
|
|
750
1351
|
if (unknownFormats.length > 0) {
|
|
751
1352
|
console.error(chalk.red(`Unknown output format(s): ${unknownFormats.join(", ")}. Valid: ${validFormats.join(", ")}`));
|
|
752
1353
|
process.exit(1);
|
|
753
1354
|
}
|
|
754
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
|
+
|
|
755
1367
|
// Load traits.
|
|
756
1368
|
const enabledTraits = config.traits?.enabled;
|
|
757
1369
|
const traits = loadTraits(coreTraitsDir, enabledTraits);
|
|
@@ -877,104 +1489,119 @@ function main(): void {
|
|
|
877
1489
|
}
|
|
878
1490
|
|
|
879
1491
|
// ---------------------------------------------------------------------------
|
|
880
|
-
// 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.
|
|
881
1494
|
// ---------------------------------------------------------------------------
|
|
882
1495
|
|
|
883
|
-
if (
|
|
884
|
-
log(chalk.cyan("\nCompiling
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
const
|
|
894
|
-
|
|
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()
|
|
895
1527
|
);
|
|
896
1528
|
|
|
897
|
-
for (const personaName of
|
|
1529
|
+
for (const personaName of nodePersonaDirs) {
|
|
898
1530
|
if (enabledPersonas && !enabledPersonas.includes(personaName)) {
|
|
899
1531
|
continue;
|
|
900
1532
|
}
|
|
901
1533
|
|
|
902
|
-
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
|
+
|
|
903
1539
|
const result = compilePersona(
|
|
904
1540
|
personaName,
|
|
905
1541
|
personaDir,
|
|
906
1542
|
traits,
|
|
907
1543
|
config,
|
|
908
1544
|
distPath,
|
|
909
|
-
`
|
|
910
|
-
groupName
|
|
1545
|
+
`nodes/${nodePath}`,
|
|
1546
|
+
groupName,
|
|
1547
|
+
teamName
|
|
911
1548
|
);
|
|
912
1549
|
allResults.push(result);
|
|
913
|
-
log(` ${chalk.green("✓")} ${
|
|
1550
|
+
log(` ${chalk.green("✓")} ${nodePath}/${personaName}`);
|
|
914
1551
|
}
|
|
915
1552
|
}
|
|
1553
|
+
|
|
1554
|
+
if (!nodePersonasFound) {
|
|
1555
|
+
log(chalk.gray(" (no node-level overrides found)"));
|
|
1556
|
+
}
|
|
916
1557
|
}
|
|
917
1558
|
|
|
918
1559
|
// ---------------------------------------------------------------------------
|
|
919
|
-
//
|
|
1560
|
+
// 2b. AB-53: Compile domain layers
|
|
920
1561
|
// ---------------------------------------------------------------------------
|
|
921
1562
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
let teamPersonasFound = false;
|
|
1563
|
+
const domainResults = compileDomains(config, configDir, distPath, traits, outputFormats);
|
|
1564
|
+
allResults.push(...domainResults);
|
|
925
1565
|
|
|
926
|
-
|
|
927
|
-
|
|
1566
|
+
// ---------------------------------------------------------------------------
|
|
1567
|
+
// 4. Generate PERSONAS.md index in each platform
|
|
1568
|
+
// ---------------------------------------------------------------------------
|
|
928
1569
|
|
|
929
|
-
|
|
930
|
-
|
|
1570
|
+
generatePersonasIndex(allResults, config, corePersonasDir, distPath, "core", outputFormats);
|
|
1571
|
+
log(chalk.gray("\n → PERSONAS.md written to each platform"));
|
|
931
1572
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1573
|
+
// ---------------------------------------------------------------------------
|
|
1574
|
+
// 5. AB-57: Plugin output generation
|
|
1575
|
+
// ---------------------------------------------------------------------------
|
|
935
1576
|
|
|
936
|
-
|
|
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
|
+
}
|
|
937
1581
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1582
|
+
// ---------------------------------------------------------------------------
|
|
1583
|
+
// 6. AB-59/60/63: Compliance & audit trail hooks
|
|
1584
|
+
// ---------------------------------------------------------------------------
|
|
941
1585
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1586
|
+
if (outputFormats.includes("claude")) {
|
|
1587
|
+
generateComplianceHooks(config, distPath, "core");
|
|
1588
|
+
generateComplianceSettingsJson(config, distPath, "core");
|
|
1589
|
+
}
|
|
946
1590
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
personaDir,
|
|
951
|
-
traits,
|
|
952
|
-
config,
|
|
953
|
-
distPath,
|
|
954
|
-
`teams/${groupName}/${teamName}`, // scopePath → dist/{platform}/teams/{group}/{team}/{persona}/
|
|
955
|
-
groupName,
|
|
956
|
-
teamName
|
|
957
|
-
);
|
|
958
|
-
allResults.push(result);
|
|
959
|
-
log(` ${chalk.green("✓")} ${groupName}/${teamName}/${personaName}`);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
}
|
|
1591
|
+
// ---------------------------------------------------------------------------
|
|
1592
|
+
// 7. AB-64: Telemetry NDJSON schema
|
|
1593
|
+
// ---------------------------------------------------------------------------
|
|
963
1594
|
|
|
964
|
-
|
|
965
|
-
log(chalk.gray(" (no team-level overrides found)"));
|
|
966
|
-
}
|
|
967
|
-
}
|
|
1595
|
+
generateTelemetrySchema(distPath);
|
|
968
1596
|
|
|
969
1597
|
// ---------------------------------------------------------------------------
|
|
970
|
-
//
|
|
1598
|
+
// 8. AB-61: Managed settings
|
|
971
1599
|
// ---------------------------------------------------------------------------
|
|
972
1600
|
|
|
973
|
-
|
|
974
|
-
log(chalk.gray("\n → PERSONAS.md written to each platform"));
|
|
1601
|
+
generateManagedSettings(config, distPath);
|
|
975
1602
|
|
|
976
1603
|
// ---------------------------------------------------------------------------
|
|
977
|
-
//
|
|
1604
|
+
// 9. AB-25: Token budget estimation
|
|
978
1605
|
// ---------------------------------------------------------------------------
|
|
979
1606
|
|
|
980
1607
|
const tokenBudget = config.output?.tokenBudget?.warnAt ?? 8000;
|
|
@@ -1003,7 +1630,6 @@ function main(): void {
|
|
|
1003
1630
|
// ---------------------------------------------------------------------------
|
|
1004
1631
|
|
|
1005
1632
|
const successCount = allResults.filter((r) => r.platforms.length > 0).length;
|
|
1006
|
-
const platformCount = allResults.reduce((acc, r) => acc + r.platforms.length, 0);
|
|
1007
1633
|
|
|
1008
1634
|
log(
|
|
1009
1635
|
chalk.bold(
|