haac-aikit 0.1.1 → 0.7.2

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 (44) hide show
  1. package/README.md +169 -69
  2. package/catalog/agents/tier1/architect.md +86 -0
  3. package/catalog/agents/tier1/debugger.md +64 -0
  4. package/catalog/agents/tier1/pr-describer.md +57 -0
  5. package/catalog/agents/{researcher.md → tier1/researcher.md} +1 -1
  6. package/catalog/agents/tier2/changelog-curator.md +60 -0
  7. package/catalog/agents/tier2/dependency-upgrader.md +67 -0
  8. package/catalog/agents/tier2/evals-author.md +65 -0
  9. package/catalog/agents/tier2/flake-hunter.md +57 -0
  10. package/catalog/agents/tier2/prompt-engineer.md +58 -0
  11. package/catalog/agents/tier2/simplifier.md +57 -0
  12. package/catalog/ci/aikit-rules.yml +55 -0
  13. package/catalog/docs/claude-md-reference.md +316 -0
  14. package/catalog/hooks/block-dangerous-bash.sh +0 -0
  15. package/catalog/hooks/block-force-push-main.sh +0 -0
  16. package/catalog/hooks/block-secrets-in-commits.sh +0 -0
  17. package/catalog/hooks/check-pattern-violations.sh +137 -0
  18. package/catalog/hooks/compaction-preservation.sh +0 -0
  19. package/catalog/hooks/file-guard.sh +0 -0
  20. package/catalog/hooks/format-on-save.sh +0 -0
  21. package/catalog/hooks/hooks.json +38 -0
  22. package/catalog/hooks/judge-rule-compliance.sh +197 -0
  23. package/catalog/hooks/log-rule-event.sh +80 -0
  24. package/catalog/hooks/session-start-prime.sh +0 -0
  25. package/catalog/husky/commit-msg +0 -0
  26. package/catalog/husky/pre-commit +0 -0
  27. package/catalog/husky/pre-push +0 -0
  28. package/catalog/rules/AGENTS.md.tmpl +28 -7
  29. package/catalog/rules/aikit-rules.json +37 -0
  30. package/catalog/rules/claude-rules/example.md +38 -0
  31. package/catalog/skills/tier1/software-architect.md +42 -0
  32. package/dist/cli.mjs +1516 -173
  33. package/dist/cli.mjs.map +1 -1
  34. package/package.json +4 -2
  35. /package/catalog/agents/{devops.md → tier1/devops.md} +0 -0
  36. /package/catalog/agents/{implementer.md → tier1/implementer.md} +0 -0
  37. /package/catalog/agents/{orchestrator.md → tier1/orchestrator.md} +0 -0
  38. /package/catalog/agents/{planner.md → tier1/planner.md} +0 -0
  39. /package/catalog/agents/{reviewer.md → tier1/reviewer.md} +0 -0
  40. /package/catalog/agents/{security-auditor.md → tier1/security-auditor.md} +0 -0
  41. /package/catalog/agents/{tester.md → tier1/tester.md} +0 -0
  42. /package/catalog/agents/{backend.md → tier2/backend.md} +0 -0
  43. /package/catalog/agents/{frontend.md → tier2/frontend.md} +0 -0
  44. /package/catalog/agents/{mobile.md → tier2/mobile.md} +0 -0
package/dist/cli.mjs CHANGED
@@ -972,6 +972,10 @@ var init_kleur = __esm({
972
972
  });
973
973
 
974
974
  // src/wizard.ts
975
+ function defaultSpecialtyAgents(scope) {
976
+ if (scope === "everything") return SPECIALTY_TIER2_AGENTS.map((a2) => a2.value);
977
+ return [];
978
+ }
975
979
  async function runWizard(projectName) {
976
980
  we(kleur_default.bgCyan().black(" haac-aikit ") + " The batteries-included AI-agentic-coding kit");
977
981
  const answers = await be(
@@ -1029,6 +1033,15 @@ async function runWizard(projectName) {
1029
1033
  initialValues: ["web"],
1030
1034
  required: false
1031
1035
  });
1036
+ },
1037
+ specialtyAgents: ({ results }) => {
1038
+ if (results.scope === "minimal") return Promise.resolve(void 0);
1039
+ return pe({
1040
+ message: "Include specialty agents? (debugger and pr-describer always installed)",
1041
+ options: SPECIALTY_TIER2_AGENTS.map((a2) => ({ value: a2.value, label: a2.label })),
1042
+ required: false,
1043
+ initialValues: defaultSpecialtyAgents(results.scope ?? "standard")
1044
+ });
1032
1045
  }
1033
1046
  },
1034
1047
  {
@@ -1044,15 +1057,24 @@ async function runWizard(projectName) {
1044
1057
  tools: answers.tools ?? ALL_TOOLS.map((t) => t.value),
1045
1058
  scope: answers.scope ?? "standard",
1046
1059
  integrations: answers.integrations ?? [],
1047
- shape: answers.shape ?? []
1060
+ shape: answers.shape ?? [],
1061
+ specialtyAgents: answers.specialtyAgents ?? []
1048
1062
  };
1049
1063
  }
