claudex-setup 1.7.0 → 1.8.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/plans.js CHANGED
@@ -6,6 +6,7 @@ const { analyzeProject } = require('./analyze');
6
6
  const { ProjectContext } = require('./context');
7
7
  const { TECHNIQUES, STACKS } = require('./techniques');
8
8
  const { TEMPLATES } = require('./setup');
9
+ const { buildSettingsForProfile } = require('./governance');
9
10
  const { writeActivityArtifact, writeRollbackArtifact } = require('./activity');
10
11
 
11
12
  const TEMPLATE_DIR_MAP = {
@@ -35,6 +36,26 @@ const TEMPLATE_MODULES = {
35
36
  };
36
37
 
37
38
  const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
39
+ const FALLBACK_TEMPLATE_BY_KEY = {
40
+ importSyntax: 'claude-md',
41
+ verificationLoop: 'claude-md',
42
+ testCommand: 'claude-md',
43
+ lintCommand: 'claude-md',
44
+ buildCommand: 'claude-md',
45
+ securityReview: 'claude-md',
46
+ compactionAwareness: 'claude-md',
47
+ contextManagement: 'claude-md',
48
+ xmlTags: 'claude-md',
49
+ roleDefinition: 'claude-md',
50
+ constraintBlocks: 'claude-md',
51
+ claudeMdFreshness: 'claude-md',
52
+ permissionDeny: 'hooks',
53
+ secretsProtection: 'hooks',
54
+ preToolUseHook: 'hooks',
55
+ postToolUseHook: 'hooks',
56
+ sessionStartHook: 'hooks',
57
+ agentsHaveMaxTurns: 'agents',
58
+ };
38
59
 
39
60
  function previewContent(content) {
40
61
  return content.split('\n').slice(0, 12).join('\n');
@@ -46,73 +67,242 @@ function riskFromImpact(impact) {
46
67
  return 'low';
47
68
  }
48
69
 
70
+ function normalizeNewlines(content) {
71
+ return (content || '').replace(/\r\n/g, '\n');
72
+ }
73
+
74
+ function ensureTrailingNewline(content) {
75
+ const normalized = normalizeNewlines(content);
76
+ return normalized.endsWith('\n') ? normalized : `${normalized}\n`;
77
+ }
78
+
79
+ function upsertManagedBlock(content, id, block) {
80
+ const start = `<!-- claudex-setup:${id}:start -->`;
81
+ const end = `<!-- claudex-setup:${id}:end -->`;
82
+ const wrapped = `${start}\n${block.trim()}\n${end}`;
83
+ const pattern = new RegExp(`${start}[\\s\\S]*?${end}`);
84
+
85
+ if (pattern.test(content)) {
86
+ return {
87
+ changed: true,
88
+ content: content.replace(pattern, wrapped),
89
+ };
90
+ }
91
+
92
+ return {
93
+ changed: true,
94
+ content: `${content.trimEnd()}\n\n${wrapped}\n`,
95
+ };
96
+ }
97
+
98
+ function extractGeneratedBuildSection(content) {
99
+ const match = content.match(/## Build & Test\n([\s\S]*?)\n\n## Working Notes/);
100
+ return match ? `## Build & Test\n${match[1].trim()}` : null;
101
+ }
102
+
103
+ function extractGeneratedVerificationBlock(content) {
104
+ const match = content.match(/<verification>\n([\s\S]*?)\n<\/verification>/);
105
+ return match ? `<verification>\n${match[1].trim()}\n</verification>` : null;
106
+ }
107
+
108
+ function extractGeneratedWorkingNotes(content) {
109
+ const match = content.match(/## Working Notes\n([\s\S]*?)\n\n<constraints>/);
110
+ return match ? `## Working Notes\n${match[1].trim()}` : null;
111
+ }
112
+
113
+ function extractGeneratedConstraintsBlock(content) {
114
+ const match = content.match(/<constraints>\n([\s\S]*?)\n<\/constraints>/);
115
+ return match ? `<constraints>\n${match[1].trim()}\n</constraints>` : null;
116
+ }
117
+
118
+ function extractGeneratedContextSection(content) {
119
+ const match = content.match(/## Context Management\n([\s\S]*?)\n\n---/);
120
+ return match ? `## Context Management\n${match[1].trim()}` : null;
121
+ }
122
+
49
123
  function getFailedTemplateGroups(ctx, only = []) {
50
124
  const groups = new Map();
51
125
  for (const [key, technique] of Object.entries(TECHNIQUES)) {
52
126
  const passed = technique.check(ctx);
53
- if (passed !== false || !technique.template) continue;
54
- if (technique.template === 'mermaid') continue;
55
- if (only.length > 0 && !only.includes(key) && !only.includes(technique.template)) continue;
56
- if (!groups.has(technique.template)) {
57
- groups.set(technique.template, []);
127
+ const templateKey = technique.template || FALLBACK_TEMPLATE_BY_KEY[key];
128
+ if (passed !== false || !templateKey) continue;
129
+ if (templateKey === 'mermaid') continue;
130
+ if (only.length > 0 && !only.includes(key) && !only.includes(templateKey)) continue;
131
+ if (!groups.has(templateKey)) {
132
+ groups.set(templateKey, []);
58
133
  }
59
- groups.get(technique.template).push({ key, ...technique });
134
+ groups.get(templateKey).push({ key, ...technique });
60
135
  }
61
136
  return groups;
62
137
  }
63
138
 
64
- function buildHookSettings(ctx, plannedHookFiles) {
139
+ function buildClaudeMdPatchFile(ctx, stacks) {
140
+ const claudePath = ctx.fileContent('CLAUDE.md') !== null
141
+ ? 'CLAUDE.md'
142
+ : (ctx.fileContent('.claude/CLAUDE.md') !== null ? '.claude/CLAUDE.md' : null);
143
+ if (!claudePath) return null;
144
+
145
+ const existing = normalizeNewlines(ctx.fileContent(claudePath));
146
+ const generated = TEMPLATES['claude-md'](stacks, ctx);
147
+ const buildSection = extractGeneratedBuildSection(generated);
148
+ const verificationBlock = extractGeneratedVerificationBlock(generated);
149
+ const workingNotes = extractGeneratedWorkingNotes(generated);
150
+ const constraintsBlock = extractGeneratedConstraintsBlock(generated);
151
+ const contextSection = extractGeneratedContextSection(generated);
152
+ let merged = existing;
153
+ let changed = false;
154
+
155
+ const hasTest = /npm test|pytest|jest|vitest|cargo test|go test|mix test|rspec/.test(merged);
156
+ const hasLint = /eslint|prettier|ruff|black|clippy|golangci-lint|rubocop/.test(merged);
157
+ const hasBuild = /npm run build|cargo build|go build|make|tsc|gradle build|mvn compile/.test(merged);
158
+ const hasVerification = merged.includes('<verification>');
159
+ const hasSecurityWorkflow = merged.toLowerCase().includes('security') || merged.includes('/security-review');
160
+ const hasImportGuidance = merged.includes('@import');
161
+ const hasRoleDefinition = /you are|your role|act as|persona|behave as/i.test(merged);
162
+ const hasConstraintBlock = /<constraints|<rules|<requirements|<boundaries/i.test(merged);
163
+ const hasCompaction = /\/compact|compaction/i.test(merged);
164
+ const hasContextManagement = /context.*(manage|window|limit|budget|token)/i.test(merged);
165
+ const modernFeatures = ['hook', 'skill', 'agent', 'subagent', 'mcp', 'worktree'];
166
+ const hasFreshness = modernFeatures.filter(feature => merged.toLowerCase().includes(feature)).length >= 2;
167
+
168
+ if ((!hasTest || !hasLint || !hasBuild) && buildSection) {
169
+ const result = upsertManagedBlock(merged, 'build-test', buildSection);
170
+ merged = result.content;
171
+ changed = true;
172
+ }
173
+
174
+ if (!hasRoleDefinition && workingNotes) {
175
+ const result = upsertManagedBlock(merged, 'working-style', workingNotes);
176
+ merged = result.content;
177
+ changed = true;
178
+ }
179
+
180
+ if (!hasConstraintBlock && constraintsBlock) {
181
+ const result = upsertManagedBlock(merged, 'constraints', constraintsBlock);
182
+ merged = result.content;
183
+ changed = true;
184
+ }
185
+
186
+ if (!hasVerification && verificationBlock) {
187
+ const result = upsertManagedBlock(merged, 'verification', verificationBlock);
188
+ merged = result.content;
189
+ changed = true;
190
+ }
191
+
192
+ if (!hasSecurityWorkflow) {
193
+ const result = upsertManagedBlock(merged, 'security-workflow', [
194
+ '## Security Workflow',
195
+ '- Run `/security-review` when touching authentication, permissions, secrets, or customer data.',
196
+ '- Treat secret access, shell commands, and risky file operations as review-worthy changes.',
197
+ ].join('\n'));
198
+ merged = result.content;
199
+ changed = true;
200
+ }
201
+
202
+ if (!hasImportGuidance) {
203
+ const result = upsertManagedBlock(merged, 'modularity', [
204
+ '## Modularity',
205
+ '- If this file grows, split it with `@import ./docs/...` so the base instructions stay concise.',
206
+ ].join('\n'));
207
+ merged = result.content;
208
+ changed = true;
209
+ }
210
+
211
+ if ((!hasCompaction || !hasContextManagement || !hasFreshness) && contextSection) {
212
+ const result = upsertManagedBlock(merged, 'context-management', contextSection);
213
+ merged = result.content;
214
+ changed = true;
215
+ }
216
+
217
+ if (!changed) {
218
+ return null;
219
+ }
220
+
221
+ return {
222
+ path: claudePath,
223
+ action: 'patch',
224
+ currentState: 'existing CLAUDE.md is missing recommended verification or security sections',
225
+ proposedState: 'append managed sections for verification, security workflow, and modularity',
226
+ content: ensureTrailingNewline(merged),
227
+ };
228
+ }
229
+
230
+ function buildAgentPatchFiles(ctx) {
231
+ if (!ctx.hasDir('.claude/agents')) {
232
+ return [];
233
+ }
234
+
235
+ return ctx.dirFiles('.claude/agents')
236
+ .filter(file => file.endsWith('.md'))
237
+ .map((file) => {
238
+ const relativePath = `.claude/agents/${file}`;
239
+ const content = normalizeNewlines(ctx.fileContent(relativePath) || '');
240
+ if (!content.startsWith('---\n') || content.includes('\nmaxTurns:')) {
241
+ return null;
242
+ }
243
+
244
+ const updated = content.replace(/^---\n([\s\S]*?)\n---/, (match, frontmatter) => `---\n${frontmatter}\nmaxTurns: 50\n---`);
245
+ if (updated === content) {
246
+ return null;
247
+ }
248
+
249
+ return {
250
+ path: relativePath,
251
+ action: 'patch',
252
+ currentState: 'existing agent is missing a maxTurns safety limit',
253
+ proposedState: 'add maxTurns: 50 to the agent frontmatter',
254
+ content: ensureTrailingNewline(updated),
255
+ };
256
+ })
257
+ .filter(Boolean);
258
+ }
259
+
260
+ function buildHookSettings(ctx, plannedHookFiles, options = {}) {
65
261
  const existing = ctx.hasDir('.claude/hooks')
66
262
  ? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh'))
67
263
  : [];
68
264
  const hookFiles = [...new Set([...existing, ...plannedHookFiles])].sort();
69
- if (hookFiles.length === 0 || ctx.fileContent('.claude/settings.json')) {
265
+ if (hookFiles.length === 0) {
70
266
  return null;
71
267
  }
268
+ const settingsPath = '.claude/settings.json';
269
+ const existingSettings = ctx.jsonFile(settingsPath);
270
+ const settings = buildSettingsForProfile({
271
+ profileKey: options.profile || 'safe-write',
272
+ hookFiles,
273
+ existingSettings,
274
+ mcpPackKeys: options.mcpPacks || [],
275
+ });
276
+ const content = `${JSON.stringify(settings, null, 2)}\n`;
277
+ const existingContent = existingSettings ? `${JSON.stringify(existingSettings, null, 2)}\n` : null;
72
278
 
73
- const settings = {
74
- permissions: {
75
- defaultMode: 'acceptEdits',
76
- deny: [
77
- 'Read(./.env*)',
78
- 'Read(./secrets/**)',
79
- 'Bash(rm -rf *)',
80
- 'Bash(git reset --hard *)',
81
- 'Bash(git checkout -- *)',
82
- 'Bash(git clean *)',
83
- 'Bash(git push --force *)',
84
- ],
85
- },
86
- hooks: {
87
- PostToolUse: [{
88
- matcher: 'Write|Edit',
89
- hooks: hookFiles.filter(file => file !== 'protect-secrets.sh').map(file => ({
90
- type: 'command',
91
- command: `bash .claude/hooks/${file}`,
92
- timeout: 10,
93
- })),
94
- }],
95
- },
96
- };
97
-
98
- if (hookFiles.includes('protect-secrets.sh')) {
99
- settings.hooks.PreToolUse = [{
100
- matcher: 'Read|Write|Edit',
101
- hooks: [{
102
- type: 'command',
103
- command: 'bash .claude/hooks/protect-secrets.sh',
104
- timeout: 5,
105
- }],
106
- }];
279
+ if (existingContent === content) {
280
+ return null;
107
281
  }
108
282
 
109
283
  return {
110
- path: '.claude/settings.json',
111
- content: `${JSON.stringify(settings, null, 2)}\n`,
284
+ path: settingsPath,
285
+ action: existingSettings ? 'patch' : 'create',
286
+ currentState: existingSettings
287
+ ? 'existing settings are missing selected profile protections or hook registrations'
288
+ : 'settings file is missing',
289
+ proposedState: existingSettings
290
+ ? `merge ${options.profile || 'safe-write'} profile protections into existing settings`
291
+ : `create settings for ${options.profile || 'safe-write'} profile and register hooks`,
292
+ content,
112
293
  };
113
294
  }
114
295
 
115
- function buildTemplateFiles(templateKey, stacks, ctx) {
296
+ function buildTemplateFiles(templateKey, stacks, ctx, triggers, options = {}) {
297
+ const patchFiles = templateKey === 'agents' ? buildAgentPatchFiles(ctx) : [];
298
+
299
+ if (templateKey === 'claude-md') {
300
+ const patchFile = buildClaudeMdPatchFile(ctx, stacks);
301
+ if (patchFile) {
302
+ return [patchFile];
303
+ }
304
+ }
305
+
116
306
  const template = TEMPLATES[templateKey];
117
307
  if (!template) return [];
118
308
 
@@ -124,10 +314,13 @@ function buildTemplateFiles(templateKey, stacks, ctx) {
124
314
  const targetDir = TEMPLATE_DIR_MAP[templateKey];
125
315
  if (!targetDir) return [];
126
316
 
127
- return Object.entries(result).map(([fileName, content]) => ({
317
+ const generatedFiles = Object.entries(result).map(([fileName, content]) => ({
128
318
  path: path.posix.join(targetDir.replace(/\\/g, '/'), fileName),
129
319
  content,
130
320
  }));
321
+
322
+ const patchedPaths = new Set(patchFiles.map(file => file.path));
323
+ return [...patchFiles, ...generatedFiles.filter(file => !patchedPaths.has(file.path))];
131
324
  }
132
325
 
133
326
  function toProposal(templateKey, triggers, templateFiles, ctx) {
@@ -139,9 +332,9 @@ function toProposal(templateKey, triggers, templateFiles, ctx) {
139
332
  const highestImpact = sortedTriggers[0]?.impact || 'medium';
140
333
  const files = templateFiles.map(file => {
141
334
  const exists = ctx.fileContent(file.path) !== null || ctx.hasDir(file.path);
142
- const action = exists ? 'manual-review' : 'create';
143
- const currentState = exists ? 'file already exists and will be preserved' : 'missing';
144
- const proposedState = exists ? 'generated baseline available for manual merge' : 'create new file';
335
+ const action = file.action || (exists ? 'manual-review' : 'create');
336
+ const currentState = file.currentState || (exists ? 'file already exists and will be preserved' : 'missing');
337
+ const proposedState = file.proposedState || (exists ? 'generated baseline available for manual merge' : 'create new file');
145
338
  const diffPreview = [
146
339
  `--- ${exists ? file.path : 'missing'}`,
147
340
  `+++ ${file.path}`,
@@ -173,7 +366,7 @@ function toProposal(templateKey, triggers, templateFiles, ctx) {
173
366
  })),
174
367
  rationale: sortedTriggers.map(trigger => trigger.fix),
175
368
  files,
176
- readyToApply: files.some(file => file.action === 'create'),
369
+ readyToApply: files.some(file => ['create', 'patch'].includes(file.action)),
177
370
  };
178
371
  }
179
372
 
@@ -185,12 +378,12 @@ async function buildProposalBundle(options) {
185
378
  const proposals = [];
186
379
 
187
380
  for (const [templateKey, triggers] of groups.entries()) {
188
- const templateFiles = buildTemplateFiles(templateKey, stacks, ctx);
381
+ const templateFiles = buildTemplateFiles(templateKey, stacks, ctx, triggers, options);
189
382
  if (templateKey === 'hooks') {
190
383
  const plannedHookFiles = templateFiles
191
384
  .map(file => path.basename(file.path))
192
385
  .filter(file => file.endsWith('.sh'));
193
- const settingsFile = buildHookSettings(ctx, plannedHookFiles);
386
+ const settingsFile = buildHookSettings(ctx, plannedHookFiles, options);
194
387
  if (settingsFile) {
195
388
  templateFiles.push(settingsFile);
196
389
  }
@@ -256,11 +449,75 @@ function writePlanFile(bundle, outFile) {
256
449
  });
257
450
  }
258
451
 
452
+ function tryParseJson(content) {
453
+ try {
454
+ return JSON.parse(content);
455
+ } catch {
456
+ return null;
457
+ }
458
+ }
459
+
460
+ function applyRuntimeSettingsOverlays(bundle, options) {
461
+ if (!bundle || !Array.isArray(bundle.proposals)) {
462
+ return bundle;
463
+ }
464
+
465
+ const ctx = new ProjectContext(options.dir);
466
+ const existingHooks = ctx.hasDir('.claude/hooks')
467
+ ? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh'))
468
+ : [];
469
+
470
+ const proposals = bundle.proposals.map((proposal) => {
471
+ const settingsIndex = proposal.files.findIndex(file => file.path === '.claude/settings.json');
472
+ if (settingsIndex === -1) {
473
+ return proposal;
474
+ }
475
+
476
+ const plannedHookFiles = proposal.files
477
+ .filter(file => file.path.startsWith('.claude/hooks/') && file.path.endsWith('.sh'))
478
+ .map(file => path.basename(file.path));
479
+ const hookFiles = [...new Set([...existingHooks, ...plannedHookFiles])].sort();
480
+ const currentSettings = tryParseJson(proposal.files[settingsIndex].content) || ctx.jsonFile('.claude/settings.json') || null;
481
+ const mergedSettings = buildSettingsForProfile({
482
+ profileKey: options.profile || 'safe-write',
483
+ hookFiles,
484
+ existingSettings: currentSettings,
485
+ mcpPackKeys: options.mcpPacks || [],
486
+ });
487
+ const updatedContent = `${JSON.stringify(mergedSettings, null, 2)}\n`;
488
+ const currentFile = proposal.files[settingsIndex];
489
+
490
+ const files = [...proposal.files];
491
+ files[settingsIndex] = {
492
+ ...currentFile,
493
+ content: updatedContent,
494
+ preview: previewContent(updatedContent),
495
+ diffPreview: [
496
+ `--- ${ctx.fileContent(currentFile.path) !== null ? currentFile.path : 'missing'}`,
497
+ `+++ ${currentFile.path}`,
498
+ ...previewContent(updatedContent).split('\n').map(line => `+${line}`),
499
+ ].join('\n'),
500
+ currentState: currentFile.currentState || 'existing settings are missing runtime-selected protections or MCP packs',
501
+ proposedState: `merge ${options.profile || 'safe-write'} profile protections and requested MCP packs into settings`,
502
+ };
503
+
504
+ return {
505
+ ...proposal,
506
+ files,
507
+ };
508
+ });
509
+
510
+ return {
511
+ ...bundle,
512
+ proposals,
513
+ };
514
+ }
515
+
259
516
  function resolvePlan(bundle, options) {
260
517
  if (options.planFile) {
261
- return JSON.parse(fs.readFileSync(options.planFile, 'utf8'));
518
+ return applyRuntimeSettingsOverlays(JSON.parse(fs.readFileSync(options.planFile, 'utf8')), options);
262
519
  }
263
- return bundle;
520
+ return applyRuntimeSettingsOverlays(bundle, options);
264
521
  }
265
522
 
266
523
  async function applyProposalBundle(options) {
@@ -275,38 +532,49 @@ async function applyProposalBundle(options) {
275
532
  });
276
533
 
277
534
  const createdFiles = [];
535
+ const patchedFiles = [];
278
536
  const skippedFiles = [];
279
537
  for (const proposal of selected) {
280
538
  for (const file of proposal.files) {
281
- if (file.action !== 'create') {
539
+ if (!['create', 'patch'].includes(file.action)) {
282
540
  skippedFiles.push(file.path);
283
541
  continue;
284
542
  }
285
543
  const fullPath = path.join(options.dir, file.path);
286
- if (fs.existsSync(fullPath)) {
544
+ if (file.action === 'create' && fs.existsSync(fullPath)) {
287
545
  skippedFiles.push(file.path);
288
546
  continue;
289
547
  }
548
+ const previousContent = fs.existsSync(fullPath) ? fs.readFileSync(fullPath, 'utf8') : null;
290
549
  if (!options.dryRun) {
291
550
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
292
551
  fs.writeFileSync(fullPath, file.content, 'utf8');
293
552
  }
294
- createdFiles.push(file.path);
553
+ if (file.action === 'create') {
554
+ createdFiles.push(file.path);
555
+ } else {
556
+ patchedFiles.push({ path: file.path, previousContent });
557
+ }
295
558
  }
296
559
  }
297
560
 
298
561
  let rollback = null;
299
562
  let activity = null;
300
- if (!options.dryRun && createdFiles.length > 0) {
563
+ if (!options.dryRun && (createdFiles.length > 0 || patchedFiles.length > 0)) {
301
564
  rollback = writeRollbackArtifact(options.dir, {
302
565
  sourcePlan: options.planFile ? path.basename(options.planFile) : 'live-plan',
303
566
  createdFiles,
304
- rollbackInstructions: createdFiles.map(file => `Delete ${file}`),
567
+ patchedFiles,
568
+ rollbackInstructions: [
569
+ ...createdFiles.map(file => `Delete ${file}`),
570
+ ...patchedFiles.map(file => `Restore previous content for ${file.path} from this manifest`),
571
+ ],
305
572
  });
306
573
  activity = writeActivityArtifact(options.dir, 'apply', {
307
574
  sourcePlan: options.planFile ? path.basename(options.planFile) : 'live-plan',
308
575
  appliedProposalIds: selected.map(item => item.id),
309
576
  createdFiles,
577
+ patchedFiles: patchedFiles.map(file => file.path),
310
578
  skippedFiles,
311
579
  rollbackArtifact: rollback.relativePath,
312
580
  });
@@ -316,6 +584,7 @@ async function applyProposalBundle(options) {
316
584
  proposalCount: bundle.proposals.length,
317
585
  appliedProposalIds: selected.map(item => item.id),
318
586
  createdFiles,
587
+ patchedFiles: patchedFiles.map(file => file.path),
319
588
  skippedFiles,
320
589
  dryRun: options.dryRun === true,
321
590
  rollbackArtifact: rollback ? rollback.relativePath : null,
@@ -337,6 +606,7 @@ function printApplyResult(result, options = {}) {
337
606
  }
338
607
  console.log(` Applied proposal bundles: ${result.appliedProposalIds.join(', ') || 'none'}`);
339
608
  console.log(` Created files: ${result.createdFiles.join(', ') || 'none'}`);
609
+ console.log(` Patched files: ${result.patchedFiles.join(', ') || 'none'}`);
340
610
  if (result.rollbackArtifact) {
341
611
  console.log(` Rollback: ${result.rollbackArtifact}`);
342
612
  }
package/src/setup.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Setup engine - applies recommended Claude Code configuration to a project.
3
- * v1.7.0 - Starter-safe setup engine with reusable planning primitives.
3
+ * v1.8.0 - Starter-safe setup engine with reusable planning primitives.
4
4
  */
5
5
 
6
6
  const fs = require('fs');
@@ -8,6 +8,7 @@ const path = require('path');
8
8
  const { TECHNIQUES, STACKS } = require('./techniques');
9
9
  const { ProjectContext } = require('./context');
10
10
  const { audit } = require('./audit');
11
+ const { buildSettingsForProfile } = require('./governance');
11
12
 
12
13
  // ============================================================
13
14
  // Helper: detect project scripts from package.json
@@ -826,6 +827,19 @@ mkdir -p "$LOG_DIR"
826
827
  TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
827
828
  echo "[$TIMESTAMP] $TOOL_NAME: $FILE_PATH" >> "$LOG_FILE"
828
829
 
830
+ exit 0
831
+ `,
832
+ 'session-start.sh': `#!/bin/bash
833
+ # SessionStart hook - prepares logs and records session entry
834
+
835
+ LOG_DIR=".claude/logs"
836
+ LOG_FILE="$LOG_DIR/sessions.log"
837
+
838
+ mkdir -p "$LOG_DIR"
839
+
840
+ TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
841
+ echo "[$TIMESTAMP] session started" >> "$LOG_FILE"
842
+
829
843
  exit 0
830
844
  `,
831
845
  }),
@@ -877,6 +891,15 @@ exit 0
877
891
  1. Run \`git diff\` to see all changes
878
892
  2. Check for: bugs, security issues, missing tests, code style
879
893
  3. Provide actionable feedback
894
+ `;
895
+
896
+ cmds['security-review.md'] = `Run a focused security review using Claude Code's built-in security workflow.
897
+
898
+ ## Steps:
899
+ 1. Review auth, permissions, secrets handling, and data access paths
900
+ 2. Run \`/security-review\` for OWASP-focused analysis
901
+ 3. Check for unsafe shell commands, token leakage, and risky file access
902
+ 4. Report findings ordered by severity with concrete fixes
880
903
  `;
881
904
 
882
905
  // Deploy - stack-specific
@@ -980,6 +1003,17 @@ Fix the GitHub issue: $ARGUMENTS
980
1003
  3. Implement the fix
981
1004
  4. Write tests
982
1005
  5. Create a descriptive commit
1006
+ `,
1007
+ 'release-check/SKILL.md': `---
1008
+ name: release-check
1009
+ description: Prepare a release candidate and verify publish readiness
1010
+ ---
1011
+ Prepare a release candidate for: $ARGUMENTS
1012
+
1013
+ 1. Read CHANGELOG.md and package.json version
1014
+ 2. Run the test suite and packaging checks
1015
+ 3. Verify docs, tags, and release notes are aligned
1016
+ 4. Flag anything that would make the release unsafe or misleading
983
1017
  `,
984
1018
  }),
985
1019
 
@@ -1027,6 +1061,12 @@ Fix the GitHub issue: $ARGUMENTS
1027
1061
  - Never skip or disable tests without a tracking issue
1028
1062
  - Mock external dependencies, not internal logic
1029
1063
  - Include both happy path and edge case tests
1064
+ `;
1065
+ rules['repository.md'] = `When changing release, packaging, or workflow files:
1066
+ - Keep package.json, CHANGELOG.md, README.md, and docs in sync
1067
+ - Prefer tagged release references over floating branch references in public docs
1068
+ - Preserve backward compatibility in CLI flags where practical
1069
+ - Any automation that writes files must document rollback expectations
1030
1070
  `;
1031
1071
  return rules;
1032
1072
  },
@@ -1037,12 +1077,26 @@ name: security-reviewer
1037
1077
  description: Reviews code for security vulnerabilities
1038
1078
  tools: [Read, Grep, Glob]
1039
1079
  model: sonnet
1080
+ maxTurns: 50
1040
1081
  ---
1041
1082
  Review code for security issues:
1042
1083
  - Injection vulnerabilities (SQL, XSS, command injection)
1043
1084
  - Authentication and authorization flaws
1044
1085
  - Secrets or credentials in code
1045
1086
  - Insecure data handling
1087
+ `,
1088
+ 'release-manager.md': `---
1089
+ name: release-manager
1090
+ description: Checks release readiness and packaging consistency
1091
+ tools: [Read, Grep, Glob]
1092
+ model: sonnet
1093
+ maxTurns: 50
1094
+ ---
1095
+ Review release readiness:
1096
+ - version alignment across package.json, changelog, and docs
1097
+ - publish safety and packaging scope
1098
+ - missing rollback or migration notes
1099
+ - documentation drift that would confuse adopters
1046
1100
  `,
1047
1101
  }),
1048
1102
 
@@ -1164,41 +1218,11 @@ async function setup(options) {
1164
1218
  if (fs.existsSync(hooksDir) && !fs.existsSync(settingsPath)) {
1165
1219
  const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh'));
1166
1220
  if (hookFiles.length > 0) {
1167
- const settings = {
1168
- permissions: {
1169
- defaultMode: "acceptEdits",
1170
- deny: [
1171
- "Read(./.env*)",
1172
- "Read(./secrets/**)",
1173
- "Bash(rm -rf *)",
1174
- "Bash(git reset --hard *)",
1175
- "Bash(git checkout -- *)",
1176
- "Bash(git clean *)",
1177
- "Bash(git push --force *)"
1178
- ]
1179
- },
1180
- hooks: {
1181
- PostToolUse: [{
1182
- matcher: "Write|Edit",
1183
- hooks: hookFiles.filter(f => f !== 'protect-secrets.sh').map(f => ({
1184
- type: "command",
1185
- command: `bash .claude/hooks/${f}`,
1186
- timeout: 10
1187
- }))
1188
- }]
1189
- }
1190
- };
1191
- // Add protect-secrets as PreToolUse if it exists
1192
- if (hookFiles.includes('protect-secrets.sh')) {
1193
- settings.hooks.PreToolUse = [{
1194
- matcher: "Read|Write|Edit",
1195
- hooks: [{
1196
- type: "command",
1197
- command: "bash .claude/hooks/protect-secrets.sh",
1198
- timeout: 5
1199
- }]
1200
- }];
1201
- }
1221
+ const settings = buildSettingsForProfile({
1222
+ profileKey: options.profile || 'safe-write',
1223
+ hookFiles,
1224
+ mcpPackKeys: options.mcpPacks || [],
1225
+ });
1202
1226
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
1203
1227
  writtenFiles.push('.claude/settings.json');
1204
1228
  log(` \x1b[32m✅\x1b[0m Created .claude/settings.json (hooks registered)`);