claudex-setup 1.7.0 → 1.9.0

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/src/benchmark.js CHANGED
@@ -5,6 +5,8 @@ const path = require('path');
5
5
  const { version } = require('../package.json');
6
6
  const { audit } = require('./audit');
7
7
  const { setup } = require('./setup');
8
+ const { analyzeProject } = require('./analyze');
9
+ const { getGovernanceSummary } = require('./governance');
8
10
 
9
11
  function copyProject(sourceDir, targetDir) {
10
12
  fs.mkdirSync(targetDir, { recursive: true });
@@ -34,20 +36,76 @@ function summarizeAudit(result) {
34
36
  };
35
37
  }
36
38
 
37
- function buildExecutiveSummary(before, after) {
39
+ function buildWorkflowEvidence(before, after, analysisReport, governanceSummary) {
40
+ const tasks = [
41
+ {
42
+ key: 'discover-without-writes',
43
+ label: 'Discover next actions without writing files',
44
+ passed: before.checkCount > 0 && Array.isArray(before.quickWins),
45
+ evidence: `Baseline audit returned ${before.checkCount} applicable checks and ${before.quickWins.length} quick wins.`,
46
+ },
47
+ {
48
+ key: 'starter-safe-improvement',
49
+ label: 'Apply starter-safe improvements in isolation',
50
+ passed: after.score >= before.score && after.failed <= before.failed,
51
+ evidence: `Score moved ${before.score} -> ${after.score}; failed checks moved ${before.failed} -> ${after.failed}.`,
52
+ },
53
+ {
54
+ key: 'governed-rollout-surface',
55
+ label: 'Expose governed rollout controls',
56
+ passed: governanceSummary.permissionProfiles.length >= 3 && governanceSummary.hookRegistry.length >= 1,
57
+ evidence: `${governanceSummary.permissionProfiles.length} profiles and ${governanceSummary.hookRegistry.length} governed hooks available.`,
58
+ },
59
+ {
60
+ key: 'domain-pack-guidance',
61
+ label: 'Recommend a domain pack for the repo',
62
+ passed: analysisReport.recommendedDomainPacks.length > 0,
63
+ evidence: analysisReport.recommendedDomainPacks.map(pack => pack.label).join(', ') || 'No domain pack recommendation generated.',
64
+ },
65
+ {
66
+ key: 'mcp-pack-guidance',
67
+ label: 'Recommend MCP packs when appropriate',
68
+ passed: analysisReport.recommendedMcpPacks.length > 0,
69
+ evidence: analysisReport.recommendedMcpPacks.map(pack => pack.label).join(', ') || 'No MCP pack recommendation generated.',
70
+ },
71
+ ];
72
+
73
+ const passed = tasks.filter(task => task.passed).length;
74
+ const total = tasks.length;
75
+ return {
76
+ taskPack: 'maintainer-core',
77
+ tasks,
78
+ summary: {
79
+ passed,
80
+ total,
81
+ coverageScore: total > 0 ? Math.round((passed / total) * 100) : 0,
82
+ },
83
+ };
84
+ }
85
+
86
+ function buildExecutiveSummary(before, after, workflowEvidence) {
38
87
  const scoreDelta = after.score - before.score;
39
88
  const organicDelta = after.organicScore - before.organicScore;
89
+ const workflowCoverage = workflowEvidence.summary.coverageScore;
90
+ let headline = 'Benchmark did not improve the score in this run.';
91
+
92
+ if (scoreDelta > 0) {
93
+ headline = `Benchmark improved readiness by ${scoreDelta} points without touching the original repo.`;
94
+ } else if (before.score >= 85 && after.score >= before.score && workflowCoverage >= 80) {
95
+ headline = 'Benchmark confirmed the repo already meets the starter-safe baseline without regression.';
96
+ }
97
+
40
98
  return {
41
- headline: scoreDelta > 0
42
- ? `Benchmark improved readiness by ${scoreDelta} points without touching the original repo.`
43
- : 'Benchmark did not improve the score in this run.',
99
+ headline,
44
100
  scoreDelta,
45
101
  organicDelta,
46
102
  decisionGuidance: scoreDelta >= 20
47
103
  ? 'Strong pilot candidate'
48
104
  : scoreDelta >= 10
49
105
  ? 'Promising but needs manual review'
50
- : 'Use suggest-only mode before rollout',
106
+ : (before.score >= 85 && workflowCoverage >= 80
107
+ ? 'Use suggest-only mode, domain packs, or task-level benchmarks next'
108
+ : 'Use suggest-only mode before rollout'),
51
109
  };
52
110
  }
53
111
 
@@ -95,6 +153,11 @@ function renderBenchmarkMarkdown(report) {
95
153
  `- ${report.executiveSummary.headline}`,
96
154
  `- Recommendation: ${report.executiveSummary.decisionGuidance}`,
97
155
  '',
156
+ '## Workflow Evidence',
157
+ `- Task pack: ${report.workflowEvidence.taskPack}`,
158
+ `- Coverage: ${report.workflowEvidence.summary.passed}/${report.workflowEvidence.summary.total} (${report.workflowEvidence.summary.coverageScore}%)`,
159
+ ...report.workflowEvidence.tasks.map(task => `- ${task.label}: ${task.passed ? 'pass' : 'not yet'} — ${task.evidence}`),
160
+ '',
98
161
  '## Case Study',
99
162
  `- Initial state: ${report.caseStudy.initialState}`,
100
163
  `- Chosen mode: ${report.caseStudy.chosenMode}`,
@@ -111,8 +174,17 @@ async function runBenchmark(options) {
111
174
 
112
175
  try {
113
176
  copyProject(options.dir, sandboxDir);
114
- const applyResult = await setup({ dir: sandboxDir, auto: true, silent: true });
177
+ const applyResult = await setup({
178
+ dir: sandboxDir,
179
+ auto: true,
180
+ silent: true,
181
+ profile: options.profile,
182
+ mcpPacks: options.mcpPacks || [],
183
+ });
115
184
  const after = await audit({ dir: sandboxDir, silent: true });
185
+ const analysisReport = await analyzeProject({ dir: sandboxDir, mode: 'suggest-only' });
186
+ const governanceSummary = getGovernanceSummary();
187
+ const workflowEvidence = buildWorkflowEvidence(before, after, analysisReport, governanceSummary);
116
188
 
117
189
  return {
118
190
  schemaVersion: 1,
@@ -133,7 +205,8 @@ async function runBenchmark(options) {
133
205
  passed: after.passed - before.passed,
134
206
  failed: after.failed - before.failed,
135
207
  },
136
- executiveSummary: buildExecutiveSummary(before, after),
208
+ workflowEvidence,
209
+ executiveSummary: buildExecutiveSummary(before, after, workflowEvidence),
137
210
  caseStudy: buildCaseStudy(before, after, applyResult),
138
211
  };
139
212
  } finally {
@@ -158,6 +231,7 @@ function printBenchmark(report, options = {}) {
158
231
  console.log('');
159
232
  console.log(` ${report.executiveSummary.headline}`);
160
233
  console.log(` Recommendation: ${report.executiveSummary.decisionGuidance}`);
234
+ console.log(` Workflow evidence: ${report.workflowEvidence.summary.passed}/${report.workflowEvidence.summary.total} tasks (${report.workflowEvidence.summary.coverageScore}%)`);
161
235
  console.log('');
162
236
  }
163
237
 
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "synced_from": "claudex",
3
- "synced_at": "2026-03-30T23:05:29Z",
3
+ "synced_at": "2026-03-31T19:30:00Z",
4
4
  "total_items": 1107,
5
5
  "tested": 948,
6
- "last_id": 1157
6
+ "last_id": 1157,
7
+ "domain_packs": 10,
8
+ "mcp_packs": 5,
9
+ "anti_patterns": 53,
10
+ "contract_version": "1.0.0"
7
11
  }
package/src/context.js CHANGED
@@ -17,11 +17,12 @@ class ProjectContext {
17
17
  try {
18
18
  const entries = fs.readdirSync(this.dir, { withFileTypes: true });
19
19
  for (const entry of entries) {
20
- if (entry.name.startsWith('.') && entry.name !== '.claude' && entry.name !== '.gitignore') continue;
21
- if (entry.name === 'node_modules' || entry.name === '__pycache__') continue;
22
20
  if (entry.isFile()) {
21
+ if (entry.name === '.DS_Store') continue;
23
22
  this.files.push(entry.name);
24
23
  } else if (entry.isDirectory()) {
24
+ if (entry.name.startsWith('.') && entry.name !== '.claude') continue;
25
+ if (entry.name === 'node_modules' || entry.name === '__pycache__') continue;
25
26
  this.files.push(entry.name + '/');
26
27
  // Scan .claude/ subdirectories
27
28
  if (entry.name === '.claude') {
@@ -0,0 +1,223 @@
1
+ const DOMAIN_PACKS = [
2
+ {
3
+ key: 'baseline-general',
4
+ label: 'Baseline General',
5
+ useWhen: 'General repos that need a pragmatic Claude baseline without domain-specific assumptions.',
6
+ recommendedModules: ['CLAUDE.md baseline', 'verification', 'safe-write profile'],
7
+ recommendedMcpPacks: ['context7-docs'],
8
+ benchmarkFocus: ['discover next actions', 'starter-safe improvement', 'governed rollout'],
9
+ },
10
+ {
11
+ key: 'backend-api',
12
+ label: 'Backend API',
13
+ useWhen: 'Service, API, or backend-heavy repos with routes, services, jobs, or data access.',
14
+ recommendedModules: ['verification', 'security workflow', 'commands', 'rules'],
15
+ recommendedMcpPacks: ['context7-docs'],
16
+ benchmarkFocus: ['test + build verification', 'security review workflow', 'safe apply on existing config'],
17
+ },
18
+ {
19
+ key: 'frontend-ui',
20
+ label: 'Frontend UI',
21
+ useWhen: 'React, Next.js, Vue, Angular, or Svelte repos with components and UI-heavy workflows.',
22
+ recommendedModules: ['frontend rules', 'design guidance', 'commands', 'benchmark'],
23
+ recommendedMcpPacks: ['context7-docs', 'next-devtools'],
24
+ benchmarkFocus: ['build checks', 'component workflow quality', 'framework-aware starter output'],
25
+ },
26
+ {
27
+ key: 'data-pipeline',
28
+ label: 'Data Pipeline',
29
+ useWhen: 'Repos with workers, DAGs, marts, ETL jobs, migrations, or analytics-heavy workflows.',
30
+ recommendedModules: ['verification', 'rules', 'agents', 'benchmark'],
31
+ recommendedMcpPacks: ['context7-docs'],
32
+ benchmarkFocus: ['pipeline safety', 'repeatable task flows', 'state-aware review artifacts'],
33
+ },
34
+ {
35
+ key: 'infra-platform',
36
+ label: 'Infra Platform',
37
+ useWhen: 'Terraform, Docker, Kubernetes, Wrangler, or deployment-oriented repos.',
38
+ recommendedModules: ['ci-devops', 'commands', 'governance', 'benchmark'],
39
+ recommendedMcpPacks: ['context7-docs'],
40
+ benchmarkFocus: ['release safety', 'policy-controlled rollout', 'infra verification loops'],
41
+ },
42
+ {
43
+ key: 'oss-library',
44
+ label: 'OSS Library',
45
+ useWhen: 'Public packages or contributor-heavy repos that need a lighter governance footprint.',
46
+ recommendedModules: ['suggest-only profile', 'light rules', 'commands', 'README-aligned CLAUDE.md'],
47
+ recommendedMcpPacks: ['context7-docs'],
48
+ benchmarkFocus: ['low-footprint adoption', 'manual review friendliness', 'contributor-safe defaults'],
49
+ },
50
+ {
51
+ key: 'enterprise-governed',
52
+ label: 'Enterprise Governed',
53
+ useWhen: 'Repos with CI, permissions, hooks, and a need for auditable change controls.',
54
+ recommendedModules: ['governance', 'activity artifacts', 'rollback manifests', 'benchmark evidence'],
55
+ recommendedMcpPacks: ['context7-docs'],
56
+ benchmarkFocus: ['policy-aware rollout', 'approval flow readiness', 'benchmark export quality'],
57
+ },
58
+ {
59
+ key: 'monorepo',
60
+ label: 'Monorepo',
61
+ useWhen: 'Nx, Turborepo, Lerna, or workspace-based repos with multiple packages sharing a root.',
62
+ recommendedModules: ['path-specific rules', 'commands per package', 'governance', 'agents'],
63
+ recommendedMcpPacks: ['context7-docs'],
64
+ benchmarkFocus: ['package-scoped rule coverage', 'cross-package safety', 'workspace-aware starter output'],
65
+ },
66
+ {
67
+ key: 'mobile',
68
+ label: 'Mobile App',
69
+ useWhen: 'React Native, Flutter, Swift, or Kotlin repos with mobile-specific build and release workflows.',
70
+ recommendedModules: ['verification', 'commands', 'rules', 'agents'],
71
+ recommendedMcpPacks: ['context7-docs'],
72
+ benchmarkFocus: ['build verification', 'platform-specific rules', 'release workflow quality'],
73
+ },
74
+ {
75
+ key: 'regulated-lite',
76
+ label: 'Regulated Lite',
77
+ useWhen: 'Repos in regulated environments (fintech, health, legal) that need auditability without full enterprise governance overhead.',
78
+ recommendedModules: ['governance', 'activity artifacts', 'suggest-only profile', 'audit logging'],
79
+ recommendedMcpPacks: ['context7-docs'],
80
+ benchmarkFocus: ['audit trail completeness', 'change traceability', 'policy compliance readiness'],
81
+ },
82
+ ];
83
+
84
+ function uniqueByKey(items) {
85
+ const seen = new Set();
86
+ const result = [];
87
+ for (const item of items) {
88
+ if (seen.has(item.key)) continue;
89
+ seen.add(item.key);
90
+ result.push(item);
91
+ }
92
+ return result;
93
+ }
94
+
95
+ function detectDomainPacks(ctx, stacks, assets = null) {
96
+ const stackKeys = new Set((stacks || []).map(stack => stack.key));
97
+ const pkg = ctx.jsonFile('package.json') || {};
98
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
99
+ const matches = [];
100
+
101
+ function addMatch(key, reasons) {
102
+ const pack = DOMAIN_PACKS.find(item => item.key === key);
103
+ if (!pack) return;
104
+ matches.push({
105
+ ...pack,
106
+ matchReasons: reasons.filter(Boolean).slice(0, 3),
107
+ });
108
+ }
109
+
110
+ const hasFrontend = stackKeys.has('react') || stackKeys.has('nextjs') || stackKeys.has('vue') ||
111
+ stackKeys.has('angular') || stackKeys.has('svelte') || ctx.hasDir('components') || ctx.hasDir('app') || ctx.hasDir('pages');
112
+ const hasBackend = stackKeys.has('node') || stackKeys.has('python') || stackKeys.has('django') ||
113
+ stackKeys.has('fastapi') || stackKeys.has('go') || stackKeys.has('rust') || stackKeys.has('java') ||
114
+ ctx.hasDir('api') || ctx.hasDir('routes') || ctx.hasDir('services') || ctx.hasDir('controllers');
115
+ const hasData = ctx.hasDir('dags') || ctx.hasDir('jobs') || ctx.hasDir('workers') ||
116
+ ctx.hasDir('models') || ctx.hasDir('migrations') || ctx.hasDir('db') ||
117
+ deps.dbt || deps['apache-airflow'] || deps.pandas || deps.polars || deps.duckdb;
118
+ const hasInfra = stackKeys.has('docker') || stackKeys.has('terraform') || stackKeys.has('kubernetes') ||
119
+ ctx.files.includes('wrangler.toml') || ctx.files.includes('serverless.yml') || ctx.files.includes('serverless.yaml') ||
120
+ ctx.files.includes('cdk.json') || ctx.hasDir('infra') || ctx.hasDir('deploy') || ctx.hasDir('helm');
121
+ const isOss = !!ctx.fileContent('LICENSE') && !!ctx.fileContent('CONTRIBUTING.md') && pkg.private !== true;
122
+ const isEnterpriseGoverned = !!(assets && assets.permissions && assets.permissions.hasDenyRules) &&
123
+ !!(assets && assets.files && assets.files.settings) && ctx.hasDir('.github/workflows');
124
+
125
+ if (hasBackend) {
126
+ addMatch('backend-api', [
127
+ 'Detected backend stack or service directories.',
128
+ ctx.hasDir('api') ? 'API-facing structure detected.' : null,
129
+ ctx.hasDir('services') ? 'Service-layer directories detected.' : null,
130
+ ]);
131
+ }
132
+
133
+ if (hasFrontend) {
134
+ addMatch('frontend-ui', [
135
+ 'Detected frontend stack or UI directories.',
136
+ ctx.hasDir('components') ? 'Component directories detected.' : null,
137
+ stackKeys.has('nextjs') ? 'Next.js stack detected.' : null,
138
+ ]);
139
+ }
140
+
141
+ if (hasData) {
142
+ addMatch('data-pipeline', [
143
+ 'Detected worker, jobs, models, or analytics-style structure.',
144
+ ctx.hasDir('jobs') ? 'Job/pipeline directories detected.' : null,
145
+ ctx.hasDir('migrations') ? 'Migration flow detected.' : null,
146
+ ]);
147
+ }
148
+
149
+ if (hasInfra) {
150
+ addMatch('infra-platform', [
151
+ 'Detected deployment or infrastructure signals.',
152
+ ctx.files.includes('wrangler.toml') ? 'Wrangler deployment config detected.' : null,
153
+ ctx.hasDir('deploy') ? 'Deployment directory detected.' : null,
154
+ ]);
155
+ }
156
+
157
+ if (isOss) {
158
+ addMatch('oss-library', [
159
+ 'License and contribution guidance suggest an open-source repo.',
160
+ pkg.private === false ? 'package.json is not marked private.' : null,
161
+ ]);
162
+ }
163
+
164
+ if (isEnterpriseGoverned) {
165
+ addMatch('enterprise-governed', [
166
+ 'Settings, deny rules, and CI indicate a governed team workflow.',
167
+ 'Repo already has policy-aware Claude assets.',
168
+ ]);
169
+ }
170
+
171
+ // Monorepo detection
172
+ const isMonorepo = ctx.files.includes('nx.json') || ctx.files.includes('turbo.json') ||
173
+ ctx.files.includes('lerna.json') || ctx.hasDir('packages') ||
174
+ (pkg.workspaces && (Array.isArray(pkg.workspaces) ? pkg.workspaces.length > 0 : true));
175
+ if (isMonorepo) {
176
+ addMatch('monorepo', [
177
+ 'Detected monorepo or workspace configuration.',
178
+ ctx.files.includes('nx.json') ? 'Nx workspace detected.' : null,
179
+ ctx.files.includes('turbo.json') ? 'Turborepo detected.' : null,
180
+ ctx.hasDir('packages') ? 'Packages directory detected.' : null,
181
+ ]);
182
+ }
183
+
184
+ // Mobile detection
185
+ const isMobile = deps['react-native'] || deps.expo || deps.flutter ||
186
+ ctx.files.includes('Podfile') || ctx.files.includes('build.gradle') ||
187
+ ctx.files.includes('build.gradle.kts') || ctx.hasDir('ios') || ctx.hasDir('android');
188
+ if (isMobile) {
189
+ addMatch('mobile', [
190
+ 'Detected mobile app structure or dependencies.',
191
+ deps['react-native'] ? 'React Native detected.' : null,
192
+ ctx.hasDir('ios') ? 'iOS directory detected.' : null,
193
+ ctx.hasDir('android') ? 'Android directory detected.' : null,
194
+ ]);
195
+ }
196
+
197
+ // Regulated-lite detection
198
+ const isRegulated = ctx.files.includes('SECURITY.md') ||
199
+ ctx.files.includes('COMPLIANCE.md') || ctx.hasDir('compliance') ||
200
+ ctx.hasDir('audit') || ctx.hasDir('policies') ||
201
+ (pkg.keywords && pkg.keywords.some(k => ['hipaa', 'fintech', 'compliance', 'regulated', 'sox', 'pci'].includes(k)));
202
+ if (isRegulated && !isEnterpriseGoverned) {
203
+ addMatch('regulated-lite', [
204
+ 'Detected compliance or regulatory signals without full enterprise governance.',
205
+ ctx.files.includes('SECURITY.md') ? 'SECURITY.md present.' : null,
206
+ ctx.hasDir('compliance') ? 'Compliance directory detected.' : null,
207
+ ]);
208
+ }
209
+
210
+ const deduped = uniqueByKey(matches);
211
+ if (deduped.length === 0) {
212
+ return [{
213
+ ...DOMAIN_PACKS.find(item => item.key === 'baseline-general'),
214
+ matchReasons: ['No stronger domain signal detected yet.'],
215
+ }];
216
+ }
217
+ return deduped;
218
+ }
219
+
220
+ module.exports = {
221
+ DOMAIN_PACKS,
222
+ detectDomainPacks,
223
+ };
package/src/governance.js CHANGED
@@ -1,3 +1,6 @@
1
+ const { DOMAIN_PACKS } = require('./domain-packs');
2
+ const { MCP_PACKS, mergeMcpServers, normalizeMcpPackKeys } = require('./mcp-packs');
3
+
1
4
  const PERMISSION_PROFILES = [
2
5
  {
3
6
  key: 'read-only',
@@ -83,6 +86,54 @@ const HOOK_REGISTRY = [
83
86
  dryRunExample: 'Edit one file and verify the log entry is appended.',
84
87
  rollbackPath: 'Remove the PostToolUse hook entry and delete the log file if desired.',
85
88
  },
89
+ {
90
+ key: 'duplicate-id-check',
91
+ file: '.claude/hooks/check-duplicate-ids.sh',
92
+ triggerPoint: 'PostToolUse',
93
+ matcher: 'Write|Edit',
94
+ purpose: 'Detects duplicate IDs in catalog or structured data files after edits.',
95
+ filesTouched: [],
96
+ sideEffects: ['Returns a systemMessage warning if duplicates are found.'],
97
+ risk: 'low',
98
+ dryRunExample: 'Edit a catalog file and verify duplicate check runs without blocking.',
99
+ rollbackPath: 'Remove the PostToolUse hook entry from settings.',
100
+ },
101
+ {
102
+ key: 'injection-defense',
103
+ file: '.claude/hooks/injection-defense.sh',
104
+ triggerPoint: 'PostToolUse',
105
+ matcher: 'WebFetch|WebSearch',
106
+ purpose: 'Scans web tool outputs for common prompt injection patterns.',
107
+ filesTouched: ['tools/failure-log.txt'],
108
+ sideEffects: ['Logs alerts to failure log.', 'Returns a systemMessage warning if patterns detected.'],
109
+ risk: 'low',
110
+ dryRunExample: 'Run a WebFetch and verify output is scanned for injection patterns.',
111
+ rollbackPath: 'Remove the PostToolUse hook entry from settings.',
112
+ },
113
+ {
114
+ key: 'trust-drift-check',
115
+ file: '.claude/hooks/trust-drift-check.sh',
116
+ triggerPoint: 'PostToolUse',
117
+ matcher: 'Write|Edit',
118
+ purpose: 'Runs trust drift validation after file changes to catch metric/docs inconsistencies.',
119
+ filesTouched: [],
120
+ sideEffects: ['Returns a systemMessage warning if drift is detected.'],
121
+ risk: 'low',
122
+ dryRunExample: 'Edit a product-facing file and verify drift check runs.',
123
+ rollbackPath: 'Remove the PostToolUse hook entry from settings.',
124
+ },
125
+ {
126
+ key: 'session-init',
127
+ file: '.claude/hooks/rotate-logs.sh',
128
+ triggerPoint: 'SessionStart',
129
+ matcher: null,
130
+ purpose: 'Rotates large log files and loads workspace context at session start.',
131
+ filesTouched: ['tools/change-log.txt', 'tools/failure-log.txt'],
132
+ sideEffects: ['Archives logs over 500KB.', 'Returns a systemMessage with workspace info.'],
133
+ risk: 'low',
134
+ dryRunExample: 'Start a new session and verify log rotation runs.',
135
+ rollbackPath: 'Remove the SessionStart hook entry from settings.',
136
+ },
86
137
  ];
87
138
 
88
139
  const POLICY_PACKS = [
@@ -137,11 +188,147 @@ const PILOT_ROLLOUT_KIT = {
137
188
  ],
138
189
  };
139
190
 
191
+ function clone(value) {
192
+ return JSON.parse(JSON.stringify(value));
193
+ }
194
+
195
+ function mergeUnique(existing = [], additions = []) {
196
+ return [...new Set([...(Array.isArray(existing) ? existing : []), ...additions])];
197
+ }
198
+
199
+ function mergeHooks(existingHooks = {}, nextHooks = {}) {
200
+ const merged = clone(existingHooks || {});
201
+
202
+ for (const [stage, blocks] of Object.entries(nextHooks)) {
203
+ const targetBlocks = Array.isArray(merged[stage]) ? clone(merged[stage]) : [];
204
+ for (const incoming of blocks) {
205
+ const index = targetBlocks.findIndex(block => block.matcher === incoming.matcher);
206
+ if (index === -1) {
207
+ targetBlocks.push(clone(incoming));
208
+ continue;
209
+ }
210
+
211
+ const current = targetBlocks[index];
212
+ const existingCommands = new Set((current.hooks || []).map(hook => `${hook.type}:${hook.command}:${hook.timeout || ''}`));
213
+ const mergedHooks = [...(current.hooks || [])];
214
+ for (const hook of incoming.hooks || []) {
215
+ const signature = `${hook.type}:${hook.command}:${hook.timeout || ''}`;
216
+ if (!existingCommands.has(signature)) {
217
+ mergedHooks.push(clone(hook));
218
+ existingCommands.add(signature);
219
+ }
220
+ }
221
+ targetBlocks[index] = { ...current, hooks: mergedHooks };
222
+ }
223
+ merged[stage] = targetBlocks;
224
+ }
225
+
226
+ return merged;
227
+ }
228
+
229
+ function getPermissionProfile(key = 'safe-write') {
230
+ return PERMISSION_PROFILES.find(profile => profile.key === key) ||
231
+ PERMISSION_PROFILES.find(profile => profile.key === 'safe-write');
232
+ }
233
+
234
+ function isWritableProfile(key = 'safe-write') {
235
+ return ['safe-write', 'power-user', 'internal-research'].includes(getPermissionProfile(key).key);
236
+ }
237
+
238
+ function ensureWritableProfile(key = 'safe-write', commandName = 'apply', dryRun = false) {
239
+ const profile = getPermissionProfile(key);
240
+ if (!dryRun && !isWritableProfile(profile.key)) {
241
+ throw new Error(`${commandName} requires a writable profile. Use --profile safe-write or --dry-run.`);
242
+ }
243
+ return profile;
244
+ }
245
+
246
+ function buildHookConfig(hookFiles, profileKey) {
247
+ const profile = getPermissionProfile(profileKey);
248
+ if (!isWritableProfile(profile.key)) {
249
+ return {};
250
+ }
251
+
252
+ const uniqueFiles = [...new Set(hookFiles)].sort();
253
+ if (uniqueFiles.length === 0) {
254
+ return {};
255
+ }
256
+
257
+ const hookConfig = {
258
+ PostToolUse: [{
259
+ matcher: 'Write|Edit',
260
+ hooks: uniqueFiles
261
+ .filter(file => file !== 'protect-secrets.sh' && file !== 'session-start.sh')
262
+ .map(file => ({
263
+ type: 'command',
264
+ command: `bash .claude/hooks/${file}`,
265
+ timeout: 10,
266
+ })),
267
+ }],
268
+ };
269
+
270
+ if (uniqueFiles.includes('protect-secrets.sh')) {
271
+ hookConfig.PreToolUse = [{
272
+ matcher: 'Read|Write|Edit',
273
+ hooks: [{
274
+ type: 'command',
275
+ command: 'bash .claude/hooks/protect-secrets.sh',
276
+ timeout: 5,
277
+ }],
278
+ }];
279
+ }
280
+
281
+ if (uniqueFiles.includes('session-start.sh')) {
282
+ hookConfig.SessionStart = [{
283
+ matcher: '*',
284
+ hooks: [{
285
+ type: 'command',
286
+ command: 'bash .claude/hooks/session-start.sh',
287
+ timeout: 5,
288
+ }],
289
+ }];
290
+ }
291
+
292
+ if ((hookConfig.PostToolUse[0].hooks || []).length === 0) {
293
+ delete hookConfig.PostToolUse;
294
+ }
295
+
296
+ return hookConfig;
297
+ }
298
+
299
+ function buildSettingsForProfile({ profileKey = 'safe-write', hookFiles = [], existingSettings = null, mcpPackKeys = [] } = {}) {
300
+ const profile = getPermissionProfile(profileKey);
301
+ const base = existingSettings ? clone(existingSettings) : {};
302
+ const selectedMcpPacks = normalizeMcpPackKeys(mcpPackKeys);
303
+ base.permissions = base.permissions || {};
304
+ base.permissions.defaultMode = profile.defaultMode;
305
+ base.permissions.deny = mergeUnique(base.permissions.deny, profile.deny);
306
+
307
+ const hookConfig = buildHookConfig(hookFiles, profile.key);
308
+ if (Object.keys(hookConfig).length > 0) {
309
+ base.hooks = mergeHooks(base.hooks, hookConfig);
310
+ }
311
+
312
+ if (selectedMcpPacks.length > 0) {
313
+ base.mcpServers = mergeMcpServers(base.mcpServers, selectedMcpPacks);
314
+ }
315
+
316
+ base.claudexSetup = {
317
+ ...(base.claudexSetup || {}),
318
+ profile: profile.key,
319
+ mcpPacks: selectedMcpPacks,
320
+ };
321
+
322
+ return base;
323
+ }
324
+
140
325
  function getGovernanceSummary() {
141
326
  return {
142
327
  permissionProfiles: PERMISSION_PROFILES,
143
328
  hookRegistry: HOOK_REGISTRY,
144
329
  policyPacks: POLICY_PACKS,
330
+ domainPacks: DOMAIN_PACKS,
331
+ mcpPacks: MCP_PACKS,
145
332
  pilotRolloutKit: PILOT_ROLLOUT_KIT,
146
333
  };
147
334
  }
@@ -168,8 +355,9 @@ function printGovernanceSummary(summary, options = {}) {
168
355
 
169
356
  console.log(' Hook Registry');
170
357
  for (const hook of summary.hookRegistry) {
171
- console.log(` - ${hook.file}`);
172
- console.log(` ${hook.triggerPoint} ${hook.matcher} -> ${hook.purpose}`);
358
+ const riskColor = hook.risk === 'low' ? '\x1b[32m' : hook.risk === 'medium' ? '\x1b[33m' : '\x1b[31m';
359
+ console.log(` - ${hook.file} ${riskColor}[${hook.risk} risk]\x1b[0m`);
360
+ console.log(` ${hook.triggerPoint}${hook.matcher ? ` ${hook.matcher}` : ''} -> ${hook.purpose}`);
173
361
  }
174
362
  console.log('');
175
363
 
@@ -179,6 +367,18 @@ function printGovernanceSummary(summary, options = {}) {
179
367
  }
180
368
  console.log('');
181
369
 
370
+ console.log(' Domain Packs');
371
+ for (const pack of summary.domainPacks) {
372
+ console.log(` - ${pack.label}: ${pack.useWhen}`);
373
+ }
374
+ console.log('');
375
+
376
+ console.log(' MCP Packs');
377
+ for (const pack of summary.mcpPacks) {
378
+ console.log(` - ${pack.label}: ${Object.keys(pack.servers).join(', ')}`);
379
+ }
380
+ console.log('');
381
+
182
382
  console.log(' Pilot Rollout Kit');
183
383
  for (const item of summary.pilotRolloutKit.recommendedScope) {
184
384
  console.log(` - ${item}`);
@@ -187,6 +387,11 @@ function printGovernanceSummary(summary, options = {}) {
187
387
  }
188
388
 
189
389
  module.exports = {
390
+ PERMISSION_PROFILES,
391
+ getPermissionProfile,
392
+ isWritableProfile,
393
+ ensureWritableProfile,
394
+ buildSettingsForProfile,
190
395
  getGovernanceSummary,
191
396
  printGovernanceSummary,
192
397
  };
package/src/index.js CHANGED
@@ -4,6 +4,8 @@ const { analyzeProject } = require('./analyze');
4
4
  const { buildProposalBundle, applyProposalBundle } = require('./plans');
5
5
  const { getGovernanceSummary } = require('./governance');
6
6
  const { runBenchmark } = require('./benchmark');
7
+ const { DOMAIN_PACKS, detectDomainPacks } = require('./domain-packs');
8
+ const { MCP_PACKS, getMcpPack, mergeMcpServers, recommendMcpPacks } = require('./mcp-packs');
7
9
 
8
10
  module.exports = {
9
11
  audit,
@@ -13,4 +15,10 @@ module.exports = {
13
15
  applyProposalBundle,
14
16
  getGovernanceSummary,
15
17
  runBenchmark,
18
+ DOMAIN_PACKS,
19
+ detectDomainPacks,
20
+ MCP_PACKS,
21
+ getMcpPack,
22
+ mergeMcpServers,
23
+ recommendMcpPacks,
16
24
  };