anvil-dev-framework 0.1.6

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 (190) hide show
  1. package/README.md +719 -0
  2. package/VERSION +1 -0
  3. package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
  4. package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
  5. package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
  6. package/docs/INSTALLATION.md +984 -0
  7. package/docs/anvil-hud.md +469 -0
  8. package/docs/anvil-init.md +255 -0
  9. package/docs/anvil-state.md +210 -0
  10. package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
  11. package/docs/command-reference.md +2022 -0
  12. package/docs/hooks-tts.md +368 -0
  13. package/docs/implementation-guide.md +810 -0
  14. package/docs/linear-github-integration.md +247 -0
  15. package/docs/local-issues.md +677 -0
  16. package/docs/patterns/README.md +419 -0
  17. package/docs/planning-responsibilities.md +139 -0
  18. package/docs/session-workflow.md +573 -0
  19. package/docs/simplification-plan-template.md +297 -0
  20. package/docs/simplification-principles.md +129 -0
  21. package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
  22. package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
  23. package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
  24. package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
  25. package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
  26. package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
  27. package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
  28. package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
  29. package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
  30. package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
  31. package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
  32. package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
  33. package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
  34. package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
  35. package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
  36. package/docs/sync.md +122 -0
  37. package/global/CLAUDE.md +140 -0
  38. package/global/agents/verify-app.md +164 -0
  39. package/global/commands/anvil-settings.md +527 -0
  40. package/global/commands/anvil-sync.md +121 -0
  41. package/global/commands/change.md +197 -0
  42. package/global/commands/clarify.md +252 -0
  43. package/global/commands/cleanup.md +292 -0
  44. package/global/commands/commit-push-pr.md +207 -0
  45. package/global/commands/decay-review.md +127 -0
  46. package/global/commands/discover.md +158 -0
  47. package/global/commands/doc-coverage.md +122 -0
  48. package/global/commands/evidence.md +307 -0
  49. package/global/commands/explore.md +121 -0
  50. package/global/commands/force-exit.md +135 -0
  51. package/global/commands/handoff.md +191 -0
  52. package/global/commands/healthcheck.md +302 -0
  53. package/global/commands/hud.md +84 -0
  54. package/global/commands/insights.md +319 -0
  55. package/global/commands/linear-setup.md +184 -0
  56. package/global/commands/lint-fix.md +198 -0
  57. package/global/commands/orient.md +510 -0
  58. package/global/commands/plan.md +228 -0
  59. package/global/commands/ralph.md +346 -0
  60. package/global/commands/ready.md +182 -0
  61. package/global/commands/release.md +305 -0
  62. package/global/commands/retro.md +96 -0
  63. package/global/commands/shard.md +166 -0
  64. package/global/commands/spec.md +227 -0
  65. package/global/commands/sprint.md +184 -0
  66. package/global/commands/tasks.md +228 -0
  67. package/global/commands/test-and-commit.md +151 -0
  68. package/global/commands/validate.md +132 -0
  69. package/global/commands/verify.md +251 -0
  70. package/global/commands/weekly-review.md +156 -0
  71. package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
  72. package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
  73. package/global/hooks/anvil_memory_observe.ts +322 -0
  74. package/global/hooks/anvil_memory_session.ts +166 -0
  75. package/global/hooks/anvil_memory_stop.ts +187 -0
  76. package/global/hooks/parse_transcript.py +116 -0
  77. package/global/hooks/post_merge_cleanup.sh +132 -0
  78. package/global/hooks/post_tool_format.sh +215 -0
  79. package/global/hooks/ralph_context_monitor.py +240 -0
  80. package/global/hooks/ralph_stop.sh +502 -0
  81. package/global/hooks/statusline.sh +1110 -0
  82. package/global/hooks/statusline_agent_sync.py +224 -0
  83. package/global/hooks/stop_gate.sh +250 -0
  84. package/global/lib/.claude/anvil-state.json +21 -0
  85. package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
  86. package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
  87. package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
  88. package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
  89. package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
  90. package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
  91. package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
  92. package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
  93. package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
  94. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  95. package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
  96. package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
  97. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  98. package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
  99. package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
  100. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  101. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  102. package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
  103. package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
  104. package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
  105. package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
  106. package/global/lib/agent_registry.py +995 -0
  107. package/global/lib/anvil-state.sh +435 -0
  108. package/global/lib/claim_service.py +515 -0
  109. package/global/lib/coderabbit_service.py +314 -0
  110. package/global/lib/config_service.py +423 -0
  111. package/global/lib/coordination_service.py +331 -0
  112. package/global/lib/doc_coverage_service.py +1305 -0
  113. package/global/lib/gate_logger.py +316 -0
  114. package/global/lib/github_service.py +310 -0
  115. package/global/lib/handoff_generator.py +775 -0
  116. package/global/lib/hygiene_service.py +712 -0
  117. package/global/lib/issue_models.py +257 -0
  118. package/global/lib/issue_provider.py +339 -0
  119. package/global/lib/linear_data_service.py +210 -0
  120. package/global/lib/linear_provider.py +987 -0
  121. package/global/lib/linear_provider.py.backup +671 -0
  122. package/global/lib/local_provider.py +486 -0
  123. package/global/lib/orient_fast.py +457 -0
  124. package/global/lib/quality_service.py +470 -0
  125. package/global/lib/ralph_prompt_generator.py +563 -0
  126. package/global/lib/ralph_state.py +1202 -0
  127. package/global/lib/state_manager.py +417 -0
  128. package/global/lib/transcript_parser.py +597 -0
  129. package/global/lib/verification_runner.py +557 -0
  130. package/global/lib/verify_iteration.py +490 -0
  131. package/global/lib/verify_subagent.py +250 -0
  132. package/global/skills/README.md +155 -0
  133. package/global/skills/quality-gates/SKILL.md +252 -0
  134. package/global/skills/skill-template/SKILL.md +109 -0
  135. package/global/skills/testing-strategies/SKILL.md +337 -0
  136. package/global/templates/CHANGE-template.md +105 -0
  137. package/global/templates/HANDOFF-template.md +63 -0
  138. package/global/templates/PLAN-template.md +111 -0
  139. package/global/templates/SPEC-template.md +93 -0
  140. package/global/templates/ralph/PROMPT.md.template +89 -0
  141. package/global/templates/ralph/fix_plan.md.template +31 -0
  142. package/global/templates/ralph/progress.txt.template +23 -0
  143. package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
  144. package/global/tests/test_doc_coverage.py +520 -0
  145. package/global/tests/test_issue_models.py +299 -0
  146. package/global/tests/test_local_provider.py +323 -0
  147. package/global/tools/README.md +178 -0
  148. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  149. package/global/tools/anvil-hud.py +3622 -0
  150. package/global/tools/anvil-hud.py.bak +3318 -0
  151. package/global/tools/anvil-issue.py +432 -0
  152. package/global/tools/anvil-memory/CLAUDE.md +49 -0
  153. package/global/tools/anvil-memory/README.md +42 -0
  154. package/global/tools/anvil-memory/bun.lock +25 -0
  155. package/global/tools/anvil-memory/bunfig.toml +9 -0
  156. package/global/tools/anvil-memory/package.json +23 -0
  157. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
  158. package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
  159. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
  160. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
  161. package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
  162. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
  163. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
  164. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
  165. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
  166. package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
  167. package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
  168. package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
  169. package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
  170. package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
  171. package/global/tools/anvil-memory/src/commands/get.ts +115 -0
  172. package/global/tools/anvil-memory/src/commands/init.ts +94 -0
  173. package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
  174. package/global/tools/anvil-memory/src/commands/search.ts +112 -0
  175. package/global/tools/anvil-memory/src/db.ts +638 -0
  176. package/global/tools/anvil-memory/src/index.ts +205 -0
  177. package/global/tools/anvil-memory/src/types.ts +122 -0
  178. package/global/tools/anvil-memory/tsconfig.json +29 -0
  179. package/global/tools/ralph-loop.sh +359 -0
  180. package/package.json +45 -0
  181. package/scripts/anvil +822 -0
  182. package/scripts/extract_patterns.py +222 -0
  183. package/scripts/init-project.sh +541 -0
  184. package/scripts/install.sh +229 -0
  185. package/scripts/postinstall.js +41 -0
  186. package/scripts/rollback.sh +188 -0
  187. package/scripts/sync.sh +623 -0
  188. package/scripts/test-statusline.sh +248 -0
  189. package/scripts/update_claude_md.py +224 -0
  190. package/scripts/verify.sh +255 -0
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env bash
2
+ # test-statusline.sh - Test statusline with various configurations
3
+ #
4
+ # Usage:
5
+ # ./scripts/test-statusline.sh # Run all tests
6
+ # ./scripts/test-statusline.sh --live # Watch live updates
7
+
8
+ set -euo pipefail
9
+
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
12
+ STATUSLINE_SCRIPT="$PROJECT_ROOT/global/hooks/statusline.sh"
13
+ CONFIG_FILE="$PROJECT_ROOT/.claude/anvil.config.json"
14
+
15
+ # Colors for test output
16
+ RED='\033[0;31m'
17
+ GREEN='\033[0;32m'
18
+ YELLOW='\033[1;33m'
19
+ CYAN='\033[0;36m'
20
+ RESET='\033[0m'
21
+
22
+ echo_header() {
23
+ echo ""
24
+ echo -e "${CYAN}═══════════════════════════════════════════════════${RESET}"
25
+ echo -e "${CYAN}$1${RESET}"
26
+ echo -e "${CYAN}═══════════════════════════════════════════════════${RESET}"
27
+ }
28
+
29
+ echo_test() {
30
+ echo -e "${YELLOW}▶ $1${RESET}"
31
+ }
32
+
33
+ echo_result() {
34
+ echo -e " ${GREEN}→${RESET} $1"
35
+ }
36
+
37
+ # Create test JSON input for statusline
38
+ # Uses the actual Claude Code input format
39
+ create_test_input() {
40
+ local context_pct="${1:-50}"
41
+ local context_size="${2:-200000}"
42
+ local model="${3:-Claude Opus 4}"
43
+
44
+ # Calculate tokens based on percentage
45
+ local current_tokens=$((context_size * context_pct / 100))
46
+ local input_tokens=$((current_tokens * 80 / 100))
47
+ local cache_write=$((current_tokens * 20 / 100))
48
+ local cache_read=$((current_tokens / 2))
49
+
50
+ cat <<EOF
51
+ {
52
+ "session_id": "test-session-123",
53
+ "model": {
54
+ "display_name": "$model"
55
+ },
56
+ "context_window": {
57
+ "context_window_size": $context_size,
58
+ "current_usage": {
59
+ "input_tokens": $input_tokens,
60
+ "cache_read_input_tokens": $cache_read,
61
+ "cache_creation_input_tokens": $cache_write
62
+ }
63
+ },
64
+ "cost": {
65
+ "total_cost_usd": 2.45
66
+ },
67
+ "workspace": {
68
+ "current_dir": "$PROJECT_ROOT"
69
+ },
70
+ "transcript_path": "/tmp/test-transcript.jsonl"
71
+ }
72
+ EOF
73
+ }
74
+
75
+ # Run statusline with test input
76
+ run_statusline() {
77
+ create_test_input "$@" | bash "$STATUSLINE_SCRIPT"
78
+ }
79
+
80
+ # Test different config scenarios
81
+ test_config_variants() {
82
+ echo_header "Testing Config Variants"
83
+
84
+ # Backup original config
85
+ local backup=""
86
+ if [[ -f "$CONFIG_FILE" ]]; then
87
+ backup=$(cat "$CONFIG_FILE")
88
+ fi
89
+
90
+ # Test 1: Default config (agent cost, never show project)
91
+ echo_test "Default config (costDisplay=agent, showProject=never)"
92
+ cat > "$CONFIG_FILE" <<EOF
93
+ {
94
+ "version": "1.3",
95
+ "statusline": {
96
+ "enabled": true,
97
+ "variant": "full",
98
+ "costDisplay": "agent",
99
+ "showProject": "never"
100
+ }
101
+ }
102
+ EOF
103
+ echo_result "$(run_statusline 50)"
104
+
105
+ # Test 2: Total cost display
106
+ echo_test "Total cost display (costDisplay=total)"
107
+ cat > "$CONFIG_FILE" <<EOF
108
+ {
109
+ "version": "1.3",
110
+ "statusline": {
111
+ "enabled": true,
112
+ "variant": "full",
113
+ "costDisplay": "total",
114
+ "showProject": "never"
115
+ }
116
+ }
117
+ EOF
118
+ echo_result "$(run_statusline 50)"
119
+
120
+ # Test 3: Both costs display
121
+ echo_test "Both costs display (costDisplay=both)"
122
+ cat > "$CONFIG_FILE" <<EOF
123
+ {
124
+ "version": "1.3",
125
+ "statusline": {
126
+ "enabled": true,
127
+ "variant": "full",
128
+ "costDisplay": "both",
129
+ "showProject": "never"
130
+ }
131
+ }
132
+ EOF
133
+ echo_result "$(run_statusline 50)"
134
+
135
+ # Test 4: No cost display
136
+ echo_test "No cost display (costDisplay=none)"
137
+ cat > "$CONFIG_FILE" <<EOF
138
+ {
139
+ "version": "1.3",
140
+ "statusline": {
141
+ "enabled": true,
142
+ "variant": "full",
143
+ "costDisplay": "none",
144
+ "showProject": "never"
145
+ }
146
+ }
147
+ EOF
148
+ echo_result "$(run_statusline 50)"
149
+
150
+ # Test 5: Always show project
151
+ echo_test "Always show project (showProject=always)"
152
+ cat > "$CONFIG_FILE" <<EOF
153
+ {
154
+ "version": "1.3",
155
+ "statusline": {
156
+ "enabled": true,
157
+ "variant": "full",
158
+ "costDisplay": "agent",
159
+ "showProject": "always"
160
+ }
161
+ }
162
+ EOF
163
+ echo_result "$(run_statusline 50)"
164
+
165
+ # Test 6: Minimal variant
166
+ echo_test "Minimal variant"
167
+ cat > "$CONFIG_FILE" <<EOF
168
+ {
169
+ "version": "1.3",
170
+ "statusline": {
171
+ "enabled": true,
172
+ "variant": "minimal",
173
+ "costDisplay": "agent",
174
+ "showProject": "never"
175
+ }
176
+ }
177
+ EOF
178
+ echo_result "$(run_statusline 50)"
179
+
180
+ # Restore original config
181
+ if [[ -n "$backup" ]]; then
182
+ echo "$backup" > "$CONFIG_FILE"
183
+ fi
184
+ }
185
+
186
+ # Test context thresholds (ANV-192: CCS L1/L2/L3 indicators)
187
+ test_context_thresholds() {
188
+ echo_header "Testing Context Thresholds (CCS Levels)"
189
+
190
+ echo_test "L0 - Normal (50% context) - No level indicator"
191
+ echo_result "$(run_statusline 50)"
192
+
193
+ echo_test "L0 - Normal (65% context) - Still no indicator"
194
+ echo_result "$(run_statusline 65)"
195
+
196
+ echo_test "L1 - Warning (70% context) - ⚠️ L1 + [CKPT: Ready]"
197
+ echo_result "$(run_statusline 70)"
198
+
199
+ echo_test "L1 - Warning (75% context)"
200
+ echo_result "$(run_statusline 75)"
201
+
202
+ echo_test "L1 - Warning (84% context) - Upper L1 bound"
203
+ echo_result "$(run_statusline 84)"
204
+
205
+ echo_test "L2 - Critical (85% context) - 🔶 L2 + token breakdown"
206
+ echo_result "$(run_statusline 85)"
207
+
208
+ echo_test "L2 - Critical (90% context)"
209
+ echo_result "$(run_statusline 90)"
210
+
211
+ echo_test "L2 - Critical (94% context) - Upper L2 bound"
212
+ echo_result "$(run_statusline 94)"
213
+
214
+ echo_test "L3 - Emergency (95% context) - 🔴 L3 + [CKPT: URGENT]"
215
+ echo_result "$(run_statusline 95)"
216
+
217
+ echo_test "L3 - Emergency (98% context)"
218
+ echo_result "$(run_statusline 98)"
219
+ }
220
+
221
+ # Live mode - watch updates
222
+ live_mode() {
223
+ echo_header "Live Mode - Press Ctrl+C to exit"
224
+ echo "Statusline will refresh every 2 seconds..."
225
+ echo ""
226
+
227
+ while true; do
228
+ # Clear line and print statusline
229
+ printf "\r\033[K"
230
+ run_statusline 50
231
+ sleep 2
232
+ done
233
+ }
234
+
235
+ # Main
236
+ main() {
237
+ if [[ "${1:-}" == "--live" ]]; then
238
+ live_mode
239
+ else
240
+ test_config_variants
241
+ test_context_thresholds
242
+
243
+ echo_header "All Tests Complete"
244
+ echo -e "${GREEN}✓ Statusline configuration tests passed${RESET}"
245
+ fi
246
+ }
247
+
248
+ main "$@"
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Update CLAUDE.md with extracted patterns.
4
+
5
+ Usage: python scripts/update_claude_md.py patterns.json [--claude-md PATH] [--dry-run]
6
+
7
+ This script:
8
+ 1. Reads extracted patterns from JSON file
9
+ 2. Parses existing Project-Learned Patterns table in CLAUDE.md
10
+ 3. Merges new patterns (avoiding duplicates by name similarity)
11
+ 4. Updates the table with dates
12
+ 5. Writes the updated file (unless --dry-run)
13
+ """
14
+
15
+ import sys
16
+ import json
17
+ import re
18
+ import argparse
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+ from difflib import SequenceMatcher
22
+
23
+
24
+ def read_file(path: str) -> str:
25
+ """Read file contents."""
26
+ return Path(path).read_text()
27
+
28
+
29
+ def write_file(path: str, content: str) -> None:
30
+ """Write content to file."""
31
+ Path(path).write_text(content)
32
+
33
+
34
+ def parse_patterns_table(content: str) -> list[dict]:
35
+ """Extract existing patterns from CLAUDE.md Project-Learned Patterns table."""
36
+ # Match the table after "### Project-Learned Patterns"
37
+ table_pattern = r'### Project-Learned Patterns.*?\n\| Pattern \| Evidence \| Added \|\n\|[-|]+\|\n((?:\|[^\n]+\|\n)*)'
38
+ match = re.search(table_pattern, content, re.DOTALL)
39
+
40
+ if not match:
41
+ return []
42
+
43
+ rows = match.group(1).strip().split('\n')
44
+ patterns = []
45
+
46
+ for row in rows:
47
+ if not row.strip():
48
+ continue
49
+ # Parse: | Pattern Name | Evidence | Date |
50
+ cols = [c.strip() for c in row.split('|')]
51
+ # Filter out empty strings from split
52
+ cols = [c for c in cols if c]
53
+
54
+ if len(cols) >= 3:
55
+ patterns.append({
56
+ 'name': cols[0],
57
+ 'evidence': cols[1],
58
+ 'added': cols[2]
59
+ })
60
+
61
+ return patterns
62
+
63
+
64
+ def similarity(a: str, b: str) -> float:
65
+ """Calculate string similarity ratio (0-1)."""
66
+ return SequenceMatcher(None, a.lower(), b.lower()).ratio()
67
+
68
+
69
+ def is_duplicate(new_pattern: str, existing_patterns: list[dict], threshold: float = 0.7) -> bool:
70
+ """Check if a pattern name is too similar to existing ones."""
71
+ for existing in existing_patterns:
72
+ if similarity(new_pattern, existing['name']) >= threshold:
73
+ return True
74
+ return False
75
+
76
+
77
+ def merge_patterns(existing: list[dict], new_patterns: list[dict]) -> tuple[list[dict], list[dict]]:
78
+ """
79
+ Merge new patterns with existing, avoiding duplicates.
80
+
81
+ Returns: (merged_list, added_patterns)
82
+ """
83
+ today = datetime.now().strftime('%Y-%m-%d')
84
+ merged = list(existing)
85
+ added = []
86
+
87
+ for pattern in new_patterns:
88
+ # Skip patterns that are too similar to existing ones
89
+ if is_duplicate(pattern['name'], existing):
90
+ continue
91
+
92
+ # Also skip if too similar to already-added patterns
93
+ if is_duplicate(pattern['name'], added):
94
+ continue
95
+
96
+ # Only include "learning" and "retro" type patterns (most valuable)
97
+ pattern_type = pattern.get('type', '')
98
+ if pattern_type not in ('learning', 'retro'):
99
+ continue
100
+
101
+ new_entry = {
102
+ 'name': clean_pattern_name(pattern['name']),
103
+ 'evidence': pattern.get('evidence', ''),
104
+ 'added': today
105
+ }
106
+ merged.append(new_entry)
107
+ added.append(new_entry)
108
+
109
+ return merged, added
110
+
111
+
112
+ def clean_pattern_name(name: str) -> str:
113
+ """Clean up pattern name for table display."""
114
+ # Remove markdown formatting
115
+ name = re.sub(r'\*\*([^*]+)\*\*', r'\1', name)
116
+ # Remove leading/trailing whitespace
117
+ name = name.strip()
118
+ # Truncate if too long
119
+ if len(name) > 50:
120
+ name = name[:47] + '...'
121
+ # Capitalize first letter
122
+ if name:
123
+ name = name[0].upper() + name[1:]
124
+ return name
125
+
126
+
127
+ def format_table(patterns: list[dict]) -> str:
128
+ """Format patterns as markdown table."""
129
+ lines = [
130
+ '| Pattern | Evidence | Added |',
131
+ '|---------|----------|-------|'
132
+ ]
133
+ for p in patterns:
134
+ # Escape pipe characters in content
135
+ name = p['name'].replace('|', '\\|')
136
+ evidence = p['evidence'].replace('|', '\\|')
137
+ added = p['added']
138
+ lines.append(f'| {name} | {evidence} | {added} |')
139
+
140
+ return '\n'.join(lines)
141
+
142
+
143
+ def update_claude_md(content: str, patterns: list[dict]) -> str:
144
+ """Replace the patterns table in CLAUDE.md content."""
145
+ format_table(patterns)
146
+
147
+ # Pattern to match the existing table
148
+ table_pattern = r'(### Project-Learned Patterns.*?\n\| Pattern \| Evidence \| Added \|\n\|[-|]+\|\n)((?:\|[^\n]+\|\n)*)'
149
+
150
+ def replacer(match):
151
+ header = match.group(1)
152
+ return header + '\n'.join(
153
+ f'| {p["name"]} | {p["evidence"]} | {p["added"]} |'
154
+ for p in patterns
155
+ ) + '\n'
156
+
157
+ updated = re.sub(table_pattern, replacer, content, flags=re.DOTALL)
158
+
159
+ return updated
160
+
161
+
162
+ def main():
163
+ parser = argparse.ArgumentParser(description='Update CLAUDE.md with extracted patterns')
164
+ parser.add_argument('patterns_file', help='JSON file with extracted patterns')
165
+ parser.add_argument('--claude-md', default='.claude/CLAUDE.md', help='Path to CLAUDE.md')
166
+ parser.add_argument('--dry-run', action='store_true', help='Show changes without writing')
167
+ parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
168
+ args = parser.parse_args()
169
+
170
+ # Read patterns JSON
171
+ try:
172
+ with open(args.patterns_file) as f:
173
+ data = json.load(f)
174
+ except FileNotFoundError:
175
+ print(f"Error: Pattern file not found: {args.patterns_file}", file=sys.stderr)
176
+ sys.exit(1)
177
+ except json.JSONDecodeError as e:
178
+ print(f"Error: Invalid JSON in {args.patterns_file}: {e}", file=sys.stderr)
179
+ sys.exit(1)
180
+
181
+ new_patterns = data.get('patterns', [])
182
+
183
+ # Read CLAUDE.md
184
+ claude_md_path = Path(args.claude_md)
185
+ if not claude_md_path.exists():
186
+ print(f"Error: CLAUDE.md not found: {args.claude_md}", file=sys.stderr)
187
+ sys.exit(1)
188
+
189
+ content = read_file(args.claude_md)
190
+ existing = parse_patterns_table(content)
191
+
192
+ if args.verbose:
193
+ print(f"Found {len(existing)} existing patterns", file=sys.stderr)
194
+ print(f"Processing {len(new_patterns)} new patterns", file=sys.stderr)
195
+
196
+ # Merge patterns
197
+ merged, added = merge_patterns(existing, new_patterns)
198
+
199
+ if not added:
200
+ print("No new patterns to add (all duplicates or non-learning types)")
201
+ sys.exit(0)
202
+
203
+ if args.verbose:
204
+ print(f"Adding {len(added)} new patterns:", file=sys.stderr)
205
+ for p in added:
206
+ print(f" - {p['name']}", file=sys.stderr)
207
+
208
+ # Update content
209
+ updated = update_claude_md(content, merged)
210
+
211
+ if args.dry_run:
212
+ print("=== DRY RUN - Changes that would be made ===")
213
+ print(f"\nNew patterns to add ({len(added)}):")
214
+ for p in added:
215
+ print(f" | {p['name']} | {p['evidence']} | {p['added']} |")
216
+ print(f"\nTotal patterns after merge: {len(merged)}")
217
+ else:
218
+ write_file(args.claude_md, updated)
219
+ print(f"Updated {args.claude_md} with {len(added)} new patterns")
220
+ print(f"Total patterns: {len(merged)}")
221
+
222
+
223
+ if __name__ == '__main__':
224
+ main()