claudex-setup 1.14.1 → 1.15.1

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/bin/cli.js CHANGED
@@ -249,6 +249,10 @@ async function main() {
249
249
  process.exit(1);
250
250
  }
251
251
 
252
+ if (options.require && normalizedCommand !== 'audit' && !['audit', 'discover'].includes(command)) {
253
+ console.error(`\n Warning: --require is only supported with the audit command. Ignoring for '${normalizedCommand}'.\n`);
254
+ }
255
+
252
256
  if (!KNOWN_COMMANDS.includes(normalizedCommand)) {
253
257
  const suggestion = suggestCommand(command);
254
258
  console.error(`\n Error: Unknown command '${command}'.`);
@@ -326,7 +330,7 @@ async function main() {
326
330
  } else if (normalizedCommand === 'insights') {
327
331
  const https = require('https');
328
332
  const url = 'https://claudex-insights.claudex.workers.dev/v1/stats';
329
- https.get(url, (res) => {
333
+ const req = https.get(url, (res) => {
330
334
  let data = '';
331
335
  res.on('data', chunk => data += chunk);
332
336
  res.on('end', () => {
@@ -357,6 +361,10 @@ async function main() {
357
361
  }).on('error', () => {
358
362
  console.log(' Could not reach insights server. Run locally: npx claudex-setup');
359
363
  });
364
+ req.setTimeout(10000, () => {
365
+ req.destroy();
366
+ console.log(' Insights request timed out. Run locally: npx claudex-setup');
367
+ });
360
368
  return; // keep process alive for http
361
369
  } else if (normalizedCommand === 'augment' || normalizedCommand === 'suggest-only') {
362
370
  const report = await analyzeProject({ ...options, mode: normalizedCommand });
@@ -445,6 +453,15 @@ async function main() {
445
453
  await watch(options);
446
454
  } else if (normalizedCommand === 'setup') {
447
455
  await setup(options);
456
+ if (options.snapshot) {
457
+ const postSetupResult = await audit({ dir: options.dir, silent: true });
458
+ const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
459
+ sourceCommand: 'setup',
460
+ });
461
+ if (!options.json) {
462
+ console.log(` Snapshot saved: ${snapshot.relativePath}`);
463
+ }
464
+ }
448
465
  } else {
449
466
  const result = await audit(options);
450
467
  const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
@@ -64,7 +64,7 @@ Key observations:
64
64
  | Metric | Before | After | Change |
65
65
  |--------|--------|-------|--------|
66
66
  | Audit score | X | X | +X |
67
- | Checks passing | X/58 | X/58 | +X |
67
+ | Checks passing | X/84 | X/84 | +X |
68
68
  | Time to first productive session | Xm | Xm | -Xm |
69
69
  | [Other metric] | | | |
70
70
 
@@ -28,14 +28,10 @@ Add to `.claude/settings.json`:
28
28
  "hooks": {
29
29
  "SessionStart": [
30
30
  {
31
- "hooks": [
32
- {
33
- "type": "command",
34
- "command": "node -e \"try{const r=require('child_process').execSync('npx claudex-setup --json 2>/dev/null',{timeout:15000}).toString();const d=JSON.parse(r);if(d.score<50)console.log(JSON.stringify({systemMessage:'⚠️ Claude Code setup score: '+d.score+'/100. Consider running: npx claudex-setup --lite'}))}catch(e){console.log('{}')}\"",
35
- "timeout": 20,
36
- "statusMessage": "Checking Claude Code setup..."
37
- }
38
- ]
31
+ "type": "command",
32
+ "command": "node -e \"try{const r=require('child_process').execSync('npx claudex-setup --json 2>/dev/null',{timeout:15000}).toString();const d=JSON.parse(r);if(d.score<50)console.log(JSON.stringify({systemMessage:'⚠️ Claude Code setup score: '+d.score+'/100. Consider running: npx claudex-setup --lite'}))}catch(e){console.log('{}')}\"",
33
+ "timeout": 20,
34
+ "statusMessage": "Checking Claude Code setup..."
39
35
  }
40
36
  ]
41
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudex-setup",
3
- "version": "1.14.1",
3
+ "version": "1.15.1",
4
4
  "description": "Score your repo's Claude Code setup against 84 checks. See gaps, apply fixes selectively with rollback, govern hooks and permissions, and benchmark impact — without breaking existing config.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/activity.js CHANGED
@@ -2,8 +2,18 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { version } = require('../package.json');
4
4
 
5
+ let _lastTimestamp = '';
6
+ let _counter = 0;
7
+
5
8
  function timestampId() {
6
- return new Date().toISOString().replace(/[:.]/g, '-');
9
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
10
+ if (ts === _lastTimestamp) {
11
+ _counter++;
12
+ return `${ts}-${_counter}`;
13
+ }
14
+ _lastTimestamp = ts;
15
+ _counter = 0;
16
+ return ts;
7
17
  }
8
18
 
9
19
  function ensureArtifactDirs(dir) {
@@ -122,6 +132,11 @@ function updateSnapshotIndex(snapshotDir, record) {
122
132
  }
123
133
 
124
134
  entries.push(record);
135
+ // Prune to keep last 200 entries
136
+ const MAX_INDEX_ENTRIES = 200;
137
+ if (entries.length > MAX_INDEX_ENTRIES) {
138
+ entries = entries.slice(entries.length - MAX_INDEX_ENTRIES);
139
+ }
125
140
  fs.writeFileSync(indexPath, JSON.stringify(entries, null, 2), 'utf8');
126
141
  }
127
142
 
package/src/analyze.js CHANGED
@@ -424,7 +424,9 @@ function printAnalysis(report, options = {}) {
424
424
  if (item.risk || item.confidence) {
425
425
  console.log(c(` Risk: ${item.risk || 'low'} | Confidence: ${item.confidence || 'medium'}`, 'dim'));
426
426
  }
427
- console.log(c(` Fix: ${item.fix}`, 'dim'));
427
+ if (item.fix && item.fix !== item.why) {
428
+ console.log(c(` Fix: ${item.fix}`, 'dim'));
429
+ }
428
430
  });
429
431
  console.log('');
430
432
  }
@@ -515,7 +517,9 @@ function exportMarkdown(report) {
515
517
  if (item.risk || item.confidence) {
516
518
  lines.push(` - Risk / Confidence: ${item.risk || 'low'} / ${item.confidence || 'medium'}`);
517
519
  }
518
- lines.push(` - Fix: ${item.fix}`);
520
+ if (item.fix && item.fix !== item.why) {
521
+ lines.push(` - Fix: ${item.fix}`);
522
+ }
519
523
  });
520
524
  lines.push('');
521
525
  }
package/src/audit.js CHANGED
@@ -85,12 +85,15 @@ function getPrioritizedFailed(failed) {
85
85
  function getQuickWins(failed) {
86
86
  const pool = getPrioritizedFailed(failed);
87
87
 
88
+ // QuickWins prioritize short fixes (easy to implement) first, then impact
88
89
  return [...pool]
89
90
  .sort((a, b) => {
91
+ const fixLenA = (a.fix || '').length;
92
+ const fixLenB = (b.fix || '').length;
93
+ if (fixLenA !== fixLenB) return fixLenA - fixLenB;
90
94
  const impactA = IMPACT_ORDER[a.impact] ?? 0;
91
95
  const impactB = IMPACT_ORDER[b.impact] ?? 0;
92
- if (impactA !== impactB) return impactB - impactA;
93
- return (a.fix || '').length - (b.fix || '').length;
96
+ return impactB - impactA;
94
97
  })
95
98
  .slice(0, 3);
96
99
  }
@@ -202,14 +205,14 @@ async function audit(options) {
202
205
  const medium = failed.filter(r => r.impact === 'medium');
203
206
 
204
207
  // Calculate score only from applicable checks
205
- const weights = { critical: 15, high: 10, medium: 5 };
208
+ const weights = { critical: 15, high: 10, medium: 5, low: 2 };
206
209
  const maxScore = applicable.reduce((sum, r) => sum + (weights[r.impact] || 5), 0);
207
210
  const earnedScore = passed.reduce((sum, r) => sum + (weights[r.impact] || 5), 0);
208
211
  const score = maxScore > 0 ? Math.round((earnedScore / maxScore) * 100) : 0;
209
212
 
210
213
  // Detect scaffolded vs organic: if CLAUDE.md contains our version stamp, some checks
211
214
  // are passing because WE generated them, not the user
212
- const claudeMd = ctx.fileContent('CLAUDE.md') || '';
215
+ const claudeMd = ctx.claudeMdContent() || '';
213
216
  const isScaffolded = claudeMd.includes('Generated by claudex-setup') ||
214
217
  claudeMd.includes('claudex-setup');
215
218
  // Scaffolded checks: things our setup creates (CLAUDE.md, hooks, commands, agents, rules, skills)
package/src/benchmark.js CHANGED
@@ -21,6 +21,9 @@ function copyProject(sourceDir, targetDir) {
21
21
  copyProject(from, to);
22
22
  } else if (entry.isFile()) {
23
23
  fs.copyFileSync(from, to);
24
+ } else if (entry.isSymbolicLink && entry.isSymbolicLink()) {
25
+ // Symlinks are skipped in benchmark sandbox — log for awareness
26
+ process.stderr.write(` Note: symlink skipped in benchmark: ${entry.name}\n`);
24
27
  }
25
28
  }
26
29
  }
@@ -89,7 +92,9 @@ function buildExecutiveSummary(before, after, workflowEvidence) {
89
92
  const workflowCoverage = workflowEvidence.summary.coverageScore;
90
93
  let headline = 'Benchmark did not improve the score in this run.';
91
94
 
92
- if (scoreDelta > 0) {
95
+ if (scoreDelta < 0) {
96
+ headline = `Warning: score decreased by ${Math.abs(scoreDelta)} points. Setup may have introduced a regression.`;
97
+ } else if (scoreDelta > 0) {
93
98
  headline = `Benchmark improved readiness by ${scoreDelta} points without touching the original repo.`;
94
99
  } else if (before.score >= 85 && after.score >= before.score && workflowCoverage >= 80) {
95
100
  headline = 'Benchmark confirmed the repo already meets the starter-safe baseline without regression.';
package/src/context.js CHANGED
@@ -67,6 +67,10 @@ class ProjectContext {
67
67
  }
68
68
  }
69
69
 
70
+ claudeMdContent() {
71
+ return this.fileContent('CLAUDE.md') || this.fileContent('.claude/CLAUDE.md');
72
+ }
73
+
70
74
  fileContent(filePath) {
71
75
  if (this._cache[filePath] !== undefined) return this._cache[filePath];
72
76
  const fullPath = path.join(this.dir, filePath);
@@ -156,17 +156,18 @@ function detectDomainPacks(ctx, stacks, assets = null) {
156
156
  }
157
157
 
158
158
  const hasFrontend = stackKeys.has('react') || stackKeys.has('nextjs') || stackKeys.has('vue') ||
159
- stackKeys.has('angular') || stackKeys.has('svelte') || ctx.hasDir('components') || ctx.hasDir('app') || ctx.hasDir('pages');
159
+ stackKeys.has('angular') || stackKeys.has('svelte') || ctx.hasDir('components') || ctx.hasDir('pages') ||
160
+ (ctx.hasDir('app') && (deps.next || deps.react || deps.vue || deps['@angular/core'] || deps.svelte));
160
161
  const hasBackend = stackKeys.has('node') || stackKeys.has('python') || stackKeys.has('django') ||
161
162
  stackKeys.has('fastapi') || stackKeys.has('go') || stackKeys.has('rust') || stackKeys.has('java') ||
162
163
  ctx.hasDir('api') || ctx.hasDir('routes') || ctx.hasDir('services') || ctx.hasDir('controllers');
163
164
  const hasData = ctx.hasDir('dags') || ctx.hasDir('jobs') || ctx.hasDir('workers') ||
164
- ctx.hasDir('models') || ctx.hasDir('migrations') || ctx.hasDir('db') ||
165
+ ctx.hasDir('migrations') || ctx.hasDir('db') ||
165
166
  deps.dbt || deps['apache-airflow'] || deps.pandas || deps.polars || deps.duckdb;
166
167
  const hasInfra = stackKeys.has('docker') || stackKeys.has('terraform') || stackKeys.has('kubernetes') ||
167
168
  ctx.files.includes('wrangler.toml') || ctx.files.includes('serverless.yml') || ctx.files.includes('serverless.yaml') ||
168
169
  ctx.files.includes('cdk.json') || ctx.hasDir('infra') || ctx.hasDir('deploy') || ctx.hasDir('helm');
169
- const isOss = !!ctx.fileContent('LICENSE') && !!ctx.fileContent('CONTRIBUTING.md') && pkg.private !== true;
170
+ const isOss = !!ctx.fileContent('LICENSE') && pkg.private !== true;
170
171
  const isEnterpriseGoverned = !!(assets && assets.permissions && assets.permissions.hasDenyRules) &&
171
172
  !!(assets && assets.files && assets.files.settings) && ctx.hasDir('.github/workflows');
172
173
 
@@ -218,7 +219,8 @@ function detectDomainPacks(ctx, stacks, assets = null) {
218
219
 
219
220
  // Monorepo detection
220
221
  const isMonorepo = ctx.files.includes('nx.json') || ctx.files.includes('turbo.json') ||
221
- ctx.files.includes('lerna.json') || ctx.hasDir('packages') ||
222
+ ctx.files.includes('lerna.json') || ctx.files.includes('pnpm-workspace.yaml') ||
223
+ ctx.hasDir('packages') ||
222
224
  (pkg.workspaces && (Array.isArray(pkg.workspaces) ? pkg.workspaces.length > 0 : true));
223
225
  if (isMonorepo) {
224
226
  addMatch('monorepo', [
@@ -259,7 +261,8 @@ function detectDomainPacks(ctx, stacks, assets = null) {
259
261
  // E-commerce detection
260
262
  const isEcommerce = deps.stripe || deps['@stripe/stripe-js'] || deps.shopify || deps['@shopify/shopify-api'] ||
261
263
  deps.woocommerce || deps.paypal || deps['@paypal/react-paypal-js'] || deps.square || deps['@adyen/adyen-web'] ||
262
- deps.medusa || deps.saleor ||
264
+ deps.medusa || deps.saleor || deps.braintree || deps['@mollie/api-client'] ||
265
+ deps.razorpay || deps['@paddle/paddle-node-sdk'] || deps['@lemonsqueezy/lemonsqueezy.js'] ||
263
266
  ctx.hasDir('products') || ctx.hasDir('checkout') || ctx.hasDir('cart');
264
267
  if (isEcommerce) {
265
268
  addMatch('ecommerce', [
@@ -274,8 +277,10 @@ function detectDomainPacks(ctx, stacks, assets = null) {
274
277
  deps['@anthropic-ai/sdk'] || deps.transformers || deps.torch || deps.tensorflow ||
275
278
  deps.llamaindex || deps['llama-index'] || deps.crewai || deps.autogen ||
276
279
  deps['@ai-sdk/core'] || deps.ollama ||
277
- ctx.hasDir('chains') || ctx.hasDir('agents') || ctx.hasDir('models') || ctx.hasDir('prompts');
278
- if (isAiMl && !hasData) {
280
+ deps['@microsoft/semantic-kernel'] || deps['haystack-ai'] || deps['dspy-ai'] ||
281
+ deps.instructor || deps['@google/generative-ai'] || deps.cohere || deps.mistralai ||
282
+ ctx.hasDir('chains') || ctx.hasDir('agents') || ctx.hasDir('prompts');
283
+ if (isAiMl) {
279
284
  addMatch('ai-ml', [
280
285
  'Detected AI/ML dependencies or agent structure.',
281
286
  deps.langchain ? 'LangChain detected.' : null,
@@ -287,7 +292,7 @@ function detectDomainPacks(ctx, stacks, assets = null) {
287
292
  const isDevopsCicd = ctx.hasDir('.github/workflows') || ctx.hasDir('.circleci') ||
288
293
  ctx.files.includes('Jenkinsfile') || ctx.files.includes('.gitlab-ci.yml') ||
289
294
  ctx.hasDir('deploy') || ctx.hasDir('scripts/deploy');
290
- if (isDevopsCicd && !hasInfra) {
295
+ if (isDevopsCicd) {
291
296
  addMatch('devops-cicd', [
292
297
  'Detected CI/CD pipelines or deployment scripts.',
293
298
  ctx.hasDir('.github/workflows') ? 'GitHub Actions detected.' : null,
@@ -298,8 +303,10 @@ function detectDomainPacks(ctx, stacks, assets = null) {
298
303
  // Design system detection
299
304
  const isDesignSystem = deps.storybook || deps['@storybook/react'] || deps['@storybook/vue3'] ||
300
305
  deps.chromatic || deps['style-dictionary'] || deps['@tokens-studio/sd-transforms'] ||
301
- ctx.hasDir('tokens') || ctx.hasDir('design-tokens') ||
302
- ctx.hasDir('.storybook') || (ctx.hasDir('components') && ctx.hasDir('.storybook'));
306
+ deps['@radix-ui/react-primitives'] || deps['@headlessui/react'] ||
307
+ ctx.hasDir('tokens') || ctx.hasDir('design-tokens') || ctx.hasDir('primitives') ||
308
+ ctx.hasDir('.storybook') ||
309
+ (ctx.hasDir('components') && (deps['tailwindcss'] || deps.tailwindcss) && ctx.hasDir('packages'));
303
310
  if (isDesignSystem) {
304
311
  addMatch('design-system', [
305
312
  'Detected design system or component library signals.',
@@ -321,8 +328,9 @@ function detectDomainPacks(ctx, stacks, assets = null) {
321
328
  }
322
329
 
323
330
  // Security-focused detection
324
- const isSecurityFocused = hasSecurityPolicy &&
325
- (hasBackend || deps.bcrypt || deps.jsonwebtoken || deps.passport || deps['next-auth']);
331
+ const hasAuthDeps = deps.bcrypt || deps.jsonwebtoken || deps.passport || deps['next-auth'] ||
332
+ deps['@auth/core'] || deps['lucia'] || deps['better-auth'];
333
+ const isSecurityFocused = (hasSecurityPolicy || hasAuthDeps) && hasBackend;
326
334
  if (isSecurityFocused && !isRegulated) {
327
335
  addMatch('security-focused', [
328
336
  'Detected security-sensitive backend with auth dependencies.',
package/src/governance.js CHANGED
@@ -124,7 +124,7 @@ const HOOK_REGISTRY = [
124
124
  },
125
125
  {
126
126
  key: 'session-init',
127
- file: '.claude/hooks/rotate-logs.sh',
127
+ file: '.claude/hooks/session-start.sh',
128
128
  triggerPoint: 'SessionStart',
129
129
  matcher: null,
130
130
  purpose: 'Rotates large log files and loads workspace context at session start.',
@@ -254,36 +254,46 @@ function buildHookConfig(hookFiles, profileKey) {
254
254
  return {};
255
255
  }
256
256
 
257
+ // Detect hook runtime: .js files use node, .sh files use bash
258
+ const hookCommand = (file) => {
259
+ if (file.endsWith('.js')) return `node .claude/hooks/${file}`;
260
+ return `bash .claude/hooks/${file}`;
261
+ };
262
+ const isSecrets = (f) => f === 'protect-secrets.sh' || f === 'protect-secrets.js';
263
+ const isSession = (f) => f === 'session-start.sh' || f === 'session-start.js';
264
+
257
265
  const hookConfig = {
258
266
  PostToolUse: [{
259
267
  matcher: 'Write|Edit',
260
268
  hooks: uniqueFiles
261
- .filter(file => file !== 'protect-secrets.sh' && file !== 'session-start.sh')
269
+ .filter(file => !isSecrets(file) && !isSession(file))
262
270
  .map(file => ({
263
271
  type: 'command',
264
- command: `bash .claude/hooks/${file}`,
272
+ command: hookCommand(file),
265
273
  timeout: 10,
266
274
  })),
267
275
  }],
268
276
  };
269
277
 
270
- if (uniqueFiles.includes('protect-secrets.sh')) {
278
+ const secretsFile = uniqueFiles.find(isSecrets);
279
+ if (secretsFile) {
271
280
  hookConfig.PreToolUse = [{
272
281
  matcher: 'Read|Write|Edit',
273
282
  hooks: [{
274
283
  type: 'command',
275
- command: 'bash .claude/hooks/protect-secrets.sh',
284
+ command: hookCommand(secretsFile),
276
285
  timeout: 5,
277
286
  }],
278
287
  }];
279
288
  }
280
289
 
281
- if (uniqueFiles.includes('session-start.sh')) {
290
+ const sessionFile = uniqueFiles.find(isSession);
291
+ if (sessionFile) {
282
292
  hookConfig.SessionStart = [{
283
293
  matcher: '*',
284
294
  hooks: [{
285
295
  type: 'command',
286
- command: 'bash .claude/hooks/session-start.sh',
296
+ command: hookCommand(sessionFile),
287
297
  timeout: 5,
288
298
  }],
289
299
  }];
@@ -451,6 +461,8 @@ function renderGovernanceMarkdown(summary) {
451
461
  }
452
462
 
453
463
  lines.push('');
464
+ lines.push('---');
465
+ lines.push(`*Generated by claudex-setup v${require('../package.json').version} on ${new Date().toISOString().split('T')[0]}*`);
454
466
  return lines.join('\n');
455
467
  }
456
468
 
package/src/mcp-packs.js CHANGED
@@ -40,12 +40,11 @@ const MCP_PACKS = [
40
40
  key: 'postgres-mcp',
41
41
  label: 'PostgreSQL',
42
42
  useWhen: 'Repos with PostgreSQL databases that benefit from schema inspection and query assistance.',
43
- adoption: 'Useful for backend-api and data-pipeline repos. Requires DATABASE_URL env var.',
43
+ adoption: 'Useful for backend-api and data-pipeline repos. Pass connection string as CLI argument.',
44
44
  servers: {
45
45
  postgres: {
46
46
  command: 'npx',
47
- args: ['-y', '@modelcontextprotocol/server-postgres'],
48
- env: { DATABASE_URL: '${DATABASE_URL}' },
47
+ args: ['-y', '@modelcontextprotocol/server-postgres', '${DATABASE_URL}'],
49
48
  },
50
49
  },
51
50
  },
@@ -77,7 +76,7 @@ const MCP_PACKS = [
77
76
  key: 'docker-mcp',
78
77
  label: 'Docker',
79
78
  useWhen: 'Repos with containerized workflows that benefit from container management during Claude sessions.',
80
- adoption: 'Useful for infra-platform and backend repos. Requires Docker running locally.',
79
+ adoption: 'Community Docker MCP server. Requires Docker running locally. Note: community-maintained package.',
81
80
  servers: {
82
81
  docker: {
83
82
  command: 'npx',
@@ -128,7 +127,7 @@ const MCP_PACKS = [
128
127
  key: 'slack-mcp',
129
128
  label: 'Slack',
130
129
  useWhen: 'Teams using Slack that want Claude to draft, preview, or post messages.',
131
- adoption: 'Useful for team workflows. Requires SLACK_BOT_TOKEN env var.',
130
+ adoption: 'Community Slack MCP server (supports OAuth and stealth mode). Requires SLACK_BOT_TOKEN or SLACK_COOKIES env var.',
132
131
  servers: {
133
132
  slack: {
134
133
  command: 'npx',
@@ -154,7 +153,7 @@ const MCP_PACKS = [
154
153
  key: 'figma-mcp',
155
154
  label: 'Figma',
156
155
  useWhen: 'Design-heavy repos where Claude needs access to Figma designs and components.',
157
- adoption: 'Useful for frontend-ui repos with design systems. Requires FIGMA_ACCESS_TOKEN env var.',
156
+ adoption: 'Community Figma MCP server. Requires FIGMA_ACCESS_TOKEN env var. Note: community-maintained package.',
158
157
  servers: {
159
158
  figma: {
160
159
  command: 'npx',
@@ -202,8 +201,8 @@ const MCP_PACKS = [
202
201
  },
203
202
  {
204
203
  key: 'jira-confluence',
205
- label: 'Jira + Confluence',
206
- useWhen: 'Teams using Atlassian for issue tracking and documentation.',
204
+ label: 'Jira',
205
+ useWhen: 'Teams using Atlassian Jira for issue tracking and project management.',
207
206
  adoption: 'Requires ATLASSIAN_API_TOKEN and ATLASSIAN_EMAIL env vars.',
208
207
  servers: {
209
208
  jira: {
@@ -217,12 +216,12 @@ const MCP_PACKS = [
217
216
  key: 'ga4-analytics',
218
217
  label: 'Google Analytics 4',
219
218
  useWhen: 'Repos with web analytics needs — live GA4 data, attribution, and audience insights.',
220
- adoption: 'Requires GA4 credentials. 1,641 stars.',
219
+ adoption: 'Requires GA4_PROPERTY_ID and either GOOGLE_APPLICATION_CREDENTIALS or ADC for auth.',
221
220
  servers: {
222
221
  ga4: {
223
222
  command: 'npx',
224
223
  args: ['-y', 'mcp-server-ga4'],
225
- env: { GA4_PROPERTY_ID: '${GA4_PROPERTY_ID}' },
224
+ env: { GA4_PROPERTY_ID: '${GA4_PROPERTY_ID}', GOOGLE_APPLICATION_CREDENTIALS: '${GOOGLE_APPLICATION_CREDENTIALS}' },
226
225
  },
227
226
  },
228
227
  },
@@ -230,12 +229,12 @@ const MCP_PACKS = [
230
229
  key: 'search-console',
231
230
  label: 'Google Search Console',
232
231
  useWhen: 'SEO-focused repos that need search performance data, indexing status, and sitemap insights.',
233
- adoption: 'Requires GSC credentials. 595 stars.',
232
+ adoption: 'Requires Google OAuth client credentials (client ID + secret). Uses OAuth consent flow.',
234
233
  servers: {
235
234
  gsc: {
236
235
  command: 'npx',
237
236
  args: ['-y', 'mcp-gsc@latest'],
238
- env: { GSC_CREDENTIALS: '${GSC_CREDENTIALS}' },
237
+ env: { GOOGLE_CLIENT_ID: '${GOOGLE_CLIENT_ID}', GOOGLE_CLIENT_SECRET: '${GOOGLE_CLIENT_SECRET}' },
239
238
  },
240
239
  },
241
240
  },
@@ -243,7 +242,7 @@ const MCP_PACKS = [
243
242
  key: 'n8n-workflows',
244
243
  label: 'n8n Workflow Automation',
245
244
  useWhen: 'Teams using n8n for workflow automation with 1,396 integration nodes.',
246
- adoption: 'Requires n8n instance URL. 17,092 stars.',
245
+ adoption: 'Requires n8n instance URL and API key.',
247
246
  servers: {
248
247
  n8n: {
249
248
  command: 'npx',
@@ -256,7 +255,7 @@ const MCP_PACKS = [
256
255
  key: 'zendesk-mcp',
257
256
  label: 'Zendesk',
258
257
  useWhen: 'Support teams using Zendesk for ticket management and help center content.',
259
- adoption: 'Requires ZENDESK_API_TOKEN env var. 79 stars.',
258
+ adoption: 'Requires ZENDESK_API_TOKEN and ZENDESK_SUBDOMAIN env vars.',
260
259
  servers: {
261
260
  zendesk: {
262
261
  command: 'npx',
@@ -269,7 +268,7 @@ const MCP_PACKS = [
269
268
  key: 'infisical-secrets',
270
269
  label: 'Infisical Secrets',
271
270
  useWhen: 'Repos using Infisical for secrets management with auto-rotation.',
272
- adoption: 'Requires INFISICAL_TOKEN env var. 25,629 stars.',
271
+ adoption: 'Requires INFISICAL_TOKEN env var.',
273
272
  servers: {
274
273
  infisical: {
275
274
  command: 'npx',
@@ -282,7 +281,7 @@ const MCP_PACKS = [
282
281
  key: 'shopify-mcp',
283
282
  label: 'Shopify',
284
283
  useWhen: 'Shopify stores and apps that need API schema access and deployment tooling.',
285
- adoption: 'Official Shopify dev MCP. Requires SHOPIFY_ACCESS_TOKEN env var.',
284
+ adoption: 'Community Shopify MCP server for GraphQL API access. Requires SHOPIFY_ACCESS_TOKEN env var.',
286
285
  servers: {
287
286
  shopify: {
288
287
  command: 'npx',
@@ -307,7 +306,7 @@ const MCP_PACKS = [
307
306
  {
308
307
  key: 'blender-mcp',
309
308
  label: 'Blender 3D',
310
- useWhen: '3D modeling, animation, or rendering repos that use Blender. 18,219 stars.',
309
+ useWhen: '3D modeling, animation, or rendering repos that use Blender.',
311
310
  adoption: 'Requires Blender installed locally. Python bridge.',
312
311
  servers: {
313
312
  blender: {
@@ -320,7 +319,7 @@ const MCP_PACKS = [
320
319
  key: 'wordpress-mcp',
321
320
  label: 'WordPress',
322
321
  useWhen: 'WordPress sites needing content management, site ops, and plugin workflows.',
323
- adoption: 'Requires WP_URL and WP_AUTH_TOKEN env vars. 115 stars.',
322
+ adoption: 'Requires WP_URL and WP_AUTH_TOKEN env vars.',
324
323
  servers: {
325
324
  wordpress: {
326
325
  command: 'npx',
@@ -534,14 +533,17 @@ function recommendMcpPacks(stacks = [], domainPacks = [], options = {}) {
534
533
  recommended.add('jira-confluence');
535
534
  }
536
535
 
537
- // Analytics for ecommerce and marketing
538
- if (domainKeys.has('ecommerce') || domainKeys.has('docs-content')) {
536
+ // Analytics for ecommerce (docs-content repos rarely need GA4/GSC)
537
+ if (domainKeys.has('ecommerce')) {
539
538
  recommended.add('ga4-analytics');
540
539
  recommended.add('search-console');
541
540
  }
542
541
 
543
- // Shopify for ecommerce
544
- if (domainKeys.has('ecommerce')) {
542
+ // Shopify only when Shopify signals are present
543
+ if (domainKeys.has('ecommerce') && ctx && (
544
+ hasDependency(deps, 'shopify') || hasDependency(deps, '@shopify/shopify-api') ||
545
+ hasFileContentMatch(ctx, '.env', /shopify/i) || hasFileContentMatch(ctx, '.env.example', /shopify/i)
546
+ )) {
545
547
  recommended.add('shopify-mcp');
546
548
  }
547
549
 
@@ -550,8 +552,11 @@ function recommendMcpPacks(stacks = [], domainPacks = [], options = {}) {
550
552
  recommended.add('huggingface-mcp');
551
553
  }
552
554
 
553
- // Zendesk for support
554
- if (domainKeys.has('enterprise-governed')) {
555
+ // Zendesk only when Zendesk signals are present
556
+ if (domainKeys.has('enterprise-governed') && ctx && (
557
+ hasDependency(deps, 'zendesk') || hasDependency(deps, 'node-zendesk') ||
558
+ hasFileContentMatch(ctx, '.env', /zendesk/i) || hasFileContentMatch(ctx, '.env.example', /zendesk/i)
559
+ )) {
555
560
  recommended.add('zendesk-mcp');
556
561
  }
557
562
 
package/src/plans.js CHANGED
@@ -63,7 +63,7 @@ function previewContent(content) {
63
63
  }
64
64
 
65
65
  function riskFromImpact(impact) {
66
- if (impact === 'critical') return 'medium';
66
+ if (impact === 'critical') return 'high';
67
67
  if (impact === 'high') return 'medium';
68
68
  return 'low';
69
69
  }
@@ -260,7 +260,7 @@ function buildAgentPatchFiles(ctx) {
260
260
 
261
261
  function buildHookSettings(ctx, plannedHookFiles, options = {}) {
262
262
  const existing = ctx.hasDir('.claude/hooks')
263
- ? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh'))
263
+ ? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh') || file.endsWith('.js'))
264
264
  : [];
265
265
  const hookFiles = [...new Set([...existing, ...plannedHookFiles])].sort();
266
266
  if (hookFiles.length === 0) {
@@ -385,7 +385,7 @@ async function buildProposalBundle(options) {
385
385
  if (templateKey === 'hooks') {
386
386
  const plannedHookFiles = templateFiles
387
387
  .map(file => path.basename(file.path))
388
- .filter(file => file.endsWith('.sh'));
388
+ .filter(file => file.endsWith('.sh') || file.endsWith('.js'));
389
389
  const settingsFile = buildHookSettings(ctx, plannedHookFiles, options);
390
390
  if (settingsFile) {
391
391
  templateFiles.push(settingsFile);
@@ -476,7 +476,7 @@ function applyRuntimeSettingsOverlays(bundle, options) {
476
476
 
477
477
  const ctx = new ProjectContext(options.dir);
478
478
  const existingHooks = ctx.hasDir('.claude/hooks')
479
- ? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh'))
479
+ ? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh') || file.endsWith('.js'))
480
480
  : [];
481
481
 
482
482
  const proposals = bundle.proposals.map((proposal) => {
package/src/setup.js CHANGED
@@ -367,7 +367,7 @@ function generateMermaid(dirs, stacks) {
367
367
  nodes.push(addNode('Entry Point', 'round'));
368
368
  }
369
369
 
370
- const root = ids['Next.js'] || ids['Django'] || ids['FastAPI'] || ids['Entry Point'];
370
+ const root = ids['Next.js'] || ids['Django'] || ids['FastAPI'] || ids['Entry Point'] || 'A';
371
371
  const pickNodeId = (...labels) => labels.map(label => ids[label]).find(Boolean) || root;
372
372
 
373
373
  // Detect layers
@@ -782,66 +782,64 @@ ${verificationSteps.join('\n')}
782
782
  },
783
783
 
784
784
  'hooks': () => ({
785
- 'on-edit-lint.sh': `#!/bin/bash
786
- # PostToolUse hook - runs linter after file edits
787
- # Detects which linter is available and runs it
788
-
789
- if command -v npx &>/dev/null; then
790
- if [ -f "package.json" ] && grep -q '"lint"' package.json 2>/dev/null; then
791
- npm run lint --silent 2>/dev/null
792
- elif [ -f ".eslintrc" ] || [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f "eslint.config.js" ]; then
793
- npx eslint --fix . --quiet 2>/dev/null
794
- fi
795
- elif command -v ruff &>/dev/null; then
796
- ruff check --fix . 2>/dev/null
797
- fi
785
+ 'on-edit-lint.js': `#!/usr/bin/env node
786
+ // PostToolUse hook - runs linter after file edits
787
+ const { execSync } = require('child_process');
788
+ const fs = require('fs');
789
+ try {
790
+ if (fs.existsSync('package.json')) {
791
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
792
+ if (pkg.scripts && pkg.scripts.lint) {
793
+ execSync('npm run lint --silent', { stdio: 'ignore', timeout: 30000 });
794
+ }
795
+ }
796
+ } catch (e) { /* linter not available or failed - non-blocking */ }
798
797
  `,
799
- 'protect-secrets.sh': `#!/bin/bash
800
- # PreToolUse hook - blocks reads of secret files
801
- INPUT=$(cat -)
802
- FILE_PATH=$(echo "$INPUT" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
803
-
804
- if echo "$FILE_PATH" | grep -qiE '\\.env$|\\.env\\.|secrets/|credentials|\\.pem$|\\.key$'; then
805
- echo '{"decision": "block", "reason": "Blocked: accessing secret/credential files is not allowed."}'
806
- exit 0
807
- fi
808
- echo '{"decision": "allow"}'
798
+ 'protect-secrets.js': `#!/usr/bin/env node
799
+ // PreToolUse hook - blocks reads of secret files
800
+ let input = '';
801
+ process.stdin.on('data', d => input += d);
802
+ process.stdin.on('end', () => {
803
+ try {
804
+ const data = JSON.parse(input);
805
+ const fp = (data.tool_input && data.tool_input.file_path) || '';
806
+ if (/\\.env$|\\.env\\.|secrets[\\/\\\\]|credentials|\\.pem$|\\.key$/i.test(fp)) {
807
+ console.log(JSON.stringify({ decision: 'block', reason: 'Blocked: accessing secret/credential files is not allowed.' }));
808
+ } else {
809
+ console.log(JSON.stringify({ decision: 'allow' }));
810
+ }
811
+ } catch (e) {
812
+ console.log(JSON.stringify({ decision: 'allow' }));
813
+ }
814
+ });
809
815
  `,
810
- 'log-changes.sh': `#!/bin/bash
811
- # PostToolUse hook - logs all file changes with timestamps
812
- # Appends to .claude/logs/file-changes.log
813
-
814
- INPUT=$(cat -)
815
- TOOL_NAME=$(echo "$INPUT" | sed -n 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
816
- TOOL_NAME=\${TOOL_NAME:-unknown}
817
- FILE_PATH=$(echo "$INPUT" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
818
-
819
- if [ -z "$FILE_PATH" ]; then
820
- exit 0
821
- fi
822
-
823
- LOG_DIR=".claude/logs"
824
- LOG_FILE="$LOG_DIR/file-changes.log"
825
-
826
- mkdir -p "$LOG_DIR"
827
-
828
- TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
829
- echo "[$TIMESTAMP] $TOOL_NAME: $FILE_PATH" >> "$LOG_FILE"
830
-
831
- exit 0
816
+ 'log-changes.js': `#!/usr/bin/env node
817
+ // PostToolUse hook - logs all file changes with timestamps
818
+ const fs = require('fs');
819
+ const path = require('path');
820
+ let input = '';
821
+ process.stdin.on('data', d => input += d);
822
+ process.stdin.on('end', () => {
823
+ try {
824
+ const data = JSON.parse(input);
825
+ const fp = (data.tool_input && data.tool_input.file_path) || '';
826
+ if (!fp) process.exit(0);
827
+ const toolName = data.tool_name || 'unknown';
828
+ const logDir = path.join('.claude', 'logs');
829
+ fs.mkdirSync(logDir, { recursive: true });
830
+ const ts = new Date().toISOString().replace('T', ' ').split('.')[0];
831
+ fs.appendFileSync(path.join(logDir, 'file-changes.log'), \`[\${ts}] \${toolName}: \${fp}\\n\`);
832
+ } catch (e) { /* non-blocking */ }
833
+ });
832
834
  `,
833
- 'session-start.sh': `#!/bin/bash
834
- # SessionStart hook - prepares logs and records session entry
835
-
836
- LOG_DIR=".claude/logs"
837
- LOG_FILE="$LOG_DIR/sessions.log"
838
-
839
- mkdir -p "$LOG_DIR"
840
-
841
- TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
842
- echo "[$TIMESTAMP] session started" >> "$LOG_FILE"
843
-
844
- exit 0
835
+ 'session-start.js': `#!/usr/bin/env node
836
+ // SessionStart hook - prepares logs and records session entry
837
+ const fs = require('fs');
838
+ const path = require('path');
839
+ const logDir = path.join('.claude', 'logs');
840
+ fs.mkdirSync(logDir, { recursive: true });
841
+ const ts = new Date().toISOString().replace('T', ' ').split('.')[0];
842
+ fs.appendFileSync(path.join(logDir, 'sessions.log'), \`[\${ts}] session started\\n\`);
845
843
  `,
846
844
  }),
847
845
 
@@ -1219,7 +1217,7 @@ async function setup(options) {
1219
1217
  const hooksDir = path.join(options.dir, '.claude/hooks');
1220
1218
  const settingsPath = path.join(options.dir, '.claude/settings.json');
1221
1219
  if (fs.existsSync(hooksDir) && !fs.existsSync(settingsPath)) {
1222
- const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh'));
1220
+ const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh') || f.endsWith('.js'));
1223
1221
  if (hookFiles.length > 0) {
1224
1222
  const settings = buildSettingsForProfile({
1225
1223
  profileKey: options.profile || 'safe-write',
package/src/techniques.js CHANGED
@@ -30,7 +30,7 @@ const TECHNIQUES = {
30
30
  id: 51,
31
31
  name: 'Mermaid architecture diagram',
32
32
  check: (ctx) => {
33
- const md = ctx.fileContent('CLAUDE.md') || '';
33
+ const md = ctx.claudeMdContent() || '';
34
34
  return md.includes('mermaid') || md.includes('graph ') || md.includes('flowchart ');
35
35
  },
36
36
  impact: 'high',
@@ -55,7 +55,7 @@ const TECHNIQUES = {
55
55
  id: 763,
56
56
  name: 'CLAUDE.md uses @import for modularity',
57
57
  check: (ctx) => {
58
- const md = ctx.fileContent('CLAUDE.md') || '';
58
+ const md = ctx.claudeMdContent() || '';
59
59
  return md.includes('@import');
60
60
  },
61
61
  impact: 'medium',
@@ -69,8 +69,8 @@ const TECHNIQUES = {
69
69
  id: 681,
70
70
  name: 'CLAUDE.md under 200 lines (concise)',
71
71
  check: (ctx) => {
72
- const md = ctx.fileContent('CLAUDE.md') || '';
73
- return md.split('\n').length < 200;
72
+ const md = ctx.claudeMdContent() || '';
73
+ return md.split('\n').length <= 200;
74
74
  },
75
75
  impact: 'medium',
76
76
  rating: 4,
@@ -87,8 +87,9 @@ const TECHNIQUES = {
87
87
  id: 93,
88
88
  name: 'Verification criteria in CLAUDE.md',
89
89
  check: (ctx) => {
90
- const md = ctx.fileContent('CLAUDE.md') || '';
91
- return md.includes('test') || md.includes('verify') || md.includes('lint') || md.includes('check');
90
+ const md = ctx.claudeMdContent() || '';
91
+ return /\b(npm test|yarn test|pnpm test|pytest|go test|make test|npm run lint|yarn lint|npx |ruff |eslint)\b/i.test(md) ||
92
+ /\b(test command|lint command|build command|verify|run tests|run lint)\b/i.test(md);
92
93
  },
93
94
  impact: 'critical',
94
95
  rating: 5,
@@ -101,7 +102,7 @@ const TECHNIQUES = {
101
102
  id: 93001,
102
103
  name: 'CLAUDE.md contains a test command',
103
104
  check: (ctx) => {
104
- const md = ctx.fileContent('CLAUDE.md') || '';
105
+ const md = ctx.claudeMdContent() || '';
105
106
  return /npm test|pytest|jest|vitest|cargo test|go test|mix test|rspec/.test(md);
106
107
  },
107
108
  impact: 'high',
@@ -115,7 +116,7 @@ const TECHNIQUES = {
115
116
  id: 93002,
116
117
  name: 'CLAUDE.md contains a lint command',
117
118
  check: (ctx) => {
118
- const md = ctx.fileContent('CLAUDE.md') || '';
119
+ const md = ctx.claudeMdContent() || '';
119
120
  return /eslint|prettier|ruff|black|clippy|golangci-lint|rubocop|npm run lint|yarn lint|pnpm lint|bun lint/.test(md);
120
121
  },
121
122
  impact: 'high',
@@ -129,7 +130,7 @@ const TECHNIQUES = {
129
130
  id: 93003,
130
131
  name: 'CLAUDE.md contains a build command',
131
132
  check: (ctx) => {
132
- const md = ctx.fileContent('CLAUDE.md') || '';
133
+ const md = ctx.claudeMdContent() || '';
133
134
  return /npm run build|cargo build|go build|make|tsc|gradle build|mvn compile/.test(md);
134
135
  },
135
136
  impact: 'medium',
@@ -199,7 +200,7 @@ const TECHNIQUES = {
199
200
  id: 1039,
200
201
  name: 'CLAUDE.md has no embedded API keys',
201
202
  check: (ctx) => {
202
- const md = ctx.fileContent('CLAUDE.md') || '';
203
+ const md = ctx.claudeMdContent() || '';
203
204
  return !/sk-[a-zA-Z0-9]{20,}|xoxb-|AKIA[A-Z0-9]{16}/.test(md);
204
205
  },
205
206
  impact: 'critical',
@@ -328,6 +329,7 @@ const TECHNIQUES = {
328
329
  id: 24,
329
330
  name: 'Permission configuration',
330
331
  check: (ctx) => {
332
+ // Prefer local (effective config) — any settings file with permissions passes
331
333
  const settings = ctx.jsonFile('.claude/settings.local.json') || ctx.jsonFile('.claude/settings.json');
332
334
  return !!(settings && settings.permissions);
333
335
  },
@@ -396,7 +398,7 @@ const TECHNIQUES = {
396
398
  id: 1031,
397
399
  name: 'Security review command awareness',
398
400
  check: (ctx) => {
399
- const md = ctx.fileContent('CLAUDE.md') || '';
401
+ const md = ctx.claudeMdContent() || '';
400
402
  return md.includes('security') || md.includes('/security-review');
401
403
  },
402
404
  impact: 'high',
@@ -498,7 +500,7 @@ const TECHNIQUES = {
498
500
  name: 'Frontend design skill for anti-AI-slop',
499
501
  check: (ctx) => {
500
502
  if (!hasFrontendSignals(ctx)) return null;
501
- const md = ctx.fileContent('CLAUDE.md') || '';
503
+ const md = ctx.claudeMdContent() || '';
502
504
  return md.includes('frontend_aesthetics') || md.includes('anti-AI-slop') || md.includes('frontend-design');
503
505
  },
504
506
  impact: 'medium',
@@ -553,11 +555,13 @@ const TECHNIQUES = {
553
555
  ciPipeline: {
554
556
  id: 260,
555
557
  name: 'CI pipeline configured',
556
- check: (ctx) => ctx.hasDir('.github/workflows'),
558
+ check: (ctx) => ctx.hasDir('.github/workflows') || ctx.hasDir('.circleci') ||
559
+ ctx.files.includes('.gitlab-ci.yml') || ctx.files.includes('Jenkinsfile') ||
560
+ ctx.files.includes('.travis.yml') || ctx.files.includes('bitbucket-pipelines.yml'),
557
561
  impact: 'high',
558
562
  rating: 4,
559
563
  category: 'devops',
560
- fix: 'Add .github/workflows/ with CI pipeline for automated testing and deployment.',
564
+ fix: 'Add a CI pipeline (GitHub Actions, GitLab CI, CircleCI, etc.) for automated testing and deployment.',
561
565
  template: null
562
566
  },
563
567
 
@@ -658,7 +662,7 @@ const TECHNIQUES = {
658
662
  id: 568,
659
663
  name: 'CLAUDE.md mentions /compact or compaction',
660
664
  check: (ctx) => {
661
- const md = ctx.fileContent('CLAUDE.md') || '';
665
+ const md = ctx.claudeMdContent() || '';
662
666
  return /\/compact|compaction|context.*(limit|manage|budget)/i.test(md);
663
667
  },
664
668
  impact: 'medium',
@@ -672,7 +676,7 @@ const TECHNIQUES = {
672
676
  id: 45,
673
677
  name: 'Context management awareness',
674
678
  check: (ctx) => {
675
- const md = ctx.fileContent('CLAUDE.md') || '';
679
+ const md = ctx.claudeMdContent() || '';
676
680
  return /context.*(manage|window|limit|budget|token)/i.test(md);
677
681
  },
678
682
  impact: 'medium',
@@ -744,7 +748,7 @@ const TECHNIQUES = {
744
748
  id: 96,
745
749
  name: 'XML tags for structured prompts',
746
750
  check: (ctx) => {
747
- const md = ctx.fileContent('CLAUDE.md') || '';
751
+ const md = ctx.claudeMdContent() || '';
748
752
  // Give credit for XML tags OR well-structured markdown with clear sections
749
753
  const hasXml = md.includes('<constraints') || md.includes('<rules') ||
750
754
  md.includes('<validation') || md.includes('<instructions');
@@ -764,7 +768,7 @@ const TECHNIQUES = {
764
768
  id: 9,
765
769
  name: 'CLAUDE.md contains code examples',
766
770
  check: (ctx) => {
767
- const md = ctx.fileContent('CLAUDE.md') || '';
771
+ const md = ctx.claudeMdContent() || '';
768
772
  return (md.match(/```/g) || []).length >= 2;
769
773
  },
770
774
  impact: 'high',
@@ -778,8 +782,8 @@ const TECHNIQUES = {
778
782
  id: 10,
779
783
  name: 'CLAUDE.md defines a role or persona',
780
784
  check: (ctx) => {
781
- const md = ctx.fileContent('CLAUDE.md') || '';
782
- return /you are|your role|act as|persona|behave as/i.test(md);
785
+ const md = ctx.claudeMdContent() || '';
786
+ return /^you are a |^your role is|^act as a |persona:|behave as a /im.test(md);
783
787
  },
784
788
  impact: 'medium',
785
789
  rating: 4,
@@ -792,7 +796,7 @@ const TECHNIQUES = {
792
796
  id: 9601,
793
797
  name: 'XML constraint blocks in CLAUDE.md',
794
798
  check: (ctx) => {
795
- const md = ctx.fileContent('CLAUDE.md') || '';
799
+ const md = ctx.claudeMdContent() || '';
796
800
  return /<constraints|<rules|<requirements|<boundaries/i.test(md);
797
801
  },
798
802
  impact: 'high',
@@ -810,10 +814,10 @@ const TECHNIQUES = {
810
814
  id: 1102,
811
815
  name: 'Claude Code Channels awareness',
812
816
  check: (ctx) => {
813
- const md = ctx.fileContent('CLAUDE.md') || '';
817
+ const md = ctx.claudeMdContent() || '';
814
818
  const settings = ctx.jsonFile('.claude/settings.local.json') || ctx.jsonFile('.claude/settings.json');
815
819
  const settingsStr = JSON.stringify(settings || {});
816
- return md.toLowerCase().includes('channel') || settingsStr.includes('channel');
820
+ return /\bchannels?\b.*\b(telegram|discord|imessage|slack|bridge)\b|\b(telegram|discord|imessage|slack|bridge)\b.*\bchannels?\b/i.test(md) || settingsStr.includes('channels');
817
821
  },
818
822
  impact: 'low',
819
823
  rating: 3,
@@ -831,7 +835,7 @@ const TECHNIQUES = {
831
835
  id: 2001,
832
836
  name: 'CLAUDE.md mentions current Claude features',
833
837
  check: (ctx) => {
834
- const md = ctx.fileContent('CLAUDE.md') || '';
838
+ const md = ctx.claudeMdContent() || '';
835
839
  if (md.length < 50) return false; // too short to evaluate
836
840
  // Check for awareness of features from 2025+
837
841
  const modernFeatures = ['hook', 'skill', 'agent', 'subagent', 'mcp', 'compact', '/clear', 'extended thinking', 'tool_use', 'worktree'];
@@ -845,13 +849,14 @@ const TECHNIQUES = {
845
849
  template: null
846
850
  },
847
851
 
852
+ // claudeMdNotOverlong removed — duplicate of underlines200 (id 681)
853
+
848
854
  claudeMdNotOverlong: {
849
855
  id: 2002,
850
856
  name: 'CLAUDE.md is concise (under 200 lines)',
851
857
  check: (ctx) => {
852
- const md = ctx.fileContent('CLAUDE.md');
853
- if (!md) return false; // no CLAUDE.md = not passing
854
- return md.split('\n').length <= 200;
858
+ // Defer to underlines200 — this check always returns null (skipped)
859
+ return null;
855
860
  },
856
861
  impact: 'medium',
857
862
  rating: 4,
@@ -864,12 +869,20 @@ const TECHNIQUES = {
864
869
  id: 2003,
865
870
  name: 'CLAUDE.md has no obvious contradictions',
866
871
  check: (ctx) => {
867
- const md = ctx.fileContent('CLAUDE.md');
872
+ const md = ctx.claudeMdContent();
868
873
  if (!md || md.length < 50) return false; // no CLAUDE.md or too short = not passing
869
874
  // Check for common contradictions
870
- const hasNever = /never.*always|always.*never/i.test(md);
871
- const hasBothStyles = /use tabs/i.test(md) && /use spaces/i.test(md);
872
- return !hasNever && !hasBothStyles;
875
+ // Check for contradictions on the SAME topic (same line or adjacent sentence)
876
+ const lines = md.split('\n');
877
+ let hasContradiction = false;
878
+ for (const line of lines) {
879
+ if (/\balways\b.*\bnever\b|\bnever\b.*\balways\b/i.test(line)) {
880
+ hasContradiction = true;
881
+ break;
882
+ }
883
+ }
884
+ const hasBothStyles = /\buse tabs\b/i.test(md) && /\buse spaces\b/i.test(md);
885
+ return !hasContradiction && !hasBothStyles;
873
886
  },
874
887
  impact: 'high',
875
888
  rating: 4,
@@ -940,14 +953,15 @@ const TECHNIQUES = {
940
953
 
941
954
  securityReviewInWorkflow: {
942
955
  id: 2008,
943
- name: '/security-review in workflow',
956
+ name: '/security-review command or workflow',
944
957
  check: (ctx) => {
945
- const md = ctx.fileContent('CLAUDE.md') || '';
946
958
  const hasCommand = ctx.hasDir('.claude/commands') &&
947
959
  (ctx.dirFiles('.claude/commands') || []).some(f => f.includes('security') || f.includes('review'));
948
- return md.toLowerCase().includes('security') || hasCommand;
960
+ const md = ctx.claudeMdContent() || '';
961
+ const hasExplicitRef = /\/security-review|security review command|security workflow/i.test(md);
962
+ return hasCommand || hasExplicitRef;
949
963
  },
950
- impact: 'high',
964
+ impact: 'medium',
951
965
  rating: 4,
952
966
  category: 'quality-deep',
953
967
  fix: 'Claude Code has built-in /security-review (OWASP Top 10). Add it to your workflow or create a /security command.',
@@ -959,7 +973,7 @@ const TECHNIQUES = {
959
973
  id: 2010,
960
974
  name: 'Test coverage or strategy mentioned',
961
975
  check: (ctx) => {
962
- const md = ctx.fileContent('CLAUDE.md') || '';
976
+ const md = ctx.claudeMdContent() || '';
963
977
  return /coverage|test.*strateg|e2e|integration test|unit test/i.test(md);
964
978
  },
965
979
  impact: 'medium', rating: 3, category: 'quality',
@@ -991,7 +1005,7 @@ const TECHNIQUES = {
991
1005
  id: 2012,
992
1006
  name: 'Auto-memory or memory management mentioned',
993
1007
  check: (ctx) => {
994
- const md = ctx.fileContent('CLAUDE.md') || '';
1008
+ const md = ctx.claudeMdContent() || '';
995
1009
  return /auto.?memory|memory.*manage|remember|persistent.*context/i.test(md);
996
1010
  },
997
1011
  impact: 'low', rating: 3, category: 'memory',
@@ -1004,7 +1018,7 @@ const TECHNIQUES = {
1004
1018
  id: 2013,
1005
1019
  name: 'Sandbox or isolation mentioned',
1006
1020
  check: (ctx) => {
1007
- const md = ctx.fileContent('CLAUDE.md') || '';
1021
+ const md = ctx.claudeMdContent() || '';
1008
1022
  const settings = ctx.jsonFile('.claude/settings.json') || {};
1009
1023
  return /sandbox|isolat/i.test(md) || !!settings.sandbox;
1010
1024
  },
@@ -1047,7 +1061,7 @@ const TECHNIQUES = {
1047
1061
  id: 2016,
1048
1062
  name: 'Effort level or thinking configuration',
1049
1063
  check: (ctx) => {
1050
- const md = ctx.fileContent('CLAUDE.md') || '';
1064
+ const md = ctx.claudeMdContent() || '';
1051
1065
  const shared = ctx.jsonFile('.claude/settings.json') || {};
1052
1066
  const local = ctx.jsonFile('.claude/settings.local.json') || {};
1053
1067
  return /effort|thinking/i.test(md) || shared.effortLevel || local.effortLevel ||
@@ -1074,7 +1088,7 @@ const TECHNIQUES = {
1074
1088
  id: 2018,
1075
1089
  name: 'Worktree or parallel sessions mentioned',
1076
1090
  check: (ctx) => {
1077
- const md = ctx.fileContent('CLAUDE.md') || '';
1091
+ const md = ctx.claudeMdContent() || '';
1078
1092
  const shared = ctx.jsonFile('.claude/settings.json') || {};
1079
1093
  return /worktree|parallel.*session/i.test(md) || !!shared.worktree;
1080
1094
  },
@@ -1088,7 +1102,7 @@ const TECHNIQUES = {
1088
1102
  id: 2019,
1089
1103
  name: 'CLAUDE.md includes "do not" instructions',
1090
1104
  check: (ctx) => {
1091
- const md = ctx.fileContent('CLAUDE.md') || '';
1105
+ const md = ctx.claudeMdContent() || '';
1092
1106
  return /do not|don't|never|avoid|must not/i.test(md);
1093
1107
  },
1094
1108
  impact: 'medium', rating: 4, category: 'prompting',
@@ -1100,8 +1114,8 @@ const TECHNIQUES = {
1100
1114
  id: 2020,
1101
1115
  name: 'CLAUDE.md includes output or style guidance',
1102
1116
  check: (ctx) => {
1103
- const md = ctx.fileContent('CLAUDE.md') || '';
1104
- return /style|format|convention|naming|pattern|prefer/i.test(md);
1117
+ const md = ctx.claudeMdContent() || '';
1118
+ return /coding style|naming convention|code style|style guide|formatting rules|\bprefer\b.*\b(single|double|tabs|spaces|camel|snake|kebab|named|default|const|let|arrow|function)\b/i.test(md);
1105
1119
  },
1106
1120
  impact: 'medium', rating: 3, category: 'prompting',
1107
1121
  fix: 'Add coding style and naming conventions to CLAUDE.md so Claude matches your project patterns.',
@@ -1127,7 +1141,7 @@ const TECHNIQUES = {
1127
1141
  id: 2022,
1128
1142
  name: 'CLAUDE.md describes what the project does',
1129
1143
  check: (ctx) => {
1130
- const md = ctx.fileContent('CLAUDE.md') || '';
1144
+ const md = ctx.claudeMdContent() || '';
1131
1145
  return /what.*does|overview|purpose|about|description|project.*is/i.test(md) && md.length > 100;
1132
1146
  },
1133
1147
  impact: 'high', rating: 4, category: 'memory',
@@ -1139,7 +1153,7 @@ const TECHNIQUES = {
1139
1153
  id: 2023,
1140
1154
  name: 'CLAUDE.md documents directory structure',
1141
1155
  check: (ctx) => {
1142
- const md = ctx.fileContent('CLAUDE.md') || '';
1156
+ const md = ctx.claudeMdContent() || '';
1143
1157
  return /src\/|app\/|lib\/|structure|director|folder/i.test(md);
1144
1158
  },
1145
1159
  impact: 'medium', rating: 4, category: 'memory',
@@ -1265,15 +1279,15 @@ const TECHNIQUES = {
1265
1279
  id: 2009,
1266
1280
  name: 'No deprecated patterns detected',
1267
1281
  check: (ctx) => {
1268
- const md = ctx.fileContent('CLAUDE.md');
1282
+ const md = ctx.claudeMdContent();
1269
1283
  if (!md) return false; // no CLAUDE.md = not passing
1270
1284
  // Check for patterns deprecated in Claude 4.x
1271
- const deprecated = [
1272
- 'prefill', // deprecated in 4.6
1273
- 'claude-3-opus', 'claude-3-sonnet', 'claude-3-haiku', // old model names
1274
- 'human_prompt', 'assistant_prompt', // old API format
1285
+ const deprecatedPatterns = [
1286
+ /\bprefill\b/i, // deprecated API pattern in 4.6
1287
+ /\bclaude-3-opus\b/i, /\bclaude-3-sonnet\b/i, /\bclaude-3-haiku\b/i, // old model names
1288
+ /\bhuman_prompt\b/i, /\bassistant_prompt\b/i, // old API format
1275
1289
  ];
1276
- return !deprecated.some(d => md.toLowerCase().includes(d));
1290
+ return !deprecatedPatterns.some(p => p.test(md));
1277
1291
  },
1278
1292
  impact: 'medium',
1279
1293
  rating: 3,