buildlog 0.6.1__py3-none-any.whl → 0.8.0__py3-none-any.whl

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 (40) hide show
  1. buildlog/__init__.py +1 -1
  2. buildlog/cli.py +589 -44
  3. buildlog/confidence.py +27 -0
  4. buildlog/core/__init__.py +12 -0
  5. buildlog/core/bandit.py +699 -0
  6. buildlog/core/operations.py +499 -11
  7. buildlog/distill.py +80 -1
  8. buildlog/engine/__init__.py +61 -0
  9. buildlog/engine/bandit.py +23 -0
  10. buildlog/engine/confidence.py +28 -0
  11. buildlog/engine/embeddings.py +28 -0
  12. buildlog/engine/experiments.py +619 -0
  13. buildlog/engine/types.py +31 -0
  14. buildlog/llm.py +461 -0
  15. buildlog/mcp/server.py +12 -6
  16. buildlog/mcp/tools.py +166 -13
  17. buildlog/render/__init__.py +19 -2
  18. buildlog/render/claude_md.py +74 -26
  19. buildlog/render/continue_dev.py +102 -0
  20. buildlog/render/copilot.py +100 -0
  21. buildlog/render/cursor.py +105 -0
  22. buildlog/render/tracking.py +20 -1
  23. buildlog/render/windsurf.py +95 -0
  24. buildlog/seeds.py +41 -0
  25. buildlog/skills.py +69 -6
  26. {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/copier.yml +0 -4
  27. buildlog-0.8.0.data/data/share/buildlog/template/buildlog/_TEMPLATE_QUICK.md +21 -0
  28. buildlog-0.8.0.dist-info/METADATA +151 -0
  29. buildlog-0.8.0.dist-info/RECORD +54 -0
  30. buildlog-0.6.1.dist-info/METADATA +0 -490
  31. buildlog-0.6.1.dist-info/RECORD +0 -41
  32. {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/post_gen.py +0 -0
  33. {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/template/buildlog/.gitkeep +0 -0
  34. {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/template/buildlog/2026-01-01-example.md +0 -0
  35. {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md +0 -0
  36. {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/template/buildlog/_TEMPLATE.md +0 -0
  37. {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/template/buildlog/assets/.gitkeep +0 -0
  38. {buildlog-0.6.1.dist-info → buildlog-0.8.0.dist-info}/WHEEL +0 -0
  39. {buildlog-0.6.1.dist-info → buildlog-0.8.0.dist-info}/entry_points.txt +0 -0
  40. {buildlog-0.6.1.dist-info → buildlog-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,105 @@
1
+ """Render skills to Cursor rules format.
2
+
3
+ Creates .cursor/rules/buildlog-rules.mdc with YAML frontmatter and
4
+ Markdown body containing learned rules.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING
12
+
13
+ from buildlog.render.tracking import get_promoted_ids, track_promoted
14
+
15
+ if TYPE_CHECKING:
16
+ from buildlog.skills import Skill
17
+
18
+
19
+ class CursorRenderer:
20
+ """Creates .cursor/rules/buildlog-rules.mdc for Cursor IDE."""
21
+
22
+ def __init__(self, path: Path | None = None, tracking_path: Path | None = None):
23
+ """Initialize renderer.
24
+
25
+ Args:
26
+ path: Path to .mdc rules file. Defaults to .cursor/rules/buildlog-rules.mdc.
27
+ tracking_path: Path to promoted.json tracking file.
28
+ Defaults to .buildlog/promoted.json.
29
+ """
30
+ self.path = path or Path(".cursor/rules/buildlog-rules.mdc")
31
+ self.tracking_path = tracking_path or Path(".buildlog/promoted.json")
32
+
33
+ def render(self, skills: list[Skill]) -> str:
34
+ """Render skills to Cursor .mdc rules file.
35
+
36
+ Overwrites the file with all promoted skills (not append-only) since
37
+ Cursor reads the entire file on each invocation.
38
+
39
+ Args:
40
+ skills: List of skills to render.
41
+
42
+ Returns:
43
+ Confirmation message.
44
+ """
45
+ if not skills:
46
+ return "No skills to promote"
47
+
48
+ # Filter out already-promoted skills
49
+ already_promoted = get_promoted_ids(self.tracking_path)
50
+ new_skills = [s for s in skills if s.id not in already_promoted]
51
+
52
+ if not new_skills:
53
+ return f"All {len(skills)} skills already promoted"
54
+
55
+ # Group by category
56
+ by_category: dict[str, list[Skill]] = {}
57
+ for skill in new_skills:
58
+ by_category.setdefault(skill.category, []).append(skill)
59
+
60
+ category_titles = {
61
+ "architectural": "Architectural",
62
+ "workflow": "Workflow",
63
+ "tool_usage": "Tool Usage",
64
+ "domain_knowledge": "Domain Knowledge",
65
+ }
66
+
67
+ # Build MDC content with YAML frontmatter
68
+ lines = [
69
+ "---",
70
+ "description: Buildlog-learned rules for code quality",
71
+ "alwaysApply: true",
72
+ "---",
73
+ "",
74
+ "# Learned Rules",
75
+ "",
76
+ f"*Auto-generated by buildlog on {datetime.now().strftime('%Y-%m-%d')}*",
77
+ "",
78
+ ]
79
+
80
+ for category, cat_skills in by_category.items():
81
+ title = category_titles.get(category, category.replace("_", " ").title())
82
+ lines.append(f"## {title}")
83
+ lines.append("")
84
+ for skill in cat_skills:
85
+ conf = skill.confidence
86
+ freq = skill.frequency
87
+ lines.append(
88
+ f"- **{skill.rule}** (confidence: {conf}, frequency: {freq})"
89
+ )
90
+ lines.append("")
91
+
92
+ content = "\n".join(lines)
93
+
94
+ # Write file
95
+ self.path.parent.mkdir(parents=True, exist_ok=True)
96
+ self.path.write_text(content)
97
+
98
+ # Track promoted skill IDs
99
+ track_promoted(new_skills, self.tracking_path)
100
+
101
+ skipped = len(skills) - len(new_skills)
102
+ msg = f"Wrote {len(new_skills)} rules to {self.path}"
103
+ if skipped > 0:
104
+ msg += f" ({skipped} already promoted, skipped)"
105
+ return msg
@@ -10,7 +10,26 @@ from typing import TYPE_CHECKING
10
10
  if TYPE_CHECKING:
11
11
  from buildlog.skills import Skill
12
12
 
13
- __all__ = ["track_promoted"]
13
+ __all__ = ["track_promoted", "get_promoted_ids"]
14
+
15
+
16
+ def get_promoted_ids(tracking_path: Path) -> set[str]:
17
+ """Get the set of already-promoted skill IDs.
18
+
19
+ Args:
20
+ tracking_path: Path to the tracking JSON file.
21
+
22
+ Returns:
23
+ Set of skill IDs that have been promoted.
24
+ """
25
+ if not tracking_path.exists():
26
+ return set()
27
+
28
+ try:
29
+ tracking = json.loads(tracking_path.read_text())
30
+ return set(tracking.get("skill_ids", []))
31
+ except json.JSONDecodeError:
32
+ return set()
14
33
 
15
34
 
16
35
  def track_promoted(skills: list[Skill], tracking_path: Path) -> None:
@@ -0,0 +1,95 @@
1
+ """Render skills to Windsurf rules format.
2
+
3
+ Creates .windsurf/rules/buildlog-rules.md with learned rules in
4
+ plain Markdown format.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING
12
+
13
+ from buildlog.render.tracking import get_promoted_ids, track_promoted
14
+ from buildlog.skills import _to_imperative
15
+
16
+ if TYPE_CHECKING:
17
+ from buildlog.skills import Skill
18
+
19
+
20
+ class WindsurfRenderer:
21
+ """Creates .windsurf/rules/buildlog-rules.md for Windsurf IDE."""
22
+
23
+ def __init__(self, path: Path | None = None, tracking_path: Path | None = None):
24
+ """Initialize renderer.
25
+
26
+ Args:
27
+ path: Path to rules file. Defaults to .windsurf/rules/buildlog-rules.md.
28
+ tracking_path: Path to promoted.json tracking file.
29
+ Defaults to .buildlog/promoted.json.
30
+ """
31
+ self.path = path or Path(".windsurf/rules/buildlog-rules.md")
32
+ self.tracking_path = tracking_path or Path(".buildlog/promoted.json")
33
+
34
+ def render(self, skills: list[Skill]) -> str:
35
+ """Render skills to Windsurf rules file.
36
+
37
+ Overwrites the file with all promoted skills.
38
+
39
+ Args:
40
+ skills: List of skills to render.
41
+
42
+ Returns:
43
+ Confirmation message.
44
+ """
45
+ if not skills:
46
+ return "No skills to promote"
47
+
48
+ # Filter out already-promoted skills
49
+ already_promoted = get_promoted_ids(self.tracking_path)
50
+ new_skills = [s for s in skills if s.id not in already_promoted]
51
+
52
+ if not new_skills:
53
+ return f"All {len(skills)} skills already promoted"
54
+
55
+ # Group by category
56
+ by_category: dict[str, list[Skill]] = {}
57
+ for skill in new_skills:
58
+ by_category.setdefault(skill.category, []).append(skill)
59
+
60
+ category_titles = {
61
+ "architectural": "Architectural",
62
+ "workflow": "Workflow",
63
+ "tool_usage": "Tool Usage",
64
+ "domain_knowledge": "Domain Knowledge",
65
+ }
66
+
67
+ # Build Markdown content
68
+ lines = [
69
+ f"## Learned Rules (buildlog {datetime.now().strftime('%Y-%m-%d')})",
70
+ "",
71
+ ]
72
+
73
+ for category, cat_skills in by_category.items():
74
+ title = category_titles.get(category, category.replace("_", " ").title())
75
+ lines.append(f"### {title}")
76
+ lines.append("")
77
+ for skill in cat_skills:
78
+ rule = _to_imperative(skill.rule, skill.confidence)
79
+ lines.append(f"- {rule}")
80
+ lines.append("")
81
+
82
+ content = "\n".join(lines)
83
+
84
+ # Write file
85
+ self.path.parent.mkdir(parents=True, exist_ok=True)
86
+ self.path.write_text(content)
87
+
88
+ # Track promoted skill IDs
89
+ track_promoted(new_skills, self.tracking_path)
90
+
91
+ skipped = len(skills) - len(new_skills)
92
+ msg = f"Wrote {len(new_skills)} rules to {self.path}"
93
+ if skipped > 0:
94
+ msg += f" ({skipped} already promoted, skipped)"
95
+ return msg
buildlog/seeds.py CHANGED
@@ -156,6 +156,36 @@ class SeedFile:
156
156
  )
157
157
 
158
158
 
159
+ def _validate_seed_schema(data: dict) -> bool:
160
+ """Validate seed file has expected schema structure.
161
+
162
+ Defense-in-depth validation for seed files. While yaml.safe_load
163
+ prevents code execution, this ensures data structure matches expectations.
164
+
165
+ Args:
166
+ data: Parsed YAML data.
167
+
168
+ Returns:
169
+ True if schema is valid, False otherwise.
170
+ """
171
+ if not isinstance(data, dict):
172
+ return False
173
+
174
+ # Rules must be a list if present
175
+ rules = data.get("rules", [])
176
+ if not isinstance(rules, list):
177
+ return False
178
+
179
+ # Each rule must be a dict with at least a "rule" key
180
+ for rule in rules:
181
+ if not isinstance(rule, dict):
182
+ return False
183
+ if "rule" not in rule:
184
+ return False
185
+
186
+ return True
187
+
188
+
159
189
  def load_seed_file(path: Path) -> SeedFile | None:
160
190
  """Load a single seed file from disk.
161
191
 
@@ -164,6 +194,10 @@ def load_seed_file(path: Path) -> SeedFile | None:
164
194
 
165
195
  Returns:
166
196
  Parsed SeedFile or None if loading fails.
197
+
198
+ Note:
199
+ Uses yaml.safe_load which is safe from code execution attacks.
200
+ Additional schema validation ensures data structure is as expected.
167
201
  """
168
202
  if not path.exists():
169
203
  logger.warning(f"Seed file not found: {path}")
@@ -171,7 +205,14 @@ def load_seed_file(path: Path) -> SeedFile | None:
171
205
 
172
206
  try:
173
207
  with open(path) as f:
208
+ # yaml.safe_load is safe - no arbitrary code execution
174
209
  data = yaml.safe_load(f)
210
+
211
+ # Validate schema before parsing
212
+ if not _validate_seed_schema(data):
213
+ logger.error(f"Invalid seed file schema: {path}")
214
+ return None
215
+
175
216
  return SeedFile.from_dict(data)
176
217
  except (yaml.YAMLError, KeyError, TypeError) as e:
177
218
  logger.error(f"Failed to parse seed file {path}: {e}")
buildlog/skills.py CHANGED
@@ -23,7 +23,10 @@ import re
23
23
  from dataclasses import dataclass, field
24
24
  from datetime import date, datetime, timezone
25
25
  from pathlib import Path
26
- from typing import Final, Literal, TypedDict
26
+ from typing import TYPE_CHECKING, Final, Literal, TypedDict
27
+
28
+ if TYPE_CHECKING:
29
+ from buildlog.llm import LLMBackend
27
30
 
28
31
  from buildlog.confidence import ConfidenceConfig, ConfidenceMetrics
29
32
  from buildlog.confidence import calculate_confidence as calculate_continuous_confidence
@@ -83,6 +86,10 @@ class SkillDict(_SkillDictRequired, total=False):
83
86
  antipattern: str # What does violation look like?
84
87
  rationale: str # Why does this matter?
85
88
  persona_tags: list[str] # Which reviewers use this rule?
89
+ # LLM-extracted scoring fields
90
+ severity: str # critical/major/minor/info
91
+ scope: str # global/module/function
92
+ applicability: list[str] # contexts where relevant
86
93
 
87
94
 
88
95
  class SkillSetDict(TypedDict):
@@ -115,6 +122,9 @@ class Skill:
115
122
  antipattern: What does violation look like? (defensibility)
116
123
  rationale: Why does this rule matter? (defensibility)
117
124
  persona_tags: Which reviewer personas use this rule?
125
+ severity: How bad is ignoring this rule? (critical/major/minor/info)
126
+ scope: How broadly does this rule apply? (global/module/function)
127
+ applicability: Contexts where this rule is relevant.
118
128
  """
119
129
 
120
130
  id: str
@@ -131,6 +141,10 @@ class Skill:
131
141
  antipattern: str | None = None
132
142
  rationale: str | None = None
133
143
  persona_tags: list[str] = field(default_factory=list)
144
+ # LLM-extracted scoring
145
+ severity: str | None = None
146
+ scope: str | None = None
147
+ applicability: list[str] = field(default_factory=list)
134
148
 
135
149
  def to_dict(self) -> SkillDict:
136
150
  """Convert to dictionary for serialization.
@@ -159,6 +173,12 @@ class Skill:
159
173
  result["rationale"] = self.rationale
160
174
  if self.persona_tags:
161
175
  result["persona_tags"] = self.persona_tags
176
+ if self.severity is not None:
177
+ result["severity"] = self.severity
178
+ if self.scope is not None:
179
+ result["scope"] = self.scope
180
+ if self.applicability:
181
+ result["applicability"] = self.applicability
162
182
  return result
163
183
 
164
184
 
@@ -326,6 +346,7 @@ def _deduplicate_insights(
326
346
  patterns: list[PatternDict],
327
347
  threshold: float = MIN_SIMILARITY_THRESHOLD,
328
348
  backend: EmbeddingBackend | None = None,
349
+ llm_backend: LLMBackend | None = None,
329
350
  ) -> list[tuple[str, int, list[str], date | None, date | None]]:
330
351
  """Deduplicate similar insights into merged rules.
331
352
 
@@ -366,9 +387,17 @@ def _deduplicate_insights(
366
387
  results: list[tuple[str, int, list[str], date | None, date | None]] = []
367
388
 
368
389
  for group in groups:
369
- # Use the shortest insight as the canonical rule (often cleaner)
370
- canonical = min(group, key=lambda p: len(p["insight"]))
371
- rule = canonical["insight"]
390
+ # Use LLM to select canonical form if available and group has >1 member
391
+ if llm_backend is not None and len(group) > 1:
392
+ try:
393
+ candidates = [p["insight"] for p in group]
394
+ rule = llm_backend.select_canonical(candidates)
395
+ except Exception:
396
+ canonical = min(group, key=lambda p: len(p["insight"]))
397
+ rule = canonical["insight"]
398
+ else:
399
+ canonical = min(group, key=lambda p: len(p["insight"]))
400
+ rule = canonical["insight"]
372
401
  frequency = len(group)
373
402
  sources = sorted(set(p["source"] for p in group))
374
403
 
@@ -434,6 +463,7 @@ def generate_skills(
434
463
  embedding_backend: str | None = None,
435
464
  confidence_config: ConfidenceConfig | None = None,
436
465
  include_review_learnings: bool = True,
466
+ llm: bool = False,
437
467
  ) -> SkillSet:
438
468
  """Generate skills from buildlog patterns and review learnings.
439
469
 
@@ -449,12 +479,21 @@ def generate_skills(
449
479
  include_review_learnings: Whether to include learnings from code reviews.
450
480
  When True, loads .buildlog/review_learnings.json and merges
451
481
  review learnings into the skill set.
482
+ llm: If True and an LLM backend is available, use LLM for extraction,
483
+ canonical selection, and scoring. Falls back gracefully.
452
484
 
453
485
  Returns:
454
486
  SkillSet with generated skills.
455
487
  """
488
+ # Resolve LLM backend if requested
489
+ llm_backend = None
490
+ if llm:
491
+ from buildlog.llm import get_llm_backend
492
+
493
+ llm_backend = get_llm_backend(buildlog_dir=buildlog_dir)
494
+
456
495
  # Get distilled patterns
457
- result = distill_all(buildlog_dir, since=since_date)
496
+ result = distill_all(buildlog_dir, since=since_date, llm=llm)
458
497
 
459
498
  # Get embedding backend
460
499
  backend = (
@@ -471,7 +510,9 @@ def generate_skills(
471
510
 
472
511
  for category in CATEGORIES:
473
512
  patterns = result.patterns.get(category, [])
474
- deduplicated = _deduplicate_insights(patterns, backend=backend)
513
+ deduplicated = _deduplicate_insights(
514
+ patterns, backend=backend, llm_backend=llm_backend
515
+ )
475
516
 
476
517
  skills: list[Skill] = []
477
518
  for rule, frequency, sources, most_recent, earliest in deduplicated:
@@ -490,6 +531,25 @@ def generate_skills(
490
531
  confidence_score, confidence_config
491
532
  ).value
492
533
 
534
+ # LLM scoring for severity/scope/applicability
535
+ severity: str | None = None
536
+ scope: str | None = None
537
+ applicability_tags: list[str] = []
538
+ if llm_backend is not None:
539
+ try:
540
+ scoring = llm_backend.score_rule(rule, category)
541
+ severity = scoring.severity
542
+ scope = scoring.scope
543
+ applicability_tags = scoring.applicability
544
+ except Exception:
545
+ pass # Keep defaults (None/empty)
546
+
547
+ # Apply severity weighting to confidence score
548
+ if confidence_score is not None and severity is not None:
549
+ from buildlog.confidence import apply_severity_weight
550
+
551
+ confidence_score = apply_severity_weight(confidence_score, severity)
552
+
493
553
  skill = Skill(
494
554
  id=_generate_skill_id(category, rule),
495
555
  category=category,
@@ -500,6 +560,9 @@ def generate_skills(
500
560
  tags=_extract_tags(rule),
501
561
  confidence_score=confidence_score,
502
562
  confidence_tier=confidence_tier,
563
+ severity=severity,
564
+ scope=scope,
565
+ applicability=applicability_tags,
503
566
  )
504
567
  skills.append(skill)
505
568
 
@@ -20,10 +20,6 @@ update_claude_md:
20
20
  help: Add buildlog instructions to CLAUDE.md if it exists?
21
21
  default: true
22
22
 
23
- # Post-generation tasks
24
- _tasks:
25
- - "{{ 'python3 post_gen.py' if update_claude_md else 'echo Skipping CLAUDE.md update' }}"
26
-
27
23
  _message_after_copy: |
28
24
  Build journal installed!
29
25
 
@@ -0,0 +1,21 @@
1
+ # Build Journal: [TITLE]
2
+
3
+ **Date:** [YYYY-MM-DD]
4
+ **Duration:** [X hours]
5
+
6
+ ## What I Did
7
+
8
+ [What you built, fixed, or changed. 2-3 sentences.]
9
+
10
+ ## What Went Wrong
11
+
12
+ [Mistakes, surprises, dead ends. Be specific — these become rules.]
13
+
14
+ ## What I Learned
15
+
16
+ ### Improvements
17
+
18
+ - [One thing to do differently next time]
19
+ - [One thing that worked well to repeat]
20
+
21
+ *More sections: see _TEMPLATE.md for the full format.*
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: buildlog
3
+ Version: 0.8.0
4
+ Summary: Engineering notebook for AI-assisted development
5
+ Project-URL: Homepage, https://github.com/Peleke/buildlog-template
6
+ Project-URL: Repository, https://github.com/Peleke/buildlog-template
7
+ Author: Peleke Sengstacke
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: ai,buildlog,development,documentation,journal
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Documentation
21
+ Classifier: Topic :: Software Development :: Documentation
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: click>=8.0.0
24
+ Requires-Dist: copier>=9.0.0
25
+ Requires-Dist: numpy>=1.21.0
26
+ Requires-Dist: pymupdf>=1.26.7
27
+ Requires-Dist: pyyaml>=6.0.0
28
+ Provides-Extra: all
29
+ Requires-Dist: anthropic>=0.40.0; extra == 'all'
30
+ Requires-Dist: mcp>=1.0.0; extra == 'all'
31
+ Requires-Dist: ollama>=0.4.0; extra == 'all'
32
+ Requires-Dist: openai>=1.0.0; extra == 'all'
33
+ Requires-Dist: sentence-transformers>=2.2.0; extra == 'all'
34
+ Provides-Extra: anthropic
35
+ Requires-Dist: anthropic>=0.40.0; extra == 'anthropic'
36
+ Provides-Extra: dev
37
+ Requires-Dist: black>=24.0.0; extra == 'dev'
38
+ Requires-Dist: flake8>=7.0.0; extra == 'dev'
39
+ Requires-Dist: isort>=5.13.0; extra == 'dev'
40
+ Requires-Dist: mkdocs-material>=9.5.0; extra == 'dev'
41
+ Requires-Dist: mypy>=1.8.0; extra == 'dev'
42
+ Requires-Dist: pre-commit>=3.6.0; extra == 'dev'
43
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
44
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
45
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
46
+ Requires-Dist: types-pyyaml>=6.0.0; extra == 'dev'
47
+ Provides-Extra: embeddings
48
+ Requires-Dist: sentence-transformers>=2.2.0; extra == 'embeddings'
49
+ Provides-Extra: engine
50
+ Provides-Extra: llm
51
+ Requires-Dist: anthropic>=0.40.0; extra == 'llm'
52
+ Requires-Dist: ollama>=0.4.0; extra == 'llm'
53
+ Provides-Extra: mcp
54
+ Requires-Dist: mcp>=1.0.0; extra == 'mcp'
55
+ Provides-Extra: ollama
56
+ Requires-Dist: ollama>=0.4.0; extra == 'ollama'
57
+ Provides-Extra: openai
58
+ Requires-Dist: openai>=1.0.0; extra == 'openai'
59
+ Description-Content-Type: text/markdown
60
+
61
+ <div align="center">
62
+
63
+ # buildlog
64
+
65
+ ### The Only Agent Learning System You Can Prove Works
66
+
67
+ [![PyPI](https://img.shields.io/pypi/v/buildlog?style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/buildlog/)
68
+ [![Python](https://img.shields.io/pypi/pyversions/buildlog?style=for-the-badge&logo=python&logoColor=white)](https://python.org/)
69
+ [![CI](https://img.shields.io/github/actions/workflow/status/Peleke/buildlog-template/ci.yml?branch=main&style=for-the-badge&logo=github&label=CI)](https://github.com/Peleke/buildlog-template/actions/workflows/ci.yml)
70
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](https://opensource.org/licenses/MIT)
71
+ [![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-blue?style=for-the-badge&logo=github)](https://peleke.github.io/buildlog-template/)
72
+
73
+ **Falsifiable claims. Measurable outcomes. No vibes.**
74
+
75
+ <img src="assets/hero-banner-perfectdeliberate.png" alt="buildlog - The Only Agent Learning System You Can Prove Works" width="800"/>
76
+
77
+ > **RE: The art** — Yes, it's AI-generated. Yes, that's hypocritical for a project about rigor over vibes. Looking for an actual artist to pay for a real logo. If you know someone good, [open an issue](https://github.com/Peleke/buildlog-template/issues) or DM me. Budget exists.
78
+
79
+ **[Read the full documentation](https://peleke.github.io/buildlog-template/)**
80
+
81
+ </div>
82
+
83
+ ---
84
+
85
+ Everyone's building "agent memory." Blog posts announce breakthroughs. Products ship with "learning" in the tagline. Ask them one question: **How do you know it works?**
86
+
87
+ buildlog gives you the infrastructure to answer with data. It captures engineering knowledge from work sessions, extracts rules, selects which rules to surface using a Thompson Sampling bandit, and measures impact via Repeated Mistake Rate (RMR) across tracked experiments.
88
+
89
+ ## Features
90
+
91
+ - **Structured capture** — Document work sessions as entries with mistakes, decisions, and outcomes
92
+ - **Rule extraction** — Distill and deduplicate patterns into actionable rules
93
+ - **Thompson Sampling bandit** — Automatic rule selection that balances exploration and exploitation
94
+ - **Experiment tracking** — Sessions, mistakes, RMR calculation with statistical rigor
95
+ - **Review gauntlet** — Curated reviewer personas (Security Karen, Test Terrorist) with HITL checkpoints
96
+ - **Multi-agent support** — Render rules to Claude Code, Cursor, GitHub Copilot, Windsurf, Continue.dev
97
+ - **MCP server** — Full Claude Code integration via `buildlog-mcp`
98
+
99
+ ## Quick Start
100
+
101
+ ```bash
102
+ uv pip install buildlog # or: pip install buildlog (inside a venv)
103
+ buildlog init
104
+ buildlog new my-feature
105
+ buildlog distill && buildlog skills
106
+ buildlog experiment start
107
+ # ... work ...
108
+ buildlog experiment end
109
+ buildlog experiment report
110
+ ```
111
+
112
+ ## Documentation
113
+
114
+ | Section | Description |
115
+ |---------|------------|
116
+ | [Installation](https://peleke.github.io/buildlog-template/getting-started/installation/) | Setup, extras, and initialization |
117
+ | [Quick Start](https://peleke.github.io/buildlog-template/getting-started/quick-start/) | Full pipeline walkthrough |
118
+ | [Core Concepts](https://peleke.github.io/buildlog-template/getting-started/concepts/) | The problem, the claim, and the metric |
119
+ | [CLI Reference](https://peleke.github.io/buildlog-template/guides/cli-reference/) | Every command documented |
120
+ | [MCP Integration](https://peleke.github.io/buildlog-template/guides/mcp-integration/) | Claude Code setup and available tools |
121
+ | [Experiments](https://peleke.github.io/buildlog-template/guides/experiments/) | Running and measuring experiments |
122
+ | [Review Gauntlet](https://peleke.github.io/buildlog-template/guides/review-gauntlet/) | Reviewer personas and the gauntlet loop |
123
+ | [Multi-Agent Setup](https://peleke.github.io/buildlog-template/guides/multi-agent/) | Render rules to any AI coding agent |
124
+ | [Theory](https://peleke.github.io/buildlog-template/theory/00-background/) | The math behind Thompson Sampling |
125
+ | [Philosophy](https://peleke.github.io/buildlog-template/philosophy/) | Principles and honest limitations |
126
+
127
+ ## Contributing
128
+
129
+ ```bash
130
+ git clone https://github.com/Peleke/buildlog-template
131
+ cd buildlog-template
132
+ uv venv && source .venv/bin/activate
133
+ uv pip install -e ".[dev]"
134
+ pytest
135
+ ```
136
+
137
+ We're especially interested in better context representations, credit assignment approaches, statistical methodology improvements, and real-world experiment results (positive or negative).
138
+
139
+ ## License
140
+
141
+ MIT License — see [LICENSE](./LICENSE)
142
+
143
+ ---
144
+
145
+ <div align="center">
146
+
147
+ **"Agent learning" without measurement is just prompt engineering with extra steps.**
148
+
149
+ **buildlog is measurement.**
150
+
151
+ </div>