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
package/scripts/cli.ts
ADDED
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentBoot CLI entry point.
|
|
5
|
+
*
|
|
6
|
+
* Provides the `agentboot` command with subcommands for building, validating,
|
|
7
|
+
* syncing, and managing agentic personas.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* agentboot build [-c config]
|
|
11
|
+
* agentboot validate [--strict]
|
|
12
|
+
* agentboot sync [--repos-file path] [--dry-run]
|
|
13
|
+
* agentboot setup [--skip-detect]
|
|
14
|
+
* agentboot add <type> <name>
|
|
15
|
+
* agentboot doctor [--format text|json]
|
|
16
|
+
* agentboot status [--format text|json]
|
|
17
|
+
* agentboot lint [--persona name] [--severity level] [--format text|json]
|
|
18
|
+
* agentboot uninstall [--repo path] [--dry-run]
|
|
19
|
+
* agentboot config [key] [value]
|
|
20
|
+
* agentboot <command> --help
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { Command } from "commander";
|
|
24
|
+
import { spawnSync } from "node:child_process";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import chalk from "chalk";
|
|
29
|
+
import { createHash } from "node:crypto";
|
|
30
|
+
import { loadConfig } from "./lib/config.js";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Paths
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
37
|
+
const __dirname = path.dirname(__filename);
|
|
38
|
+
const SCRIPTS_DIR = __dirname;
|
|
39
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Version (read from package.json)
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
function getVersion(): string {
|
|
46
|
+
const pkgPath = path.join(ROOT, "package.json");
|
|
47
|
+
try {
|
|
48
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
49
|
+
return pkg.version ?? "0.0.0";
|
|
50
|
+
} catch {
|
|
51
|
+
return "0.0.0";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Script runner — delegates to existing tsx scripts
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
interface RunOptions {
|
|
60
|
+
script: string;
|
|
61
|
+
args: string[];
|
|
62
|
+
verbose?: boolean;
|
|
63
|
+
quiet?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runScript({ script, args, verbose, quiet }: RunOptions): never {
|
|
67
|
+
const scriptPath = path.join(SCRIPTS_DIR, script);
|
|
68
|
+
|
|
69
|
+
if (!fs.existsSync(scriptPath)) {
|
|
70
|
+
console.error(`Error: script not found: ${scriptPath}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (verbose) {
|
|
75
|
+
console.log(`→ tsx ${scriptPath} ${args.join(" ")}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = spawnSync("npx", ["tsx", scriptPath, ...args], {
|
|
79
|
+
cwd: ROOT,
|
|
80
|
+
stdio: quiet ? ["inherit", "ignore", "pipe"] : "inherit",
|
|
81
|
+
env: { ...process.env },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (result.error) {
|
|
85
|
+
console.error(`Failed to run script: ${result.error.message}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
process.exit(result.status ?? 1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Helpers
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
/** Collect global flags that should be forwarded to scripts. */
|
|
97
|
+
function collectGlobalArgs(opts: { config?: string }): string[] {
|
|
98
|
+
const args: string[] = [];
|
|
99
|
+
if (opts.config) {
|
|
100
|
+
args.push("--config", opts.config);
|
|
101
|
+
}
|
|
102
|
+
return args;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Program
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
const program = new Command();
|
|
110
|
+
|
|
111
|
+
program
|
|
112
|
+
.name("agentboot")
|
|
113
|
+
.description(
|
|
114
|
+
"Convention over configuration for agentic development teams.\nCompile, validate, and distribute agentic personas.",
|
|
115
|
+
)
|
|
116
|
+
.version(getVersion(), "-v, --version")
|
|
117
|
+
.option("-c, --config <path>", "path to agentboot.config.json")
|
|
118
|
+
.option("--verbose", "show detailed output")
|
|
119
|
+
.option("--quiet", "suppress non-error output");
|
|
120
|
+
|
|
121
|
+
// ---- build ----------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
program
|
|
124
|
+
.command("build")
|
|
125
|
+
.description("Compile traits into persona output files")
|
|
126
|
+
.action((_opts, cmd) => {
|
|
127
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
128
|
+
const args = collectGlobalArgs({ config: globalOpts.config });
|
|
129
|
+
|
|
130
|
+
runScript({
|
|
131
|
+
script: "compile.ts",
|
|
132
|
+
args,
|
|
133
|
+
verbose: globalOpts.verbose,
|
|
134
|
+
quiet: globalOpts.quiet,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ---- validate -------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
program
|
|
141
|
+
.command("validate")
|
|
142
|
+
.description("Run pre-build validation checks")
|
|
143
|
+
.option("--strict", "treat warnings as errors")
|
|
144
|
+
.action((opts, cmd) => {
|
|
145
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
146
|
+
const args = collectGlobalArgs({ config: globalOpts.config });
|
|
147
|
+
|
|
148
|
+
if (opts.strict) {
|
|
149
|
+
args.push("--strict");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
runScript({
|
|
153
|
+
script: "validate.ts",
|
|
154
|
+
args,
|
|
155
|
+
verbose: globalOpts.verbose,
|
|
156
|
+
quiet: globalOpts.quiet,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ---- sync -----------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
program
|
|
163
|
+
.command("sync")
|
|
164
|
+
.description("Distribute compiled output to target repositories")
|
|
165
|
+
.option("--repos-file <path>", "path to repos.json")
|
|
166
|
+
.option("--dry-run", "preview changes without writing")
|
|
167
|
+
.action((opts, cmd) => {
|
|
168
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
169
|
+
const args = collectGlobalArgs({ config: globalOpts.config });
|
|
170
|
+
|
|
171
|
+
if (opts.reposFile) {
|
|
172
|
+
args.push("--repos", opts.reposFile);
|
|
173
|
+
}
|
|
174
|
+
if (opts.dryRun) {
|
|
175
|
+
args.push("--dry-run");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
runScript({
|
|
179
|
+
script: "sync.ts",
|
|
180
|
+
args,
|
|
181
|
+
verbose: globalOpts.verbose,
|
|
182
|
+
quiet: globalOpts.quiet,
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ---- dev-sync -------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
program
|
|
189
|
+
.command("dev-sync", { hidden: true })
|
|
190
|
+
.description("Copy dist/ to local repo for dogfooding (internal)")
|
|
191
|
+
.action((_opts, cmd) => {
|
|
192
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
193
|
+
const args = collectGlobalArgs({ config: globalOpts.config });
|
|
194
|
+
|
|
195
|
+
runScript({
|
|
196
|
+
script: "dev-sync.ts",
|
|
197
|
+
args,
|
|
198
|
+
verbose: globalOpts.verbose,
|
|
199
|
+
quiet: globalOpts.quiet,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ---- full-build -----------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
program
|
|
206
|
+
.command("full-build")
|
|
207
|
+
.description("Run clean → validate → build → dev-sync pipeline")
|
|
208
|
+
.action((_opts, cmd) => {
|
|
209
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
210
|
+
const baseArgs = collectGlobalArgs({ config: globalOpts.config });
|
|
211
|
+
const quiet = globalOpts.quiet;
|
|
212
|
+
|
|
213
|
+
// Clean
|
|
214
|
+
if (!quiet) console.log("→ clean");
|
|
215
|
+
const distPath = path.join(ROOT, "dist");
|
|
216
|
+
if (fs.existsSync(distPath)) {
|
|
217
|
+
fs.rmSync(distPath, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Validate
|
|
221
|
+
if (!quiet) console.log("→ validate");
|
|
222
|
+
const valResult = spawnSync(
|
|
223
|
+
"npx",
|
|
224
|
+
["tsx", path.join(SCRIPTS_DIR, "validate.ts"), ...baseArgs],
|
|
225
|
+
{ cwd: ROOT, stdio: quiet ? ["inherit", "ignore", "pipe"] : "inherit" },
|
|
226
|
+
);
|
|
227
|
+
if (valResult.status !== 0) {
|
|
228
|
+
console.error("Validation failed.");
|
|
229
|
+
process.exit(valResult.status ?? 1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Build
|
|
233
|
+
if (!quiet) console.log("→ build");
|
|
234
|
+
const buildResult = spawnSync(
|
|
235
|
+
"npx",
|
|
236
|
+
["tsx", path.join(SCRIPTS_DIR, "compile.ts"), ...baseArgs],
|
|
237
|
+
{ cwd: ROOT, stdio: quiet ? ["inherit", "ignore", "pipe"] : "inherit" },
|
|
238
|
+
);
|
|
239
|
+
if (buildResult.status !== 0) {
|
|
240
|
+
console.error("Build failed.");
|
|
241
|
+
process.exit(buildResult.status ?? 1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Dev-sync
|
|
245
|
+
if (!quiet) console.log("→ dev-sync");
|
|
246
|
+
const syncResult = spawnSync(
|
|
247
|
+
"npx",
|
|
248
|
+
["tsx", path.join(SCRIPTS_DIR, "dev-sync.ts"), ...baseArgs],
|
|
249
|
+
{ cwd: ROOT, stdio: quiet ? ["inherit", "ignore", "pipe"] : "inherit" },
|
|
250
|
+
);
|
|
251
|
+
if (syncResult.status !== 0) {
|
|
252
|
+
console.error("Dev-sync failed.");
|
|
253
|
+
process.exit(syncResult.status ?? 1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!quiet) console.log("✓ full-build complete");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// ---- setup (AB-33) --------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
program
|
|
262
|
+
.command("setup")
|
|
263
|
+
.description("Interactive setup wizard for new repos")
|
|
264
|
+
.option("--skip-detect", "skip auto-detection")
|
|
265
|
+
.action(async (opts) => {
|
|
266
|
+
const cwd = process.cwd();
|
|
267
|
+
console.log(chalk.bold("\nAgentBoot — setup\n"));
|
|
268
|
+
|
|
269
|
+
// Detect existing setup
|
|
270
|
+
if (!opts.skipDetect) {
|
|
271
|
+
const hasConfig = fs.existsSync(path.join(cwd, "agentboot.config.json"));
|
|
272
|
+
const hasClaude = fs.existsSync(path.join(cwd, ".claude"));
|
|
273
|
+
if (hasConfig) {
|
|
274
|
+
console.log(chalk.yellow(" ⚠ agentboot.config.json already exists in this directory."));
|
|
275
|
+
console.log(chalk.gray(" Run `agentboot doctor` to check your configuration.\n"));
|
|
276
|
+
process.exit(0);
|
|
277
|
+
}
|
|
278
|
+
if (hasClaude) {
|
|
279
|
+
console.log(chalk.gray(" Detected existing .claude/ directory."));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Detect org from git remote
|
|
284
|
+
let orgName = "my-org";
|
|
285
|
+
try {
|
|
286
|
+
const gitResult = spawnSync("git", ["remote", "get-url", "origin"], {
|
|
287
|
+
cwd,
|
|
288
|
+
encoding: "utf-8",
|
|
289
|
+
});
|
|
290
|
+
if (gitResult.stdout) {
|
|
291
|
+
const match = gitResult.stdout.match(/[/:]([\w-]+)\//);
|
|
292
|
+
if (match) orgName = match[1]!;
|
|
293
|
+
}
|
|
294
|
+
} catch { /* no git, use default */ }
|
|
295
|
+
|
|
296
|
+
console.log(chalk.cyan(` Detected org: ${orgName}`));
|
|
297
|
+
|
|
298
|
+
// Scaffold config
|
|
299
|
+
const configContent = JSON.stringify({
|
|
300
|
+
org: orgName,
|
|
301
|
+
orgDisplayName: orgName,
|
|
302
|
+
groups: {},
|
|
303
|
+
personas: {
|
|
304
|
+
enabled: ["code-reviewer", "security-reviewer", "test-generator", "test-data-expert"],
|
|
305
|
+
outputFormats: ["skill", "claude", "copilot"],
|
|
306
|
+
},
|
|
307
|
+
traits: {
|
|
308
|
+
enabled: [
|
|
309
|
+
"critical-thinking", "structured-output", "source-citation",
|
|
310
|
+
"confidence-signaling", "audit-trail", "schema-awareness",
|
|
311
|
+
],
|
|
312
|
+
},
|
|
313
|
+
instructions: { enabled: ["baseline.instructions", "security.instructions"] },
|
|
314
|
+
output: { distPath: "./dist", provenanceHeaders: true, tokenBudget: { warnAt: 8000 } },
|
|
315
|
+
sync: { repos: "./repos.json", dryRun: false },
|
|
316
|
+
}, null, 2);
|
|
317
|
+
|
|
318
|
+
fs.writeFileSync(path.join(cwd, "agentboot.config.json"), configContent + "\n", "utf-8");
|
|
319
|
+
console.log(chalk.green(" ✓ Created agentboot.config.json"));
|
|
320
|
+
|
|
321
|
+
// Scaffold repos.json if it doesn't exist
|
|
322
|
+
if (!fs.existsSync(path.join(cwd, "repos.json"))) {
|
|
323
|
+
fs.writeFileSync(path.join(cwd, "repos.json"), "[]\n", "utf-8");
|
|
324
|
+
console.log(chalk.green(" ✓ Created repos.json"));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Create core directories
|
|
328
|
+
const dirs = ["core/personas", "core/traits", "core/instructions", "core/gotchas"];
|
|
329
|
+
for (const dir of dirs) {
|
|
330
|
+
const fullPath = path.join(cwd, dir);
|
|
331
|
+
if (!fs.existsSync(fullPath)) {
|
|
332
|
+
fs.mkdirSync(fullPath, { recursive: true });
|
|
333
|
+
console.log(chalk.green(` ✓ Created ${dir}/`));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
console.log(chalk.bold(`\n${chalk.green("✓")} Setup complete.`));
|
|
338
|
+
console.log(chalk.gray("\n Next steps:"));
|
|
339
|
+
console.log(chalk.gray(" 1. Add personas to core/personas/"));
|
|
340
|
+
console.log(chalk.gray(" 2. Add traits to core/traits/"));
|
|
341
|
+
console.log(chalk.gray(" 3. Run: agentboot build"));
|
|
342
|
+
console.log(chalk.gray(" 4. Run: agentboot sync\n"));
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// ---- add (AB-34/35/55) ----------------------------------------------------
|
|
346
|
+
|
|
347
|
+
program
|
|
348
|
+
.command("add")
|
|
349
|
+
.description("Scaffold a new persona, trait, or gotcha")
|
|
350
|
+
.argument("<type>", "what to add: persona, trait, gotcha")
|
|
351
|
+
.argument("<name>", "name for the new item (lowercase-with-hyphens)")
|
|
352
|
+
.action((type: string, name: string) => {
|
|
353
|
+
// Validate name format
|
|
354
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
355
|
+
console.error(chalk.red(`Name must be lowercase alphanumeric with hyphens: got '${name}'`));
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const cwd = process.cwd();
|
|
360
|
+
|
|
361
|
+
if (type === "persona") {
|
|
362
|
+
const personaDir = path.join(cwd, "core", "personas", name);
|
|
363
|
+
if (fs.existsSync(personaDir)) {
|
|
364
|
+
console.error(chalk.red(`Persona '${name}' already exists at core/personas/${name}/`));
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fs.mkdirSync(personaDir, { recursive: true });
|
|
369
|
+
|
|
370
|
+
// AB-55: Prompt style guide baked into scaffold template
|
|
371
|
+
const skillMd = `---
|
|
372
|
+
name: ${name}
|
|
373
|
+
description: TODO — one sentence describing this persona's purpose
|
|
374
|
+
version: 0.1.0
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
# ${name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}
|
|
378
|
+
|
|
379
|
+
## Identity
|
|
380
|
+
|
|
381
|
+
<!-- One sentence: role + specialization + stance -->
|
|
382
|
+
|
|
383
|
+
## Setup
|
|
384
|
+
|
|
385
|
+
<!-- Numbered steps to execute before producing output -->
|
|
386
|
+
1. Read the diff, file, or context provided
|
|
387
|
+
2. Determine operating mode from arguments
|
|
388
|
+
|
|
389
|
+
## Rules
|
|
390
|
+
|
|
391
|
+
<!-- Numbered checklist. Specific, imperative, testable. 20 rules maximum.
|
|
392
|
+
Style guide:
|
|
393
|
+
- Use imperative voice: "Verify that..." not "It should be verified..."
|
|
394
|
+
- Be specific: "Check that every async function has a try/catch" not "Handle errors"
|
|
395
|
+
- Make rules falsifiable — each should be testable as pass/fail
|
|
396
|
+
- Each rule addresses one concern
|
|
397
|
+
- Show examples of violations where possible
|
|
398
|
+
- Cite sources when relevant (e.g., "Per OWASP A03:2021")
|
|
399
|
+
- Include confidence guidance: "Flag as WARN if uncertain, ERROR if confirmed"
|
|
400
|
+
-->
|
|
401
|
+
|
|
402
|
+
1. TODO — First rule
|
|
403
|
+
|
|
404
|
+
<!-- traits:start -->
|
|
405
|
+
<!-- traits:end -->
|
|
406
|
+
|
|
407
|
+
## Output Format
|
|
408
|
+
|
|
409
|
+
<!-- Define exact output schema. Include severity levels if this is a reviewer persona.
|
|
410
|
+
Example:
|
|
411
|
+
| Severity | When to use |
|
|
412
|
+
|----------|-------------|
|
|
413
|
+
| CRITICAL | Security vulnerability, data loss risk |
|
|
414
|
+
| ERROR | Bug that will cause incorrect behavior |
|
|
415
|
+
| WARN | Code smell, potential issue |
|
|
416
|
+
| INFO | Suggestion, style preference |
|
|
417
|
+
-->
|
|
418
|
+
|
|
419
|
+
## What Not To Do
|
|
420
|
+
|
|
421
|
+
<!-- Explicit exclusions and anti-patterns.
|
|
422
|
+
- Do not suggest changes outside the scope of what was requested
|
|
423
|
+
- Do not refactor code that was not asked to be refactored
|
|
424
|
+
-->
|
|
425
|
+
`;
|
|
426
|
+
|
|
427
|
+
const configJson = JSON.stringify({
|
|
428
|
+
name: name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
|
|
429
|
+
description: "TODO — one sentence describing this persona's purpose",
|
|
430
|
+
invocation: `/${name}`,
|
|
431
|
+
traits: [],
|
|
432
|
+
}, null, 2);
|
|
433
|
+
|
|
434
|
+
fs.writeFileSync(path.join(personaDir, "SKILL.md"), skillMd, "utf-8");
|
|
435
|
+
fs.writeFileSync(path.join(personaDir, "persona.config.json"), configJson + "\n", "utf-8");
|
|
436
|
+
|
|
437
|
+
console.log(chalk.bold(`\n${chalk.green("✓")} Created persona: ${name}\n`));
|
|
438
|
+
console.log(chalk.gray(` core/personas/${name}/`));
|
|
439
|
+
console.log(chalk.gray(` ├── SKILL.md`));
|
|
440
|
+
console.log(chalk.gray(` └── persona.config.json\n`));
|
|
441
|
+
console.log(chalk.gray(` Next: Edit SKILL.md to define your persona's rules.`));
|
|
442
|
+
console.log(chalk.gray(` Then: agentboot validate && agentboot build\n`));
|
|
443
|
+
|
|
444
|
+
} else if (type === "trait") {
|
|
445
|
+
const traitsDir = path.join(cwd, "core", "traits");
|
|
446
|
+
const traitPath = path.join(traitsDir, `${name}.md`);
|
|
447
|
+
if (fs.existsSync(traitPath)) {
|
|
448
|
+
console.error(chalk.red(`Trait '${name}' already exists at core/traits/${name}.md`));
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!fs.existsSync(traitsDir)) {
|
|
453
|
+
fs.mkdirSync(traitsDir, { recursive: true });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const traitMd = `# ${name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}
|
|
457
|
+
|
|
458
|
+
## When to Apply
|
|
459
|
+
|
|
460
|
+
<!-- Describe the activation condition for this trait.
|
|
461
|
+
Example: "When reviewing code that handles authentication or authorization" -->
|
|
462
|
+
|
|
463
|
+
## What to Do
|
|
464
|
+
|
|
465
|
+
<!-- Specific behavioral guidance. Use imperative voice.
|
|
466
|
+
Example: "Verify that all authentication checks occur before authorization checks" -->
|
|
467
|
+
|
|
468
|
+
## What Not to Do
|
|
469
|
+
|
|
470
|
+
<!-- Anti-patterns to avoid.
|
|
471
|
+
Example: "Do not suggest disabling TLS verification even in test environments" -->
|
|
472
|
+
`;
|
|
473
|
+
|
|
474
|
+
fs.writeFileSync(traitPath, traitMd, "utf-8");
|
|
475
|
+
|
|
476
|
+
console.log(chalk.bold(`\n${chalk.green("✓")} Created trait: ${name}\n`));
|
|
477
|
+
console.log(chalk.gray(` core/traits/${name}.md\n`));
|
|
478
|
+
console.log(chalk.gray(` Next: Edit the trait file and add it to a persona's traits list.\n`));
|
|
479
|
+
|
|
480
|
+
} else if (type === "gotcha") {
|
|
481
|
+
const gotchasDir = path.join(cwd, "core", "gotchas");
|
|
482
|
+
const gotchaPath = path.join(gotchasDir, `${name}.md`);
|
|
483
|
+
if (fs.existsSync(gotchaPath)) {
|
|
484
|
+
console.error(chalk.red(`Gotcha '${name}' already exists at core/gotchas/${name}.md`));
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!fs.existsSync(gotchasDir)) {
|
|
489
|
+
fs.mkdirSync(gotchasDir, { recursive: true });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const gotchaMd = `---
|
|
493
|
+
description: "TODO — brief description of this gotcha"
|
|
494
|
+
paths:
|
|
495
|
+
- "**/*.ts"
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
# ${name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}
|
|
499
|
+
|
|
500
|
+
<!-- Path-scoped knowledge: battle-tested rules that activate for matching files.
|
|
501
|
+
Sources: post-incident reviews, onboarding notes, repeated code review comments. -->
|
|
502
|
+
|
|
503
|
+
- **TODO:** First gotcha rule — explain the what AND the why
|
|
504
|
+
`;
|
|
505
|
+
|
|
506
|
+
fs.writeFileSync(gotchaPath, gotchaMd, "utf-8");
|
|
507
|
+
|
|
508
|
+
console.log(chalk.bold(`\n${chalk.green("✓")} Created gotcha: ${name}\n`));
|
|
509
|
+
console.log(chalk.gray(` core/gotchas/${name}.md\n`));
|
|
510
|
+
console.log(chalk.gray(` Next: Edit the paths: frontmatter and add your rules.\n`));
|
|
511
|
+
|
|
512
|
+
} else {
|
|
513
|
+
console.error(chalk.red(`Unknown type: '${type}'. Use: persona, trait, gotcha`));
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// ---- doctor (AB-36) -------------------------------------------------------
|
|
519
|
+
|
|
520
|
+
program
|
|
521
|
+
.command("doctor")
|
|
522
|
+
.description("Check environment and diagnose configuration issues")
|
|
523
|
+
.option("--format <fmt>", "output format: text, json", "text")
|
|
524
|
+
.action((opts, cmd) => {
|
|
525
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
526
|
+
const isJson = opts.format === "json";
|
|
527
|
+
if (!isJson) console.log(chalk.bold("\nAgentBoot — doctor\n"));
|
|
528
|
+
const cwd = process.cwd();
|
|
529
|
+
let issues = 0;
|
|
530
|
+
|
|
531
|
+
interface DoctorCheck { name: string; status: "ok" | "fail" | "warn"; message: string }
|
|
532
|
+
const checks: DoctorCheck[] = [];
|
|
533
|
+
|
|
534
|
+
function ok(msg: string) { checks.push({ name: msg, status: "ok", message: msg }); if (!isJson) console.log(` ${chalk.green("✓")} ${msg}`); }
|
|
535
|
+
function fail(msg: string) { issues++; checks.push({ name: msg, status: "fail", message: msg }); if (!isJson) console.log(` ${chalk.red("✗")} ${msg}`); }
|
|
536
|
+
function warn(msg: string) { checks.push({ name: msg, status: "warn", message: msg }); if (!isJson) console.log(` ${chalk.yellow("⚠")} ${msg}`); }
|
|
537
|
+
|
|
538
|
+
// 1. Environment
|
|
539
|
+
if (!isJson) console.log(chalk.cyan("Environment"));
|
|
540
|
+
const nodeV = process.version;
|
|
541
|
+
const nodeMajor = parseInt(nodeV.slice(1), 10);
|
|
542
|
+
if (nodeMajor >= 18) ok(`Node.js ${nodeV}`);
|
|
543
|
+
else fail(`Node.js ${nodeV} — requires >=18`);
|
|
544
|
+
|
|
545
|
+
const gitResult = spawnSync("git", ["--version"], { encoding: "utf-8" });
|
|
546
|
+
if (gitResult.status === 0) ok(gitResult.stdout.trim());
|
|
547
|
+
else fail("git not found");
|
|
548
|
+
|
|
549
|
+
const claudeResult = spawnSync("claude", ["--version"], { encoding: "utf-8" });
|
|
550
|
+
if (claudeResult.status === 0) ok(`Claude Code ${claudeResult.stdout.trim()}`);
|
|
551
|
+
else warn("Claude Code not found (optional)");
|
|
552
|
+
|
|
553
|
+
if (!isJson) console.log("");
|
|
554
|
+
|
|
555
|
+
// 2. Configuration
|
|
556
|
+
if (!isJson) console.log(chalk.cyan("Configuration"));
|
|
557
|
+
const configPath = globalOpts.config
|
|
558
|
+
? path.resolve(globalOpts.config)
|
|
559
|
+
: path.join(cwd, "agentboot.config.json");
|
|
560
|
+
|
|
561
|
+
if (fs.existsSync(configPath)) {
|
|
562
|
+
ok(`agentboot.config.json found`);
|
|
563
|
+
try {
|
|
564
|
+
const config = loadConfig(configPath);
|
|
565
|
+
ok(`Config parses successfully (org: ${config.org})`);
|
|
566
|
+
|
|
567
|
+
// Check personas
|
|
568
|
+
const enabledPersonas = config.personas?.enabled ?? [];
|
|
569
|
+
const personasDir = path.join(cwd, "core", "personas");
|
|
570
|
+
let personaIssues = 0;
|
|
571
|
+
for (const p of enabledPersonas) {
|
|
572
|
+
const pDir = path.join(personasDir, p);
|
|
573
|
+
if (!fs.existsSync(pDir)) { personaIssues++; fail(`Persona not found: ${p}`); }
|
|
574
|
+
else if (!fs.existsSync(path.join(pDir, "SKILL.md"))) { personaIssues++; fail(`Missing SKILL.md: ${p}`); }
|
|
575
|
+
}
|
|
576
|
+
if (personaIssues === 0) ok(`All ${enabledPersonas.length} enabled personas found`);
|
|
577
|
+
|
|
578
|
+
// Check traits
|
|
579
|
+
const enabledTraits = config.traits?.enabled ?? [];
|
|
580
|
+
const traitsDir = path.join(cwd, "core", "traits");
|
|
581
|
+
let traitIssues = 0;
|
|
582
|
+
for (const t of enabledTraits) {
|
|
583
|
+
if (!fs.existsSync(path.join(traitsDir, `${t}.md`))) { traitIssues++; fail(`Trait not found: ${t}`); }
|
|
584
|
+
}
|
|
585
|
+
if (traitIssues === 0) ok(`All ${enabledTraits.length} enabled traits found`);
|
|
586
|
+
|
|
587
|
+
// Check repos.json
|
|
588
|
+
const reposPath = config.sync?.repos ?? "./repos.json";
|
|
589
|
+
const fullReposPath = path.resolve(path.dirname(configPath), reposPath);
|
|
590
|
+
if (fs.existsSync(fullReposPath)) ok(`repos.json found`);
|
|
591
|
+
else warn(`repos.json not found at ${reposPath}`);
|
|
592
|
+
|
|
593
|
+
// Check dist/
|
|
594
|
+
const distPath = path.resolve(cwd, config.output?.distPath ?? "./dist");
|
|
595
|
+
if (fs.existsSync(distPath)) ok(`dist/ exists (built)`);
|
|
596
|
+
else warn(`dist/ not found — run \`agentboot build\``);
|
|
597
|
+
|
|
598
|
+
} catch (e: unknown) {
|
|
599
|
+
fail(`Config parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
fail("agentboot.config.json not found");
|
|
603
|
+
if (!isJson) console.log(chalk.gray(" Run `agentboot setup` to create one."));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!isJson) console.log("");
|
|
607
|
+
|
|
608
|
+
if (isJson) {
|
|
609
|
+
console.log(JSON.stringify({ issues, checks }, null, 2));
|
|
610
|
+
process.exit(issues > 0 ? 1 : 0);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (issues > 0) {
|
|
614
|
+
console.log(chalk.bold(chalk.red(`✗ ${issues} issue${issues !== 1 ? "s" : ""} found\n`)));
|
|
615
|
+
process.exit(1);
|
|
616
|
+
} else {
|
|
617
|
+
console.log(chalk.bold(chalk.green("✓ All checks passed\n")));
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// ---- status (AB-37) -------------------------------------------------------
|
|
622
|
+
|
|
623
|
+
program
|
|
624
|
+
.command("status")
|
|
625
|
+
.description("Show deployment status across synced repositories")
|
|
626
|
+
.option("--format <fmt>", "output format: text, json", "text")
|
|
627
|
+
.action((opts, cmd) => {
|
|
628
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
629
|
+
const cwd = process.cwd();
|
|
630
|
+
const configPath = globalOpts.config
|
|
631
|
+
? path.resolve(globalOpts.config)
|
|
632
|
+
: path.join(cwd, "agentboot.config.json");
|
|
633
|
+
|
|
634
|
+
if (!fs.existsSync(configPath)) {
|
|
635
|
+
console.error(chalk.red("No agentboot.config.json found. Run `agentboot setup`."));
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const config = loadConfig(configPath);
|
|
640
|
+
const pkgPath = path.join(ROOT, "package.json");
|
|
641
|
+
const version = fs.existsSync(pkgPath) ? JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version : "unknown";
|
|
642
|
+
|
|
643
|
+
const enabledPersonas = config.personas?.enabled ?? [];
|
|
644
|
+
const enabledTraits = config.traits?.enabled ?? [];
|
|
645
|
+
const outputFormats = config.personas?.outputFormats ?? ["skill", "claude", "copilot"];
|
|
646
|
+
const targetDir = config.sync?.targetDir ?? ".claude";
|
|
647
|
+
|
|
648
|
+
// Load repos
|
|
649
|
+
const reposPath = path.resolve(path.dirname(configPath), config.sync?.repos ?? "./repos.json");
|
|
650
|
+
let repos: Array<{ path: string; platform?: string; group?: string; team?: string; label?: string }> = [];
|
|
651
|
+
if (fs.existsSync(reposPath)) {
|
|
652
|
+
try { repos = JSON.parse(fs.readFileSync(reposPath, "utf-8")); } catch { /* empty */ }
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (opts.format === "json") {
|
|
656
|
+
const status = {
|
|
657
|
+
org: config.org,
|
|
658
|
+
version,
|
|
659
|
+
personas: enabledPersonas,
|
|
660
|
+
traits: enabledTraits,
|
|
661
|
+
outputFormats,
|
|
662
|
+
repos: repos.map((r) => {
|
|
663
|
+
const manifestPath = path.join(r.path, targetDir, ".agentboot-manifest.json");
|
|
664
|
+
let manifest = null;
|
|
665
|
+
if (fs.existsSync(manifestPath)) {
|
|
666
|
+
try { manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")); } catch { /* skip */ }
|
|
667
|
+
}
|
|
668
|
+
return { ...r, manifest };
|
|
669
|
+
}),
|
|
670
|
+
};
|
|
671
|
+
console.log(JSON.stringify(status, null, 2));
|
|
672
|
+
process.exit(0);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
console.log(chalk.bold("\nAgentBoot — status\n"));
|
|
676
|
+
console.log(` Org: ${chalk.cyan(config.orgDisplayName ?? config.org)}`);
|
|
677
|
+
console.log(` Version: ${version}`);
|
|
678
|
+
console.log(` Personas: ${enabledPersonas.length} enabled (${enabledPersonas.join(", ")})`);
|
|
679
|
+
console.log(` Traits: ${enabledTraits.length} enabled`);
|
|
680
|
+
console.log(` Platforms: ${outputFormats.join(", ")}`);
|
|
681
|
+
console.log("");
|
|
682
|
+
|
|
683
|
+
if (repos.length === 0) {
|
|
684
|
+
console.log(chalk.gray(" No repos registered in repos.json.\n"));
|
|
685
|
+
} else {
|
|
686
|
+
console.log(chalk.cyan(` Repos (${repos.length}):`));
|
|
687
|
+
for (const repo of repos) {
|
|
688
|
+
const label = repo.label ?? repo.path;
|
|
689
|
+
const manifestPath = path.join(repo.path, targetDir, ".agentboot-manifest.json");
|
|
690
|
+
let syncInfo = chalk.gray("never synced");
|
|
691
|
+
|
|
692
|
+
if (fs.existsSync(manifestPath)) {
|
|
693
|
+
try {
|
|
694
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
695
|
+
const syncedAt = manifest.synced_at ?? "unknown";
|
|
696
|
+
const fileCount = manifest.files?.length ?? 0;
|
|
697
|
+
syncInfo = chalk.green(`synced ${syncedAt} (${fileCount} files)`);
|
|
698
|
+
} catch { /* skip */ }
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const scope = repo.team ? `${repo.group}/${repo.team}` : repo.group ?? "core";
|
|
702
|
+
console.log(` ${label} [${scope}] — ${syncInfo}`);
|
|
703
|
+
}
|
|
704
|
+
console.log("");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Check dist/ freshness
|
|
708
|
+
const distPath = path.resolve(cwd, config.output?.distPath ?? "./dist");
|
|
709
|
+
if (fs.existsSync(distPath)) {
|
|
710
|
+
const stat = fs.statSync(distPath);
|
|
711
|
+
console.log(chalk.gray(` Last build: ${stat.mtime.toISOString()}\n`));
|
|
712
|
+
} else {
|
|
713
|
+
console.log(chalk.yellow(" dist/ not found — run `agentboot build`\n"));
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// ---- lint (AB-38) ---------------------------------------------------------
|
|
718
|
+
|
|
719
|
+
program
|
|
720
|
+
.command("lint")
|
|
721
|
+
.description("Static analysis for prompt quality and token budgets")
|
|
722
|
+
.option("--persona <name>", "lint specific persona only")
|
|
723
|
+
.option("--severity <level>", "minimum severity: info, warn, error", "warn")
|
|
724
|
+
.option("--format <fmt>", "output format: text, json", "text")
|
|
725
|
+
.action((opts, cmd) => {
|
|
726
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
727
|
+
const cwd = process.cwd();
|
|
728
|
+
const configPath = globalOpts.config
|
|
729
|
+
? path.resolve(globalOpts.config)
|
|
730
|
+
: path.join(cwd, "agentboot.config.json");
|
|
731
|
+
|
|
732
|
+
if (!fs.existsSync(configPath)) {
|
|
733
|
+
console.error(chalk.red("No agentboot.config.json found."));
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const config = loadConfig(configPath);
|
|
738
|
+
const isJson = opts.format === "json";
|
|
739
|
+
if (!isJson) console.log(chalk.bold("\nAgentBoot — lint\n"));
|
|
740
|
+
|
|
741
|
+
interface Finding {
|
|
742
|
+
rule: string;
|
|
743
|
+
severity: "info" | "warn" | "error";
|
|
744
|
+
file: string;
|
|
745
|
+
line?: number;
|
|
746
|
+
message: string;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const findings: Finding[] = [];
|
|
750
|
+
const severityOrder = { info: 0, warn: 1, error: 2 };
|
|
751
|
+
const minSeverity = severityOrder[opts.severity as keyof typeof severityOrder] ?? 1;
|
|
752
|
+
|
|
753
|
+
const personasDir = path.join(cwd, "core", "personas");
|
|
754
|
+
const enabledPersonas = config.personas?.enabled ?? [];
|
|
755
|
+
const tokenBudget = config.output?.tokenBudget?.warnAt ?? 8000;
|
|
756
|
+
|
|
757
|
+
// Vague language patterns
|
|
758
|
+
const vaguePatterns = [
|
|
759
|
+
{ pattern: /\bbe thorough\b/i, msg: "Vague: 'be thorough' — specify what to check" },
|
|
760
|
+
{ pattern: /\btry to\b/i, msg: "Weak: 'try to' — use imperative voice" },
|
|
761
|
+
{ pattern: /\bif possible\b/i, msg: "Vague: 'if possible' — specify the condition" },
|
|
762
|
+
{ pattern: /\bbest practice/i, msg: "Vague: 'best practice' — cite the specific practice" },
|
|
763
|
+
{ pattern: /\bwhen appropriate\b/i, msg: "Vague: 'when appropriate' — define the criteria" },
|
|
764
|
+
{ pattern: /\bas needed\b/i, msg: "Vague: 'as needed' — specify the condition" },
|
|
765
|
+
];
|
|
766
|
+
|
|
767
|
+
// Secret patterns
|
|
768
|
+
const secretPatterns = [
|
|
769
|
+
{ pattern: /\bsk-[a-zA-Z0-9]{20,}/, msg: "Possible API key (sk-...)" },
|
|
770
|
+
{ pattern: /\bghp_[a-zA-Z0-9]{36}/, msg: "Possible GitHub token (ghp_...)" },
|
|
771
|
+
{ pattern: /\bAKIA[A-Z0-9]{16}/, msg: "Possible AWS key (AKIA...)" },
|
|
772
|
+
{ pattern: /\beyJ[a-zA-Z0-9_-]{10,}\.eyJ/, msg: "Possible JWT token" },
|
|
773
|
+
{ pattern: /password\s*[:=]\s*["'][^"']+["']/i, msg: "Hardcoded password" },
|
|
774
|
+
];
|
|
775
|
+
|
|
776
|
+
for (const personaName of enabledPersonas) {
|
|
777
|
+
if (opts.persona && personaName !== opts.persona) continue;
|
|
778
|
+
|
|
779
|
+
const personaDir = path.join(personasDir, personaName);
|
|
780
|
+
const skillPath = path.join(personaDir, "SKILL.md");
|
|
781
|
+
|
|
782
|
+
if (!fs.existsSync(skillPath)) continue;
|
|
783
|
+
|
|
784
|
+
const content = fs.readFileSync(skillPath, "utf-8");
|
|
785
|
+
const lines = content.split("\n");
|
|
786
|
+
|
|
787
|
+
// Token budget check
|
|
788
|
+
const estimatedTokens = Math.ceil(content.length / 4);
|
|
789
|
+
if (estimatedTokens > tokenBudget) {
|
|
790
|
+
findings.push({
|
|
791
|
+
rule: "prompt-too-long",
|
|
792
|
+
severity: "error",
|
|
793
|
+
file: `core/personas/${personaName}/SKILL.md`,
|
|
794
|
+
message: `Estimated ${estimatedTokens} tokens exceeds budget of ${tokenBudget}`,
|
|
795
|
+
});
|
|
796
|
+
} else if (estimatedTokens > tokenBudget * 0.8) {
|
|
797
|
+
findings.push({
|
|
798
|
+
rule: "prompt-too-long",
|
|
799
|
+
severity: "warn",
|
|
800
|
+
file: `core/personas/${personaName}/SKILL.md`,
|
|
801
|
+
message: `Estimated ${estimatedTokens} tokens — approaching budget of ${tokenBudget}`,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Line count check
|
|
806
|
+
if (lines.length > 1000) {
|
|
807
|
+
findings.push({ rule: "prompt-too-long", severity: "error", file: `core/personas/${personaName}/SKILL.md`, message: `${lines.length} lines — max recommended is 1000` });
|
|
808
|
+
} else if (lines.length > 500) {
|
|
809
|
+
findings.push({ rule: "prompt-too-long", severity: "warn", file: `core/personas/${personaName}/SKILL.md`, message: `${lines.length} lines — consider trimming (warn at 500)` });
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Vague language
|
|
813
|
+
for (let i = 0; i < lines.length; i++) {
|
|
814
|
+
for (const vp of vaguePatterns) {
|
|
815
|
+
if (vp.pattern.test(lines[i]!)) {
|
|
816
|
+
findings.push({
|
|
817
|
+
rule: "vague-instruction",
|
|
818
|
+
severity: "warn",
|
|
819
|
+
file: `core/personas/${personaName}/SKILL.md`,
|
|
820
|
+
line: i + 1,
|
|
821
|
+
message: vp.msg,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Secrets
|
|
827
|
+
for (const sp of secretPatterns) {
|
|
828
|
+
if (sp.pattern.test(lines[i]!)) {
|
|
829
|
+
findings.push({
|
|
830
|
+
rule: "credential-in-prompt",
|
|
831
|
+
severity: "error",
|
|
832
|
+
file: `core/personas/${personaName}/SKILL.md`,
|
|
833
|
+
line: i + 1,
|
|
834
|
+
message: sp.msg,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Missing output format section
|
|
841
|
+
if (!/## output format/i.test(content)) {
|
|
842
|
+
findings.push({
|
|
843
|
+
rule: "missing-output-format",
|
|
844
|
+
severity: "info",
|
|
845
|
+
file: `core/personas/${personaName}/SKILL.md`,
|
|
846
|
+
message: "No '## Output Format' section found",
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Also lint traits
|
|
852
|
+
const traitsDir = path.join(cwd, "core", "traits");
|
|
853
|
+
if (fs.existsSync(traitsDir)) {
|
|
854
|
+
for (const file of fs.readdirSync(traitsDir).filter((f) => f.endsWith(".md"))) {
|
|
855
|
+
const content = fs.readFileSync(path.join(traitsDir, file), "utf-8");
|
|
856
|
+
const lines = content.split("\n");
|
|
857
|
+
|
|
858
|
+
if (lines.length > 100) {
|
|
859
|
+
findings.push({ rule: "trait-too-long", severity: "warn", file: `core/traits/${file}`, message: `${lines.length} lines — traits should be concise (<100 lines)` });
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Check for unused trait
|
|
863
|
+
const traitName = file.replace(/\.md$/, "");
|
|
864
|
+
const enabledTraits = config.traits?.enabled ?? [];
|
|
865
|
+
if (enabledTraits.length > 0 && !enabledTraits.includes(traitName)) {
|
|
866
|
+
findings.push({ rule: "unused-trait", severity: "info", file: `core/traits/${file}`, message: `Trait not in traits.enabled list` });
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Filter by severity
|
|
872
|
+
const filtered = findings.filter((f) => severityOrder[f.severity] >= minSeverity);
|
|
873
|
+
|
|
874
|
+
if (opts.format === "json") {
|
|
875
|
+
console.log(JSON.stringify(filtered, null, 2));
|
|
876
|
+
const hasErrors = filtered.some((f) => f.severity === "error");
|
|
877
|
+
process.exit(hasErrors ? 1 : 0);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (filtered.length === 0) {
|
|
881
|
+
console.log(chalk.bold(chalk.green("✓ No issues found\n")));
|
|
882
|
+
process.exit(0);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Group by file
|
|
886
|
+
const byFile = new Map<string, Finding[]>();
|
|
887
|
+
for (const f of filtered) {
|
|
888
|
+
const list = byFile.get(f.file) ?? [];
|
|
889
|
+
list.push(f);
|
|
890
|
+
byFile.set(f.file, list);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
for (const [file, fileFindings] of byFile) {
|
|
894
|
+
console.log(chalk.cyan(` ${file}`));
|
|
895
|
+
for (const f of fileFindings) {
|
|
896
|
+
const sev = f.severity === "error" ? chalk.red(f.severity.toUpperCase())
|
|
897
|
+
: f.severity === "warn" ? chalk.yellow(f.severity.toUpperCase())
|
|
898
|
+
: chalk.gray(f.severity.toUpperCase());
|
|
899
|
+
const loc = f.line ? `:${f.line}` : "";
|
|
900
|
+
console.log(` ${sev} [${f.rule}]${loc} ${f.message}`);
|
|
901
|
+
}
|
|
902
|
+
console.log("");
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const errorCount = filtered.filter((f) => f.severity === "error").length;
|
|
906
|
+
const warnCount = filtered.filter((f) => f.severity === "warn").length;
|
|
907
|
+
const infoCount = filtered.filter((f) => f.severity === "info").length;
|
|
908
|
+
|
|
909
|
+
const parts: string[] = [];
|
|
910
|
+
if (errorCount) parts.push(chalk.red(`${errorCount} error${errorCount !== 1 ? "s" : ""}`));
|
|
911
|
+
if (warnCount) parts.push(chalk.yellow(`${warnCount} warning${warnCount !== 1 ? "s" : ""}`));
|
|
912
|
+
if (infoCount) parts.push(chalk.gray(`${infoCount} info`));
|
|
913
|
+
|
|
914
|
+
console.log(` ${parts.join(", ")}\n`);
|
|
915
|
+
process.exit(errorCount > 0 ? 1 : 0);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
// ---- uninstall (AB-45) ----------------------------------------------------
|
|
919
|
+
|
|
920
|
+
program
|
|
921
|
+
.command("uninstall")
|
|
922
|
+
.description("Remove AgentBoot managed files from a repository")
|
|
923
|
+
.option("--repo <path>", "target repository path")
|
|
924
|
+
.option("--dry-run", "preview what would be removed")
|
|
925
|
+
.action((opts) => {
|
|
926
|
+
const targetRepo = opts.repo ? path.resolve(opts.repo) : process.cwd();
|
|
927
|
+
const dryRun = opts.dryRun ?? false;
|
|
928
|
+
const targetDir = ".claude";
|
|
929
|
+
const manifestPath = path.join(targetRepo, targetDir, ".agentboot-manifest.json");
|
|
930
|
+
|
|
931
|
+
console.log(chalk.bold("\nAgentBoot — uninstall\n"));
|
|
932
|
+
console.log(chalk.gray(` Target: ${targetRepo}`));
|
|
933
|
+
|
|
934
|
+
if (dryRun) {
|
|
935
|
+
console.log(chalk.yellow(" DRY RUN — no files will be removed\n"));
|
|
936
|
+
} else {
|
|
937
|
+
console.log("");
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (!fs.existsSync(manifestPath)) {
|
|
941
|
+
console.log(chalk.yellow(" No .agentboot-manifest.json found — nothing to uninstall."));
|
|
942
|
+
console.log(chalk.gray(" This repo may not have been synced by AgentBoot.\n"));
|
|
943
|
+
process.exit(0);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
let manifest: { files?: Array<{ path: string; hash: string }>; version?: string; synced_at?: string };
|
|
947
|
+
try {
|
|
948
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
949
|
+
} catch {
|
|
950
|
+
console.error(chalk.red(" Failed to parse manifest file."));
|
|
951
|
+
process.exit(1);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const files = manifest.files ?? [];
|
|
955
|
+
console.log(chalk.cyan(` Found ${files.length} managed file(s) (synced ${manifest.synced_at ?? "unknown"})\n`));
|
|
956
|
+
|
|
957
|
+
let removed = 0;
|
|
958
|
+
let modified = 0;
|
|
959
|
+
let missing = 0;
|
|
960
|
+
|
|
961
|
+
// Resolve boundary for path traversal protection
|
|
962
|
+
const boundary = path.resolve(targetRepo);
|
|
963
|
+
|
|
964
|
+
for (const entry of files) {
|
|
965
|
+
// Manifest paths are repo-relative (include .claude/ prefix)
|
|
966
|
+
const fullPath = path.resolve(targetRepo, entry.path);
|
|
967
|
+
|
|
968
|
+
// Path traversal protection: reject paths that escape the repo
|
|
969
|
+
if (!fullPath.startsWith(boundary + path.sep) && fullPath !== boundary) {
|
|
970
|
+
console.log(chalk.red(` rejected ${entry.path} (path escapes repo boundary)`));
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (!fs.existsSync(fullPath)) {
|
|
975
|
+
missing++;
|
|
976
|
+
console.log(chalk.gray(` skip ${entry.path} (already removed)`));
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Check if file was modified after sync (read as Buffer to match sync.ts hashing)
|
|
981
|
+
const currentContent = fs.readFileSync(fullPath);
|
|
982
|
+
const currentHash = createHash("sha256").update(currentContent).digest("hex");
|
|
983
|
+
|
|
984
|
+
if (currentHash !== entry.hash) {
|
|
985
|
+
modified++;
|
|
986
|
+
console.log(chalk.yellow(` modified ${entry.path} (hash mismatch — skipping)`));
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (dryRun) {
|
|
991
|
+
console.log(chalk.gray(` would remove ${entry.path}`));
|
|
992
|
+
} else {
|
|
993
|
+
fs.unlinkSync(fullPath);
|
|
994
|
+
// Clean up empty parent directories (stay within repo boundary)
|
|
995
|
+
let dir = path.dirname(fullPath);
|
|
996
|
+
while (dir.startsWith(boundary + path.sep) && dir !== boundary) {
|
|
997
|
+
try {
|
|
998
|
+
const entries = fs.readdirSync(dir);
|
|
999
|
+
if (entries.length === 0) { fs.rmdirSync(dir); dir = path.dirname(dir); }
|
|
1000
|
+
else break;
|
|
1001
|
+
} catch { break; }
|
|
1002
|
+
}
|
|
1003
|
+
console.log(chalk.green(` removed ${entry.path}`));
|
|
1004
|
+
}
|
|
1005
|
+
removed++;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Remove manifest itself (also when all files were already gone)
|
|
1009
|
+
if (!dryRun && (removed > 0 || (missing > 0 && modified === 0))) {
|
|
1010
|
+
fs.unlinkSync(manifestPath);
|
|
1011
|
+
console.log(chalk.green(` removed .agentboot-manifest.json`));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
console.log("");
|
|
1015
|
+
const verb = dryRun ? "would remove" : "removed";
|
|
1016
|
+
console.log(chalk.bold(` ${verb}: ${removed}, skipped (modified): ${modified}, already gone: ${missing}\n`));
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
// ---- config ---------------------------------------------------------------
|
|
1020
|
+
|
|
1021
|
+
program
|
|
1022
|
+
.command("config")
|
|
1023
|
+
.description("View configuration (read-only)")
|
|
1024
|
+
.argument("[key]", "config key (e.g., personas.enabled)")
|
|
1025
|
+
.argument("[value]", "not yet supported")
|
|
1026
|
+
.action((key?: string, value?: string) => {
|
|
1027
|
+
const cwd = process.cwd();
|
|
1028
|
+
const configPath = path.join(cwd, "agentboot.config.json");
|
|
1029
|
+
|
|
1030
|
+
if (!fs.existsSync(configPath)) {
|
|
1031
|
+
console.error(chalk.red("No agentboot.config.json found."));
|
|
1032
|
+
process.exit(1);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (!key) {
|
|
1036
|
+
// Show current config
|
|
1037
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
1038
|
+
console.log(content);
|
|
1039
|
+
process.exit(0);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (!value) {
|
|
1043
|
+
// Read a specific key
|
|
1044
|
+
const config = loadConfig(configPath);
|
|
1045
|
+
const keys = key.split(".");
|
|
1046
|
+
let current: unknown = config;
|
|
1047
|
+
for (const k of keys) {
|
|
1048
|
+
if (current && typeof current === "object" && k in current) {
|
|
1049
|
+
current = (current as Record<string, unknown>)[k];
|
|
1050
|
+
} else {
|
|
1051
|
+
console.error(chalk.red(`Key not found: ${key}`));
|
|
1052
|
+
process.exit(1);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
console.log(typeof current === "object" ? JSON.stringify(current, null, 2) : String(current));
|
|
1056
|
+
process.exit(0);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
console.error(chalk.red(`Config writes are not yet supported. Edit agentboot.config.json directly.`));
|
|
1060
|
+
console.error(chalk.gray(` agentboot config ${key} ← read a value`));
|
|
1061
|
+
console.error(chalk.gray(` agentboot config ← show full config`));
|
|
1062
|
+
process.exit(1);
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
// ---------------------------------------------------------------------------
|
|
1066
|
+
// Parse
|
|
1067
|
+
// ---------------------------------------------------------------------------
|
|
1068
|
+
|
|
1069
|
+
program.parse();
|