@kennethsolomon/shipkit 1.0.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.
Files changed (117) hide show
  1. package/README.md +321 -0
  2. package/bin/shipkit.js +146 -0
  3. package/commands/sk/brainstorm.md +63 -0
  4. package/commands/sk/branch.md +35 -0
  5. package/commands/sk/config.md +96 -0
  6. package/commands/sk/execute-plan.md +85 -0
  7. package/commands/sk/features.md +238 -0
  8. package/commands/sk/finish-feature.md +154 -0
  9. package/commands/sk/help.md +103 -0
  10. package/commands/sk/hotfix.md +61 -0
  11. package/commands/sk/plan.md +30 -0
  12. package/commands/sk/release.md +72 -0
  13. package/commands/sk/security-check.md +188 -0
  14. package/commands/sk/set-profile.md +71 -0
  15. package/commands/sk/status.md +25 -0
  16. package/commands/sk/update-task.md +35 -0
  17. package/commands/sk/write-plan.md +72 -0
  18. package/package.json +23 -0
  19. package/skills/sk:accessibility/LICENSE.txt +177 -0
  20. package/skills/sk:accessibility/SKILL.md +150 -0
  21. package/skills/sk:api-design/LICENSE.txt +177 -0
  22. package/skills/sk:api-design/SKILL.md +158 -0
  23. package/skills/sk:brainstorming/SKILL.md +124 -0
  24. package/skills/sk:debug/SKILL.md +252 -0
  25. package/skills/sk:debug/debug_conductor.py +177 -0
  26. package/skills/sk:debug/lib/__init__.py +1 -0
  27. package/skills/sk:debug/lib/bug_gatherer.py +55 -0
  28. package/skills/sk:debug/lib/context_reader.py +139 -0
  29. package/skills/sk:debug/lib/findings_writer.py +76 -0
  30. package/skills/sk:debug/lib/lessons_writer.py +165 -0
  31. package/skills/sk:debug/lib/step_runner.py +326 -0
  32. package/skills/sk:features/SKILL.md +238 -0
  33. package/skills/sk:frontend-design/LICENSE.txt +177 -0
  34. package/skills/sk:frontend-design/SKILL.md +191 -0
  35. package/skills/sk:laravel-init/SKILL.md +37 -0
  36. package/skills/sk:laravel-new/SKILL.md +68 -0
  37. package/skills/sk:lint/SKILL.md +113 -0
  38. package/skills/sk:perf/LICENSE.txt +177 -0
  39. package/skills/sk:perf/SKILL.md +188 -0
  40. package/skills/sk:release/SKILL.md +113 -0
  41. package/skills/sk:release/references/android-checklist.md +269 -0
  42. package/skills/sk:release/references/ios-checklist.md +339 -0
  43. package/skills/sk:release/release.sh +378 -0
  44. package/skills/sk:review/SKILL.md +346 -0
  45. package/skills/sk:review/references/security-checklist.md +223 -0
  46. package/skills/sk:schema-migrate/SKILL.md +125 -0
  47. package/skills/sk:schema-migrate/orms/drizzle.md +546 -0
  48. package/skills/sk:schema-migrate/orms/laravel.md +367 -0
  49. package/skills/sk:schema-migrate/orms/prisma.md +357 -0
  50. package/skills/sk:schema-migrate/orms/rails.md +351 -0
  51. package/skills/sk:schema-migrate/orms/sqlalchemy.md +385 -0
  52. package/skills/sk:schema-migrate/references/detection.md +110 -0
  53. package/skills/sk:setup-claude/SKILL.md +365 -0
  54. package/skills/sk:setup-claude/references/detection.md +6 -0
  55. package/skills/sk:setup-claude/references/templates.md +11 -0
  56. package/skills/sk:setup-claude/scripts/apply_setup_claude.py +443 -0
  57. package/skills/sk:setup-claude/scripts/detect_arch_changes.py +437 -0
  58. package/skills/sk:setup-claude/templates/.claude/docs/arch-changelog-guide.md.template +6 -0
  59. package/skills/sk:setup-claude/templates/.claude/docs/changelog-guide.md.template +12 -0
  60. package/skills/sk:setup-claude/templates/CHANGELOG.md.template +21 -0
  61. package/skills/sk:setup-claude/templates/CLAUDE.md.template +299 -0
  62. package/skills/sk:setup-claude/templates/arch-changelog-guide.md.template +3 -0
  63. package/skills/sk:setup-claude/templates/changelog-guide.md.template +3 -0
  64. package/skills/sk:setup-claude/templates/commands/brainstorm.md.template +74 -0
  65. package/skills/sk:setup-claude/templates/commands/execute-plan.md.template +57 -0
  66. package/skills/sk:setup-claude/templates/commands/features.md.template +238 -0
  67. package/skills/sk:setup-claude/templates/commands/finish-feature.md.template +155 -0
  68. package/skills/sk:setup-claude/templates/commands/plan.md.template +30 -0
  69. package/skills/sk:setup-claude/templates/commands/re-setup.md.template +38 -0
  70. package/skills/sk:setup-claude/templates/commands/release.md.template +74 -0
  71. package/skills/sk:setup-claude/templates/commands/security-check.md.template +172 -0
  72. package/skills/sk:setup-claude/templates/commands/status.md.template +17 -0
  73. package/skills/sk:setup-claude/templates/commands/write-plan.md.template +34 -0
  74. package/skills/sk:setup-claude/templates/finish-feature.md.template +3 -0
  75. package/skills/sk:setup-claude/templates/plan.md.template +3 -0
  76. package/skills/sk:setup-claude/templates/status.md.template +3 -0
  77. package/skills/sk:setup-claude/templates/tasks/findings.md.template +19 -0
  78. package/skills/sk:setup-claude/templates/tasks/lessons.md.template +26 -0
  79. package/skills/sk:setup-claude/templates/tasks/progress.md.template +20 -0
  80. package/skills/sk:setup-claude/templates/tasks/security-findings.md.template +5 -0
  81. package/skills/sk:setup-claude/templates/tasks/todo.md.template +26 -0
  82. package/skills/sk:setup-claude/templates/tasks/workflow-status.md.template +31 -0
  83. package/skills/sk:setup-claude/templates/tasks-findings.md.template +3 -0
  84. package/skills/sk:setup-claude/templates/tasks-lessons.md.template +3 -0
  85. package/skills/sk:setup-claude/templates/tasks-progress.md.template +3 -0
  86. package/skills/sk:setup-claude/templates/tasks-todo.md.template +3 -0
  87. package/skills/sk:setup-claude/tests/test_apply_setup_claude.py +193 -0
  88. package/skills/sk:setup-optimizer/SKILL.md +184 -0
  89. package/skills/sk:setup-optimizer/lib/__init__.py +24 -0
  90. package/skills/sk:setup-optimizer/lib/detect.py +205 -0
  91. package/skills/sk:setup-optimizer/lib/discover.py +221 -0
  92. package/skills/sk:setup-optimizer/lib/enrich.py +163 -0
  93. package/skills/sk:setup-optimizer/lib/merge.py +277 -0
  94. package/skills/sk:setup-optimizer/lib/sidecar.py +129 -0
  95. package/skills/sk:setup-optimizer/optimize_claude.py +174 -0
  96. package/skills/sk:setup-optimizer/templates/CLAUDE.md.template +105 -0
  97. package/skills/sk:skill-creator/LICENSE.txt +202 -0
  98. package/skills/sk:skill-creator/SKILL.md +479 -0
  99. package/skills/sk:skill-creator/agents/analyzer.md +274 -0
  100. package/skills/sk:skill-creator/agents/comparator.md +202 -0
  101. package/skills/sk:skill-creator/agents/grader.md +223 -0
  102. package/skills/sk:skill-creator/assets/eval_review.html +146 -0
  103. package/skills/sk:skill-creator/eval-viewer/generate_review.py +471 -0
  104. package/skills/sk:skill-creator/eval-viewer/viewer.html +1325 -0
  105. package/skills/sk:skill-creator/references/schemas.md +430 -0
  106. package/skills/sk:skill-creator/scripts/aggregate_benchmark.py +401 -0
  107. package/skills/sk:skill-creator/scripts/generate_report.py +326 -0
  108. package/skills/sk:skill-creator/scripts/improve_description.py +248 -0
  109. package/skills/sk:skill-creator/scripts/package_skill.py +136 -0
  110. package/skills/sk:skill-creator/scripts/quick_validate.py +103 -0
  111. package/skills/sk:skill-creator/scripts/run_eval.py +310 -0
  112. package/skills/sk:skill-creator/scripts/run_loop.py +332 -0
  113. package/skills/sk:skill-creator/scripts/utils.py +47 -0
  114. package/skills/sk:smart-commit/SKILL.md +175 -0
  115. package/skills/sk:test/SKILL.md +171 -0
  116. package/skills/sk:write-tests/SKILL.md +195 -0
  117. package/skills/sk:write-tests/references/patterns.md +209 -0