1050
- var ALL_TOOLS, ALL_SHAPES, ALL_INTEGRATIONS;
1064
+ var SPECIALTY_TIER2_AGENTS, ALL_TOOLS, ALL_SHAPES, ALL_INTEGRATIONS;
1051
1065
  var init_wizard = __esm({
1052
1066
  "src/wizard.ts"() {
1053
1067
  "use strict";
1054
1068
  init_dist2();
1055
1069
  init_kleur();
1070
+ SPECIALTY_TIER2_AGENTS = [
1071
+ { value: "flake-hunter", label: "flake-hunter \u2014 diagnose intermittent test failures" },
1072
+ { value: "simplifier", label: "simplifier \u2014 DRY, dead code, complexity reduction" },
1073
+ { value: "prompt-engineer", label: "prompt-engineer \u2014 author/optimize prompts" },
1074
+ { value: "evals-author", label: "evals-author \u2014 eval datasets & benchmarks" },
1075
+ { value: "changelog-curator", label: "changelog-curator \u2014 generate CHANGELOG from commits" },
1076
+ { value: "dependency-upgrader", label: "dependency-upgrader \u2014 npm major bumps + codemods" }
1077
+ ];
1056
1078
  ALL_TOOLS = [
1057
1079
  { value: "claude", label: "Claude Code", hint: "CLAUDE.md + .claude/" },
1058
1080
  { value: "cursor", label: "Cursor", hint: ".cursor/rules/" },
@@ -1131,6 +1153,15 @@ ${end}
1131
1153
  `;
1132
1154
  return { hasMarkers: false, replaced };
1133
1155
  }
1156
+ function extractMarkerRegion(content, filePath) {
1157
+ const dialect = detectDialect(filePath);
1158
+ const begin = makeBegin(dialect);
1159
+ const end = makeEnd(dialect);
1160
+ const beginIdx = content.indexOf(begin);
1161
+ const endIdx = content.indexOf(end);
1162
+ if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx) return null;
1163
+ return content.slice(beginIdx + begin.length, endIdx).trim();
1164
+ }
1134
1165
  var MARKER;
1135
1166
  var init_markers = __esm({
1136
1167
  "src/render/markers.ts"() {
@@ -1147,11 +1178,18 @@ function safeWrite(filePath, content, opts = {}) {
1147
1178
  const existed = existsSync2(filePath);
1148
1179
  if (existed) {
1149
1180
  if (useMarkers && managedContent !== void 0) {
1150
- const existing = readFileSync2(filePath, "utf8");
1151
- const { replaced } = upsertMarkerRegion(existing, managedContent, filePath);
1181
+ const existing2 = readFileSync2(filePath, "utf8");
1182
+ const { replaced } = upsertMarkerRegion(existing2, managedContent, filePath);
1183
+ if (existing2 === replaced) {
1184
+ return { path: filePath, action: "skipped" };
1185
+ }
1152
1186
  if (!dryRun) writeFileSync2(filePath, replaced, "utf8");
1153
1187
  return { path: filePath, action: "updated" };
1154
1188
  }
1189
+ const existing = readFileSync2(filePath, "utf8");
1190
+ if (existing === content) {
1191
+ return { path: filePath, action: "skipped" };
1192
+ }
1155
1193
  if (!force) {
1156
1194
  return { path: filePath, action: "conflict" };
1157
1195
  }
@@ -1187,6 +1225,7 @@ var init_gitignore = __esm({
1187
1225
  ENTRIES = [
1188
1226
  ".claude/settings.local.json",
1189
1227
  ".claude/backups/",
1228
+ ".aikit/",
1190
1229
  ".env.local",
1191
1230
  ".env*.local"
1192
1231
  ];
@@ -1209,11 +1248,24 @@ var init_template = __esm({
1209
1248
  });
1210
1249
 
1211
1250
  // src/catalog/index.ts
1212
- import { readFileSync as readFileSync4 } from "fs";
1251
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
1213
1252
  import { fileURLToPath } from "url";
1214
1253
  import { dirname as dirname2, join } from "path";
1254
+ function findCatalogRoot() {
1255
+ let dir = dirname2(fileURLToPath(import.meta.url));
1256
+ for (let i = 0; i < 6; i++) {
1257
+ const candidate = join(dir, "catalog");
1258
+ if (existsSync4(join(candidate, "rules", "AGENTS.md.tmpl"))) {
1259
+ return candidate;
1260
+ }
1261
+ const parent = dirname2(dir);
1262
+ if (parent === dir) break;
1263
+ dir = parent;
1264
+ }
1265
+ throw new Error("haac-aikit: catalog/ directory not found relative to module location");
1266
+ }
1215
1267
  function read(rel) {
1216
- return readFileSync4(join(ROOT, rel), "utf8");
1268
+ return readFileSync4(join(CATALOG_ROOT, rel), "utf8");
1217
1269
  }
1218
1270
  function loadCatalog() {
1219
1271
  return {
@@ -1226,26 +1278,241 @@ function loadCatalog() {
1226
1278
  aiderConf: () => read("rules/aider.conf.yml"),
1227
1279
  geminiMd: () => read("rules/GEMINI.md.shim"),
1228
1280
  mcpJson: () => read("mcp/mcp.json"),
1229
- settingsJson: () => read("settings/settings.json")
1281
+ settingsJson: () => read("settings/settings.json"),
1282
+ claudeMdReference: () => read("docs/claude-md-reference.md"),
1283
+ aikitRulesJson: () => read("rules/aikit-rules.json")
1230
1284
  };
1231
1285
  }
1232
- var ROOT;
1286
+ var CATALOG_ROOT;
1233
1287
  var init_catalog = __esm({
1234
1288
  "src/catalog/index.ts"() {
1235
1289
  "use strict";
1236
1290
  init_template();
1237
- ROOT = join(dirname2(fileURLToPath(import.meta.url)), "..", "catalog");
1291
+ CATALOG_ROOT = findCatalogRoot();
1292
+ }
1293
+ });
1294
+
1295
+ // src/catalog/shape-agents.ts
1296
+ function resolveShapeAgents(shapes) {
1297
+ const set = /* @__PURE__ */ new Set();
1298
+ for (const shape of shapes) {
1299
+ for (const agent of SHAPE_AGENTS[shape] ?? []) {
1300
+ set.add(agent);
1301
+ }
1302
+ }
1303
+ return Array.from(set);
1304
+ }
1305
+ var SHAPE_AGENTS;
1306
+ var init_shape_agents = __esm({
1307
+ "src/catalog/shape-agents.ts"() {
1308
+ "use strict";
1309
+ SHAPE_AGENTS = {
1310
+ web: ["frontend"],
1311
+ fullstack: ["frontend", "backend"],
1312
+ backend: ["backend"],
1313
+ mobile: ["mobile"],
1314
+ library: ["backend"]
1315
+ };
1316
+ }
1317
+ });
1318
+
1319
+ // src/render/dialects/parser.ts
1320
+ function parseRuleSet(content, projectName, description) {
1321
+ const rules = [];
1322
+ const rawSections = /* @__PURE__ */ new Map();
1323
+ let currentSection = "";
1324
+ let sectionBuffer = [];
1325
+ const flushSection = () => {
1326
+ if (currentSection && sectionBuffer.length > 0) {
1327
+ rawSections.set(currentSection, sectionBuffer.join("\n").trim());
1328
+ }
1329
+ };
1330
+ for (const line of content.split("\n")) {
1331
+ const sectionMatch = line.match(SECTION_RX);
1332
+ if (sectionMatch?.[1]) {
1333
+ flushSection();
1334
+ currentSection = sectionMatch[1].trim();
1335
+ sectionBuffer = [];
1336
+ continue;
1337
+ }
1338
+ sectionBuffer.push(line);
1339
+ const idMatch = line.match(ID_LINE_RX);
1340
+ if (idMatch?.groups?.id) {
1341
+ const text = line.replace(RULE_PREFIX_RX, "").trim();
1342
+ rules.push({
1343
+ id: idMatch.groups.id,
1344
+ text: text || idMatch.groups.id,
1345
+ meta: parseMeta(idMatch.groups.meta ?? ""),
1346
+ section: currentSection
1347
+ });
1348
+ }
1349
+ }
1350
+ flushSection();
1351
+ return { projectName, description, rules, rawSections };
1352
+ }
1353
+ function parseMeta(metaString) {
1354
+ const meta = {};
1355
+ for (const part of metaString.trim().split(/\s+/).filter(Boolean)) {
1356
+ const eq = part.indexOf("=");
1357
+ if (eq < 1) continue;
1358
+ const key = part.slice(0, eq);
1359
+ const value = part.slice(eq + 1);
1360
+ if (key === "emphasis" && (value === "high" || value === "normal" || value === "low")) {
1361
+ meta.emphasis = value;
1362
+ } else if (key === "paths") {
1363
+ meta.paths = value.split(",").map((p2) => p2.trim()).filter(Boolean);
1364
+ }
1365
+ }
1366
+ return meta;
1367
+ }
1368
+ var ID_LINE_RX, RULE_PREFIX_RX, SECTION_RX;
1369
+ var init_parser = __esm({
1370
+ "src/render/dialects/parser.ts"() {
1371
+ "use strict";
1372
+ ID_LINE_RX = /<!--\s*id:\s*(?<id>[a-zA-Z][a-zA-Z0-9_-]*\.[a-zA-Z0-9._-]+)(?<meta>(?:\s+[a-zA-Z][a-zA-Z0-9_-]*=[^>\s]+)*)\s*-->/;
1373
+ RULE_PREFIX_RX = /^(\s*[-*]\s*)?<!--\s*id:[^>]+-->\s*/;
1374
+ SECTION_RX = /^##\s+(.+)$/;
1375
+ }
1376
+ });
1377
+
1378
+ // src/render/dialects/cursor.ts
1379
+ function translateForCursor(ruleSet) {
1380
+ const lines = [];
1381
+ lines.push("---");
1382
+ lines.push(`description: Project rules for ${ruleSet.projectName} \u2014 generated from AGENTS.md by haac-aikit`);
1383
+ lines.push("alwaysApply: true");
1384
+ lines.push("---");
1385
+ lines.push("");
1386
+ lines.push("<!-- BEGIN:haac-aikit -->");
1387
+ lines.push(`# ${ruleSet.projectName}`);
1388
+ lines.push("");
1389
+ if (ruleSet.description) {
1390
+ lines.push(ruleSet.description);
1391
+ lines.push("");
1392
+ }
1393
+ lines.push(`See [AGENTS.md](../../AGENTS.md) for the authoritative rule set. Cursor-specific dialect:`);
1394
+ lines.push("");
1395
+ if (ruleSet.rules.length === 0) {
1396
+ lines.push("_No annotated rules found in AGENTS.md. Add `<!-- id: x.y -->` comments to opt rules into Cursor translation._");
1397
+ } else {
1398
+ const bySection = /* @__PURE__ */ new Map();
1399
+ for (const rule of ruleSet.rules) {
1400
+ const section = rule.section || "Rules";
1401
+ const existing = bySection.get(section) ?? [];
1402
+ existing.push(rule);
1403
+ bySection.set(section, existing);
1404
+ }
1405
+ for (const [section, rules] of bySection) {
1406
+ lines.push(`## ${section}`);
1407
+ lines.push("");
1408
+ for (const rule of rules) {
1409
+ const prefix = rule.meta.emphasis === "high" ? "**" : "";
1410
+ const suffix = rule.meta.emphasis === "high" ? "**" : "";
1411
+ const pathsHint = rule.meta.paths && rule.meta.paths.length > 0 ? ` _(applies to: \`${rule.meta.paths.join("`, `")}\`)_` : "";
1412
+ lines.push(`- ${prefix}${rule.text}${suffix}${pathsHint} <!-- id: ${rule.id} -->`);
1413
+ }
1414
+ lines.push("");
1415
+ }
1416
+ }
1417
+ lines.push("<!-- END:haac-aikit -->");
1418
+ return lines.join("\n");
1419
+ }
1420
+ var init_cursor = __esm({
1421
+ "src/render/dialects/cursor.ts"() {
1422
+ "use strict";
1423
+ }
1424
+ });
1425
+
1426
+ // src/render/dialects/index.ts
1427
+ var init_dialects = __esm({
1428
+ "src/render/dialects/index.ts"() {
1429
+ "use strict";
1430
+ init_parser();
1431
+ init_cursor();
1432
+ }
1433
+ });
1434
+
1435
+ // src/fs/diff.ts
1436
+ import { createPatch } from "diff";
1437
+ function formatUnifiedDiff(local, incoming) {
1438
+ if (local === incoming) return "";
1439
+ const raw = createPatch("local", local, incoming, "", "", { context: 3 });
1440
+ const lines = [];
1441
+ let inHunk = false;
1442
+ for (const line of raw.split("\n")) {
1443
+ if (line.startsWith("@@")) {
1444
+ inHunk = true;
1445
+ continue;
1446
+ }
1447
+ if (!inHunk) continue;
1448
+ if (line.startsWith("+") && !line.startsWith("+++")) {
1449
+ lines.push(kleur_default.green(`+ ${line.slice(1)}`));
1450
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
1451
+ lines.push(kleur_default.red(`- ${line.slice(1)}`));
1452
+ } else if (line.startsWith(" ")) {
1453
+ lines.push(kleur_default.dim(` ${line.slice(1)}`));
1454
+ }
1455
+ }
1456
+ return lines.join("\n");
1457
+ }
1458
+ var init_diff = __esm({
1459
+ "src/fs/diff.ts"() {
1460
+ "use strict";
1461
+ init_kleur();
1462
+ }
1463
+ });
1464
+
1465
+ // src/fs/conflict.ts
1466
+ import { basename } from "path";
1467
+ function inferTier3Slot(filePath) {
1468
+ if ((filePath.includes("/.claude/agents/") || filePath.startsWith(".claude/agents/")) && filePath.endsWith(".md")) return "agents";
1469
+ if ((filePath.includes("/.claude/skills/") || filePath.startsWith(".claude/skills/")) && filePath.endsWith(".md")) return "skills";
1470
+ return null;
1471
+ }
1472
+ var interactivePrompt;
1473
+ var init_conflict = __esm({
1474
+ "src/fs/conflict.ts"() {
1475
+ "use strict";
1476
+ init_dist2();
1477
+ init_diff();
1478
+ interactivePrompt = {
1479
+ async ask(filePath, local, incoming) {
1480
+ while (true) {
1481
+ const choice = await de({
1482
+ message: `Modified locally: ${filePath}`,
1483
+ options: [
1484
+ { value: "replace", label: "Replace with catalog version (recommended)" },
1485
+ { value: "keep", label: "Keep local version (mark as tier3 to silence future prompts)" },
1486
+ { value: "diff", label: "Show diff first" },
1487
+ { value: "replace_all", label: "Replace all remaining conflicts" },
1488
+ { value: "skip_all", label: "Skip all remaining conflicts" }
1489
+ ],
1490
+ initialValue: "replace"
1491
+ });
1492
+ if (BD(choice)) {
1493
+ ve("Sync cancelled");
1494
+ process.exit(0);
1495
+ }
1496
+ if (choice === "diff") {
1497
+ const out = formatUnifiedDiff(local, incoming);
1498
+ ye(out, basename(filePath));
1499
+ continue;
1500
+ }
1501
+ return choice;
1502
+ }
1503
+ }
1504
+ };
1238
1505
  }
1239
1506
  });
1240
1507
 
1241
1508
  // src/commands/sync.ts
1242
1509
  var sync_exports = {};
1243
1510
  __export(sync_exports, {
1511
+ copyAction: () => copyAction,
1244
1512
  runSync: () => runSync
1245
1513
  });
1246
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, copyFileSync, readdirSync, readFileSync as readFileSync5 } from "fs";
1247
- import { join as join2, dirname as dirname3 } from "path";
1248
- import { fileURLToPath as fileURLToPath2 } from "url";
1514
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, copyFileSync, readdirSync, readFileSync as readFileSync5, chmodSync } from "fs";
1515
+ import { basename as basename2, dirname as dirname3, join as join2 } from "path";
1249
1516
  async function runSync(argv) {
1250
1517
  const dryRun = argv["dry-run"];
1251
1518
  const force = argv.force;
@@ -1262,16 +1529,24 @@ async function runSync(argv) {
1262
1529
  projectName: config.projectName,
1263
1530
  description: config.projectDescription ?? ""
1264
1531
  });
1265
- results.push(safeWrite("AGENTS.md", agentsMdContent, { ...opts, useMarkers: true, managedContent: agentsMdContent }));
1532
+ const agentsMdManaged = extractMarkerRegion(agentsMdContent, "AGENTS.md") ?? agentsMdContent;
1533
+ results.push(safeWrite("AGENTS.md", agentsMdContent, { ...opts, useMarkers: true, managedContent: agentsMdManaged }));
1266
1534
  if (config.tools.includes("claude")) {
1267
1535
  results.push(safeWrite("CLAUDE.md", catalog.claudeMd(), { ...opts, useMarkers: false }));
1268
1536
  results.push(safeWrite(".claude/settings.json", catalog.settingsJson(), { ...opts, useMarkers: false }));
1537
+ if (config.scope !== "minimal") {
1538
+ results.push(safeWrite("docs/claude-md-reference.md", catalog.claudeMdReference(), { ...opts, useMarkers: false }));
1539
+ results.push(safeWrite(".claude/aikit-rules.json", catalog.aikitRulesJson(), { ...opts, useMarkers: false }));
1540
+ results.push(...syncDir("rules/claude-rules", ".claude/rules", opts, [".md"]));
1541
+ }
1269
1542
  }
1270
1543
  if (config.tools.includes("copilot")) {
1271
1544
  results.push(safeWrite(".github/copilot-instructions.md", catalog.copilotInstructions(), { ...opts, useMarkers: false }));
1272
1545
  }
1273
1546
  if (config.tools.includes("cursor")) {
1274
- results.push(safeWrite(".cursor/rules/000-base.mdc", catalog.cursorBase(), { ...opts, useMarkers: false }));
1547
+ const ruleSet = parseRuleSet(agentsMdContent, config.projectName, config.projectDescription ?? "");
1548
+ const cursorContent = translateForCursor(ruleSet);
1549
+ results.push(safeWrite(".cursor/rules/000-base.mdc", cursorContent, { ...opts, useMarkers: false }));
1275
1550
  }
1276
1551
  if (config.tools.includes("windsurf")) {
1277
1552
  results.push(safeWrite(".windsurf/rules/project.md", catalog.windsurfRules(), { ...opts, useMarkers: false }));
@@ -1287,148 +1562,232 @@ async function runSync(argv) {
1287
1562
  results.push(safeWrite(".mcp.json", catalog.mcpJson(), { ...opts, useMarkers: false }));
1288
1563
  }
1289
1564
  if (config.scope !== "minimal") {
1290
- results.push(...syncSkills("tier1", dryRun));
1291
- results.push(...syncSkills("tier2", dryRun));
1565
+ results.push(...syncSkills("tier1", opts));
1566
+ results.push(...syncSkills("tier2", opts));
1292
1567
  }
1293
1568
  if (config.integrations.hooks) {
1294
- results.push(...syncHooks(dryRun));
1569
+ results.push(...syncHooks(opts));
1295
1570
  }
1296
1571
  if (config.integrations.subagents) {
1297
- results.push(...syncAgents(config.shape, dryRun));
1572
+ results.push(...syncAgents(config, opts));
1298
1573
  }
1299
1574
  if (config.integrations.commands) {
1300
- results.push(...syncCommands(dryRun));
1575
+ results.push(...syncCommands(opts));
1301
1576
  }
1302
1577
  if (config.integrations.ci) {
1303
- results.push(...syncCI(dryRun));
1578
+ results.push(...syncCI(opts));
1304
1579
  }
1305
1580
  if (config.integrations.devcontainer) {
1306
- results.push(...syncDir("devcontainer", ".devcontainer", dryRun, [".json"]));
1581
+ results.push(...syncDir("devcontainer", ".devcontainer", opts, [".json"]));
1307
1582
  }
1308
1583
  if (config.integrations.otel) {
1309
1584
  results.push(safeWrite(".env.example", readCatalogFile("settings/env.example"), { dryRun, useMarkers: false }));
1310
1585
  }
1586
+ if (config.integrations.husky) {
1587
+ results.push(...syncDir("husky", ".husky", opts, []));
1588
+ }
1589
+ if (config.integrations.plugin) {
1590
+ const tmpl = readCatalogFile("plugin/plugin.json");
1591
+ const content = interpolate(tmpl, { projectName: config.projectName });
1592
+ results.push(safeWrite(".claude/plugin/plugin.json", content, { ...opts, useMarkers: false }));
1593
+ }
1311
1594
  ensureGitignoreEntries(dryRun);
1595
+ const conflicts = results.filter((r2) => r2.action === "conflict");
1596
+ if (conflicts.length > 0 && !force) {
1597
+ const isInteractive2 = (Boolean(process.stdin.isTTY) || argv._conflictPrompt != null) && !argv.yes;
1598
+ if (isInteractive2) {
1599
+ const prompt = argv._conflictPrompt ?? interactivePrompt;
1600
+ let bulkAction = null;
1601
+ let configMutated = false;
1602
+ let workingConfig = config;
1603
+ v2.info(`Found ${conflicts.length} file(s) modified locally. Reviewing each\u2026`);
1604
+ for (const conflict of conflicts) {
1605
+ const incomingSrc = conflict.src;
1606
+ if (!incomingSrc) continue;
1607
+ if (bulkAction === "skip_all") {
1608
+ conflict.action = "skipped";
1609
+ continue;
1610
+ }
1611
+ let resolution;
1612
+ if (bulkAction === "replace_all") {
1613
+ resolution = "replace";
1614
+ } else {
1615
+ const local = readFileSync5(conflict.path, "utf8");
1616
+ const incoming = readFileSync5(incomingSrc, "utf8");
1617
+ resolution = await prompt.ask(conflict.path, local, incoming);
1618
+ if (resolution === "replace_all") bulkAction = "replace_all";
1619
+ if (resolution === "skip_all") {
1620
+ bulkAction = "skip_all";
1621
+ conflict.action = "skipped";
1622
+ continue;
1623
+ }
1624
+ }
1625
+ if (resolution === "replace" || resolution === "replace_all") {
1626
+ if (!opts.dryRun) copyFileSync(incomingSrc, conflict.path);
1627
+ conflict.action = "updated";
1628
+ } else if (resolution === "keep") {
1629
+ const slot = inferTier3Slot(conflict.path);
1630
+ if (slot) {
1631
+ const name = basename2(conflict.path).replace(/\.md$/, "");
1632
+ const current = workingConfig[slot] ?? { tier1: "all", tier2: "all", tier3: [] };
1633
+ if (!current.tier3.includes(name)) {
1634
+ workingConfig = {
1635
+ ...workingConfig,
1636
+ [slot]: { ...current, tier3: [...current.tier3, name] }
1637
+ };
1638
+ configMutated = true;
1639
+ }
1640
+ } else {
1641
+ v2.warn(
1642
+ `Kept ${conflict.path} \u2014 no tier3 protection available; this file will be flagged again on next sync.`
1643
+ );
1644
+ }
1645
+ conflict.action = "skipped";
1646
+ }
1647
+ }
1648
+ if (configMutated && !opts.dryRun) {
1649
+ writeConfig(workingConfig);
1650
+ }
1651
+ }
1652
+ }
1312
1653
  const created = results.filter((r2) => r2.action === "created").length;
1313
1654
  const updated = results.filter((r2) => r2.action === "updated").length;
1314
- const conflicts = results.filter((r2) => r2.action === "conflict");
1655
+ const skipped = results.filter((r2) => r2.action === "skipped").length;
1656
+ const remainingConflicts = results.filter((r2) => r2.action === "conflict");
1657
+ const visible = dryRun ? results : results.filter((r2) => r2.action !== "skipped");
1315
1658
  if (dryRun) {
1316
- ye(results.map((r2) => `${r2.action.padEnd(9)} ${r2.path}`).join("\n"), "Dry run");
1659
+ ye(visible.map((r2) => `${r2.action.padEnd(9)} ${r2.path}`).join("\n"), "Dry run");
1317
1660
  return;
1318
1661
  }
1319
- ye(
1320
- results.map((r2) => `${r2.action.padEnd(9)} ${r2.path}`).join("\n"),
1321
- `${created} created, ${updated} updated`
1322
- );
1323
- if (conflicts.length > 0) {
1324
- v2.warn(`${conflicts.length} conflict(s) skipped (use --force to overwrite)`);
1662
+ if (visible.length > 0) {
1663
+ ye(
1664
+ visible.map((r2) => `${r2.action.padEnd(9)} ${r2.path}`).join("\n"),
1665
+ `${created} created, ${updated} updated, ${skipped} unchanged`
1666
+ );
1667
+ } else {
1668
+ v2.info(`Already up to date \u2014 ${skipped} files match the catalog.`);
1669
+ }
1670
+ if (remainingConflicts.length > 0) {
1671
+ v2.warn(`${remainingConflicts.length} conflict(s) skipped (use --force to overwrite)`);
1325
1672
  }
1326
1673
  fe("Sync complete.");
1327
1674
  }
1328
- function syncSkills(tier, dryRun) {
1675
+ function copyAction(srcPath, destPath, opts) {
1676
+ const existed = existsSync5(destPath);
1677
+ const incoming = readFileSync5(srcPath, "utf8");
1678
+ if (existed) {
1679
+ const current = readFileSync5(destPath, "utf8");
1680
+ if (current === incoming) {
1681
+ return { path: destPath, action: "skipped", src: srcPath };
1682
+ }
1683
+ if (!opts.force) {
1684
+ return { path: destPath, action: "conflict", src: srcPath };
1685
+ }
1686
+ }
1687
+ if (!opts.dryRun) {
1688
+ mkdirSync2(dirname3(destPath), { recursive: true });
1689
+ copyFileSync(srcPath, destPath);
1690
+ if (destPath.endsWith(".sh") || destPath.includes("/.husky/") || destPath.includes("/.claude/hooks/")) {
1691
+ chmodSync(destPath, 493);
1692
+ }
1693
+ }
1694
+ return { path: destPath, action: existed ? "updated" : "created", src: srcPath };
1695
+ }
1696
+ function syncSkills(tier, opts) {
1329
1697
  const srcDir = join2(CATALOG_ROOT, "skills", tier);
1330
1698
  const destDir = `.claude/skills`;
1331
1699
  const results = [];
1332
- if (!existsSync4(srcDir)) return results;
1700
+ if (!existsSync5(srcDir)) return results;
1333
1701
  const files = readdirSync(srcDir).filter((f2) => f2.endsWith(".md"));
1334
- if (!dryRun) mkdirSync2(destDir, { recursive: true });
1702
+ if (!opts.dryRun) mkdirSync2(destDir, { recursive: true });
1335
1703
  for (const file of files) {
1336
- const dest = join2(destDir, file);
1337
- if (!dryRun) copyFileSync(join2(srcDir, file), dest);
1338
- results.push({ path: dest, action: existsSync4(dest) ? "updated" : "created" });
1704
+ results.push(copyAction(join2(srcDir, file), join2(destDir, file), opts));
1339
1705
  }
1340
1706
  return results;
1341
1707
  }
1342
- function syncHooks(dryRun) {
1708
+ function syncHooks(opts) {
1343
1709
  const srcDir = join2(CATALOG_ROOT, "hooks");
1344
1710
  const destDir = `.claude/hooks`;
1345
1711
  const results = [];
1346
- if (!existsSync4(srcDir)) return results;
1712
+ if (!existsSync5(srcDir)) return results;
1347
1713
  const files = readdirSync(srcDir).filter((f2) => f2.endsWith(".sh") || f2 === "hooks.json");
1348
- if (!dryRun) mkdirSync2(destDir, { recursive: true });
1714
+ if (!opts.dryRun) mkdirSync2(destDir, { recursive: true });
1349
1715
  for (const file of files) {
1350
- const dest = join2(destDir, file);
1351
- if (!dryRun) copyFileSync(join2(srcDir, file), dest);
1352
- results.push({ path: dest, action: existsSync4(dest) ? "updated" : "created" });
1716
+ results.push(copyAction(join2(srcDir, file), join2(destDir, file), opts));
1353
1717
  }
1354
1718
  return results;
1355
1719
  }
1356
- function syncAgents(shapes, dryRun) {
1357
- const srcDir = join2(CATALOG_ROOT, "agents");
1358
- const destDir = `.claude/agents`;
1720
+ function syncAgents(config, opts) {
1359
1721
  const results = [];
1360
- if (!existsSync4(srcDir)) return results;
1361
- const CORE_AGENTS = ["orchestrator", "planner", "researcher", "implementer", "reviewer", "tester", "security-auditor", "devops"];
1362
- const SHAPE_AGENTS = {
1363
- web: ["frontend"],
1364
- fullstack: ["frontend", "backend"],
1365
- backend: ["backend"],
1366
- mobile: ["mobile"],
1367
- library: ["backend"]
1368
- };
1369
- const agentsToInstall = new Set(CORE_AGENTS);
1370
- for (const shape of shapes) {
1371
- for (const agent of SHAPE_AGENTS[shape] ?? []) {
1372
- agentsToInstall.add(agent);
1373
- }
1722
+ results.push(...syncAgentTier("tier1", config.agents?.tier1 ?? "all", opts));
1723
+ results.push(...syncAgentTier("tier2", resolveTier2Set(config), opts));
1724
+ return results;
1725
+ }
1726
+ function resolveTier2Set(config) {
1727
+ if (config.agents?.tier2 === "all") return "all";
1728
+ const set = new Set(
1729
+ Array.isArray(config.agents?.tier2) ? config.agents.tier2 : []
1730
+ );
1731
+ for (const agent of resolveShapeAgents(config.shape)) {
1732
+ set.add(agent);
1374
1733
  }
1375
- if (!dryRun) mkdirSync2(destDir, { recursive: true });
1734
+ return Array.from(set);
1735
+ }
1736
+ function syncAgentTier(tier, selection, opts) {
1737
+ const srcDir = join2(CATALOG_ROOT, "agents", tier);
1738
+ const destDir = ".claude/agents";
1739
+ const results = [];
1740
+ if (!existsSync5(srcDir)) return results;
1741
+ const allAgents = readdirSync(srcDir).filter((f2) => f2.endsWith(".md")).map((f2) => f2.replace(/\.md$/, ""));
1742
+ const agentsToInstall = selection === "all" ? allAgents : allAgents.filter((a2) => selection.includes(a2));
1743
+ if (!opts.dryRun) mkdirSync2(destDir, { recursive: true });
1376
1744
  for (const agent of agentsToInstall) {
1377
1745
  const src = join2(srcDir, `${agent}.md`);
1378
1746
  const dest = join2(destDir, `${agent}.md`);
1379
- if (!existsSync4(src)) continue;
1380
- if (!dryRun) copyFileSync(src, dest);
1381
- results.push({ path: dest, action: existsSync4(dest) ? "updated" : "created" });
1747
+ results.push(copyAction(src, dest, opts));
1382
1748
  }
1383
1749
  return results;
1384
1750
  }
1385
- function syncCommands(dryRun) {
1751
+ function syncCommands(opts) {
1386
1752
  const srcDir = join2(CATALOG_ROOT, "commands");
1387
1753
  const destDir = `.claude/commands`;
1388
1754
  const results = [];
1389
- if (!existsSync4(srcDir)) return results;
1755
+ if (!existsSync5(srcDir)) return results;
1390
1756
  const files = readdirSync(srcDir).filter((f2) => f2.endsWith(".md"));
1391
- if (!dryRun) mkdirSync2(destDir, { recursive: true });
1757
+ if (!opts.dryRun) mkdirSync2(destDir, { recursive: true });
1392
1758
  for (const file of files) {
1393
- const dest = join2(destDir, file);
1394
- if (!dryRun) copyFileSync(join2(srcDir, file), dest);
1395
- results.push({ path: dest, action: existsSync4(dest) ? "updated" : "created" });
1759
+ results.push(copyAction(join2(srcDir, file), join2(destDir, file), opts));
1396
1760
  }
1397
1761
  return results;
1398
1762
  }
1399
- function syncCI(dryRun) {
1763
+ function syncCI(opts) {
1400
1764
  const srcDir = join2(CATALOG_ROOT, "ci");
1401
1765
  const destDir = `.github/workflows`;
1402
1766
  const results = [];
1403
- if (!existsSync4(srcDir)) return results;
1767
+ if (!existsSync5(srcDir)) return results;
1404
1768
  const files = readdirSync(srcDir).filter((f2) => f2.endsWith(".yml"));
1405
- if (!dryRun) mkdirSync2(destDir, { recursive: true });
1769
+ if (!opts.dryRun) mkdirSync2(destDir, { recursive: true });
1406
1770
  for (const file of files) {
1407
- const dest = join2(destDir, file);
1408
- if (!dryRun) copyFileSync(join2(srcDir, file), dest);
1409
- results.push({ path: dest, action: existsSync4(dest) ? "updated" : "created" });
1771
+ results.push(copyAction(join2(srcDir, file), join2(destDir, file), opts));
1410
1772
  }
1411
1773
  return results;
1412
1774
  }
1413
- function syncDir(catalogSubdir, destDir, dryRun, extensions = [".md", ".json", ".yml", ".yaml"]) {
1775
+ function syncDir(catalogSubdir, destDir, opts, extensions = [".md", ".json", ".yml", ".yaml"]) {
1414
1776
  const srcDir = join2(CATALOG_ROOT, catalogSubdir);
1415
1777
  const results = [];
1416
- if (!existsSync4(srcDir)) return results;
1778
+ if (!existsSync5(srcDir)) return results;
1417
1779
  const files = readdirSync(srcDir).filter(
1418
- (f2) => extensions.some((ext) => f2.endsWith(ext))
1780
+ (f2) => extensions.length === 0 || extensions.some((ext) => f2.endsWith(ext))
1419
1781
  );
1420
- if (!dryRun) mkdirSync2(destDir, { recursive: true });
1782
+ if (!opts.dryRun) mkdirSync2(destDir, { recursive: true });
1421
1783
  for (const file of files) {
1422
- const dest = join2(destDir, file);
1423
- if (!dryRun) copyFileSync(join2(srcDir, file), dest);
1424
- results.push({ path: dest, action: existsSync4(dest) ? "updated" : "created" });
1784
+ results.push(copyAction(join2(srcDir, file), join2(destDir, file), opts));
1425
1785
  }
1426
1786
  return results;
1427
1787
  }
1428
1788
  function readCatalogFile(rel) {
1429
1789
  return readFileSync5(join2(CATALOG_ROOT, rel), "utf8");
1430
1790
  }
1431
- var CATALOG_ROOT;
1432
1791
  var init_sync = __esm({
1433
1792
  "src/commands/sync.ts"() {
1434
1793
  "use strict";
@@ -1437,7 +1796,11 @@ var init_sync = __esm({
1437
1796
  init_safeWrite();
1438
1797
  init_gitignore();
1439
1798
  init_catalog();
1440
- CATALOG_ROOT = join2(dirname3(fileURLToPath2(import.meta.url)), "..", "catalog");
1799
+ init_shape_agents();
1800
+ init_dialects();
1801
+ init_template();
1802
+ init_markers();
1803
+ init_conflict();
1441
1804
  }
1442
1805
  });
1443
1806
 
@@ -1446,7 +1809,7 @@ var init_exports = {};
1446
1809
  __export(init_exports, {
1447
1810
  runInit: () => runInit
1448
1811
  });
1449
- import { basename } from "path";
1812
+ import { basename as basename3 } from "path";
1450
1813
  async function runInit(argv, headless) {
1451
1814
  const dryRun = argv["dry-run"];
1452
1815
  if (!argv["skip-git-check"] && !headless && isGitRepo() && isDirtyTree()) {
@@ -1467,13 +1830,13 @@ async function runInit(argv, headless) {
1467
1830
  }
1468
1831
  let config;
1469
1832
  if (headless || argv.yes) {
1470
- const projectName = basename(process.cwd());
1833
+ const projectName = basename3(process.cwd());
1471
1834
  const tools = parseTools(argv.tools);
1472
1835
  const scope = argv.preset ?? "standard";
1473
1836
  const integrations = defaultIntegrationsForScope(scope);
1474
- config = buildDefaultConfig(projectName, "", tools, scope, integrations, []);
1837
+ config = buildDefaultConfig(projectName, "", tools, scope, integrations, [], defaultSpecialtyAgents(scope));
1475
1838
  } else {
1476
- const answers = await runWizard(basename(process.cwd()));
1839
+ const answers = await runWizard(basename3(process.cwd()));
1477
1840
  const defaultIntegrations = defaultIntegrationsForScope(answers.scope);
1478
1841
  const integrations = answers.integrations.length > 0 ? answers.integrations : defaultIntegrations;
1479
1842
  config = buildDefaultConfig(
@@ -1482,7 +1845,8 @@ async function runInit(argv, headless) {
1482
1845
  answers.tools,
1483
1846
  answers.scope,
1484
1847
  integrations,
1485
- answers.shape
1848
+ answers.shape,
1849
+ answers.specialtyAgents
1486
1850
  );
1487
1851
  }
1488
1852
  writeConfig(config, argv.config ?? ".aikitrc.json", dryRun);
@@ -1500,7 +1864,9 @@ function defaultIntegrationsForScope(scope) {
1500
1864
  if (scope === "standard") return ["mcp", "hooks", "commands", "subagents", "ci", "husky"];
1501
1865
  return ["mcp", "hooks", "commands", "subagents", "ci", "husky", "devcontainer", "plugin", "otel"];
1502
1866
  }
1503
- function buildDefaultConfig(projectName, projectDescription, tools, scope, integrations, shape) {
1867
+ function buildDefaultConfig(projectName, projectDescription, tools, scope, integrations, shape, specialtyAgents) {
1868
+ const allSpecialtyValues = new Set(SPECIALTY_TIER2_AGENTS.map((a2) => a2.value));
1869
+ const agentTier2 = specialtyAgents.length === allSpecialtyValues.size && specialtyAgents.every((a2) => allSpecialtyValues.has(a2)) ? "all" : specialtyAgents;
1504
1870
  return {
1505
1871
  version: 1,
1506
1872
  projectName,
@@ -1520,6 +1886,7 @@ function buildDefaultConfig(projectName, projectDescription, tools, scope, integ
1520
1886
  otel: integrations.includes("otel")
1521
1887
  },
1522
1888
  skills: { tier1: "all", tier2: "all", tier3: [] },
1889
+ agents: { tier1: "all", tier2: agentTier2, tier3: [] },
1523
1890
  canonical: "AGENTS.md"
1524
1891
  };
1525
1892
  }
@@ -1538,9 +1905,8 @@ var diff_exports = {};
1538
1905
  __export(diff_exports, {
1539
1906
  runDiff: () => runDiff
1540
1907
  });
1541
- import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync as readdirSync2 } from "fs";
1542
- import { dirname as dirname4, join as join3 } from "path";
1543
- import { fileURLToPath as fileURLToPath3 } from "url";
1908
+ import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync2 } from "fs";
1909
+ import { join as join3 } from "path";
1544
1910
  async function runDiff(argv) {
1545
1911
  const config = readConfig(argv.config);
1546
1912
  if (!config) {
@@ -1550,7 +1916,7 @@ async function runDiff(argv) {
1550
1916
  const catalog = loadCatalog();
1551
1917
  const drifted = [];
1552
1918
  const missing = [];
1553
- if (existsSync5("AGENTS.md")) {
1919
+ if (existsSync6("AGENTS.md")) {
1554
1920
  const current = readFileSync6("AGENTS.md", "utf8");
1555
1921
  const freshContent = catalog.agentsMd({
1556
1922
  projectName: config.projectName,
@@ -1568,8 +1934,30 @@ async function runDiff(argv) {
1568
1934
  }
1569
1935
  checkCatalogDir("skills/tier1", ".claude/skills", missing, drifted);
1570
1936
  checkCatalogDir("skills/tier2", ".claude/skills", missing, drifted);
1571
- checkCatalogDir("agents", ".claude/agents", missing, drifted);
1937
+ checkCatalogDir("agents/tier1", ".claude/agents", missing, drifted);
1938
+ checkCatalogAgentsTier2(config, missing, drifted);
1572
1939
  checkCatalogDir("hooks", ".claude/hooks", missing, drifted, [".sh"]);
1940
+ if (config.tools.includes("claude") && config.scope !== "minimal") {
1941
+ checkCatalogDir("rules/claude-rules", ".claude/rules", missing, drifted);
1942
+ const refPath = "docs/claude-md-reference.md";
1943
+ if (!existsSync6(refPath)) {
1944
+ missing.push(refPath);
1945
+ } else {
1946
+ const fresh = catalog.claudeMdReference();
1947
+ if (readFileSync6(refPath, "utf8") !== fresh) {
1948
+ drifted.push(refPath);
1949
+ }
1950
+ }
1951
+ const rulesPath = ".claude/aikit-rules.json";
1952
+ if (!existsSync6(rulesPath)) {
1953
+ missing.push(rulesPath);
1954
+ } else {
1955
+ const fresh = catalog.aikitRulesJson();
1956
+ if (readFileSync6(rulesPath, "utf8") !== fresh) {
1957
+ drifted.push(rulesPath);
1958
+ }
1959
+ }
1960
+ }
1573
1961
  if (missing.length === 0 && drifted.length === 0) {
1574
1962
  process.stdout.write(kleur_default.green("\u2713 No drift \u2014 project is in sync with catalog.\n"));
1575
1963
  return;
@@ -1596,15 +1984,43 @@ function extractRegion(content, begin, end) {
1596
1984
  if (bi === -1 || ei === -1) return "";
1597
1985
  return content.slice(bi + begin.length, ei).trim();
1598
1986
  }
1987
+ function checkCatalogAgentsTier2(config, missing, drifted) {
1988
+ const tier2Selection = config.agents?.tier2;
1989
+ if (tier2Selection === "all") {
1990
+ checkCatalogDir("agents/tier2", ".claude/agents", missing, drifted);
1991
+ return;
1992
+ }
1993
+ const expected = new Set(
1994
+ Array.isArray(tier2Selection) ? tier2Selection : []
1995
+ );
1996
+ for (const agent of resolveShapeAgents(config.shape)) {
1997
+ expected.add(agent);
1998
+ }
1999
+ if (expected.size === 0) return;
2000
+ const tier2Dir = join3(CATALOG_ROOT, "agents", "tier2");
2001
+ if (!existsSync6(tier2Dir)) return;
2002
+ for (const name of expected) {
2003
+ const catalogPath = join3(tier2Dir, `${name}.md`);
2004
+ const installedPath = join3(".claude/agents", `${name}.md`);
2005
+ if (!existsSync6(catalogPath)) continue;
2006
+ if (!existsSync6(installedPath)) {
2007
+ missing.push(`agents/${name}.md`);
2008
+ continue;
2009
+ }
2010
+ if (readFileSync6(catalogPath, "utf8") !== readFileSync6(installedPath, "utf8")) {
2011
+ drifted.push(`agents/${name}.md`);
2012
+ }
2013
+ }
2014
+ }
1599
2015
  function checkCatalogDir(catalogSubdir, installedDir, missing, drifted, extensions = [".md"]) {
1600
- const catalogDir = join3(CATALOG_ROOT2, catalogSubdir);
1601
- if (!existsSync5(catalogDir)) return;
2016
+ const catalogDir = join3(CATALOG_ROOT, catalogSubdir);
2017
+ if (!existsSync6(catalogDir)) return;
1602
2018
  const files = readdirSync2(catalogDir).filter(
1603
2019
  (f2) => extensions.some((ext) => f2.endsWith(ext))
1604
2020
  );
1605
2021
  for (const file of files) {
1606
2022
  const installedPath = join3(installedDir, file);
1607
- if (!existsSync5(installedPath)) {
2023
+ if (!existsSync6(installedPath)) {
1608
2024
  missing.push(`${installedDir}/${file}`);
1609
2025
  continue;
1610
2026
  }
@@ -1615,14 +2031,13 @@ function checkCatalogDir(catalogSubdir, installedDir, missing, drifted, extensio
1615
2031
  }
1616
2032
  }
1617
2033
  }
1618
- var CATALOG_ROOT2;
1619
- var init_diff = __esm({
2034
+ var init_diff2 = __esm({
1620
2035
  "src/commands/diff.ts"() {
1621
2036
  "use strict";
1622
2037
  init_kleur();
1623
2038
  init_readConfig();
1624
2039
  init_catalog();
1625
- CATALOG_ROOT2 = join3(dirname4(fileURLToPath3(import.meta.url)), "..", "catalog");
2040
+ init_shape_agents();
1626
2041
  }
1627
2042
  });
1628
2043
 
@@ -1637,13 +2052,20 @@ async function runUpdate(argv) {
1637
2052
  v2.error(".aikitrc.json not found. Run `aikit` to initialise.");
1638
2053
  process.exit(1);
1639
2054
  }
1640
- const { runDiff: runDiff2 } = await Promise.resolve().then(() => (init_diff(), diff_exports));
2055
+ const { runDiff: runDiff2 } = await Promise.resolve().then(() => (init_diff2(), diff_exports));
1641
2056
  await runDiff2({ ...argv, "dry-run": true });
1642
2057
  if (argv.yes) {
1643
2058
  const { runSync: runSync3 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
1644
2059
  await runSync3(argv);
1645
2060
  return;
1646
2061
  }
2062
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
2063
+ process.stderr.write(
2064
+ kleur_default.yellow("aikit update is interactive. ") + `In a non-TTY context (CI, pipes, scripts), run ${kleur_default.cyan("aikit update --yes")} or ${kleur_default.cyan("aikit sync --force")} instead.
2065
+ `
2066
+ );
2067
+ process.exit(1);
2068
+ }
1647
2069
  const proceed = await me({
1648
2070
  message: "Apply these updates?",
1649
2071
  initialValue: true
@@ -1659,6 +2081,7 @@ var init_update = __esm({
1659
2081
  "src/commands/update.ts"() {
1660
2082
  "use strict";
1661
2083
  init_dist2();
2084
+ init_kleur();
1662
2085
  init_readConfig();
1663
2086
  }
1664
2087
  });
@@ -1668,74 +2091,196 @@ var add_exports = {};
1668
2091
  __export(add_exports, {
1669
2092
  runAdd: () => runAdd
1670
2093
  });
1671
- import { copyFileSync as copyFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync3 } from "fs";
1672
- import { dirname as dirname5, join as join4 } from "path";
1673
- import { fileURLToPath as fileURLToPath4 } from "url";
2094
+ import { copyFileSync as copyFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync3 } from "fs";
2095
+ import { join as join4 } from "path";
1674
2096
  async function runAdd(argv) {
1675
2097
  const itemArg = argv._[1];
1676
2098
  if (!itemArg) {
1677
2099
  v2.error("Usage: aikit add <name> (e.g. aikit add brainstorming)");
1678
2100
  process.exit(1);
1679
2101
  }
1680
- for (const [_type, spec] of Object.entries(ITEM_DIRS)) {
1681
- for (const tier of ["tier1", "tier2", ""]) {
1682
- const catalogDir = join4(CATALOG_ROOT3, tier ? `skills/${tier}` : spec.catalog);
1683
- if (!existsSync6(catalogDir)) continue;
1684
- for (const ext of spec.ext) {
1685
- const srcFile = join4(catalogDir, `${itemArg}${ext}`);
1686
- if (existsSync6(srcFile)) {
1687
- const dest = spec.dest;
1688
- const destFile = join4(dest, `${itemArg}${ext}`);
1689
- if (existsSync6(destFile) && !argv.force) {
1690
- v2.warn(`${destFile} already exists. Use --force to overwrite.`);
1691
- return;
1692
- }
1693
- if (!argv["dry-run"]) {
1694
- mkdirSync3(dest, { recursive: true });
1695
- copyFileSync2(srcFile, destFile);
1696
- }
1697
- process.stdout.write(
1698
- `${argv["dry-run"] ? "[dry-run] " : ""}${kleur_default.green("\u2713")} Added ${destFile}
1699
- `
1700
- );
1701
- return;
1702
- }
1703
- }
2102
+ const found = findCatalogItem(itemArg);
2103
+ if (!found) {
2104
+ v2.error(`"${itemArg}" not found in catalog.`);
2105
+ const similar = listAllCatalogItems().filter(
2106
+ (n) => n.includes(itemArg) || itemArg.includes(n.split("-")[0] ?? "")
2107
+ );
2108
+ if (similar.length > 0) {
2109
+ process.stdout.write(`Did you mean: ${similar.slice(0, 5).join(", ")}?
2110
+ `);
1704
2111
  }
1705
- }
1706
- const allItems = listAllCatalogItems();
1707
- const similar = allItems.filter((n) => n.includes(itemArg) || itemArg.includes(n.split("-")[0] ?? ""));
1708
- v2.error(`"${itemArg}" not found in catalog.`);
1709
- if (similar.length > 0) {
1710
- process.stdout.write(`Did you mean: ${similar.slice(0, 5).join(", ")}?
2112
+ process.stdout.write(`Run ${kleur_default.cyan("aikit list")} to see all available items.
1711
2113
  `);
2114
+ process.exit(1);
2115
+ }
2116
+ if (existsSync7(found.destFile) && !argv.force) {
2117
+ v2.warn(`${found.destFile} already exists. Use --force to overwrite.`);
2118
+ return;
1712
2119
  }
1713
- process.stdout.write(`Run ${kleur_default.cyan("aikit list")} to see all available items.
2120
+ if (!argv["dry-run"]) {
2121
+ mkdirSync3(found.destDir, { recursive: true });
2122
+ copyFileSync2(found.srcFile, found.destFile);
2123
+ }
2124
+ const dryPrefix = argv["dry-run"] ? "[dry-run] " : "";
2125
+ process.stdout.write(`${dryPrefix}${kleur_default.green("\u2713")} Added ${found.destFile}
1714
2126
  `);
1715
- process.exit(1);
2127
+ if (!argv["dry-run"]) {
2128
+ const configChange = updateConfigForAddition(argv.config, found);
2129
+ if (configChange) {
2130
+ process.stdout.write(` ${kleur_default.dim(configChange)}
2131
+ `);
2132
+ }
2133
+ }
2134
+ }
2135
+ function findCatalogItem(name) {
2136
+ for (const tier of ["tier1", "tier2"]) {
2137
+ const dir = join4(CATALOG_ROOT, "skills", tier);
2138
+ if (existsSync7(dir)) {
2139
+ const candidate = join4(dir, `${name}.md`);
2140
+ if (existsSync7(candidate)) {
2141
+ return {
2142
+ type: "skill",
2143
+ name,
2144
+ srcFile: candidate,
2145
+ destFile: join4(".claude/skills", `${name}.md`),
2146
+ destDir: ".claude/skills"
2147
+ };
2148
+ }
2149
+ }
2150
+ }
2151
+ for (const tier of ["tier1", "tier2"]) {
2152
+ const dir = join4(CATALOG_ROOT, "agents", tier);
2153
+ if (existsSync7(dir)) {
2154
+ const candidate = join4(dir, `${name}.md`);
2155
+ if (existsSync7(candidate)) {
2156
+ return {
2157
+ type: "agent",
2158
+ name,
2159
+ srcFile: candidate,
2160
+ destFile: join4(".claude/agents", `${name}.md`),
2161
+ destDir: ".claude/agents"
2162
+ };
2163
+ }
2164
+ }
2165
+ }
2166
+ for (const type of ["hook"]) {
2167
+ const spec = ITEM_DIRS[type];
2168
+ const catalogDir = join4(CATALOG_ROOT, spec.catalog);
2169
+ if (!existsSync7(catalogDir)) continue;
2170
+ for (const ext of spec.ext) {
2171
+ const candidate = join4(catalogDir, `${name}${ext}`);
2172
+ if (existsSync7(candidate)) {
2173
+ return {
2174
+ type,
2175
+ name,
2176
+ srcFile: candidate,
2177
+ destFile: join4(spec.dest, `${name}${ext}`),
2178
+ destDir: spec.dest
2179
+ };
2180
+ }
2181
+ }
2182
+ }
2183
+ return null;
2184
+ }
2185
+ function updateConfigForAddition(configPath, item) {
2186
+ const config = readConfig(configPath);
2187
+ if (!config) return null;
2188
+ let updated = null;
2189
+ let message = "";
2190
+ if (item.type === "agent") {
2191
+ const shape = AGENT_TO_SHAPE[item.name];
2192
+ if (shape && !config.shape.includes(shape)) {
2193
+ updated = { ...config, shape: [...config.shape, shape] };
2194
+ message = `Added "${shape}" to .aikitrc.json shape (so sync re-installs ${item.name})`;
2195
+ }
2196
+ const agentTier = detectAgentTier(item.name);
2197
+ if (agentTier === "tier2" && !shape) {
2198
+ const currentAgents = config.agents ?? { tier1: "all", tier2: "all", tier3: [] };
2199
+ const tier2 = currentAgents.tier2;
2200
+ if (tier2 !== "all" && !tier2.includes(item.name)) {
2201
+ updated = {
2202
+ ...updated ?? config,
2203
+ agents: { ...currentAgents, tier2: [...tier2, item.name] }
2204
+ };
2205
+ message = `Added "${item.name}" to .aikitrc.json agents.tier2`;
2206
+ }
2207
+ }
2208
+ if (agentTier === "tier3" && !(config.agents?.tier3 ?? []).includes(item.name)) {
2209
+ const currentAgents = config.agents ?? { tier1: "all", tier2: "all", tier3: [] };
2210
+ updated = {
2211
+ ...updated ?? config,
2212
+ agents: { ...currentAgents, tier3: [...currentAgents.tier3, item.name] }
2213
+ };
2214
+ message = `Added "${item.name}" to .aikitrc.json agents.tier3`;
2215
+ }
2216
+ } else if (item.type === "skill") {
2217
+ const skillTier = detectSkillTier(item.name);
2218
+ if (skillTier === "tier3" && !config.skills.tier3.includes(item.name)) {
2219
+ updated = {
2220
+ ...config,
2221
+ skills: { ...config.skills, tier3: [...config.skills.tier3, item.name] }
2222
+ };
2223
+ message = `Added "${item.name}" to .aikitrc.json skills.tier3`;
2224
+ }
2225
+ } else if (item.type === "hook") {
2226
+ if (!config.integrations.hooks) {
2227
+ updated = {
2228
+ ...config,
2229
+ integrations: { ...config.integrations, hooks: true }
2230
+ };
2231
+ message = "Set integrations.hooks=true in .aikitrc.json";
2232
+ }
2233
+ }
2234
+ if (updated) {
2235
+ writeConfig(updated, configPath);
2236
+ return message;
2237
+ }
2238
+ return null;
2239
+ }
2240
+ function detectSkillTier(name) {
2241
+ for (const tier of ["tier1", "tier2"]) {
2242
+ if (existsSync7(join4(CATALOG_ROOT, "skills", tier, `${name}.md`))) {
2243
+ return tier;
2244
+ }
2245
+ }
2246
+ return "tier3";
2247
+ }
2248
+ function detectAgentTier(name) {
2249
+ for (const tier of ["tier1", "tier2"]) {
2250
+ if (existsSync7(join4(CATALOG_ROOT, "agents", tier, `${name}.md`))) {
2251
+ return tier;
2252
+ }
2253
+ }
2254
+ return "tier3";
1716
2255
  }
1717
2256
  function listAllCatalogItems() {
1718
2257
  const items = [];
1719
- for (const subdir of ["skills/tier1", "skills/tier2", "agents", "hooks"]) {
1720
- const dir = join4(CATALOG_ROOT3, subdir);
1721
- if (existsSync6(dir)) {
2258
+ for (const subdir of ["skills/tier1", "skills/tier2", "agents/tier1", "agents/tier2", "hooks"]) {
2259
+ const dir = join4(CATALOG_ROOT, subdir);
2260
+ if (existsSync7(dir)) {
1722
2261
  readdirSync3(dir).filter((f2) => f2.endsWith(".md") || f2.endsWith(".sh")).forEach((f2) => items.push(f2.replace(/\.(md|sh)$/, "")));
1723
2262
  }
1724
2263
  }
1725
2264
  return items;
1726
2265
  }
1727
- var CATALOG_ROOT3, ITEM_DIRS;
2266
+ var ITEM_DIRS, AGENT_TO_SHAPE;
1728
2267
  var init_add = __esm({
1729
2268
  "src/commands/add.ts"() {
1730
2269
  "use strict";
1731
2270
  init_dist2();
1732
2271
  init_kleur();
1733
- CATALOG_ROOT3 = join4(dirname5(fileURLToPath4(import.meta.url)), "..", "catalog");
2272
+ init_catalog();
2273
+ init_readConfig();
1734
2274
  ITEM_DIRS = {
1735
2275
  skill: { catalog: "skills/tier1", dest: ".claude/skills", ext: [".md"] },
1736
- agent: { catalog: "agents", dest: ".claude/agents", ext: [".md"] },
2276
+ agent: { catalog: "agents/tier1", dest: ".claude/agents", ext: [".md"] },
1737
2277
  hook: { catalog: "hooks", dest: ".claude/hooks", ext: [".sh"] }
1738
2278
  };
2279
+ AGENT_TO_SHAPE = {
2280
+ frontend: "web",
2281
+ backend: "backend",
2282
+ mobile: "mobile"
2283
+ };
1739
2284
  }
1740
2285
  });
1741
2286
 
@@ -1744,16 +2289,17 @@ var list_exports = {};
1744
2289
  __export(list_exports, {
1745
2290
  runList: () => runList
1746
2291
  });
1747
- import { existsSync as existsSync7, readdirSync as readdirSync4 } from "fs";
1748
- import { dirname as dirname6, join as join5 } from "path";
1749
- import { fileURLToPath as fileURLToPath5 } from "url";
2292
+ import { existsSync as existsSync8, readdirSync as readdirSync4 } from "fs";
2293
+ import { join as join5 } from "path";
1750
2294
  async function runList(_argv) {
1751
2295
  const sections = [
1752
2296
  { label: "Skills \u2014 Tier 1", items: listCategory("skills/tier1", ".claude/skills") },
1753
2297
  { label: "Skills \u2014 Tier 2", items: listCategory("skills/tier2", ".claude/skills") },
2298
+ { label: "Agents \u2014 Tier 1", items: listCategory("agents/tier1", ".claude/agents") },
2299
+ { label: "Agents \u2014 Tier 2", items: listCategory("agents/tier2", ".claude/agents") },
1754
2300
  { label: "Slash commands", items: listCategory("commands", ".claude/commands") },
1755
- { label: "Agents", items: listCategory("agents", ".claude/agents") },
1756
- { label: "Hooks", items: listCategory("hooks", ".claude/hooks", [".sh"]) }
2301
+ { label: "Hooks", items: listCategory("hooks", ".claude/hooks", [".sh"]) },
2302
+ { label: "Path-scoped rules", items: listClaudeRules() }
1757
2303
  ];
1758
2304
  let total = 0;
1759
2305
  let installed = 0;
@@ -1782,19 +2328,52 @@ ${installed}/${total} catalog items installed.
1782
2328
  }
1783
2329
  }
1784
2330
  function listCategory(catalogSubdir, installedDir, extensions = [".md"]) {
1785
- const catalogDir = join5(CATALOG_ROOT4, catalogSubdir);
1786
- if (!existsSync7(catalogDir)) return [];
2331
+ const catalogDir = join5(CATALOG_ROOT, catalogSubdir);
2332
+ if (!existsSync8(catalogDir)) return [];
1787
2333
  const installedFiles = new Set(
1788
- existsSync7(installedDir) ? readdirSync4(installedDir).filter((f2) => extensions.some((ext) => f2.endsWith(ext))) : []
2334
+ existsSync8(installedDir) ? readdirSync4(installedDir).filter((f2) => extensions.some((ext) => f2.endsWith(ext))) : []
1789
2335
  );
1790
- return readdirSync4(catalogDir).filter((f2) => extensions.some((ext) => f2.endsWith(ext))).map((f2) => ({ name: f2.replace(/\.md$/, ""), installed: installedFiles.has(f2) }));
2336
+ return readdirSync4(catalogDir).filter((f2) => extensions.some((ext) => f2.endsWith(ext))).map((f2) => ({
2337
+ name: stripExtension(f2, extensions),
2338
+ installed: installedFiles.has(f2)
2339
+ }));
2340
+ }
2341
+ function listClaudeRules() {
2342
+ const catalogDir = join5(CATALOG_ROOT, "rules", "claude-rules");
2343
+ if (!existsSync8(catalogDir)) return [];
2344
+ const installedDir = ".claude/rules";
2345
+ const installedFiles = new Set(
2346
+ existsSync8(installedDir) ? readdirSync4(installedDir).filter((f2) => f2.endsWith(".md")) : []
2347
+ );
2348
+ return readdirSync4(catalogDir).filter((f2) => f2.endsWith(".md")).map((f2) => ({ name: stripExtension(f2, [".md"]), installed: installedFiles.has(f2) }));
2349
+ }
2350
+ function stripExtension(filename, extensions) {
2351
+ for (const ext of extensions) {
2352
+ if (filename.endsWith(ext)) return filename.slice(0, -ext.length);
2353
+ }
2354
+ return filename;
1791
2355
  }
1792
- var CATALOG_ROOT4;
1793
2356
  var init_list = __esm({
1794
2357
  "src/commands/list.ts"() {
1795
2358
  "use strict";
1796
2359
  init_kleur();
1797
- CATALOG_ROOT4 = join5(dirname6(fileURLToPath5(import.meta.url)), "..", "catalog");
2360
+ init_catalog();
2361
+ }
2362
+ });
2363
+
2364
+ // src/render/rule-ids.ts
2365
+ function extractRuleIds(content) {
2366
+ const ids = [];
2367
+ for (const m3 of content.matchAll(RULE_ID_REGEX)) {
2368
+ if (m3[1]) ids.push(m3[1]);
2369
+ }
2370
+ return ids;
2371
+ }
2372
+ var RULE_ID_REGEX;
2373
+ var init_rule_ids = __esm({
2374
+ "src/render/rule-ids.ts"() {
2375
+ "use strict";
2376
+ RULE_ID_REGEX = /<!--\s*id:\s*([a-zA-Z][a-zA-Z0-9_-]*\.[a-zA-Z0-9._-]+)(?:\s+[a-zA-Z][a-zA-Z0-9_-]*=[^>\s]+)*\s*-->/g;
1798
2377
  }
1799
2378
  });
1800
2379
 
@@ -1803,9 +2382,13 @@ var doctor_exports = {};
1803
2382
  __export(doctor_exports, {
1804
2383
  runDoctor: () => runDoctor
1805
2384
  });
1806
- import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "fs";
2385
+ import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "fs";
1807
2386
  import { join as join6 } from "path";
1808
2387
  async function runDoctor(argv) {
2388
+ if (argv.rules) {
2389
+ runRulesReport(argv.format === "json" ? "json" : "text");
2390
+ return;
2391
+ }
1809
2392
  const findings = [];
1810
2393
  const config = readConfig(argv.config);
1811
2394
  if (!config) {
@@ -1814,7 +2397,7 @@ async function runDoctor(argv) {
1814
2397
  return;
1815
2398
  }
1816
2399
  findings.push({ level: "ok", check: ".aikitrc.json", message: `version ${config.version}, scope: ${config.scope}` });
1817
- if (existsSync8("AGENTS.md")) {
2400
+ if (existsSync9("AGENTS.md")) {
1818
2401
  const content = readFileSync7("AGENTS.md", "utf8");
1819
2402
  if (content.includes("<!-- BEGIN:haac-aikit -->")) {
1820
2403
  const lines = content.split("\n").length;
@@ -1840,14 +2423,14 @@ async function runDoctor(argv) {
1840
2423
  for (const tool of config.tools) {
1841
2424
  const file = toolChecks[tool];
1842
2425
  if (!file) continue;
1843
- if (existsSync8(file)) {
2426
+ if (existsSync9(file)) {
1844
2427
  findings.push({ level: "ok", check: `${tool} shim`, message: file });
1845
2428
  } else {
1846
2429
  findings.push({ level: "warn", check: `${tool} shim`, message: `${file} missing \u2014 run \`aikit sync\`` });
1847
2430
  }
1848
2431
  }
1849
2432
  if (config.tools.includes("claude")) {
1850
- if (existsSync8(".claude/settings.json")) {
2433
+ if (existsSync9(".claude/settings.json")) {
1851
2434
  findings.push({ level: "ok", check: "settings.json", message: ".claude/settings.json present" });
1852
2435
  } else {
1853
2436
  findings.push({ level: "warn", check: "settings.json", message: "Missing \u2014 run `aikit sync`" });
@@ -1855,7 +2438,7 @@ async function runDoctor(argv) {
1855
2438
  }
1856
2439
  if (config.scope !== "minimal") {
1857
2440
  const skillsDir2 = ".claude/skills";
1858
- if (existsSync8(skillsDir2)) {
2441
+ if (existsSync9(skillsDir2)) {
1859
2442
  const count = readdirSync5(skillsDir2).filter((f2) => f2.endsWith(".md")).length;
1860
2443
  findings.push({ level: "ok", check: "skills", message: `${count} installed` });
1861
2444
  } else {
@@ -1863,7 +2446,7 @@ async function runDoctor(argv) {
1863
2446
  }
1864
2447
  }
1865
2448
  const skillsDir = ".claude/skills";
1866
- if (existsSync8(skillsDir)) {
2449
+ if (existsSync9(skillsDir)) {
1867
2450
  const skills = readdirSync5(skillsDir).filter((f2) => f2.endsWith(".md"));
1868
2451
  let descErrors = 0;
1869
2452
  for (const skill of skills) {
@@ -1882,14 +2465,37 @@ async function runDoctor(argv) {
1882
2465
  findings.push({ level: "ok", check: "skill descriptions", message: "All within 600-char limit" });
1883
2466
  }
1884
2467
  }
2468
+ if (config.tools.includes("claude") && config.scope !== "minimal") {
2469
+ for (const path of [
2470
+ ".claude/aikit-rules.json",
2471
+ "docs/claude-md-reference.md",
2472
+ ".claude/rules/example.md"
2473
+ ]) {
2474
+ if (existsSync9(path)) {
2475
+ findings.push({ level: "ok", check: path, message: "present" });
2476
+ } else {
2477
+ findings.push({ level: "warn", check: path, message: "Missing \u2014 run `aikit sync`" });
2478
+ }
2479
+ }
2480
+ if (config.integrations.hooks) {
2481
+ for (const hook of ["log-rule-event.sh", "check-pattern-violations.sh", "judge-rule-compliance.sh"]) {
2482
+ const path = `.claude/hooks/${hook}`;
2483
+ if (existsSync9(path)) {
2484
+ findings.push({ level: "ok", check: `telemetry:${hook}`, message: "installed" });
2485
+ } else {
2486
+ findings.push({ level: "warn", check: `telemetry:${hook}`, message: "Missing \u2014 run `aikit sync`" });
2487
+ }
2488
+ }
2489
+ }
2490
+ }
1885
2491
  if (config.integrations.hooks) {
1886
- if (existsSync8(".claude/hooks/hooks.json")) {
2492
+ if (existsSync9(".claude/hooks/hooks.json")) {
1887
2493
  findings.push({ level: "ok", check: "hooks", message: "hooks.json present" });
1888
2494
  } else {
1889
2495
  findings.push({ level: "warn", check: "hooks", message: "hooks.json missing \u2014 run `aikit sync`" });
1890
2496
  }
1891
2497
  }
1892
- if (existsSync8(".gitignore")) {
2498
+ if (existsSync9(".gitignore")) {
1893
2499
  const gi = readFileSync7(".gitignore", "utf8");
1894
2500
  if (gi.includes(".env")) {
1895
2501
  findings.push({ level: "ok", check: ".gitignore", message: ".env* entries present" });
@@ -1923,11 +2529,730 @@ function printFindings(findings) {
1923
2529
  `));
1924
2530
  }
1925
2531
  }
2532
+ function runRulesReport(format) {
2533
+ const eventsPath = ".aikit/events.jsonl";
2534
+ if (!existsSync9(eventsPath)) {
2535
+ if (format === "json") {
2536
+ process.stdout.write(
2537
+ JSON.stringify({ status: "no_telemetry", message: ".aikit/events.jsonl not found" }, null, 2) + "\n"
2538
+ );
2539
+ } else {
2540
+ process.stdout.write(
2541
+ kleur_default.yellow("No telemetry yet. ") + "Hooks log to .aikit/events.jsonl on first session.\nRun a Claude Code session in this repo, then re-run `aikit doctor --rules`.\n"
2542
+ );
2543
+ }
2544
+ return;
2545
+ }
2546
+ const { events, malformed } = parseEvents(readFileSync7(eventsPath, "utf8"));
2547
+ if (events.length === 0) {
2548
+ if (format === "json") {
2549
+ process.stdout.write(JSON.stringify({ status: "empty_log", malformed }, null, 2) + "\n");
2550
+ } else {
2551
+ process.stdout.write(kleur_default.yellow("Telemetry log is empty. No rule events recorded yet.\n"));
2552
+ }
2553
+ return;
2554
+ }
2555
+ const known = collectKnownRuleIds();
2556
+ const stats = aggregateStats(events, known);
2557
+ const compileErrors = collectCompileErrors(events);
2558
+ if (format === "json") {
2559
+ process.stdout.write(JSON.stringify(toRulesJson(stats, events.length, malformed, compileErrors), null, 2) + "\n");
2560
+ return;
2561
+ }
2562
+ printRulesReport(stats, events.length, malformed, compileErrors);
2563
+ }
2564
+ function toRulesJson(stats, totalEvents, malformed, compileErrors) {
2565
+ return {
2566
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
2567
+ total_events: totalEvents,
2568
+ malformed_lines: malformed,
2569
+ rules: [...stats.entries()].map(([id, s]) => {
2570
+ const cls = classifyRule(s);
2571
+ return {
2572
+ id,
2573
+ bucket: cls.bucket,
2574
+ loaded: s.loadedCount,
2575
+ cited: s.citedCount,
2576
+ violations: s.violationCount,
2577
+ judged_violations: s.judgedCount,
2578
+ last_seen: s.lastSeen,
2579
+ advice: cls.advice
2580
+ };
2581
+ }),
2582
+ compile_errors: compileErrors.map((ce2) => ({
2583
+ rule_id: ce2.ruleId,
2584
+ pattern: ce2.pattern,
2585
+ error: ce2.error,
2586
+ ts: ce2.ts
2587
+ }))
2588
+ };
2589
+ }
2590
+ function collectCompileErrors(events) {
2591
+ const latestByRule = /* @__PURE__ */ new Map();
2592
+ for (const e2 of events) {
2593
+ if (e2.event !== "rule_compile_error") continue;
2594
+ const existing = latestByRule.get(e2.rule_id);
2595
+ if (!existing || e2.ts > existing.ts) {
2596
+ latestByRule.set(e2.rule_id, {
2597
+ ruleId: e2.rule_id,
2598
+ pattern: e2.pattern ?? "",
2599
+ error: e2.error ?? "(no detail)",
2600
+ ts: e2.ts
2601
+ });
2602
+ }
2603
+ }
2604
+ return [...latestByRule.values()];
2605
+ }
2606
+ function parseEvents(content) {
2607
+ const out = [];
2608
+ let malformed = 0;
2609
+ for (const line of content.split("\n")) {
2610
+ const trimmed = line.trim();
2611
+ if (!trimmed) continue;
2612
+ try {
2613
+ const parsed = JSON.parse(trimmed);
2614
+ if (parsed.rule_id && parsed.event) out.push(parsed);
2615
+ else malformed++;
2616
+ } catch {
2617
+ malformed++;
2618
+ }
2619
+ }
2620
+ return { events: out, malformed };
2621
+ }
2622
+ function collectKnownRuleIds() {
2623
+ const ids = /* @__PURE__ */ new Set();
2624
+ const candidates = ["AGENTS.md", "CLAUDE.md", ".claude/CLAUDE.md"];
2625
+ if (existsSync9(".claude/rules")) {
2626
+ for (const f2 of readdirSync5(".claude/rules")) {
2627
+ if (f2.endsWith(".md")) candidates.push(join6(".claude/rules", f2));
2628
+ }
2629
+ }
2630
+ for (const path of candidates) {
2631
+ if (!existsSync9(path)) continue;
2632
+ for (const id of extractRuleIds(readFileSync7(path, "utf8"))) ids.add(id);
2633
+ }
2634
+ return ids;
2635
+ }
2636
+ function aggregateStats(events, known) {
2637
+ const stats = /* @__PURE__ */ new Map();
2638
+ const seenIds = /* @__PURE__ */ new Set([...known]);
2639
+ for (const e2 of events) {
2640
+ seenIds.add(e2.rule_id);
2641
+ let s = stats.get(e2.rule_id);
2642
+ if (!s) {
2643
+ s = { loadedCount: 0, violationCount: 0, citedCount: 0, judgedCount: 0, lastSeen: e2.ts, firstSeen: e2.ts, sources: /* @__PURE__ */ new Set() };
2644
+ stats.set(e2.rule_id, s);
2645
+ }
2646
+ if (e2.event === "loaded") s.loadedCount++;
2647
+ else if (e2.event === "violation") s.violationCount++;
2648
+ else if (e2.event === "cited") s.citedCount++;
2649
+ else if (e2.event === "judged_violation") s.judgedCount++;
2650
+ if (e2.source) s.sources.add(e2.source);
2651
+ if (e2.file) s.sources.add(e2.file);
2652
+ if (e2.ts > s.lastSeen) s.lastSeen = e2.ts;
2653
+ if (e2.ts < s.firstSeen) s.firstSeen = e2.ts;
2654
+ }
2655
+ for (const id of seenIds) {
2656
+ if (!stats.has(id)) {
2657
+ stats.set(id, {
2658
+ loadedCount: 0,
2659
+ violationCount: 0,
2660
+ citedCount: 0,
2661
+ judgedCount: 0,
2662
+ lastSeen: "",
2663
+ firstSeen: "",
2664
+ sources: /* @__PURE__ */ new Set()
2665
+ });
2666
+ }
2667
+ }
2668
+ return stats;
2669
+ }
2670
+ function classifyRule(s) {
2671
+ const negative = s.violationCount + s.judgedCount;
2672
+ const interactions = s.loadedCount + s.citedCount + negative;
2673
+ if (interactions === 0) {
2674
+ return { bucket: "dead", advice: "Never loaded, cited, or violated \u2014 consider removing or rephrasing." };
2675
+ }
2676
+ if (s.loadedCount === 0 && s.citedCount === 0) {
2677
+ return { bucket: "unused", advice: "Pattern violations recorded but rule not loaded \u2014 check rule file presence." };
2678
+ }
2679
+ if (negative > 0) {
2680
+ const denominator = s.citedCount > 0 ? s.citedCount + negative : Math.max(s.loadedCount, 1);
2681
+ const ratio = negative / denominator;
2682
+ if (ratio > 0.3) {
2683
+ return { bucket: "disputed", advice: "Frequently violated \u2014 strengthen with IMPORTANT/YOU MUST or move to a hook." };
2684
+ }
2685
+ return { bucket: "hot", advice: "Active rule with occasional violations \u2014 keep monitoring." };
2686
+ }
2687
+ return { bucket: "hot", advice: "Loaded and obeyed \u2014 keep." };
2688
+ }
2689
+ function printRulesReport(stats, totalEvents, malformed, compileErrors) {
2690
+ const rows = [];
2691
+ for (const [id, s] of stats) {
2692
+ const cls = classifyRule(s);
2693
+ rows.push({ id, stats: s, bucket: cls.bucket, advice: cls.advice });
2694
+ }
2695
+ const hot = rows.filter((r2) => r2.bucket === "hot");
2696
+ const disputed = rows.filter((r2) => r2.bucket === "disputed");
2697
+ const dead = rows.filter((r2) => r2.bucket === "dead");
2698
+ const unused = rows.filter((r2) => r2.bucket === "unused");
2699
+ process.stdout.write(kleur_default.bold("Rule observability \u2014 ") + kleur_default.dim(`${totalEvents} events across ${rows.length} rules`) + "\n\n");
2700
+ if (hot.length > 0) {
2701
+ process.stdout.write(kleur_default.green("\u2713 Hot rules (working as intended)\n"));
2702
+ for (const r2 of hot) {
2703
+ const v3 = r2.stats.violationCount > 0 ? kleur_default.yellow(` ${r2.stats.violationCount} violations`) : "";
2704
+ process.stdout.write(` ${kleur_default.bold(r2.id)} \u2014 ${r2.stats.loadedCount} loads${v3}
2705
+ `);
2706
+ }
2707
+ process.stdout.write("\n");
2708
+ }
2709
+ if (disputed.length > 0) {
2710
+ process.stdout.write(kleur_default.yellow("\u26A0 Disputed rules (>30% violation rate)\n"));
2711
+ for (const r2 of disputed) {
2712
+ process.stdout.write(` ${kleur_default.bold(r2.id)} \u2014 ${r2.stats.loadedCount} loads, ${r2.stats.violationCount} violations
2713
+ `);
2714
+ process.stdout.write(` ${kleur_default.dim(r2.advice)}
2715
+ `);
2716
+ }
2717
+ process.stdout.write("\n");
2718
+ }
2719
+ if (dead.length > 0) {
2720
+ process.stdout.write(kleur_default.red("\u2717 Dead rules (never observed)\n"));
2721
+ for (const r2 of dead) {
2722
+ process.stdout.write(` ${kleur_default.bold(r2.id)}
2723
+ `);
2724
+ process.stdout.write(` ${kleur_default.dim(r2.advice)}
2725
+ `);
2726
+ }
2727
+ process.stdout.write("\n");
2728
+ }
2729
+ if (unused.length > 0) {
2730
+ process.stdout.write(kleur_default.yellow("\u26A0 Pattern hits without rule load\n"));
2731
+ for (const r2 of unused) {
2732
+ process.stdout.write(` ${kleur_default.bold(r2.id)} \u2014 ${r2.stats.violationCount} pattern hits
2733
+ `);
2734
+ process.stdout.write(` ${kleur_default.dim(r2.advice)}
2735
+ `);
2736
+ }
2737
+ process.stdout.write("\n");
2738
+ }
2739
+ process.stdout.write(kleur_default.dim(`Hot: ${hot.length} \xB7 Disputed: ${disputed.length} \xB7 Dead: ${dead.length} \xB7 Unmatched: ${unused.length}
2740
+ `));
2741
+ if (compileErrors.length > 0) {
2742
+ process.stdout.write(kleur_default.red(`
2743
+ \u2717 Rule pattern compile errors (${compileErrors.length})
2744
+ `));
2745
+ for (const ce2 of compileErrors) {
2746
+ process.stdout.write(` ${kleur_default.bold(ce2.ruleId)} \u2014 ${kleur_default.dim(ce2.error)}
2747
+ `);
2748
+ if (ce2.pattern) process.stdout.write(` pattern: ${kleur_default.dim(ce2.pattern)}
2749
+ `);
2750
+ }
2751
+ process.stdout.write(kleur_default.dim(" Fix these regex patterns in .claude/aikit-rules.json.\n"));
2752
+ }
2753
+ if (malformed > 0) {
2754
+ process.stdout.write(
2755
+ kleur_default.yellow(`
2756
+ \u26A0 ${malformed} malformed line(s) in .aikit/events.jsonl were skipped.
2757
+ `) + kleur_default.dim(" This usually means a hook crashed mid-write. Adherence numbers reflect only the parsed lines.\n")
2758
+ );
2759
+ }
2760
+ }
1926
2761
  var init_doctor = __esm({
1927
2762
  "src/commands/doctor.ts"() {
1928
2763
  "use strict";
1929
2764
  init_kleur();
1930
2765
  init_readConfig();
2766
+ init_rule_ids();
2767
+ }
2768
+ });
2769
+
2770
+ // src/commands/report.ts
2771
+ var report_exports = {};
2772
+ __export(report_exports, {
2773
+ runReport: () => runReport
2774
+ });
2775
+ import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync6 } from "fs";
2776
+ import { join as join7 } from "path";
2777
+ async function runReport(argv) {
2778
+ const format = argv.format ?? "markdown";
2779
+ if (!existsSync10(EVENTS_PATH)) {
2780
+ process.stdout.write(noTelemetryReport(format));
2781
+ return;
2782
+ }
2783
+ const { events, malformed } = parseEvents2(readFileSync8(EVENTS_PATH, "utf8"));
2784
+ if (events.length === 0) {
2785
+ process.stdout.write(noTelemetryReport(format));
2786
+ return;
2787
+ }
2788
+ const since = argv.since ? new Date(argv.since).toISOString() : null;
2789
+ const filtered = since ? events.filter((e2) => e2.ts >= since) : events;
2790
+ const known = collectKnownRuleIds2();
2791
+ const stats = aggregate(filtered, known);
2792
+ if (format === "json") {
2793
+ process.stdout.write(JSON.stringify(toJsonReport(stats, filtered.length, since, malformed), null, 2) + "\n");
2794
+ return;
2795
+ }
2796
+ process.stdout.write(toMarkdownReport(stats, filtered.length, since, malformed));
2797
+ }
2798
+ function parseEvents2(content) {
2799
+ const out = [];
2800
+ let malformed = 0;
2801
+ for (const line of content.split("\n")) {
2802
+ const trimmed = line.trim();
2803
+ if (!trimmed) continue;
2804
+ try {
2805
+ const parsed = JSON.parse(trimmed);
2806
+ if (parsed.rule_id && parsed.event) out.push(parsed);
2807
+ else malformed++;
2808
+ } catch {
2809
+ malformed++;
2810
+ }
2811
+ }
2812
+ return { events: out, malformed };
2813
+ }
2814
+ function collectKnownRuleIds2() {
2815
+ const ids = /* @__PURE__ */ new Set();
2816
+ const candidates = ["AGENTS.md", "CLAUDE.md", ".claude/CLAUDE.md"];
2817
+ if (existsSync10(".claude/rules")) {
2818
+ for (const f2 of readdirSync6(".claude/rules")) {
2819
+ if (f2.endsWith(".md")) candidates.push(join7(".claude/rules", f2));
2820
+ }
2821
+ }
2822
+ for (const path of candidates) {
2823
+ if (!existsSync10(path)) continue;
2824
+ for (const id of extractRuleIds(readFileSync8(path, "utf8"))) ids.add(id);
2825
+ }
2826
+ return ids;
2827
+ }
2828
+ function aggregate(events, known) {
2829
+ const stats = /* @__PURE__ */ new Map();
2830
+ const seen = /* @__PURE__ */ new Set([...known]);
2831
+ for (const e2 of events) {
2832
+ seen.add(e2.rule_id);
2833
+ let s = stats.get(e2.rule_id);
2834
+ if (!s) {
2835
+ s = { loaded: 0, cited: 0, violations: 0, judged: 0, files: /* @__PURE__ */ new Set(), lastSeen: e2.ts };
2836
+ stats.set(e2.rule_id, s);
2837
+ }
2838
+ if (e2.event === "loaded") s.loaded++;
2839
+ else if (e2.event === "cited") s.cited++;
2840
+ else if (e2.event === "violation") s.violations++;
2841
+ else if (e2.event === "judged_violation") s.judged++;
2842
+ if (e2.file) s.files.add(e2.file);
2843
+ if (e2.ts > s.lastSeen) s.lastSeen = e2.ts;
2844
+ }
2845
+ for (const id of seen) {
2846
+ if (!stats.has(id)) {
2847
+ stats.set(id, { loaded: 0, cited: 0, violations: 0, judged: 0, files: /* @__PURE__ */ new Set(), lastSeen: "" });
2848
+ }
2849
+ }
2850
+ return stats;
2851
+ }
2852
+ function adherence(stats) {
2853
+ let observed = 0;
2854
+ let followed = 0;
2855
+ let totalCited = 0;
2856
+ for (const s of stats.values()) {
2857
+ const negative = s.violations + s.judged;
2858
+ const positive = s.cited;
2859
+ totalCited += positive;
2860
+ if (positive + negative === 0) continue;
2861
+ observed++;
2862
+ followed += positive / (positive + negative);
2863
+ }
2864
+ if (totalCited === 0) {
2865
+ return { score: null, observed: 0, basis: "no-evidence" };
2866
+ }
2867
+ return { score: Math.round(followed / observed * 100), observed, basis: "judge" };
2868
+ }
2869
+ function noTelemetryReport(format) {
2870
+ if (format === "json") {
2871
+ return JSON.stringify({ status: "no_telemetry", message: "No .aikit/events.jsonl found yet." }, null, 2) + "\n";
2872
+ }
2873
+ return [
2874
+ "## Rule Observability",
2875
+ "",
2876
+ "_No telemetry yet. Run a Claude Code session in this repo, then re-run the report._",
2877
+ ""
2878
+ ].join("\n");
2879
+ }
2880
+ function toJsonReport(stats, totalEvents, since, malformedLines) {
2881
+ const { score, observed, basis } = adherence(stats);
2882
+ return {
2883
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
2884
+ since,
2885
+ total_events: totalEvents,
2886
+ malformed_lines: malformedLines,
2887
+ rule_count: stats.size,
2888
+ observed_rules: observed,
2889
+ adherence_score: score,
2890
+ adherence_basis: basis,
2891
+ rules: [...stats.entries()].map(([id, s]) => ({
2892
+ id,
2893
+ loaded: s.loaded,
2894
+ cited: s.cited,
2895
+ violations: s.violations,
2896
+ judged_violations: s.judged,
2897
+ files: [...s.files],
2898
+ last_seen: s.lastSeen
2899
+ }))
2900
+ };
2901
+ }
2902
+ function toMarkdownReport(stats, totalEvents, since, malformedLines) {
2903
+ const { score, observed, basis } = adherence(stats);
2904
+ const rows = [...stats.entries()].sort(([, a2], [, b2]) => b2.loaded + b2.violations - (a2.loaded + a2.violations));
2905
+ const hot = rows.filter(([, s]) => s.loaded > 0 && s.violations + s.judged === 0);
2906
+ const disputed = rows.filter(
2907
+ ([, s]) => (s.loaded > 0 || s.cited > 0) && s.violations + s.judged > 0
2908
+ );
2909
+ const unmatched = rows.filter(
2910
+ ([, s]) => s.loaded === 0 && s.cited === 0 && s.violations + s.judged > 0
2911
+ );
2912
+ const dead = rows.filter(([, s]) => s.loaded === 0 && s.cited === 0 && s.violations === 0 && s.judged === 0);
2913
+ const out = [];
2914
+ out.push("## \u{1F52D} Rule Observability");
2915
+ out.push("");
2916
+ if (since) out.push(`_Since: \`${since}\`_`);
2917
+ if (basis === "judge" && score !== null) {
2918
+ out.push(`_Adherence: **${score}%** across ${observed} judge-observed rules \xB7 ${totalEvents} events recorded_`);
2919
+ } else {
2920
+ out.push(`_Adherence: **N/A** \u2014 enable \`AIKIT_JUDGE=1\` for compliance scoring \xB7 ${totalEvents} events recorded_`);
2921
+ }
2922
+ out.push("");
2923
+ if (disputed.length > 0) {
2924
+ out.push("### \u26A0\uFE0F Rules under pressure");
2925
+ out.push("_Declared rules with violations \u2014 strengthen wording or move to a hook._");
2926
+ out.push("");
2927
+ out.push("| Rule | Loads | Cited | Pattern violations | Judged violations |");
2928
+ out.push("|---|---:|---:|---:|---:|");
2929
+ for (const [id, s] of disputed) {
2930
+ out.push(`| \`${id}\` | ${s.loaded} | ${s.cited} | ${s.violations} | ${s.judged} |`);
2931
+ }
2932
+ out.push("");
2933
+ }
2934
+ if (unmatched.length > 0) {
2935
+ out.push("### \u{1F7E1} Pattern hits without a declared rule");
2936
+ out.push(
2937
+ "_Pattern matched in `aikit-rules.json` but no rule with this ID is in your AGENTS.md / `.claude/rules/`. Either declare the rule or remove the pattern config._"
2938
+ );
2939
+ out.push("");
2940
+ out.push("| Rule ID | Pattern hits |");
2941
+ out.push("|---|---:|");
2942
+ for (const [id, s] of unmatched) {
2943
+ out.push(`| \`${id}\` | ${s.violations + s.judged} |`);
2944
+ }
2945
+ out.push("");
2946
+ }
2947
+ if (hot.length > 0) {
2948
+ out.push("### \u2705 Hot rules (loaded, no violations)");
2949
+ out.push("");
2950
+ for (const [id, s] of hot) {
2951
+ out.push(`- \`${id}\` \u2014 ${s.loaded} loads`);
2952
+ }
2953
+ out.push("");
2954
+ }
2955
+ if (dead.length > 0) {
2956
+ out.push("### \u{1F480} Dead rules (declared but never observed)");
2957
+ out.push("");
2958
+ for (const [id] of dead) {
2959
+ out.push(`- \`${id}\` \u2014 consider removing or rephrasing`);
2960
+ }
2961
+ out.push("");
2962
+ }
2963
+ if (malformedLines > 0) {
2964
+ out.push(`> \u26A0\uFE0F ${malformedLines} malformed line(s) in \`.aikit/events.jsonl\` were skipped \u2014 adherence reflects only parsed events.`);
2965
+ out.push("");
2966
+ }
2967
+ out.push("---");
2968
+ out.push(`<sub>Generated by \`aikit report\` \xB7 ${(/* @__PURE__ */ new Date()).toISOString()}</sub>`);
2969
+ out.push("");
2970
+ return out.join("\n");
2971
+ }
2972
+ var EVENTS_PATH;
2973
+ var init_report = __esm({
2974
+ "src/commands/report.ts"() {
2975
+ "use strict";
2976
+ init_rule_ids();
2977
+ EVENTS_PATH = ".aikit/events.jsonl";
2978
+ }
2979
+ });
2980
+
2981
+ // src/commands/learn.ts
2982
+ var learn_exports = {};
2983
+ __export(learn_exports, {
2984
+ clusterCandidates: () => clusterCandidates,
2985
+ idFromText: () => idFromText,
2986
+ isTeachingComment: () => isTeachingComment,
2987
+ runLearn: () => runLearn
2988
+ });
2989
+ import { execFile } from "child_process";
2990
+ import { promisify } from "util";
2991
+ async function runLearn(argv) {
2992
+ const limit = Number(argv.limit ?? 30);
2993
+ if (!Number.isFinite(limit) || limit <= 0) {
2994
+ process.stderr.write("--limit must be a positive number\n");
2995
+ process.exit(1);
2996
+ }
2997
+ if (!await ensureGh()) {
2998
+ process.stderr.write(
2999
+ kleur_default.red("aikit learn requires the GitHub CLI (`gh`) to be installed and authenticated.\n") + "Install: https://cli.github.com/ \xB7 Authenticate: `gh auth login`\n"
3000
+ );
3001
+ process.exit(1);
3002
+ }
3003
+ if (!await hasGitHubRemote()) {
3004
+ process.stderr.write(
3005
+ kleur_default.red("This repo has no GitHub remote. ") + "`aikit learn` mines PR review history via `gh`, which requires a GitHub-hosted repo.\nAdd a remote with `git remote add origin <url>` then re-run.\n"
3006
+ );
3007
+ process.exit(1);
3008
+ }
3009
+ process.stdout.write(kleur_default.dim(`Fetching last ${limit} merged PRs...
3010
+ `));
3011
+ let prs;
3012
+ try {
3013
+ prs = await fetchMergedPRs(limit);
3014
+ } catch (err) {
3015
+ process.stderr.write(kleur_default.red(`Failed to list PRs via gh: ${ghErrorMessage(err)}
3016
+ `));
3017
+ process.exit(1);
3018
+ }
3019
+ if (prs.length === 0) {
3020
+ process.stdout.write(kleur_default.yellow("No merged PRs found in this repo.\n"));
3021
+ return;
3022
+ }
3023
+ process.stdout.write(kleur_default.dim(`Pulling review comments from ${prs.length} PRs...
3024
+ `));
3025
+ const comments = [];
3026
+ let skipped = 0;
3027
+ for (const pr of prs) {
3028
+ const prComments = await fetchReviewComments(pr.number);
3029
+ if (prComments === null) {
3030
+ skipped++;
3031
+ continue;
3032
+ }
3033
+ for (const c2 of prComments) comments.push({ body: c2, prNumber: pr.number });
3034
+ }
3035
+ if (skipped > 0) {
3036
+ process.stdout.write(kleur_default.yellow(` warn: ${skipped}/${prs.length} PRs skipped due to gh fetch errors
3037
+ `));
3038
+ }
3039
+ if (comments.length === 0) {
3040
+ process.stdout.write(kleur_default.yellow("No review comments found across the scanned PRs.\n"));
3041
+ return;
3042
+ }
3043
+ const teachingComments = comments.filter((c2) => isTeachingComment(c2.body));
3044
+ process.stdout.write(
3045
+ kleur_default.dim(`Found ${teachingComments.length} teaching comments out of ${comments.length} total.
3046
+
3047
+ `)
3048
+ );
3049
+ const candidates = clusterCandidates(teachingComments);
3050
+ printCandidates(candidates);
3051
+ }
3052
+ async function ensureGh() {
3053
+ try {
3054
+ await runCmd("gh", ["auth", "status"], { encoding: "utf8" });
3055
+ return true;
3056
+ } catch {
3057
+ return false;
3058
+ }
3059
+ }
3060
+ async function hasGitHubRemote() {
3061
+ try {
3062
+ const { stdout } = await runCmd("git", ["remote", "-v"], { encoding: "utf8" });
3063
+ return stdout.includes("github.com");
3064
+ } catch {
3065
+ return false;
3066
+ }
3067
+ }
3068
+ function ghErrorMessage(err) {
3069
+ const e2 = err;
3070
+ const stderr = typeof e2?.stderr === "string" ? e2.stderr.trim() : "";
3071
+ if (stderr) return stderr.split("\n")[0] ?? stderr;
3072
+ if (typeof e2?.message === "string") return e2.message.split("\n")[0] ?? e2.message;
3073
+ return String(err);
3074
+ }
3075
+ async function fetchMergedPRs(limit) {
3076
+ const { stdout } = await runCmd(
3077
+ "gh",
3078
+ ["pr", "list", "--state", "merged", "--limit", String(limit), "--json", "number,title"],
3079
+ { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 }
3080
+ );
3081
+ return JSON.parse(stdout);
3082
+ }
3083
+ async function fetchReviewComments(prNumber) {
3084
+ try {
3085
+ const { stdout } = await runCmd(
3086
+ "gh",
3087
+ ["pr", "view", String(prNumber), "--json", "reviews,comments"],
3088
+ { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 }
3089
+ );
3090
+ const data = JSON.parse(stdout);
3091
+ const out = [];
3092
+ for (const r2 of data.reviews ?? []) if (r2.body) out.push(r2.body);
3093
+ for (const c2 of data.comments ?? []) if (c2.body) out.push(c2.body);
3094
+ return out;
3095
+ } catch {
3096
+ return null;
3097
+ }
3098
+ }
3099
+ function isTeachingComment(body) {
3100
+ if (body.length < 10 || body.length > 1500) return false;
3101
+ return CORRECTION_PHRASES.some((rx) => rx.test(body));
3102
+ }
3103
+ function tokens(text) {
3104
+ return new Set(
3105
+ text.toLowerCase().replace(/[^a-z0-9 ]+/g, " ").split(/\s+/).filter((t) => t.length > 2 && !STOPWORDS.has(t)).map(stem)
3106
+ );
3107
+ }
3108
+ function stem(t) {
3109
+ if (t.length < 5) return t;
3110
+ if (t.endsWith("ies")) return t.slice(0, -3) + "y";
3111
+ if (t.endsWith("ing")) return t.slice(0, -3);
3112
+ if (t.endsWith("ed")) return t.slice(0, -2);
3113
+ if (t.endsWith("es")) return t.slice(0, -2);
3114
+ if (t.endsWith("s")) return t.slice(0, -1);
3115
+ return t;
3116
+ }
3117
+ function jaccard(a2, b2) {
3118
+ if (a2.size === 0 || b2.size === 0) return 0;
3119
+ let intersect = 0;
3120
+ for (const t of a2) if (b2.has(t)) intersect++;
3121
+ return intersect / (a2.size + b2.size - intersect);
3122
+ }
3123
+ function clusterCandidates(comments) {
3124
+ const clusters = [];
3125
+ const SIMILARITY_THRESHOLD = 0.15;
3126
+ for (const c2 of comments) {
3127
+ const tk = tokens(c2.body);
3128
+ if (tk.size < 2) continue;
3129
+ let placed = false;
3130
+ for (const cluster of clusters) {
3131
+ if (jaccard(tk, cluster.seedTokens) >= SIMILARITY_THRESHOLD) {
3132
+ cluster.comments.push(c2);
3133
+ placed = true;
3134
+ break;
3135
+ }
3136
+ }
3137
+ if (!placed) clusters.push({ seedTokens: tk, comments: [c2] });
3138
+ }
3139
+ return clusters.filter((c2) => c2.comments.length >= 2).map((c2) => candidateFromCluster(c2.comments)).sort((a2, b2) => b2.evidenceCount - a2.evidenceCount);
3140
+ }
3141
+ function candidateFromCluster(comments) {
3142
+ const exemplar = [...comments].map((c2) => c2.body.trim().split("\n").find((l2) => l2.trim().length > 10) ?? c2.body.trim()).sort((a2, b2) => a2.length - b2.length)[0] ?? comments[0]?.body ?? "";
3143
+ const id = idFromText(exemplar);
3144
+ return {
3145
+ id,
3146
+ text: exemplar.slice(0, 240),
3147
+ evidenceCount: comments.length,
3148
+ exemplar,
3149
+ prs: [...new Set(comments.map((c2) => c2.prNumber))].sort((a2, b2) => a2 - b2)
3150
+ };
3151
+ }
3152
+ function idFromText(text) {
3153
+ const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").split("-").filter((t) => t.length > 2 && !STOPWORDS.has(t)).slice(0, 4).join("-");
3154
+ return `learned.${slug || "unnamed"}`;
3155
+ }
3156
+ function printCandidates(candidates) {
3157
+ if (candidates.length === 0) {
3158
+ process.stdout.write(kleur_default.yellow("No repeated correction patterns found. Try increasing --limit.\n"));
3159
+ return;
3160
+ }
3161
+ process.stdout.write(kleur_default.bold(`Proposed rule candidates (${candidates.length})
3162
+ `));
3163
+ process.stdout.write(kleur_default.dim("Each candidate is backed by \u22652 PR review comments.\n\n"));
3164
+ for (const c2 of candidates) {
3165
+ const prList = c2.prs.slice(0, 5).map((n) => `#${n}`).join(", ");
3166
+ const more = c2.prs.length > 5 ? `, +${c2.prs.length - 5} more` : "";
3167
+ process.stdout.write(kleur_default.bold(`\u25B8 ${c2.id}
3168
+ `));
3169
+ process.stdout.write(` ${kleur_default.dim("evidence:")} ${c2.evidenceCount} comments across ${prList}${more}
3170
+ `);
3171
+ process.stdout.write(` ${kleur_default.dim("exemplar:")} ${truncate(c2.exemplar, 200)}
3172
+
3173
+ `);
3174
+ }
3175
+ process.stdout.write(kleur_default.bold("Suggested AGENTS.md additions\n"));
3176
+ process.stdout.write(kleur_default.dim("Copy the block below into AGENTS.md inside a <!-- BEGIN:learned --> ... <!-- END:learned --> region:\n\n"));
3177
+ process.stdout.write("<!-- BEGIN:learned -->\n");
3178
+ process.stdout.write("## Learned conventions\n");
3179
+ for (const c2 of candidates) {
3180
+ process.stdout.write(`- <!-- id: ${c2.id} --> ${truncate(c2.exemplar, 240)}
3181
+ `);
3182
+ }
3183
+ process.stdout.write("<!-- END:learned -->\n");
3184
+ }
3185
+ function truncate(s, n) {
3186
+ const flat = s.replace(/\s+/g, " ").trim();
3187
+ return flat.length > n ? `${flat.slice(0, n - 1)}\u2026` : flat;
3188
+ }
3189
+ var runCmd, CORRECTION_PHRASES, STOPWORDS;
3190
+ var init_learn = __esm({
3191
+ "src/commands/learn.ts"() {
3192
+ "use strict";
3193
+ init_kleur();
3194
+ runCmd = promisify(execFile);
3195
+ CORRECTION_PHRASES = [
3196
+ /\bwe (?:always|usually|never|don't|do not|prefer|tend to|should|shouldn't)\b/i,
3197
+ /\b(?:please )?(?:always|never|don't|do not) [a-z]/i,
3198
+ /\bactually,?\s+(?:we|let's|i'd)/i,
3199
+ /\bshould (?:always |never |)be /i,
3200
+ /\b(?:our )?convention is to\b/i,
3201
+ /\bnit:?\s+/i,
3202
+ /\b(?:can we|could we|would prefer)\b/i
3203
+ ];
3204
+ STOPWORDS = /* @__PURE__ */ new Set([
3205
+ "the",
3206
+ "a",
3207
+ "an",
3208
+ "and",
3209
+ "or",
3210
+ "but",
3211
+ "if",
3212
+ "is",
3213
+ "are",
3214
+ "was",
3215
+ "were",
3216
+ "be",
3217
+ "been",
3218
+ "being",
3219
+ "to",
3220
+ "of",
3221
+ "in",
3222
+ "on",
3223
+ "at",
3224
+ "by",
3225
+ "for",
3226
+ "with",
3227
+ "from",
3228
+ "this",
3229
+ "that",
3230
+ "these",
3231
+ "those",
3232
+ "it",
3233
+ "its",
3234
+ "we",
3235
+ "you",
3236
+ "i",
3237
+ "my",
3238
+ "your",
3239
+ "our",
3240
+ "their",
3241
+ "as",
3242
+ "so",
3243
+ "do",
3244
+ "does",
3245
+ "did",
3246
+ "have",
3247
+ "has",
3248
+ "had",
3249
+ "can",
3250
+ "could",
3251
+ "would",
3252
+ "should",
3253
+ "will",
3254
+ "may"
3255
+ ]);
1931
3256
  }
1932
3257
  });
1933
3258
 
@@ -2052,7 +3377,7 @@ function isInteractive() {
2052
3377
  }
2053
3378
 
2054
3379
  // src/cli.ts
2055
- var VERSION = "0.1.0";
3380
+ var VERSION = "0.7.1";
2056
3381
  var HELP = `
2057
3382
  haac-aikit \u2014 the batteries-included AI-agentic-coding kit
2058
3383
 
@@ -2061,13 +3386,16 @@ USAGE
2061
3386
  aikit [command] [flags] (if installed globally)
2062
3387
 
2063
3388
  COMMANDS
2064
- (default) Interactive wizard \u2014 drop a complete AI setup into this repo
2065
- sync Regenerate per-tool files from .aikitrc.json (idempotent)
2066
- update Pull latest templates; show diff; prompt before writing
2067
- diff Show drift between current state and a fresh generation
2068
- add <item> Add a single skill, command, agent, or hook
2069
- list Show installed items + available catalog
2070
- doctor Sanity-check: schema, triggers, broken links
3389
+ (default) Interactive wizard \u2014 drop a complete AI setup into this repo
3390
+ sync Regenerate per-tool files from .aikitrc.json (idempotent)
3391
+ update Pull latest templates; show diff; prompt before writing
3392
+ diff Show drift between current state and a fresh generation
3393
+ add <item> Add a single skill, command, agent, or hook
3394
+ list Show installed items + available catalog
3395
+ doctor Sanity-check: schema, triggers, broken links
3396
+ doctor --rules Rule observability report \u2014 which rules fire, are followed, are dead
3397
+ report Markdown / JSON rule-adherence summary (for PR comments / CI)
3398
+ learn Mine recent PR review comments for repeated corrections; propose rules
2071
3399
 
2072
3400
  FLAGS
2073
3401
  --yes, -y Accept all defaults
@@ -2078,13 +3406,17 @@ FLAGS
2078
3406
  --config=<path> Use a specific .aikitrc.json location
2079
3407
  --tools=<list> Comma-separated tool list (claude,cursor,copilot,...)
2080
3408
  --preset=<scope> minimal | standard | everything
3409
+ --rules (with doctor) Show rule-observability buckets
3410
+ --format=<fmt> (with report / doctor --rules) markdown | json
3411
+ --since=<date> (with report) Restrict events to after this ISO date
3412
+ --limit=<n> (with learn) How many merged PRs to scan (default 30)
2081
3413
  --help, -h Show this help
2082
3414
  --version, -v Show version
2083
3415
  `;
2084
3416
  async function main() {
2085
3417
  const argv = lib_default(process.argv.slice(2), {
2086
- boolean: ["yes", "dry-run", "force", "skip-git-check", "no-color", "help", "version"],
2087
- string: ["config", "tools", "preset"],
3418
+ boolean: ["yes", "dry-run", "force", "skip-git-check", "no-color", "help", "version", "rules"],
3419
+ string: ["config", "tools", "preset", "format", "since", "limit"],
2088
3420
  alias: { y: "yes", h: "help", v: "version" },
2089
3421
  default: {
2090
3422
  yes: false,
@@ -2093,7 +3425,8 @@ async function main() {
2093
3425
  "skip-git-check": false,
2094
3426
  "no-color": false,
2095
3427
  help: false,
2096
- version: false
3428
+ version: false,
3429
+ rules: false
2097
3430
  }
2098
3431
  });
2099
3432
  if (argv.version) {
@@ -2125,7 +3458,7 @@ async function main() {
2125
3458
  break;
2126
3459
  }
2127
3460
  case "diff": {
2128
- const { runDiff: runDiff2 } = await Promise.resolve().then(() => (init_diff(), diff_exports));
3461
+ const { runDiff: runDiff2 } = await Promise.resolve().then(() => (init_diff2(), diff_exports));
2129
3462
  await runDiff2(argv);
2130
3463
  break;
2131
3464
  }
@@ -2144,6 +3477,16 @@ async function main() {
2144
3477
  await runDoctor2(argv);
2145
3478
  break;
2146
3479
  }
3480
+ case "report": {
3481
+ const { runReport: runReport2 } = await Promise.resolve().then(() => (init_report(), report_exports));
3482
+ await runReport2(argv);
3483
+ break;
3484
+ }
3485
+ case "learn": {
3486
+ const { runLearn: runLearn2 } = await Promise.resolve().then(() => (init_learn(), learn_exports));
3487
+ await runLearn2(argv);
3488
+ break;
3489
+ }
2147
3490
  default:
2148
3491
  process.stderr.write(`Unknown command: ${command}
2149
3492
  Run --help for usage.