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