@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/index.js CHANGED
@@ -734,31 +734,115 @@ function sampleMatching(comments, patterns, max) {
734
734
  }
735
735
  return matches;
736
736
  }
737
- function extractThemes(comments) {
737
+ function extractThemes(comments, commentSeverities) {
738
738
  if (comments.length === 0) return [];
739
739
  const themes = [];
740
+ const severities = commentSeverities ?? [];
740
741
  for (const def of THEME_DEFS) {
741
742
  const matchingComments = [];
742
- for (const comment of comments) {
743
+ const matchingSeverities = [];
744
+ let totalLength = 0;
745
+ for (let i = 0; i < comments.length; i++) {
746
+ const comment = comments[i];
743
747
  const lower = comment.toLowerCase();
744
748
  if (def.patterns.some((p) => p.test(lower))) {
745
749
  matchingComments.push(comment);
750
+ totalLength += comment.length;
751
+ if (i < severities.length) {
752
+ matchingSeverities.push(severities[i]);
753
+ }
746
754
  }
747
755
  }
748
756
  if (matchingComments.length < 2) continue;
749
757
  const ratio = Math.round(matchingComments.length / comments.length * 100) / 100;
750
758
  const snippets = matchingComments.filter((c) => c.length > 20 && c.length < 300).slice(0, 3).map((c) => c.length > 150 ? c.slice(0, 150) + "..." : c);
759
+ const severityBreakdown = {};
760
+ for (const sev of matchingSeverities) {
761
+ severityBreakdown[sev] = (severityBreakdown[sev] ?? 0) + 1;
762
+ }
751
763
  themes.push({
752
764
  theme: def.theme,
753
765
  label: def.label,
754
766
  count: matchingComments.length,
755
767
  ratio,
756
- exampleSnippets: snippets
768
+ exampleSnippets: snippets,
769
+ severityBreakdown: matchingSeverities.length > 0 ? severityBreakdown : void 0,
770
+ avgCommentLength: Math.round(totalLength / matchingComments.length)
757
771
  });
758
772
  }
759
773
  themes.sort((a, b) => b.count - a.count);
760
774
  return themes;
761
775
  }
776
+ var HIGH_SEVERITY = /* @__PURE__ */ new Set(["blocking", "major"]);
777
+ var MED_SEVERITY = /* @__PURE__ */ new Set(["suggestion", "question", "thought"]);
778
+ var LOW_SEVERITY = /* @__PURE__ */ new Set(["nit", "minor"]);
779
+ function dominantSeverity(breakdown) {
780
+ if (!breakdown) return "unknown";
781
+ let bestCategory = "unknown";
782
+ let bestCount = 0;
783
+ let highCount = 0;
784
+ let medCount = 0;
785
+ let lowCount = 0;
786
+ for (const [sev, count] of Object.entries(breakdown)) {
787
+ if (HIGH_SEVERITY.has(sev)) highCount += count;
788
+ else if (MED_SEVERITY.has(sev)) medCount += count;
789
+ else if (LOW_SEVERITY.has(sev)) lowCount += count;
790
+ }
791
+ if (highCount > bestCount) {
792
+ bestCategory = "blocking";
793
+ bestCount = highCount;
794
+ }
795
+ if (medCount > bestCount) {
796
+ bestCategory = "suggestion";
797
+ bestCount = medCount;
798
+ }
799
+ if (lowCount > bestCount) {
800
+ bestCategory = "nit";
801
+ bestCount = lowCount;
802
+ }
803
+ return bestCategory;
804
+ }
805
+ function computeWeightedDimensions(themes) {
806
+ if (themes.length === 0) return [];
807
+ const maxRatio = Math.max(...themes.map((t) => t.ratio));
808
+ const maxAvgLen = Math.max(
809
+ ...themes.map((t) => t.avgCommentLength ?? 0),
810
+ 1
811
+ );
812
+ return themes.map((theme) => {
813
+ const frequencyScore = maxRatio > 0 ? theme.ratio / maxRatio : 0;
814
+ let severityScore = 0.5;
815
+ if (theme.severityBreakdown) {
816
+ const total = Object.values(theme.severityBreakdown).reduce(
817
+ (a, b) => a + b,
818
+ 0
819
+ );
820
+ if (total > 0) {
821
+ let highCount = 0;
822
+ for (const [sev, count] of Object.entries(theme.severityBreakdown)) {
823
+ if (HIGH_SEVERITY.has(sev)) highCount += count;
824
+ }
825
+ severityScore = highCount / total;
826
+ }
827
+ }
828
+ const avgLen = theme.avgCommentLength ?? 0;
829
+ const specificityScore = maxAvgLen > 0 ? avgLen / maxAvgLen : 0;
830
+ const rawWeight = frequencyScore * 0.5 + severityScore * 0.3 + specificityScore * 0.2;
831
+ const weight = Math.round(Math.min(1, Math.max(0, rawWeight)) * 100) / 100;
832
+ let confidence;
833
+ if (theme.count >= 20) confidence = "high";
834
+ else if (theme.count >= 10) confidence = "moderate";
835
+ else confidence = "low";
836
+ return {
837
+ dimension: theme.theme,
838
+ label: theme.label,
839
+ weight,
840
+ confidence,
841
+ commentCount: theme.count,
842
+ defaultSeverity: dominantSeverity(theme.severityBreakdown)
843
+ };
844
+ });
845
+ }
762
846
  function buildToneProfile(comments) {
763
847
  if (comments.length < 5) return void 0;
764
848
  const n = comments.length;
@@ -802,10 +886,13 @@ function summariseReview(signals) {
802
886
  indices.filter((i) => i >= 0 && i < n).map((i) => sorted[i])
803
887
  )
804
888
  ];
805
- obs.reviewThemes = extractThemes(comments);
889
+ obs.reviewThemes = extractThemes(comments, signals.commentSeverities);
806
890
  obs.toneProfile = buildToneProfile(comments);
807
891
  obs.recurringQuestions = extractRecurringQuestions(comments);
808
892
  obs.recurringPhrases = extractRecurringPhrases(comments);
893
+ if (obs.reviewThemes.length > 0) {
894
+ obs.weightedDimensions = computeWeightedDimensions(obs.reviewThemes);
895
+ }
809
896
  return obs;
810
897
  }
811
898
 
@@ -815,6 +902,7 @@ function extractGitLabSignals(exportPath, contributor, verbose) {
815
902
  reviewComments: [],
816
903
  commentLengths: [],
817
904
  severityPrefixes: {},
905
+ commentSeverities: [],
818
906
  questionComments: 0,
819
907
  totalComments: 0,
820
908
  source: "gitlab"
@@ -852,7 +940,11 @@ function extractGitLabSignals(exportPath, contributor, verbose) {
852
940
  signals.totalComments += 1;
853
941
  const prefixMatch = body.match(prefixRe);
854
942
  if (prefixMatch) {
855
- increment(signals.severityPrefixes, prefixMatch[1].toLowerCase());
943
+ const severity = prefixMatch[1].toLowerCase();
944
+ increment(signals.severityPrefixes, severity);
945
+ signals.commentSeverities.push(severity);
946
+ } else {
947
+ signals.commentSeverities.push("none");
856
948
  }
857
949
  if (body.endsWith("?") || body.toLowerCase().startsWith("do we") || body.toLowerCase().startsWith("should we")) {
858
950
  signals.questionComments += 1;
@@ -945,6 +1037,7 @@ async function extractGitHubSignals(owner, repo, contributor, token, verbose) {
945
1037
  reviewComments: [],
946
1038
  commentLengths: [],
947
1039
  severityPrefixes: {},
1040
+ commentSeverities: [],
948
1041
  questionComments: 0,
949
1042
  totalComments: 0,
950
1043
  source: "github"
@@ -988,7 +1081,11 @@ async function extractGitHubSignals(owner, repo, contributor, token, verbose) {
988
1081
  signals.totalComments += 1;
989
1082
  const prefixMatch = body.match(prefixRe);
990
1083
  if (prefixMatch) {
991
- increment(signals.severityPrefixes, prefixMatch[1].toLowerCase());
1084
+ const severity = prefixMatch[1].toLowerCase();
1085
+ increment(signals.severityPrefixes, severity);
1086
+ signals.commentSeverities.push(severity);
1087
+ } else {
1088
+ signals.commentSeverities.push("none");
992
1089
  }
993
1090
  if (body.endsWith("?") || body.toLowerCase().startsWith("do we") || body.toLowerCase().startsWith("should we")) {
994
1091
  signals.questionComments += 1;
@@ -1096,6 +1193,18 @@ function extractDocsSignals(docsDir, contributor, verbose) {
1096
1193
  }
1097
1194
 
1098
1195
  // src/generator.ts
1196
+ import { readFileSync as readFileSync4 } from "fs";
1197
+ import { fileURLToPath } from "url";
1198
+ import { dirname, join } from "path";
1199
+ function getCliVersion() {
1200
+ try {
1201
+ const dir = dirname(fileURLToPath(import.meta.url));
1202
+ const pkg = JSON.parse(readFileSync4(join(dir, "..", "package.json"), "utf-8"));
1203
+ return pkg.version ?? "unknown";
1204
+ } catch {
1205
+ return "unknown";
1206
+ }
1207
+ }
1099
1208
  function formatPairs(pairs, suffix = "") {
1100
1209
  return pairs.map(([name, count]) => `${name}${suffix} (${count})`).join(", ");
1101
1210
  }
@@ -1121,23 +1230,66 @@ function buildGhostMarkdown(input) {
1121
1230
  const { contributor, slug, gitObs, codeStyleObs, reviewObs, slackObs, docsSignals, sourcesUsed } = input;
1122
1231
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1123
1232
  const domains = gitObs.inferredDomains?.length ? gitObs.inferredDomains.join(", ") : "_[fill in manually]_";
1233
+ const cliVersion = getCliVersion();
1124
1234
  const lines = [
1125
1235
  `# Contributor Soul: ${contributor}`,
1126
1236
  "",
1127
1237
  "## Identity",
1128
1238
  `- **Slug**: ${slug}`,
1239
+ `- **Version**: 0.1.0`,
1240
+ `- **Status**: draft`,
1129
1241
  "- **Role**: _[fill in manually]_",
1130
1242
  `- **Primary domains**: ${domains}`,
1131
1243
  `- **Soul last updated**: ${today}`,
1132
1244
  `- **Sources used**: ${sourcesUsed.join(", ")}`,
1245
+ `- **Generated by**: @poltergeist-ai/cli@${cliVersion}`,
1246
+ "",
1247
+ "---",
1248
+ ""
1249
+ ];
1250
+ const weighted = reviewObs.weightedDimensions;
1251
+ const themes = reviewObs.reviewThemes;
1252
+ if (weighted && weighted.length > 0) {
1253
+ lines.push(
1254
+ "## Review Heuristics",
1255
+ "",
1256
+ `_Inferred from ${reviewObs.totalReviewComments ?? "?"} review comments \u2014 adjust weights as needed_`,
1257
+ "",
1258
+ "| Dimension | Weight | Confidence | Default Severity |",
1259
+ "|---|---|---|---|"
1260
+ );
1261
+ for (const dim of weighted) {
1262
+ lines.push(
1263
+ `| ${dim.label} | ${dim.weight.toFixed(2)} | ${dim.confidence} (${dim.commentCount} comments) | ${dim.defaultSeverity} |`
1264
+ );
1265
+ }
1266
+ lines.push("");
1267
+ }
1268
+ lines.push(
1269
+ "### Tradeoff Preferences",
1270
+ "_How this contributor resolves common engineering tensions. Fill in from review patterns._",
1271
+ "",
1272
+ "- abstraction vs duplication: _[prefer-abstraction | prefer-duplication | balanced]_",
1273
+ "- readability vs performance: _[prefer-readability | prefer-performance | balanced]_",
1274
+ "- speed vs correctness: _[prefer-speed | prefer-correctness | balanced]_",
1275
+ "- local vs system optimization: _[prefer-local | prefer-system | balanced]_",
1276
+ ""
1277
+ );
1278
+ lines.push(
1279
+ "### Scars",
1280
+ "_Historical incidents that make this contributor unusually sensitive to certain patterns._",
1281
+ "_Format: **pattern** (multiplier) \u2014 description. Amplifies: dimension names._",
1133
1282
  "",
1283
+ "_[Fill in manually \u2014 e.g.: **shared-mutable-state** (\xD71.8) \u2014 production incident. Amplifies: error_handling, readability]_",
1284
+ ""
1285
+ );
1286
+ lines.push(
1134
1287
  "---",
1135
1288
  "",
1136
1289
  "## Review Philosophy",
1137
1290
  "",
1138
1291
  "### What they care about most (ranked)"
1139
- ];
1140
- const themes = reviewObs.reviewThemes;
1292
+ );
1141
1293
  if (themes && themes.length > 0) {
1142
1294
  lines.push(
1143
1295
  `_Inferred from ${reviewObs.totalReviewComments ?? "?"} review comments \u2014 verify and re-order as needed_`
@@ -0,0 +1,152 @@
1
+ # Contributor Ghost: Alex Chen
2
+
3
+ ## Identity
4
+ - **Slug**: alex-chen
5
+ - **Version**: 0.2.0
6
+ - **Status**: active
7
+ - **Role**: Senior Frontend Engineer
8
+ - **Primary domains**: Vue.js, GraphQL, component architecture, DX
9
+ - **Soul last updated**: 2025-01-15
10
+ - **Sources used**: git-history, gitlab-comments, slack-export
11
+ - **Generated by**: @poltergeist-ai/cli@0.1.6
12
+
13
+ ---
14
+
15
+ ## Review Heuristics
16
+
17
+ | Dimension | Weight | Confidence | Default Severity |
18
+ |---|---|---|---|
19
+ | Error handling / edge cases | 0.92 | high (28 comments) | blocking |
20
+ | Decomposition / single responsibility | 0.85 | high (22 comments) | suggestion |
21
+ | Naming clarity | 0.78 | moderate (15 comments) | nit |
22
+ | Testing / test coverage | 0.74 | moderate (12 comments) | blocking |
23
+ | Consistency with existing patterns | 0.61 | moderate (10 comments) | suggestion |
24
+ | Readability / clarity | 0.45 | low (6 comments) | suggestion |
25
+
26
+ ### Tradeoff Preferences
27
+
28
+ - abstraction vs duplication: balanced
29
+ - readability vs performance: prefer-readability
30
+ - speed vs correctness: prefer-correctness
31
+ - local vs system optimization: prefer-system
32
+
33
+ ### Scars
34
+
35
+ - **unhandled-api-errors** (×1.8) — production 500 from missing error handling on a third-party API call. Amplifies: error_handling
36
+ - **untested-business-logic** (×1.5) — regression shipped because a critical code path had no test coverage. Amplifies: testing
37
+
38
+ ---
39
+
40
+ ## Review Philosophy
41
+
42
+ ### What they care about most (ranked)
43
+ 1. Correctness — does this work in all states (loading, error, empty)?
44
+ 2. Naming — is this self-documenting without needing to read the implementation?
45
+ 3. Component boundaries — is this doing one thing?
46
+ 4. Test coverage — are the failure paths tested, not just happy paths?
47
+ 5. Consistency — does this follow what we've already established?
48
+
49
+ ### What they tend to ignore
50
+ - Minor formatting (defers entirely to linter/prettier)
51
+ - Build config / bundler changes
52
+ - Dependency bumps unless breaking changes are involved
53
+ - Comment documentation unless the logic is genuinely non-obvious
54
+
55
+ ### Dealbreakers
56
+ - API calls with no error handling
57
+ - Business logic with no tests
58
+ - Breaking changes with no migration path or callout in the MR description
59
+ - TypeScript `any` types without a justifying comment
60
+
61
+ ### Recurring questions they ask
62
+ - "What does the user see when this fails?"
63
+ - "Do we need this complexity right now?"
64
+ - "Is this the right abstraction or are we solving the wrong problem?"
65
+ - "Can we test this?"
66
+
67
+ ---
68
+
69
+ ## Communication Style
70
+
71
+ ### Tone
72
+ Direct but constructive. Doesn't over-explain. Not snarky. Will say when something is good — but rarely.
73
+
74
+ ### Positive feedback
75
+ Sparse and genuine. A single "nice." carries weight. Never leaves a meaningless LGTM.
76
+
77
+ ### How they frame critiques
78
+ Prefers questions over directives — "Do we need this?" not "Remove this."
79
+ Explains *why* when it's not obvious, but skips the why when it is.
80
+
81
+ ### Severity prefixes they use
82
+ - `nit:` — minor, genuinely optional
83
+ - `suggestion:` — worthwhile, not blocking
84
+ - `question:` — genuinely curious, not necessarily a problem
85
+ - `blocking:` — must resolve before merge
86
+ - _(no prefix)_ — treated as a suggestion
87
+
88
+ ### Vocabulary / phrases they use
89
+ - "I'd lean towards..."
90
+ - "This feels like it could bite us later"
91
+ - "Happy path looks good, but..."
92
+ - "Can we test this?"
93
+ - "Not sure I follow the logic here — can you add a comment?"
94
+ - "nit: I'd call this X rather than Y — [brief reason]"
95
+ - "This is clean."
96
+
97
+ ### Comment length
98
+ Very brief. 1–2 sentences unless explaining an alternative approach. Rarely writes paragraphs.
99
+
100
+ ---
101
+
102
+ ## Code Patterns
103
+
104
+ ### Patterns they introduce / prefer
105
+ - Composables over mixins
106
+ - Early returns to avoid deep nesting
107
+ - Named exports (easier to grep and refactor)
108
+ - Constants extracted to the top of the file with descriptive names
109
+ - Explicit TypeScript interfaces over inline types
110
+ - `useQuery` / `useMutation` from vue-query rather than ad-hoc fetch logic
111
+
112
+ ### Patterns they push back on
113
+ - Boolean props that should be variant strings (`type="primary"` not `:isPrimary="true"`)
114
+ - Components over ~200 lines without a good reason
115
+ - Prop drilling more than 2 levels deep
116
+ - Inline styles
117
+ - `any` types without justification
118
+ - Side effects inside computed properties
119
+
120
+ ### Refactors they commonly suggest
121
+ - "Extract this into a composable"
122
+ - "This component is doing two things — can we split it?"
123
+ - "Replace this magic number with a named constant"
124
+
125
+ ---
126
+
127
+ ## Known Blind Spots
128
+
129
+ - Accessibility (aria attributes, keyboard navigation, focus management)
130
+ - Loading states on UI components
131
+ - Mobile / responsive edge cases
132
+ - i18n completeness (sometimes misses that new strings need translating)
133
+
134
+ ---
135
+
136
+ ## Example Review Comments
137
+
138
+ > `nit:` I'd name this `useSubmissionState` rather than `submissionHelper` — the `use` prefix makes it clear it's a composable.
139
+
140
+ > `blocking:` No error handling on the API call on line 42. What does the user see if this 500s?
141
+
142
+ > `question:` Do we need both the `v-if` and the `loading` prop here? Wondering if one can drive the other.
143
+
144
+ > `suggestion:` This could be a composable — we have similar logic in the disclosure form. Worth extracting before this grows.
145
+
146
+ > This is clean. Nice use of the composable pattern.
147
+
148
+ > `blocking:` No tests for the failure case. The happy path test is there but if the API errors we have no coverage.
149
+
150
+ > `nit:` `handleClick` doesn't say much — `handleSubmitForm` or `onSubmit` would be clearer.
151
+
152
+ > Not sure I follow the logic here — can you add a brief comment explaining why we check `isLoaded` before `hasPermission`?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poltergeist-ai/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.9",
4
4
  "description": "Build contributor ghost profiles from git, GitLab, Slack, and docs for simulated code reviews",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,11 +14,16 @@
14
14
  }
15
15
  },
16
16
  "files": [
17
- "dist"
17
+ "dist",
18
+ "skills",
19
+ "ghosts"
18
20
  ],
19
21
  "engines": {
20
22
  "node": ">=18.17.0"
21
23
  },
24
+ "dependencies": {
25
+ "@poltergeist-ai/llm-rules": "0.1.0"
26
+ },
22
27
  "devDependencies": {
23
28
  "@types/node": "^22.0.0",
24
29
  "tsup": "^8.0.0",
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: extract
3
+ description: >
4
+ Build a contributor ghost profile by extracting signals from git history,
5
+ GitHub/GitLab review comments, Slack exports, and design docs.
6
+ Trigger when the user says "build ghost for <name>", "extract ghost",
7
+ "create a ghost profile", "update ghost for <name>", or asks how to
8
+ capture a contributor's review style.
9
+ ---
10
+
11
+ # Extract Ghost Profile
12
+
13
+ Build a contributor ghost by running the poltergeist extractor CLI.
14
+
15
+ ## Gather information
16
+
17
+ Ask the user for the following:
18
+
19
+ **Required:**
20
+ - **Contributor name** (`--contributor`): Use their GitHub username for best GitHub PR comment extraction.
21
+
22
+ **At least one data source:**
23
+ - **Git repo** (`--git-repo`): Local path or remote URL (GitHub/GitLab). If a GitHub URL is provided, the CLI auto-fetches PR review comments.
24
+ - **GitLab export** (`--gitlab-export`): Path to GitLab MR comments JSON export. Most valuable data source for review heuristics.
25
+ - **Slack export** (`--slack-export`): Path to Slack export directory.
26
+ - **Docs directory** (`--docs-dir`): Path to design docs or ADRs.
27
+
28
+ **Optional:**
29
+ - **Email** (`--email`): For git log filtering when contributor name differs from git author.
30
+ - **GitHub token** (`--github-token`): For higher GitHub API rate limits (5000 vs 60 req/hr). Also reads from `GITHUB_PERSONAL_ACCESS_TOKEN` or `GITHUB_TOKEN` environment variables or `.env` file.
31
+ - **Output** (`--output`): Defaults to `.poltergeist/ghosts/<slug>.md`.
32
+ - **Verbose** (`--verbose`): Detailed extraction progress.
33
+
34
+ ## Build and present the command
35
+
36
+ Construct the `npx` command and present it to the user:
37
+
38
+ ```bash
39
+ npx @poltergeist-ai/cli extract \
40
+ --contributor "<name>" \
41
+ --git-repo <path-or-url> \
42
+ [--email <email>] \
43
+ [--gitlab-export <path>] \
44
+ [--slack-export <path>] \
45
+ [--docs-dir <path>] \
46
+ [--github-token <token>] \
47
+ [--output <path>] \
48
+ [--verbose]
49
+ ```
50
+
51
+ ## After extraction
52
+
53
+ 1. Read the generated ghost file
54
+ 2. Check that the `## Review Heuristics` table populated (requires review comment data — GitHub PRs or GitLab exports)
55
+ 3. Identify gaps — especially `_[fill in manually]_` sections
56
+ 4. Ask the user to validate the ranked values, example comments, and tradeoff preferences
57
+ 5. Help fill in scars (historical incidents) and blind spots — these require human knowledge
58
+ 6. Update `Status` from `draft` to `active` once validated
59
+
60
+ ## Ghost file locations
61
+
62
+ - Generated ghosts: `.poltergeist/ghosts/<slug>.md`
63
+ - Feedback data: `.poltergeist/feedback/<slug>.json`
64
+ - Slug format: lowercase hyphenated (`alice-smith.md`)
@@ -0,0 +1,169 @@
1
+ ---
2
+ name: poltergeist
3
+ description: >
4
+ Perform a code review from the perspective of a specific contributor — using their
5
+ voice, values, heuristics, and communication style — even when they are not present.
6
+ Trigger when the user says "review as @name", "review with [name]'s lens",
7
+ "what would [name] say about this", "summon [name]'s ghost", or any phrasing that
8
+ asks for a review through a specific contributor's perspective.
9
+ ---
10
+
11
+ # Poltergeist — Ghost Review
12
+
13
+ Code reviews from contributors who aren't in the room.
14
+
15
+ ---
16
+
17
+ ## Step 1: Load the ghost
18
+
19
+ Read `.poltergeist/ghosts/<slug>.md` in the current project directory. If not found, check `${CLAUDE_PLUGIN_ROOT}/ghosts/<slug>.md` (bundled examples). Slug is lowercase hyphenated: `alice-smith.md`.
20
+
21
+ If no ghost exists for the requested contributor, tell the user to build one first:
22
+
23
+ ```bash
24
+ npx @poltergeist-ai/cli extract --contributor "Name" --git-repo <path-or-url> --output .poltergeist/ghosts/<slug>.md
25
+ ```
26
+
27
+ ## Step 2: Read the code
28
+
29
+ - Piped diff → use directly
30
+ - GitLab MR URL → fetch diff via `glab mr diff <iid>`
31
+ - File path → read the file
32
+
33
+ Read the full diff before writing any comments.
34
+
35
+ ## Step 3: Check for prior feedback
36
+
37
+ If `.poltergeist/feedback/<slug>.json` exists, read it. Use feedback to:
38
+ - Increase attention to dimensions where previous reviews missed concerns
39
+ - Reduce attention to dimensions where previous reviews over-flagged
40
+ - Adjust tone based on voice accuracy notes
41
+
42
+ ## Step 4: Construct the review
43
+
44
+ ### Guiding principles
45
+
46
+ 1. **Voice first** — Sound like them, not like Claude. Read their vocabulary, tone, and example comments before writing a word.
47
+ 2. **Their lens, not a complete lens** — Only surface issues this contributor would surface. This is their perspective, not a comprehensive audit.
48
+ 3. **Evidence-linked** — Each comment should make clear what triggered the concern.
49
+ 4. **Match their density** — If they're terse, produce five comments. If they're thorough, produce fifteen. Don't inflate the review.
50
+ 5. **Acknowledge blind spots** — List what falls outside this ghost's scope so the team knows where a real review may still be needed.
51
+
52
+ ### Using weighted heuristics
53
+
54
+ If the ghost contains a `## Review Heuristics` table, it takes precedence over the ordinal ranked list:
55
+
56
+ 1. **Weights control comment distribution:**
57
+ - Weight > 0.7 → allocate the majority of review comments. These are core concerns.
58
+ - Weight 0.4–0.7 → include 1–3 comments if relevant issues exist.
59
+ - Weight < 0.4 → only mention for egregious violations.
60
+ - Weight < 0.2 → skip entirely unless dealbreaker-level.
61
+
62
+ 2. **Use `Default Severity`** from the table to set severity prefixes for that dimension.
63
+
64
+ 3. **Check tradeoff preferences** — when a change creates tension (e.g., new abstraction vs keeping duplication), frame the comment through the contributor's stated preference:
65
+ > _"I'd normally lean towards keeping this duplicated until we see the pattern repeat."_ (abstraction vs duplication: prefer-duplication)
66
+
67
+ 4. **Check scars** — if a scar pattern is triggered, escalate severity one level (nit → suggestion, suggestion → blocking) and note it:
68
+ > _`blocking:` This introduces shared mutable state across modules. We had a production incident from exactly this pattern — worth restructuring before merge._
69
+
70
+ 5. **Qualify low-confidence dimensions** — if confidence is `low`, add: _(inferred from limited data)_
71
+
72
+ 6. **Cite heuristic basis** — each comment should make clear which dimension triggered it.
73
+
74
+ If no heuristics table exists, fall back to the ordinal ranked list under "What they care about most."
75
+
76
+ ### Core rules
77
+
78
+ - Adopt the contributor's tone and vocabulary from their ghost file
79
+ - Surface issues *they* would surface, in the order *they* would care about them
80
+ - Skip things they historically don't comment on — list as "out of scope for this ghost"
81
+ - Use their severity prefixes (nit:, blocking:, suggestion:, question:)
82
+ - End with a verdict in their voice
83
+
84
+ ---
85
+
86
+ ## Review output format
87
+
88
+ ```
89
+ ## 👻 Review by [Name]
90
+ > `[filename or MR description]`
91
+
92
+ ---
93
+
94
+ ### 🔴 Blocking
95
+ - **[File:line or area]** — [Comment in their voice]
96
+
97
+ ---
98
+
99
+ ### 💬 Suggestions
100
+ - **[File:line or area]** — [Comment in their voice]
101
+
102
+ ---
103
+
104
+ ### 🔹 Nits
105
+ - **[File:line or area]** — [Comment in their voice]
106
+
107
+ ---
108
+
109
+ ### ✅ What's good
110
+
111
+ ---
112
+
113
+ ### Overall
114
+
115
+ ---
116
+
117
+ ### 👻 Out of scope for this ghost
118
+ - [area] — not typically reviewed by [Name]
119
+
120
+ ---
121
+ _Simulated review · poltergeist · ghost: .poltergeist/ghosts/[slug].md · updated [date]_
122
+ _Sources: [git-history | gitlab-comments | slack | docs]_
123
+
124
+ _Calibration: Anything [Name] would've caught that I missed, or anything I flagged that they wouldn't? Your feedback improves future reviews._
125
+ ```
126
+
127
+ ### Section rules
128
+
129
+ - **Blocking** — Only if the ghost's dealbreaker criteria are triggered. Don't invent blocking issues.
130
+ - **Suggestions** — Main body. Order by the ghost's ranked values or weighted dimensions.
131
+ - **Nits** — Only if this contributor leaves nits (check severity prefixes). If they don't, omit entirely.
132
+ - **What's good** — Only if they leave positive feedback. Don't put praise in a terse reviewer's mouth.
133
+ - **Overall** — 1–3 sentences. Most voice-sensitive section. Match their style exactly.
134
+ - **Out of scope** — Always include. Pull from Known Blind Spots. Not optional.
135
+
136
+ ### Confidence signalling
137
+
138
+ Where inferring (not directly supported by ghost data), signal lightly — once or twice per review max:
139
+
140
+ > _(inferred from patterns — no direct example from [Name] for this case)_
141
+
142
+ ---
143
+
144
+ ## Calibration feedback
145
+
146
+ If the user responds to the calibration prompt with feedback:
147
+
148
+ 1. Parse into structured observations:
149
+ - **missed**: concerns the real contributor would have raised
150
+ - **overFlagged**: concerns the ghost raised that the contributor wouldn't care about
151
+ - **voiceAccuracy**: how well tone/phrasing matched ("good", "close", "off")
152
+ - **notes**: any additional context
153
+
154
+ 2. Write or append to `.poltergeist/feedback/<slug>.json`:
155
+ ```json
156
+ {
157
+ "entries": [
158
+ {
159
+ "date": "2026-04-03",
160
+ "missed": ["would have flagged the missing error boundary"],
161
+ "overFlagged": ["wouldn't care about the naming nit on line 42"],
162
+ "voiceAccuracy": "good",
163
+ "notes": ""
164
+ }
165
+ ]
166
+ }
167
+ ```
168
+
169
+ 3. Briefly suggest which heuristic weights might need adjustment based on accumulated feedback.