@poltergeist-ai/cli 0.1.5 → 0.1.9

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/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { appendFileSync, existsSync, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
4
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
5
5
  import path4 from "path";
6
6
  import { execFileSync as execFileSync2 } from "child_process";
7
7
  import { parseArgs } from "util";
@@ -742,31 +742,115 @@ function sampleMatching(comments, patterns, max) {
742
742
  }
743
743
  return matches;
744
744
  }
745
- function extractThemes(comments) {
745
+ function extractThemes(comments, commentSeverities) {
746
746
  if (comments.length === 0) return [];
747
747
  const themes = [];
748
+ const severities = commentSeverities ?? [];
748
749
  for (const def of THEME_DEFS) {
749
750
  const matchingComments = [];
750
- for (const comment of comments) {
751
+ const matchingSeverities = [];
752
+ let totalLength = 0;
753
+ for (let i = 0; i < comments.length; i++) {
754
+ const comment = comments[i];
751
755
  const lower = comment.toLowerCase();
752
756
  if (def.patterns.some((p) => p.test(lower))) {
753
757
  matchingComments.push(comment);
758
+ totalLength += comment.length;
759
+ if (i < severities.length) {
760
+ matchingSeverities.push(severities[i]);
761
+ }
754
762
  }
755
763
  }
756
764
  if (matchingComments.length < 2) continue;
757
765
  const ratio = Math.round(matchingComments.length / comments.length * 100) / 100;
758
766
  const snippets = matchingComments.filter((c) => c.length > 20 && c.length < 300).slice(0, 3).map((c) => c.length > 150 ? c.slice(0, 150) + "..." : c);
767
+ const severityBreakdown = {};
768
+ for (const sev of matchingSeverities) {
769
+ severityBreakdown[sev] = (severityBreakdown[sev] ?? 0) + 1;
770
+ }
759
771
  themes.push({
760
772
  theme: def.theme,
761
773
  label: def.label,
762
774
  count: matchingComments.length,
763
775
  ratio,
764
- exampleSnippets: snippets
776
+ exampleSnippets: snippets,
777
+ severityBreakdown: matchingSeverities.length > 0 ? severityBreakdown : void 0,
778
+ avgCommentLength: Math.round(totalLength / matchingComments.length)
765
779
  });
766
780
  }
767
781
  themes.sort((a, b) => b.count - a.count);
768
782
  return themes;
769
783
  }
784
+ var HIGH_SEVERITY = /* @__PURE__ */ new Set(["blocking", "major"]);
785
+ var MED_SEVERITY = /* @__PURE__ */ new Set(["suggestion", "question", "thought"]);
786
+ var LOW_SEVERITY = /* @__PURE__ */ new Set(["nit", "minor"]);
787
+ function dominantSeverity(breakdown) {
788
+ if (!breakdown) return "unknown";
789
+ let bestCategory = "unknown";
790
+ let bestCount = 0;
791
+ let highCount = 0;
792
+ let medCount = 0;
793
+ let lowCount = 0;
794
+ for (const [sev, count] of Object.entries(breakdown)) {
795
+ if (HIGH_SEVERITY.has(sev)) highCount += count;
796
+ else if (MED_SEVERITY.has(sev)) medCount += count;
797
+ else if (LOW_SEVERITY.has(sev)) lowCount += count;
798
+ }
799
+ if (highCount > bestCount) {
800
+ bestCategory = "blocking";
801
+ bestCount = highCount;
802
+ }
803
+ if (medCount > bestCount) {
804
+ bestCategory = "suggestion";
805
+ bestCount = medCount;
806
+ }
807
+ if (lowCount > bestCount) {
808
+ bestCategory = "nit";
809
+ bestCount = lowCount;
810
+ }
811
+ return bestCategory;
812
+ }
813
+ function computeWeightedDimensions(themes) {
814
+ if (themes.length === 0) return [];
815
+ const maxRatio = Math.max(...themes.map((t) => t.ratio));
816
+ const maxAvgLen = Math.max(
817
+ ...themes.map((t) => t.avgCommentLength ?? 0),
818
+ 1
819
+ );
820
+ return themes.map((theme) => {
821
+ const frequencyScore = maxRatio > 0 ? theme.ratio / maxRatio : 0;
822
+ let severityScore = 0.5;
823
+ if (theme.severityBreakdown) {
824
+ const total = Object.values(theme.severityBreakdown).reduce(
825
+ (a, b) => a + b,
826
+ 0
827
+ );
828
+ if (total > 0) {
829
+ let highCount = 0;
830
+ for (const [sev, count] of Object.entries(theme.severityBreakdown)) {
831
+ if (HIGH_SEVERITY.has(sev)) highCount += count;
832
+ }
833
+ severityScore = highCount / total;
834
+ }
835
+ }
836
+ const avgLen = theme.avgCommentLength ?? 0;
837
+ const specificityScore = maxAvgLen > 0 ? avgLen / maxAvgLen : 0;
838
+ const rawWeight = frequencyScore * 0.5 + severityScore * 0.3 + specificityScore * 0.2;
839
+ const weight = Math.round(Math.min(1, Math.max(0, rawWeight)) * 100) / 100;
840
+ let confidence;
841
+ if (theme.count >= 20) confidence = "high";
842
+ else if (theme.count >= 10) confidence = "moderate";
843
+ else confidence = "low";
844
+ return {
845
+ dimension: theme.theme,
846
+ label: theme.label,
847
+ weight,
848
+ confidence,
849
+ commentCount: theme.count,
850
+ defaultSeverity: dominantSeverity(theme.severityBreakdown)
851
+ };
852
+ });
853
+ }
770
854
  function buildToneProfile(comments) {
771
855
  if (comments.length < 5) return void 0;
772
856
  const n = comments.length;
@@ -810,10 +894,13 @@ function summariseReview(signals) {
810
894
  indices.filter((i) => i >= 0 && i < n).map((i) => sorted[i])
811
895
  )
812
896
  ];
813
- obs.reviewThemes = extractThemes(comments);
897
+ obs.reviewThemes = extractThemes(comments, signals.commentSeverities);
814
898
  obs.toneProfile = buildToneProfile(comments);
815
899
  obs.recurringQuestions = extractRecurringQuestions(comments);
816
900
  obs.recurringPhrases = extractRecurringPhrases(comments);
901
+ if (obs.reviewThemes.length > 0) {
902
+ obs.weightedDimensions = computeWeightedDimensions(obs.reviewThemes);
903
+ }
817
904
  return obs;
818
905
  }
819
906
 
@@ -823,6 +910,7 @@ function extractGitLabSignals(exportPath, contributor, verbose) {
823
910
  reviewComments: [],
824
911
  commentLengths: [],
825
912
  severityPrefixes: {},
913
+ commentSeverities: [],
826
914
  questionComments: 0,
827
915
  totalComments: 0,
828
916
  source: "gitlab"
@@ -860,7 +948,11 @@ function extractGitLabSignals(exportPath, contributor, verbose) {
860
948
  signals.totalComments += 1;
861
949
  const prefixMatch = body.match(prefixRe);
862
950
  if (prefixMatch) {
863
- increment(signals.severityPrefixes, prefixMatch[1].toLowerCase());
951
+ const severity = prefixMatch[1].toLowerCase();
952
+ increment(signals.severityPrefixes, severity);
953
+ signals.commentSeverities.push(severity);
954
+ } else {
955
+ signals.commentSeverities.push("none");
864
956
  }
865
957
  if (body.endsWith("?") || body.toLowerCase().startsWith("do we") || body.toLowerCase().startsWith("should we")) {
866
958
  signals.questionComments += 1;
@@ -953,6 +1045,7 @@ async function extractGitHubSignals(owner, repo, contributor, token, verbose) {
953
1045
  reviewComments: [],
954
1046
  commentLengths: [],
955
1047
  severityPrefixes: {},
1048
+ commentSeverities: [],
956
1049
  questionComments: 0,
957
1050
  totalComments: 0,
958
1051
  source: "github"
@@ -996,7 +1089,11 @@ async function extractGitHubSignals(owner, repo, contributor, token, verbose) {
996
1089
  signals.totalComments += 1;
997
1090
  const prefixMatch = body.match(prefixRe);
998
1091
  if (prefixMatch) {
999
- increment(signals.severityPrefixes, prefixMatch[1].toLowerCase());
1092
+ const severity = prefixMatch[1].toLowerCase();
1093
+ increment(signals.severityPrefixes, severity);
1094
+ signals.commentSeverities.push(severity);
1095
+ } else {
1096
+ signals.commentSeverities.push("none");
1000
1097
  }
1001
1098
  if (body.endsWith("?") || body.toLowerCase().startsWith("do we") || body.toLowerCase().startsWith("should we")) {
1002
1099
  signals.questionComments += 1;
@@ -1104,6 +1201,18 @@ function extractDocsSignals(docsDir, contributor, verbose) {
1104
1201
  }
1105
1202
 
1106
1203
  // src/generator.ts
1204
+ import { readFileSync as readFileSync4 } from "fs";
1205
+ import { fileURLToPath } from "url";
1206
+ import { dirname, join } from "path";
1207
+ function getCliVersion() {
1208
+ try {
1209
+ const dir = dirname(fileURLToPath(import.meta.url));
1210
+ const pkg = JSON.parse(readFileSync4(join(dir, "..", "package.json"), "utf-8"));
1211
+ return pkg.version ?? "unknown";
1212
+ } catch {
1213
+ return "unknown";
1214
+ }
1215
+ }
1107
1216
  function formatPairs(pairs, suffix = "") {
1108
1217
  return pairs.map(([name, count]) => `${name}${suffix} (${count})`).join(", ");
1109
1218
  }
@@ -1129,23 +1238,66 @@ function buildGhostMarkdown(input) {
1129
1238
  const { contributor, slug, gitObs, codeStyleObs, reviewObs, slackObs, docsSignals, sourcesUsed } = input;
1130
1239
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1131
1240
  const domains = gitObs.inferredDomains?.length ? gitObs.inferredDomains.join(", ") : "_[fill in manually]_";
1241
+ const cliVersion = getCliVersion();
1132
1242
  const lines = [
1133
1243
  `# Contributor Soul: ${contributor}`,
1134
1244
  "",
1135
1245
  "## Identity",
1136
1246
  `- **Slug**: ${slug}`,
1247
+ `- **Version**: 0.1.0`,
1248
+ `- **Status**: draft`,
1137
1249
  "- **Role**: _[fill in manually]_",
1138
1250
  `- **Primary domains**: ${domains}`,
1139
1251
  `- **Soul last updated**: ${today}`,
1140
1252
  `- **Sources used**: ${sourcesUsed.join(", ")}`,
1253
+ `- **Generated by**: @poltergeist-ai/cli@${cliVersion}`,
1141
1254
  "",
1255
+ "---",
1256
+ ""
1257
+ ];
1258
+ const weighted = reviewObs.weightedDimensions;
1259
+ const themes = reviewObs.reviewThemes;
1260
+ if (weighted && weighted.length > 0) {
1261
+ lines.push(
1262
+ "## Review Heuristics",
1263
+ "",
1264
+ `_Inferred from ${reviewObs.totalReviewComments ?? "?"} review comments \u2014 adjust weights as needed_`,
1265
+ "",
1266
+ "| Dimension | Weight | Confidence | Default Severity |",
1267
+ "|---|---|---|---|"
1268
+ );
1269
+ for (const dim of weighted) {
1270
+ lines.push(
1271
+ `| ${dim.label} | ${dim.weight.toFixed(2)} | ${dim.confidence} (${dim.commentCount} comments) | ${dim.defaultSeverity} |`
1272
+ );
1273
+ }
1274
+ lines.push("");
1275
+ }
1276
+ lines.push(
1277
+ "### Tradeoff Preferences",
1278
+ "_How this contributor resolves common engineering tensions. Fill in from review patterns._",
1279
+ "",
1280
+ "- abstraction vs duplication: _[prefer-abstraction | prefer-duplication | balanced]_",
1281
+ "- readability vs performance: _[prefer-readability | prefer-performance | balanced]_",
1282
+ "- speed vs correctness: _[prefer-speed | prefer-correctness | balanced]_",
1283
+ "- local vs system optimization: _[prefer-local | prefer-system | balanced]_",
1284
+ ""
1285
+ );
1286
+ lines.push(
1287
+ "### Scars",
1288
+ "_Historical incidents that make this contributor unusually sensitive to certain patterns._",
1289
+ "_Format: **pattern** (multiplier) \u2014 description. Amplifies: dimension names._",
1290
+ "",
1291
+ "_[Fill in manually \u2014 e.g.: **shared-mutable-state** (\xD71.8) \u2014 production incident. Amplifies: error_handling, readability]_",
1292
+ ""
1293
+ );
1294
+ lines.push(
1142
1295
  "---",
1143
1296
  "",
1144
1297
  "## Review Philosophy",
1145
1298
  "",
1146
1299
  "### What they care about most (ranked)"
1147
- ];
1148
- const themes = reviewObs.reviewThemes;
1300
+ );
1149
1301
  if (themes && themes.length > 0) {
1150
1302
  lines.push(
1151
1303
  `_Inferred from ${reviewObs.totalReviewComments ?? "?"} review comments \u2014 verify and re-order as needed_`
@@ -1416,6 +1568,132 @@ function buildGhostMarkdown(input) {
1416
1568
  return lines.join("\n");
1417
1569
  }
1418
1570
 
1571
+ // src/setup.ts
1572
+ import { existsSync, mkdirSync, readFileSync as readFileSync5, writeFileSync } from "fs";
1573
+ import { dirname as dirname2, join as join2 } from "path";
1574
+ import { fileURLToPath as fileURLToPath2 } from "url";
1575
+ import { createInterface } from "readline";
1576
+ import { installRules, supportedTools } from "@poltergeist-ai/llm-rules";
1577
+ function getSkillsDir() {
1578
+ return join2(dirname2(fileURLToPath2(import.meta.url)), "..", "skills");
1579
+ }
1580
+ function loadSkill(filePath) {
1581
+ const raw = readFileSync5(filePath, "utf-8");
1582
+ const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
1583
+ if (!fmMatch) {
1584
+ return { name: "", description: "", body: raw, raw };
1585
+ }
1586
+ const frontmatter = fmMatch[1];
1587
+ const body = fmMatch[2].trim();
1588
+ let name = "";
1589
+ let description = "";
1590
+ const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
1591
+ if (nameMatch) name = nameMatch[1].trim();
1592
+ const descMatch = frontmatter.match(/description:\s*>\s*\n([\s\S]*?)$/);
1593
+ if (descMatch) {
1594
+ description = descMatch[1].replace(/\n\s*/g, " ").trim();
1595
+ } else {
1596
+ const descSimple = frontmatter.match(/^description:\s*(?!>)(.+)$/m);
1597
+ if (descSimple) description = descSimple[1].trim();
1598
+ }
1599
+ return { name, description, body, raw };
1600
+ }
1601
+ function stripPluginRoot(content) {
1602
+ return content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}\//g, ".poltergeist/");
1603
+ }
1604
+ function prompt(question) {
1605
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1606
+ return new Promise((resolve) => {
1607
+ rl.question(question, (answer) => {
1608
+ rl.close();
1609
+ resolve(answer.trim());
1610
+ });
1611
+ });
1612
+ }
1613
+ async function runSetup(toolFlag) {
1614
+ console.log("\n Poltergeist Setup\n");
1615
+ const tools = supportedTools();
1616
+ let selectedToolIds;
1617
+ if (toolFlag) {
1618
+ const ids = toolFlag.split(",").map((s) => s.trim().toLowerCase());
1619
+ const valid = ids.filter((id) => tools.some((t) => t.id === id));
1620
+ if (valid.length === 0) {
1621
+ console.error(
1622
+ `Unknown tool(s): ${toolFlag}
1623
+ Available: ${tools.map((t) => t.id).join(", ")}`
1624
+ );
1625
+ return 1;
1626
+ }
1627
+ selectedToolIds = valid;
1628
+ } else {
1629
+ console.log(" Available tools:\n");
1630
+ for (let i = 0; i < tools.length; i++) {
1631
+ console.log(` ${i + 1}. ${tools[i].name} (${tools[i].id})`);
1632
+ }
1633
+ console.log(` a. All
1634
+ `);
1635
+ const answer = await prompt(" Install for (numbers comma-separated, or 'a' for all): ");
1636
+ if (answer.toLowerCase() === "a") {
1637
+ selectedToolIds = tools.map((t) => t.id);
1638
+ } else {
1639
+ const indices = answer.split(",").map((s) => parseInt(s.trim()) - 1).filter((i) => i >= 0 && i < tools.length);
1640
+ if (indices.length === 0) {
1641
+ console.log("No tools selected.");
1642
+ return 0;
1643
+ }
1644
+ selectedToolIds = indices.map((i) => tools[i].id);
1645
+ }
1646
+ }
1647
+ const skillsDir = getSkillsDir();
1648
+ const reviewSkillPath = join2(skillsDir, "poltergeist", "SKILL.md");
1649
+ const extractSkillPath = join2(skillsDir, "extract", "SKILL.md");
1650
+ const rules = [];
1651
+ if (existsSync(reviewSkillPath)) {
1652
+ const skill = loadSkill(reviewSkillPath);
1653
+ rules.push({
1654
+ name: skill.name || "poltergeist",
1655
+ description: skill.description || "Poltergeist review skill",
1656
+ content: stripPluginRoot(skill.raw)
1657
+ });
1658
+ } else {
1659
+ console.error(` [error] Review skill not found at ${reviewSkillPath}`);
1660
+ return 1;
1661
+ }
1662
+ if (existsSync(extractSkillPath)) {
1663
+ const skill = loadSkill(extractSkillPath);
1664
+ rules.push({
1665
+ name: skill.name || "extract",
1666
+ description: skill.description || "Poltergeist extract skill",
1667
+ content: stripPluginRoot(skill.raw)
1668
+ });
1669
+ }
1670
+ console.log(`
1671
+ Installing for ${selectedToolIds.join(", ")}...`);
1672
+ const results = installRules(rules, {
1673
+ tools: selectedToolIds,
1674
+ force: true,
1675
+ namespace: "poltergeist"
1676
+ });
1677
+ for (const result of results) {
1678
+ console.log(` \u2713 ${result.path} (${result.action})`);
1679
+ }
1680
+ const exampleGhostPath = join2(skillsDir, "..", "ghosts", "example-ghost.md");
1681
+ if (existsSync(exampleGhostPath)) {
1682
+ const ghostDest = ".poltergeist/ghosts/example-ghost.md";
1683
+ if (!existsSync(ghostDest)) {
1684
+ mkdirSync(dirname2(ghostDest), { recursive: true });
1685
+ writeFileSync(ghostDest, readFileSync5(exampleGhostPath));
1686
+ console.log(`
1687
+ \u2713 ${ghostDest}`);
1688
+ }
1689
+ }
1690
+ console.log("\n Done. Next steps:");
1691
+ console.log(" 1. Build a ghost: npx @poltergeist-ai/cli extract --contributor <name> --git-repo <url>");
1692
+ console.log(' 2. Run a review: git diff main | claude "review as @<slug>"');
1693
+ console.log("");
1694
+ return 0;
1695
+ }
1696
+
1419
1697
  // src/cli.ts
1420
1698
  var POLTERGEIST_DIR = ".poltergeist";
1421
1699
  var CACHE_DIR = `${POLTERGEIST_DIR}/repos`;
@@ -1423,25 +1701,27 @@ var GHOSTS_DIR = `${POLTERGEIST_DIR}/ghosts`;
1423
1701
  function ensureReposGitignored() {
1424
1702
  const gitignorePath = ".gitignore";
1425
1703
  const entry = ".poltergeist/repos/";
1426
- if (existsSync(gitignorePath)) {
1427
- const content = readFileSync4(gitignorePath, "utf-8");
1704
+ if (existsSync2(gitignorePath)) {
1705
+ const content = readFileSync6(gitignorePath, "utf-8");
1428
1706
  if (content.includes(entry)) return;
1429
1707
  appendFileSync(gitignorePath, `
1430
1708
  # Poltergeist cached clones
1431
1709
  ${entry}
1432
1710
  `);
1433
1711
  } else {
1434
- writeFileSync(gitignorePath, `# Poltergeist cached clones
1712
+ writeFileSync2(gitignorePath, `# Poltergeist cached clones
1435
1713
  ${entry}
1436
1714
  `);
1437
1715
  }
1438
1716
  }
1439
1717
  function printUsage() {
1440
- console.log(`Usage: poltergeist [extract] [options]
1718
+ console.log(`Usage: poltergeist <command> [options]
1441
1719
 
1442
- Build a contributor ghost profile from data sources.
1720
+ Commands:
1721
+ extract Build a contributor ghost profile from data sources
1722
+ setup Install poltergeist skills for your AI coding tool
1443
1723
 
1444
- Options:
1724
+ Extract options:
1445
1725
  --contributor <name> Contributor name (required; use GitHub username for best results)
1446
1726
  --email <email> Contributor email (for git log filtering)
1447
1727
  --slug <slug> Output slug (default: derived from name)
@@ -1452,6 +1732,11 @@ Options:
1452
1732
  --github-token <token> GitHub personal access token (for higher API rate limits)
1453
1733
  --output <path> Output path (default: .poltergeist/ghosts/<slug>.md)
1454
1734
  --verbose Enable verbose logging
1735
+
1736
+ Setup options:
1737
+ --tool <id> Tool to install for (claude-code,codex,cursor,windsurf,cline)
1738
+ Comma-separated for multiple. Omit to choose interactively.
1739
+
1455
1740
  --help Show this help message`);
1456
1741
  }
1457
1742
  function isRemoteUrl(value) {
@@ -1464,7 +1749,7 @@ function resolveGitRepo(value, verbose) {
1464
1749
  if (!isRemoteUrl(value)) return value;
1465
1750
  const slug = repoSlug(value);
1466
1751
  const cloneDir = path4.join(CACHE_DIR, slug);
1467
- if (existsSync(cloneDir)) {
1752
+ if (existsSync2(cloneDir)) {
1468
1753
  console.log(`[extract] Using cached clone at ${cloneDir}`);
1469
1754
  try {
1470
1755
  execFileSync2("git", ["-C", cloneDir, "fetch", "--quiet"], {
@@ -1478,7 +1763,7 @@ function resolveGitRepo(value, verbose) {
1478
1763
  return cloneDir;
1479
1764
  }
1480
1765
  console.log(`[extract] Cloning ${value} into ${cloneDir}...`);
1481
- mkdirSync(CACHE_DIR, { recursive: true });
1766
+ mkdirSync2(CACHE_DIR, { recursive: true });
1482
1767
  ensureReposGitignored();
1483
1768
  execFileSync2(
1484
1769
  "git",
@@ -1497,6 +1782,16 @@ async function run() {
1497
1782
  printUsage();
1498
1783
  return 0;
1499
1784
  }
1785
+ if (rawArgs[0] === "setup") {
1786
+ const setupArgs = rawArgs.slice(1);
1787
+ let toolFlag;
1788
+ for (let i = 0; i < setupArgs.length; i++) {
1789
+ if (setupArgs[i] === "--tool" && setupArgs[i + 1]) {
1790
+ toolFlag = setupArgs[i + 1];
1791
+ }
1792
+ }
1793
+ return runSetup(toolFlag);
1794
+ }
1500
1795
  const args = rawArgs[0] === "extract" ? rawArgs.slice(1) : rawArgs;
1501
1796
  const { values } = parseArgs({
1502
1797
  args,
@@ -1529,7 +1824,7 @@ async function run() {
1529
1824
  const slug = values.slug ?? slugify(contributor);
1530
1825
  const outputPath = values.output ?? `${GHOSTS_DIR}/${slug}.md`;
1531
1826
  const verbose = values.verbose ?? false;
1532
- const githubToken = values["github-token"];
1827
+ const githubToken = values["github-token"] ?? process.env.GITHUB_PERSONAL_ACCESS_TOKEN ?? process.env.GITHUB_TOKEN;
1533
1828
  const sourcesUsed = [];
1534
1829
  let gitObs = {};
1535
1830
  let codeStyleObs = { observations: [], totalLinesAnalyzed: 0 };
@@ -1632,9 +1927,9 @@ async function run() {
1632
1927
  });
1633
1928
  const dir = path4.dirname(outputPath);
1634
1929
  if (dir && dir !== ".") {
1635
- mkdirSync(dir, { recursive: true });
1930
+ mkdirSync2(dir, { recursive: true });
1636
1931
  }
1637
- writeFileSync(outputPath, ghostMd);
1932
+ writeFileSync2(outputPath, ghostMd);
1638
1933
  console.log(`
1639
1934
  Ghost draft written to: ${outputPath}`);
1640
1935
  console.log("\nNext steps:");
package/dist/index.d.ts CHANGED
@@ -34,6 +34,8 @@ interface ReviewSignals {
34
34
  reviewComments: string[];
35
35
  commentLengths: number[];
36
36
  severityPrefixes: Record<string, number>;
37
+ /** Per-comment severity prefix (parallel array to reviewComments) */
38
+ commentSeverities?: string[];
37
39
  questionComments: number;
38
40
  totalComments: number;
39
41
  source: "github" | "gitlab";
@@ -46,6 +48,10 @@ interface ReviewTheme {
46
48
  ratio: number;
47
49
  /** Verbatim snippets that matched this theme */
48
50
  exampleSnippets: string[];
51
+ /** Breakdown of severity prefixes for comments matching this theme */
52
+ severityBreakdown?: Record<string, number>;
53
+ /** Average character length of comments matching this theme */
54
+ avgCommentLength?: number;
49
55
  }
50
56
  interface CommentToneProfile {
51
57
  /** Fraction of comments that include praise / positive reinforcement */
@@ -61,6 +67,16 @@ interface CommentToneProfile {
61
67
  /** Sample explanatory comments */
62
68
  explanationExamples: string[];
63
69
  }
70
+ interface WeightedDimension {
71
+ dimension: string;
72
+ label: string;
73
+ /** Composite weight (0.0–1.0) derived from frequency, severity, and specificity */
74
+ weight: number;
75
+ confidence: "high" | "moderate" | "low";
76
+ commentCount: number;
77
+ /** Most common severity when this theme appears */
78
+ defaultSeverity: "blocking" | "suggestion" | "nit" | "unknown";
79
+ }
64
80
  interface ReviewObservations {
65
81
  totalReviewComments?: number;
66
82
  avgCommentLength?: number;
@@ -77,6 +93,8 @@ interface ReviewObservations {
77
93
  recurringQuestions?: string[];
78
94
  /** Phrases/vocabulary the contributor uses repeatedly */
79
95
  recurringPhrases?: string[];
96
+ /** Weighted dimensions computed from frequency, severity, and specificity */
97
+ weightedDimensions?: WeightedDimension[];
80
98
  }
81
99
  /** @deprecated Use ReviewSignals instead */
82
100
  type GitLabSignals = ReviewSignals;