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/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +34 -12
- package/bin/cli.js +42 -5
- package/package.json +3 -2
- package/src/analyze.js +156 -4
- package/src/audit.js +2 -2
- package/src/benchmark.js +81 -7
- package/src/claudex-sync.json +6 -2
- package/src/context.js +3 -2
- package/src/domain-packs.js +223 -0
- package/src/governance.js +207 -2
- package/src/index.js +8 -0
- package/src/mcp-packs.js +139 -0
- package/src/plans.js +329 -59
- package/src/setup.js +60 -36
- package/src/techniques.js +30 -10
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
|
|
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
|
|
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
|
-
:
|
|
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({
|
|
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
|
-
|
|
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
|
|
package/src/claudex-sync.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"synced_from": "claudex",
|
|
3
|
-
"synced_at": "2026-03-
|
|
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
|
-
|
|
172
|
-
console.log(`
|
|
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
|
};
|