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.
- package/README.md +169 -69
- package/catalog/agents/tier1/architect.md +86 -0
- package/catalog/agents/tier1/debugger.md +64 -0
- package/catalog/agents/tier1/pr-describer.md +57 -0
- package/catalog/agents/{researcher.md → tier1/researcher.md} +1 -1
- package/catalog/agents/tier2/changelog-curator.md +60 -0
- package/catalog/agents/tier2/dependency-upgrader.md +67 -0
- package/catalog/agents/tier2/evals-author.md +65 -0
- package/catalog/agents/tier2/flake-hunter.md +57 -0
- package/catalog/agents/tier2/prompt-engineer.md +58 -0
- package/catalog/agents/tier2/simplifier.md +57 -0
- package/catalog/ci/aikit-rules.yml +55 -0
- package/catalog/docs/claude-md-reference.md +316 -0
- package/catalog/hooks/block-dangerous-bash.sh +0 -0
- package/catalog/hooks/block-force-push-main.sh +0 -0
- package/catalog/hooks/block-secrets-in-commits.sh +0 -0
- package/catalog/hooks/check-pattern-violations.sh +137 -0
- package/catalog/hooks/compaction-preservation.sh +0 -0
- package/catalog/hooks/file-guard.sh +0 -0
- package/catalog/hooks/format-on-save.sh +0 -0
- package/catalog/hooks/hooks.json +38 -0
- package/catalog/hooks/judge-rule-compliance.sh +197 -0
- package/catalog/hooks/log-rule-event.sh +80 -0
- package/catalog/hooks/session-start-prime.sh +0 -0
- package/catalog/husky/commit-msg +0 -0
- package/catalog/husky/pre-commit +0 -0
- package/catalog/husky/pre-push +0 -0
- package/catalog/rules/AGENTS.md.tmpl +28 -7
- package/catalog/rules/aikit-rules.json +37 -0
- package/catalog/rules/claude-rules/example.md +38 -0
- package/catalog/skills/tier1/software-architect.md +42 -0
- package/dist/cli.mjs +1516 -173
- package/dist/cli.mjs.map +1 -1
- package/package.json +4 -2
- /package/catalog/agents/{devops.md → tier1/devops.md} +0 -0
- /package/catalog/agents/{implementer.md → tier1/implementer.md} +0 -0
- /package/catalog/agents/{orchestrator.md → tier1/orchestrator.md} +0 -0
- /package/catalog/agents/{planner.md → tier1/planner.md} +0 -0
- /package/catalog/agents/{reviewer.md → tier1/reviewer.md} +0 -0
- /package/catalog/agents/{security-auditor.md → tier1/security-auditor.md} +0 -0
- /package/catalog/agents/{tester.md → tier1/tester.md} +0 -0
- /package/catalog/agents/{backend.md → tier2/backend.md} +0 -0
- /package/catalog/agents/{frontend.md → tier2/frontend.md} +0 -0
- /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
|
|
1151
|
-
const { replaced } = upsertMarkerRegion(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
1247
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
1291
|
-
results.push(...syncSkills("tier2",
|
|
1565
|
+
results.push(...syncSkills("tier1", opts));
|
|
1566
|
+
results.push(...syncSkills("tier2", opts));
|
|
1292
1567
|
}
|
|
1293
1568
|
if (config.integrations.hooks) {
|
|
1294
|
-
results.push(...syncHooks(
|
|
1569
|
+
results.push(...syncHooks(opts));
|
|
1295
1570
|
}
|
|
1296
1571
|
if (config.integrations.subagents) {
|
|
1297
|
-
results.push(...syncAgents(config
|
|
1572
|
+
results.push(...syncAgents(config, opts));
|
|
1298
1573
|
}
|
|
1299
1574
|
if (config.integrations.commands) {
|
|
1300
|
-
results.push(...syncCommands(
|
|
1575
|
+
results.push(...syncCommands(opts));
|
|
1301
1576
|
}
|
|
1302
1577
|
if (config.integrations.ci) {
|
|
1303
|
-
results.push(...syncCI(
|
|
1578
|
+
results.push(...syncCI(opts));
|
|
1304
1579
|
}
|
|
1305
1580
|
if (config.integrations.devcontainer) {
|
|
1306
|
-
results.push(...syncDir("devcontainer", ".devcontainer",
|
|
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
|
|
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(
|
|
1659
|
+
ye(visible.map((r2) => `${r2.action.padEnd(9)} ${r2.path}`).join("\n"), "Dry run");
|
|
1317
1660
|
return;
|
|
1318
1661
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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(
|
|
1708
|
+
function syncHooks(opts) {
|
|
1343
1709
|
const srcDir = join2(CATALOG_ROOT, "hooks");
|
|
1344
1710
|
const destDir = `.claude/hooks`;
|
|
1345
1711
|
const results = [];
|
|
1346
|
-
if (!
|
|
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
|
-
|
|
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(
|
|
1357
|
-
const srcDir = join2(CATALOG_ROOT, "agents");
|
|
1358
|
-
const destDir = `.claude/agents`;
|
|
1720
|
+
function syncAgents(config, opts) {
|
|
1359
1721
|
const results = [];
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
const
|
|
1370
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1751
|
+
function syncCommands(opts) {
|
|
1386
1752
|
const srcDir = join2(CATALOG_ROOT, "commands");
|
|
1387
1753
|
const destDir = `.claude/commands`;
|
|
1388
1754
|
const results = [];
|
|
1389
|
-
if (!
|
|
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
|
-
|
|
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(
|
|
1763
|
+
function syncCI(opts) {
|
|
1400
1764
|
const srcDir = join2(CATALOG_ROOT, "ci");
|
|
1401
1765
|
const destDir = `.github/workflows`;
|
|
1402
1766
|
const results = [];
|
|
1403
|
-
if (!
|
|
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
|
-
|
|
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,
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
1542
|
-
import {
|
|
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 (
|
|
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(
|
|
1601
|
-
if (!
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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(() => (
|
|
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
|
|
1672
|
-
import {
|
|
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
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1721
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
|
1748
|
-
import {
|
|
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: "
|
|
1756
|
-
{ label: "
|
|
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(
|
|
1786
|
-
if (!
|
|
2331
|
+
const catalogDir = join5(CATALOG_ROOT, catalogSubdir);
|
|
2332
|
+
if (!existsSync8(catalogDir)) return [];
|
|
1787
2333
|
const installedFiles = new Set(
|
|
1788
|
-
|
|
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) => ({
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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)
|
|
2065
|
-
sync
|
|
2066
|
-
update
|
|
2067
|
-
diff
|
|
2068
|
-
add <item>
|
|
2069
|
-
list
|
|
2070
|
-
doctor
|
|
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(() => (
|
|
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.
|