@@ -0,0 +1,221 @@
1
+ """Intelligent project structure, documentation, and workflow discovery."""
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, List, Tuple
5
+
6
+
7
+ # Directories to exclude from discovery
8
+ EXCLUDE_DIRS = {
9
+ 'node_modules', '.next', 'dist', 'build', 'out',
10
+ '.venv', 'venv', 'env', '__pycache__',
11
+ '.git', '.github/actions', 'vendor',
12
+ '.pytest_cache', '.mypy_cache', '.tox',
13
+ 'coverage', '.coverage', 'htmlcov',
14
+ 'egg-info', '.eggs', '*.egg-info',
15
+ }
16
+
17
+ # Documentation patterns to look for
18
+ DOC_PATTERNS = {
19
+ 'README.md': 'Project overview',
20
+ 'CONTRIBUTING.md': 'How to contribute',
21
+ 'CODE_OF_CONDUCT.md': 'Code of conduct',
22
+ 'CHANGELOG.md': 'Version history',
23
+ 'LICENSE': 'License information',
24
+ }
25
+
26
+
27
+ def discover_directories(root_path: Path) -> Dict[str, str]:
28
+ """Auto-discover key project directories and their purposes.
29
+
30
+ Args:
31
+ root_path: Project root directory
32
+
33
+ Returns:
34
+ Dict mapping directory names to descriptions
35
+ """
36
+ found_dirs = {}
37
+
38
+ # Define directory patterns and their descriptions
39
+ dir_patterns = {
40
+ 'src': 'Application source code',
41
+ 'lib': 'Library utilities and helpers',
42
+ 'app': 'Application code',
43
+ 'source': 'Source code',
44
+ 'tests': 'Test suite',
45
+ 'test': 'Test suite',
46
+ '__tests__': 'Test suite',
47
+ 'spec': 'Test specifications',
48
+ 'docs': 'Documentation',
49
+ 'public': 'Static assets and public files',
50
+ 'static': 'Static assets',
51
+ 'assets': 'Project assets',
52
+ 'scripts': 'Utility and build scripts',
53
+ 'config': 'Configuration files',
54
+ 'migrations': 'Database migrations',
55
+ 'prisma': 'Prisma database schema',
56
+ 'alembic': 'SQLAlchemy migrations',
57
+ '.github': 'GitHub configuration and workflows',
58
+ 'infra': 'Infrastructure as code',
59
+ 'terraform': 'Terraform configuration',
60
+ 'docker': 'Docker configuration',
61
+ }
62
+
63
+ for dir_name, description in dir_patterns.items():
64
+ dir_path = root_path / dir_name
65
+ if dir_path.exists() and dir_path.is_dir():
66
+ # Skip if in exclude list
67
+ if dir_name not in EXCLUDE_DIRS:
68
+ found_dirs[dir_name] = description
69
+
70
+ return found_dirs
71
+
72
+
73
+ def discover_documentation(root_path: Path) -> Dict[str, str]:
74
+ """Find documentation files and link them.
75
+
76
+ Args:
77
+ root_path: Project root directory
78
+
79
+ Returns:
80
+ Dict mapping doc file paths to descriptions
81
+ """
82
+ found_docs = {}
83
+
84
+ # Check root level documentation
85
+ for pattern, description in DOC_PATTERNS.items():
86
+ doc_path = root_path / pattern
87
+ if doc_path.exists() and doc_path.is_file():
88
+ found_docs[pattern] = description
89
+
90
+ # Check docs/ directory for additional documentation
91
+ docs_dir = root_path / 'docs'
92
+ if docs_dir.exists() and docs_dir.is_dir():
93
+ # Look for common subdirectories and files
94
+ doc_files = {
95
+ 'API.md': 'API documentation',
96
+ 'api.md': 'API documentation',
97
+ 'ARCHITECTURE.md': 'Architecture overview',
98
+ 'architecture.md': 'Architecture overview',
99
+ 'DEPLOYMENT.md': 'Deployment guide',
100
+ 'deployment.md': 'Deployment guide',
101
+ 'SETUP.md': 'Setup instructions',
102
+ 'setup.md': 'Setup instructions',
103
+ 'TROUBLESHOOTING.md': 'Troubleshooting guide',
104
+ 'troubleshooting.md': 'Troubleshooting guide',
105
+ }
106
+
107
+ for filename, description in doc_files.items():
108
+ doc_file = docs_dir / filename
109
+ if doc_file.exists() and doc_file.is_file():
110
+ found_docs[f'docs/{filename}'] = description
111
+
112
+ # Check for subdirectories
113
+ try:
114
+ for item in docs_dir.iterdir():
115
+ if item.is_dir() and item.name not in EXCLUDE_DIRS:
116
+ found_docs[f'docs/{item.name}/'] = f'{item.name.capitalize()} documentation'
117
+ except PermissionError:
118
+ pass
119
+
120
+ # Check .github for documentation
121
+ github_dir = root_path / '.github'
122
+ if github_dir.exists() and github_dir.is_dir():
123
+ contributing = github_dir / 'CONTRIBUTING.md'
124
+ if contributing.exists():
125
+ found_docs['.github/CONTRIBUTING.md'] = 'GitHub contribution guidelines'
126
+
127
+ return found_docs
128
+
129
+
130
+ def discover_workflows(root_path: Path) -> Dict[str, List[str]]:
131
+ """Discover build workflows, scripts, and common commands.
132
+
133
+ Args:
134
+ root_path: Project root directory
135
+
136
+ Returns:
137
+ Dict mapping workflow type to list of commands/targets
138
+ """
139
+ workflows = {}
140
+
141
+ # Check Makefile for targets
142
+ makefile_targets = _extract_makefile_targets(root_path)
143
+ if makefile_targets:
144
+ workflows['make'] = makefile_targets
145
+
146
+ # Check package.json for npm/yarn scripts
147
+ npm_scripts = _extract_npm_scripts(root_path)
148
+ if npm_scripts:
149
+ workflows['npm'] = npm_scripts
150
+
151
+ # Check for GitHub Actions workflows
152
+ github_workflows = _find_github_workflows(root_path)
153
+ if github_workflows:
154
+ workflows['workflows'] = github_workflows
155
+
156
+ return workflows
157
+
158
+
159
+ def _extract_makefile_targets(root_path: Path) -> List[str]:
160
+ """Extract targets from Makefile."""
161
+ makefile = root_path / 'Makefile'
162
+ if not makefile.exists():
163
+ return []
164
+
165
+ targets = []
166
+ try:
167
+ content = makefile.read_text()
168
+ for line in content.split('\n'):
169
+ if line.startswith('.PHONY'):
170
+ # Extract targets from .PHONY line
171
+ targets_str = line.split(':')[1].strip()
172
+ targets.extend(targets_str.split())
173
+ elif ':' in line and not line.startswith('\t') and not line.startswith(' '):
174
+ target = line.split(':')[0].strip()
175
+ if target and not target.startswith('.'):
176
+ targets.append(target)
177
+ except Exception:
178
+ pass
179
+
180
+ return list(set(targets))[:10] # Limit to 10 targets
181
+
182
+
183
+ def _extract_npm_scripts(root_path: Path) -> List[str]:
184
+ """Extract scripts from package.json."""
185
+ import json
186
+
187
+ package_json = root_path / 'package.json'
188
+ if not package_json.exists():
189
+ return []
190
+
191
+ scripts = []
192
+ try:
193
+ content = json.loads(package_json.read_text())
194
+ if 'scripts' in content:
195
+ # Include common scripts, exclude default ones
196
+ exclude = {'test', 'start', 'dev', 'build'}
197
+ for script_name in content['scripts'].keys():
198
+ if script_name not in exclude:
199
+ scripts.append(script_name)
200
+ except Exception:
201
+ pass
202
+
203
+ return scripts[:8] # Limit to 8 scripts
204
+
205
+
206
+ def _find_github_workflows(root_path: Path) -> List[str]:
207
+ """Find GitHub Actions workflows."""
208
+ workflows_dir = root_path / '.github' / 'workflows'
209
+ if not workflows_dir.exists():
210
+ return []
211
+
212
+ workflows = []
213
+ try:
214
+ for workflow_file in workflows_dir.glob('*.yml'):
215
+ workflows.append(workflow_file.stem)
216
+ for workflow_file in workflows_dir.glob('*.yaml'):
217
+ workflows.append(workflow_file.stem)
218
+ except Exception:
219
+ pass
220
+
221
+ return workflows[:5] # Limit to 5 workflows
@@ -0,0 +1,163 @@
1
+ """Enrich CLAUDE.md with comprehensive project context sections."""
2
+
3
+ from typing import Dict, List
4
+
5
+
6
+ def generate_directories_section(discovered: Dict[str, str]) -> str:
7
+ """Generate Key Directories section from discovered structure.
8
+
9
+ Args:
10
+ discovered: Dict of directory_name -> description
11
+
12
+ Returns:
13
+ Formatted section content
14
+ """
15
+ if not discovered:
16
+ return ''
17
+
18
+ lines = []
19
+ for dir_name in sorted(discovered.keys()):
20
+ description = discovered[dir_name]
21
+ lines.append(f'- **{dir_name}/** - {description}')
22
+
23
+ return '\n'.join(lines)
24
+
25
+
26
+ def generate_documentation_section(docs: Dict[str, str]) -> str:
27
+ """Generate Documentation & Resources section from discovered files.
28
+
29
+ Args:
30
+ docs: Dict of file_path -> description
31
+
32
+ Returns:
33
+ Formatted section content
34
+ """
35
+ if not docs:
36
+ return ''
37
+
38
+ lines = []
39
+
40
+ # Prioritize certain docs
41
+ priority_order = [
42
+ 'README.md', 'CONTRIBUTING.md', 'docs/API.md', 'docs/api.md',
43
+ 'docs/ARCHITECTURE.md', 'docs/architecture.md',
44
+ ]
45
+
46
+ added = set()
47
+
48
+ # Add priority docs first
49
+ for pattern in priority_order:
50
+ if pattern in docs:
51
+ lines.append(f'- **{pattern}** - {docs[pattern]}')
52
+ added.add(pattern)
53
+
54
+ # Add remaining docs
55
+ for doc_path in sorted(docs.keys()):
56
+ if doc_path not in added:
57
+ description = docs[doc_path]
58
+ lines.append(f'- **{doc_path}** - {description}')
59
+
60
+ return '\n'.join(lines)
61
+
62
+
63
+ def generate_workflows_section(workflows: Dict[str, List[str]]) -> str:
64
+ """Generate Common Workflows section from discovered commands.
65
+
66
+ Args:
67
+ workflows: Dict of workflow_type -> list of commands
68
+
69
+ Returns:
70
+ Formatted section content
71
+ """
72
+ if not workflows:
73
+ return ''
74
+
75
+ lines = []
76
+
77
+ # npm/yarn scripts
78
+ if 'npm' in workflows:
79
+ lines.append('**Common npm scripts:**')
80
+ for script in workflows['npm'][:5]:
81
+ lines.append(f'- `npm run {script}`')
82
+ lines.append('')
83
+
84
+ # Makefile targets
85
+ if 'make' in workflows:
86
+ lines.append('**Makefile targets:**')
87
+ for target in workflows['make'][:5]:
88
+ lines.append(f'- `make {target}`')
89
+ lines.append('')
90
+
91
+ # GitHub workflows
92
+ if 'workflows' in workflows:
93
+ lines.append('**GitHub Actions workflows:**')
94
+ for workflow in workflows['workflows'][:3]:
95
+ lines.append(f'- `.github/workflows/{workflow}.yml`')
96
+ lines.append('')
97
+
98
+ return '\n'.join(lines).strip()
99
+
100
+
101
+ def generate_directories_section_compact(discovered: Dict[str, str]) -> str:
102
+ """Generate a compact version of Key Directories section.
103
+
104
+ Args:
105
+ discovered: Dict of directory_name -> description
106
+
107
+ Returns:
108
+ Compact formatted section
109
+ """
110
+ if not discovered:
111
+ return ''
112
+
113
+ # For compact mode, use inline format
114
+ lines = []
115
+ items = []
116
+
117
+ for dir_name in sorted(discovered.keys()):
118
+ items.append(f'`{dir_name}/`')
119
+
120
+ if items:
121
+ lines.append('Project structure:')
122
+ lines.append(', '.join(items))
123
+
124
+ return '\n'.join(lines)
125
+
126
+
127
+ def merge_into_development_section(
128
+ base_content: str,
129
+ discovered_workflows: Dict[str, List[str]],
130
+ ) -> str:
131
+ """Merge discovered workflows into Development section.
132
+
133
+ Args:
134
+ base_content: Base development section content
135
+ discovered_workflows: Discovered workflows
136
+
137
+ Returns:
138
+ Enhanced development section
139
+ """
140
+ lines = [base_content.rstrip()]
141
+
142
+ # Add discovered workflows if any
143
+ if discovered_workflows:
144
+ lines.append('')
145
+ lines.append('### Discovered Workflows')
146
+ lines.append(generate_workflows_section(discovered_workflows))
147
+
148
+ return '\n'.join(lines)
149
+
150
+
151
+ def estimate_content_lines(section_dict: Dict[str, str]) -> int:
152
+ """Estimate total lines for section content.
153
+
154
+ Args:
155
+ section_dict: Dict of sections
156
+
157
+ Returns:
158
+ Approximate total line count
159
+ """
160
+ total = 0
161
+ for content in section_dict.values():
162
+ total += len(content.split('\n'))
163
+ return total
@@ -0,0 +1,277 @@
1
+ """Smart merging of CLAUDE.md sections with user customization preservation."""
2
+
3
+ import re
4
+ from typing import Dict, Tuple
5
+
6
+
7
+ def parse_claude_md(content: str) -> Dict[str, str]:
8
+ """Parse CLAUDE.md into sections.
9
+
10
+ Args:
11
+ content: CLAUDE.md content
12
+
13
+ Returns:
14
+ Dict mapping section names to their content
15
+ """
16
+ sections = {}
17
+ current_section = None
18
+ current_content = []
19
+
20
+ lines = content.split('\n')
21
+ in_code_block = False
22
+
23
+ for line in lines:
24
+ # Track code blocks to avoid treating ``` as section markers
25
+ if line.strip().startswith('```'):
26
+ in_code_block = not in_code_block
27
+
28
+ # Check for section headers (## level)
29
+ if line.startswith('## ') and not in_code_block:
30
+ # Save previous section
31
+ if current_section:
32
+ sections[current_section] = '\n'.join(current_content).strip()
33
+
34
+ # Start new section
35
+ current_section = line[3:].strip()
36
+ current_content = []
37
+ elif current_section:
38
+ current_content.append(line)
39
+
40
+ # Save last section
41
+ if current_section:
42
+ sections[current_section] = '\n'.join(current_content).strip()
43
+
44
+ return sections
45
+
46
+
47
+ def detect_user_edits(current: str, generated: str) -> bool:
48
+ """Detect if a section has been user-customized.
49
+
50
+ Uses dual detection: content comparison + marker detection.
51
+
52
+ Args:
53
+ current: Current section content from file
54
+ generated: What we would generate from template
55
+
56
+ Returns:
57
+ True if section appears to be user-edited
58
+ """
59
+ current = current.strip()
60
+ generated = generated.strip()
61
+
62
+ # Check for user edit marker
63
+ if '<!-- EDITED -->' in current:
64
+ return True
65
+
66
+ # Check if content differs significantly from generated version
67
+ if current == generated:
68
+ return False
69
+
70
+ # If content is substantially different, mark as user-edited
71
+ current_len = len(current)
72
+ generated_len = len(generated)
73
+
74
+ # Allow small variations (formatting), but flag major differences
75
+ if current_len == 0 and generated_len == 0:
76
+ return False
77
+
78
+ if current_len == 0 or generated_len == 0:
79
+ return True
80
+
81
+ # Check if content matches at least 50% (allowing for additions)
82
+ matching_ratio = len(set(current) & set(generated)) / max(current_len, generated_len)
83
+ if matching_ratio < 0.5:
84
+ return True
85
+
86
+ return False
87
+
88
+
89
+ def should_lock_section(section_name: str, current_content: str) -> bool:
90
+ """Determine if a section should be locked from regeneration.
91
+
92
+ Sections are locked if:
93
+ 1. They have <!-- LOCK --> marker
94
+ 2. They're in the smart default lock list AND have user content
95
+ 3. They're marked as edited
96
+
97
+ Args:
98
+ section_name: Name of the section
99
+ current_content: Current content of the section
100
+
101
+ Returns:
102
+ True if section should be locked
103
+ """
104
+ # Explicit lock marker
105
+ if '<!-- LOCK -->' in current_content:
106
+ return True
107
+
108
+ # Smart defaults - auto-lock these if they have user content
109
+ auto_lock_sections = {
110
+ 'Important Context': True,
111
+ 'Known Issues': True,
112
+ 'Notes': True,
113
+ 'Custom Configuration': True,
114
+ 'Recommended Workflow': True,
115
+ }
116
+
117
+ if section_name in auto_lock_sections and current_content.strip():
118
+ # Check if it looks like user content (not template boilerplate)
119
+ if not current_content.startswith('[') and len(current_content) > 20:
120
+ return True
121
+
122
+ return False
123
+
124
+
125
+ def merge_sections(
126
+ existing: Dict[str, str],
127
+ generated: Dict[str, str],
128
+ ) -> Tuple[Dict[str, str], Dict[str, str]]:
129
+ """Merge existing and generated sections smartly.
130
+
131
+ Strategy:
132
+ - Keep generated sections as base
133
+ - Override with existing sections if user-edited
134
+ - Lock sections if needed
135
+ - Return (merged_sections, preservation_report)
136
+
137
+ Args:
138
+ existing: Current sections from file
139
+ generated: Newly generated sections from template
140
+
141
+ Returns:
142
+ Tuple of (merged sections dict, report dict of what was preserved)
143
+ """
144
+ merged = {}
145
+ preserved = {}
146
+
147
+ # Process all sections (both existing and generated)
148
+ all_sections = set(list(existing.keys()) + list(generated.keys()))
149
+
150
+ for section_name in all_sections:
151
+ existing_content = existing.get(section_name, '').strip()
152
+ generated_content = generated.get(section_name, '').strip()
153
+
154
+ # Determine if user customized this section
155
+ is_user_edited = (
156
+ existing_content and
157
+ detect_user_edits(existing_content, generated_content)
158
+ )
159
+
160
+ # Determine if section should be locked
161
+ is_locked = should_lock_section(section_name, existing_content)
162
+
163
+ if is_locked or is_user_edited:
164
+ # Preserve user content
165
+ merged[section_name] = existing_content
166
+ preserved[section_name] = 'user-edited' if is_user_edited else 'locked'
167
+ elif existing_content and generated_content:
168
+ # Both exist - prefer generated but keep structure
169
+ merged[section_name] = generated_content
170
+ elif existing_content:
171
+ # Only exists in current
172
+ merged[section_name] = existing_content
173
+ preserved[section_name] = 'preserved'
174
+ else:
175
+ # Only in generated or both same
176
+ merged[section_name] = generated_content
177
+
178
+ return merged, preserved
179
+
180
+
181
+ def reconstruct_claude_md(
182
+ sections: Dict[str, str],
183
+ include_marker: bool = True,
184
+ ) -> str:
185
+ """Reconstruct CLAUDE.md from sections.
186
+
187
+ Args:
188
+ sections: Dict of section_name -> content
189
+ include_marker: Whether to add generation marker
190
+
191
+ Returns:
192
+ Reconstructed CLAUDE.md content
193
+ """
194
+ # Define standard section order
195
+ section_order = [
196
+ 'Project Header', # Placeholder for title/description
197
+ 'Stack',
198
+ 'Quick Start',
199
+ 'Key Directories',
200
+ 'Documentation & Resources',
201
+ 'Development',
202
+ 'Common Workflows',
203
+ 'Build & Deploy',
204
+ 'Recommended Workflow',
205
+ 'Important Context',
206
+ 'Environment Variables',
207
+ 'Common Tasks',
208
+ ]
209
+
210
+ lines = []
211
+
212
+ # Add sections in order (putting unordered sections at end)
213
+ added_sections = set()
214
+ for section_name in section_order:
215
+ if section_name in sections and section_name not in added_sections:
216
+ content = sections[section_name].strip()
217
+ if content:
218
+ lines.append(f'## {section_name}')
219
+ lines.append('')
220
+ lines.append(content)
221
+ lines.append('')
222
+ added_sections.add(section_name)
223
+
224
+ # Add any remaining sections
225
+ for section_name in sorted(sections.keys()):
226
+ if section_name not in added_sections:
227
+ content = sections[section_name].strip()
228
+ if content:
229
+ lines.append(f'## {section_name}')
230
+ lines.append('')
231
+ lines.append(content)
232
+ lines.append('')
233
+
234
+ # Add marker
235
+ if include_marker:
236
+ lines.append('<!-- Generated by /setup-claude-tools -->')
237
+
238
+ result = '\n'.join(lines).strip()
239
+ return result + '\n'
240
+
241
+
242
+ def extract_preservation_report(preserved: Dict[str, str]) -> str:
243
+ """Format a human-readable report of preserved content.
244
+
245
+ Args:
246
+ preserved: Dict of section_name -> preservation_reason
247
+
248
+ Returns:
249
+ Formatted report string
250
+ """
251
+ if not preserved:
252
+ return ''
253
+
254
+ report_lines = ['📝 Preservation Report:', '']
255
+
256
+ user_edited = [s for s, r in preserved.items() if r == 'user-edited']
257
+ locked = [s for s, r in preserved.items() if r == 'locked']
258
+ kept = [s for s, r in preserved.items() if r == 'preserved']
259
+
260
+ if user_edited:
261
+ report_lines.append('✅ Preserved (user-edited):')
262
+ for section in user_edited:
263
+ report_lines.append(f' - {section}')
264
+ report_lines.append('')
265
+
266
+ if locked:
267
+ report_lines.append('🔒 Preserved (locked):')
268
+ for section in locked:
269
+ report_lines.append(f' - {section}')
270
+ report_lines.append('')
271
+
272
+ if kept:
273
+ report_lines.append('📌 Preserved (as-is):')
274
+ for section in kept:
275
+ report_lines.append(f' - {section}')
276
+
277
+ return '\n'.join(report_lines)