buildlog 0.8.0__py3-none-any.whl → 0.9.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.
buildlog/cli.py CHANGED
@@ -298,11 +298,11 @@ def new(slug: str, entry_date: str | None, quick: bool):
298
298
  # Determine date
299
299
  if entry_date:
300
300
  try:
301
- # Validate date format
302
- year, month, day = entry_date.split("-")
303
- date_str = f"{int(year):04d}-{int(month):02d}-{int(day):02d}"
301
+ # Validate date format AND range (month 1-12, day 1-31)
302
+ parsed = datetime.strptime(entry_date, "%Y-%m-%d").date()
303
+ date_str = parsed.isoformat()
304
304
  except ValueError:
305
- click.echo("Invalid date format. Use YYYY-MM-DD.", err=True)
305
+ click.echo("Invalid date. Use YYYY-MM-DD with valid values.", err=True)
306
306
  raise SystemExit(1)
307
307
  else:
308
308
  date_str = date.today().isoformat()
@@ -331,6 +331,161 @@ def new(slug: str, entry_date: str | None, quick: bool):
331
331
  click.echo(f"\nOpen it: $EDITOR {entry_path}")
332
332
 
333
333
 
334
+ @main.command(
335
+ context_settings={"ignore_unknown_options": True, "allow_extra_args": True}
336
+ )
337
+ @click.option(
338
+ "--slug",
339
+ "-s",
340
+ default=None,
341
+ help="Entry slug (default: derived from branch name)",
342
+ )
343
+ @click.option(
344
+ "--entry",
345
+ "-e",
346
+ type=click.Path(),
347
+ default=None,
348
+ help="Explicit entry file to append to",
349
+ )
350
+ @click.option(
351
+ "--no-entry",
352
+ is_flag=True,
353
+ help="Skip buildlog entry update (just run git commit)",
354
+ )
355
+ @click.pass_context
356
+ def commit(ctx, slug: str | None, entry: str | None, no_entry: bool):
357
+ """Commit code and update the buildlog entry in one step.
358
+
359
+ Wraps `git commit` and appends commit context to today's buildlog
360
+ entry. If no entry exists for today, creates one automatically.
361
+
362
+ All unknown options/arguments are passed through to git commit.
363
+
364
+ Examples:
365
+
366
+ buildlog commit -m "feat: add LLM extractor"
367
+ buildlog commit --slug llm-extractor -m "feat: add LLM extractor"
368
+ buildlog commit --no-entry -m "chore: formatting"
369
+ """
370
+ buildlog_dir = Path("buildlog")
371
+
372
+ # Build git commit command — extra args are passed through from Click context
373
+ git_cmd = ["git", "commit", *ctx.args]
374
+
375
+ # Run git commit first
376
+ result = subprocess.run(git_cmd, capture_output=True, text=True)
377
+ sys.stdout.write(result.stdout)
378
+ sys.stderr.write(result.stderr)
379
+
380
+ if result.returncode != 0:
381
+ raise SystemExit(result.returncode)
382
+
383
+ if no_entry or not buildlog_dir.exists():
384
+ return
385
+
386
+ # Get commit info from what we just committed
387
+ try:
388
+ commit_hash = subprocess.run(
389
+ ["git", "rev-parse", "--short", "HEAD"],
390
+ capture_output=True,
391
+ text=True,
392
+ check=True,
393
+ ).stdout.strip()
394
+ commit_msg = subprocess.run(
395
+ ["git", "log", "-1", "--format=%s"],
396
+ capture_output=True,
397
+ text=True,
398
+ check=True,
399
+ ).stdout.strip()
400
+ # diff-tree needs special handling for root commit
401
+ diff_result = subprocess.run(
402
+ ["git", "diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD"],
403
+ capture_output=True,
404
+ text=True,
405
+ )
406
+ if diff_result.returncode == 0 and diff_result.stdout.strip():
407
+ files_changed = diff_result.stdout.strip()
408
+ else:
409
+ # Root commit fallback
410
+ files_changed = subprocess.run(
411
+ ["git", "diff", "--name-only", "--cached", "HEAD~1"],
412
+ capture_output=True,
413
+ text=True,
414
+ ).stdout.strip()
415
+ if not files_changed:
416
+ # Truly initial commit — list all tracked files
417
+ files_changed = subprocess.run(
418
+ ["git", "ls-tree", "--name-only", "-r", "HEAD"],
419
+ capture_output=True,
420
+ text=True,
421
+ ).stdout.strip()
422
+ except subprocess.CalledProcessError:
423
+ click.echo("Warning: could not read commit info", err=True)
424
+ return
425
+
426
+ # Resolve entry file
427
+ today = date.today().isoformat()
428
+ entry_path = _resolve_entry_path(buildlog_dir, today, slug, entry)
429
+
430
+ # Append commit block
431
+ commit_block = f"\n### `{commit_hash}` — {commit_msg}\n\n"
432
+ if files_changed:
433
+ file_list = files_changed.split("\n")
434
+ commit_block += "Files:\n"
435
+ for f in file_list[:20]: # cap at 20 to avoid noise
436
+ commit_block += f"- `{f}`\n"
437
+ if len(file_list) > 20:
438
+ commit_block += f"- ...and {len(file_list) - 20} more\n"
439
+ commit_block += "\n"
440
+
441
+ # Ensure commits section exists, append to it
442
+ if entry_path.exists():
443
+ content = entry_path.read_text()
444
+ if "## Commits" not in content:
445
+ content = content.rstrip() + "\n\n## Commits\n"
446
+ content += commit_block
447
+ else:
448
+ # Auto-create minimal entry
449
+ content = f"# {today}\n\n## Commits\n{commit_block}"
450
+
451
+ entry_path.write_text(content)
452
+ click.echo(f"buildlog: updated {entry_path}")
453
+
454
+
455
+ def _resolve_entry_path(
456
+ buildlog_dir: Path, today: str, slug: str | None, explicit: str | None
457
+ ) -> Path:
458
+ """Find or create the entry path for today."""
459
+ if explicit:
460
+ return Path(explicit)
461
+
462
+ # Check for existing entry with today's date
463
+ existing = list(buildlog_dir.glob(f"{today}-*.md"))
464
+ if existing:
465
+ return existing[0]
466
+
467
+ # Derive slug from branch name if not provided
468
+ if slug is None:
469
+ try:
470
+ branch = subprocess.run(
471
+ ["git", "branch", "--show-current"],
472
+ capture_output=True,
473
+ text=True,
474
+ check=True,
475
+ ).stdout.strip()
476
+ # Clean branch name into slug
477
+ slug = branch.split("/")[-1] # strip prefix like feat/
478
+ slug = slug.lower().replace("_", "-")
479
+ slug = "".join(c for c in slug if c.isalnum() or c == "-")
480
+ except subprocess.CalledProcessError:
481
+ slug = "session"
482
+
483
+ if not slug:
484
+ slug = "session"
485
+
486
+ return buildlog_dir / f"{today}-{slug}.md"
487
+
488
+
334
489
  @main.command("list")
335
490
  def list_entries():
336
491
  """List all buildlog entries."""
@@ -1277,6 +1432,7 @@ PERSONAS = {
1277
1432
  "security_karen": "OWASP Top 10 security review",
1278
1433
  "test_terrorist": "Comprehensive testing coverage audit",
1279
1434
  "ruthless_reviewer": "Code quality and functional principles",
1435
+ "bragi": "Detect and flag LLM-ish prose patterns in markdown",
1280
1436
  }
1281
1437
 
1282
1438
 
@@ -1656,6 +1812,69 @@ def gauntlet_learn(issues_file: str, source: str | None, output_json: bool):
1656
1812
  click.echo(f" Total processed: {result.total_issues_processed}")
1657
1813
 
1658
1814
 
1815
+ @gauntlet.command("generate")
1816
+ @click.argument("source_text", type=click.Path(exists=True))
1817
+ @click.option("--persona", "-p", required=True, help="Persona name for the seed file")
1818
+ @click.option(
1819
+ "--output-dir",
1820
+ "-o",
1821
+ type=click.Path(),
1822
+ default=".buildlog/seeds",
1823
+ help="Output directory for seed YAML",
1824
+ )
1825
+ @click.option("--dry-run", is_flag=True, help="Preview without writing")
1826
+ def gauntlet_generate(source_text: str, persona: str, output_dir: str, dry_run: bool):
1827
+ """Generate seed rules from source text using LLM extraction.
1828
+
1829
+ Runs the seed engine pipeline with LLMExtractor to produce
1830
+ a YAML seed file from arbitrary source content.
1831
+
1832
+ Examples:
1833
+
1834
+ buildlog gauntlet generate docs/security.md --persona security_karen
1835
+ buildlog gauntlet generate notes.txt -p test_terrorist --dry-run
1836
+ """
1837
+ import json as json_module
1838
+
1839
+ from buildlog.llm import get_llm_backend
1840
+ from buildlog.seed_engine import Pipeline, Source, SourceType
1841
+
1842
+ backend = get_llm_backend()
1843
+ if backend is None:
1844
+ click.echo(
1845
+ "No LLM backend available. Install ollama or set ANTHROPIC_API_KEY.",
1846
+ err=True,
1847
+ )
1848
+ raise SystemExit(1)
1849
+
1850
+ content = Path(source_text).read_text()
1851
+ source = Source(
1852
+ name=Path(source_text).stem,
1853
+ url=f"file://{Path(source_text).resolve()}",
1854
+ source_type=SourceType.REFERENCE_DOC,
1855
+ domain=persona.split("_")[0] if "_" in persona else "general",
1856
+ description=content,
1857
+ )
1858
+
1859
+ pipeline = Pipeline.with_llm(
1860
+ persona=persona,
1861
+ backend=backend,
1862
+ source_content={source.url: content},
1863
+ )
1864
+
1865
+ if dry_run:
1866
+ preview = pipeline.dry_run([source])
1867
+ click.echo(json_module.dumps(preview, indent=2))
1868
+ return
1869
+
1870
+ out = Path(output_dir)
1871
+ out.mkdir(parents=True, exist_ok=True)
1872
+ result = pipeline.run([source], output_dir=out)
1873
+ click.echo(f"Generated {result.rule_count} rules for {persona}")
1874
+ if result.output_path:
1875
+ click.echo(f"Seed file: {result.output_path}")
1876
+
1877
+
1659
1878
  @gauntlet.command("loop")
1660
1879
  @click.argument("target", type=click.Path(exists=True))
1661
1880
  @click.option(
@@ -653,7 +653,7 @@ def reject(
653
653
  rejected = {"rejected_at": {}, "skill_ids": []}
654
654
 
655
655
  # Add new rejections
656
- now = datetime.now().isoformat()
656
+ now = datetime.now(timezone.utc).isoformat()
657
657
  newly_rejected: list[str] = []
658
658
  for skill_id in skill_ids:
659
659
  if skill_id not in rejected["skill_ids"]:
@@ -2035,6 +2035,18 @@ def gauntlet_process_issues(
2035
2035
  )
2036
2036
 
2037
2037
 
2038
+ def _sanitize_for_gh(text: str, max_len: int = 256) -> str:
2039
+ """Sanitize text for GitHub issue fields.
2040
+
2041
+ Defense-in-depth: we use list args (not shell=True) for subprocess,
2042
+ but sanitize anyway to prevent injection via gh's argument parsing.
2043
+ """
2044
+ sanitized = text.replace("\n", " ").replace("\r", " ")
2045
+ if len(sanitized) > max_len:
2046
+ sanitized = sanitized[: max_len - 3] + "..."
2047
+ return sanitized.strip()
2048
+
2049
+
2038
2050
  def gauntlet_accept_risk(
2039
2051
  remaining_issues: list[dict],
2040
2052
  create_github_issues: bool = False,
@@ -2062,17 +2074,6 @@ def gauntlet_accept_risk(
2062
2074
  description = issue.get("description", "")
2063
2075
  location = issue.get("location", "")
2064
2076
 
2065
- # Sanitize inputs for GitHub issue creation
2066
- # Note: We use list args (not shell=True), so this is defense-in-depth
2067
- def _sanitize_for_gh(text: str, max_len: int = 256) -> str:
2068
- """Sanitize text for GitHub issue fields."""
2069
- # Remove/replace problematic characters
2070
- sanitized = text.replace("\n", " ").replace("\r", " ")
2071
- # Truncate to max length
2072
- if len(sanitized) > max_len:
2073
- sanitized = sanitized[: max_len - 3] + "..."
2074
- return sanitized.strip()
2075
-
2076
2077
  safe_severity = _sanitize_for_gh(str(severity), 20)
2077
2078
  safe_rule = _sanitize_for_gh(str(rule), 200)
2078
2079
  safe_description = _sanitize_for_gh(str(description), 1000)
@@ -2116,7 +2117,9 @@ def gauntlet_accept_risk(
2116
2117
  cmd.extend(["--repo", repo])
2117
2118
 
2118
2119
  try:
2119
- result = subprocess.run(cmd, capture_output=True, text=True, check=True)
2120
+ result = subprocess.run(
2121
+ cmd, capture_output=True, text=True, check=True, timeout=30
2122
+ )
2120
2123
  # gh issue create outputs the URL
2121
2124
  url = result.stdout.strip()
2122
2125
  if url:
@@ -2124,6 +2127,9 @@ def gauntlet_accept_risk(
2124
2127
  except subprocess.CalledProcessError as e:
2125
2128
  # Don't fail entirely, just note the error
2126
2129
  error = f"Failed to create some GitHub issues: {e.stderr}"
2130
+ except subprocess.TimeoutExpired:
2131
+ error = "GitHub issue creation timed out (30s limit)."
2132
+ break
2127
2133
  except FileNotFoundError:
2128
2134
  error = "gh CLI not found. Install GitHub CLI to create issues."
2129
2135
  break
buildlog/llm.py CHANGED
@@ -28,8 +28,10 @@ __all__ = [
28
28
  import json
29
29
  import logging
30
30
  import os
31
+ import time
31
32
  from dataclasses import dataclass, field
32
33
  from pathlib import Path
34
+ from types import MappingProxyType
33
35
  from typing import Protocol, runtime_checkable
34
36
 
35
37
  logger = logging.getLogger(__name__)
@@ -84,6 +86,14 @@ class LLMConfig:
84
86
  base_url: str | None = None # Override endpoint
85
87
  api_key: str | None = None # From config or env var
86
88
 
89
+ def __repr__(self) -> str:
90
+ """Redact api_key to prevent accidental exposure in logs/tracebacks."""
91
+ key_display = "***" if self.api_key else "None"
92
+ return (
93
+ f"LLMConfig(provider={self.provider!r}, model={self.model!r}, "
94
+ f"base_url={self.base_url!r}, api_key={key_display})"
95
+ )
96
+
87
97
  @classmethod
88
98
  def from_buildlog_config(cls, buildlog_dir: Path) -> LLMConfig | None:
89
99
  """Read from .buildlog/config.yml [llm] section."""
@@ -225,6 +235,28 @@ def _parse_json_response(text: str) -> list | dict:
225
235
  return json.loads(text)
226
236
 
227
237
 
238
+ # --- Rate limiting ---
239
+
240
+ # Minimum seconds between API calls (per-backend instance).
241
+ _MIN_CALL_INTERVAL = 0.5
242
+
243
+
244
+ class _RateLimiter:
245
+ """Simple per-instance rate limiter to prevent API abuse."""
246
+
247
+ def __init__(self, min_interval: float = _MIN_CALL_INTERVAL):
248
+ self._min_interval = min_interval
249
+ self._last_call: float = 0.0
250
+
251
+ def wait(self) -> None:
252
+ """Block until min_interval has elapsed since last call."""
253
+ now = time.monotonic()
254
+ elapsed = now - self._last_call
255
+ if elapsed < self._min_interval:
256
+ time.sleep(self._min_interval - elapsed)
257
+ self._last_call = time.monotonic()
258
+
259
+
228
260
  # --- Implementations ---
229
261
 
230
262
 
@@ -235,6 +267,7 @@ class OllamaBackend:
235
267
  self._model = model
236
268
  self._base_url = base_url
237
269
  self._resolved_model: str | None = None
270
+ self._rate_limiter = _RateLimiter()
238
271
 
239
272
  def _get_model(self) -> str:
240
273
  """Resolve model name, auto-detecting largest if not specified."""
@@ -269,6 +302,7 @@ class OllamaBackend:
269
302
 
270
303
  def _chat(self, prompt: str) -> str:
271
304
  """Send a prompt to Ollama and return the response text."""
305
+ self._rate_limiter.wait()
272
306
  import ollama as ollama_lib
273
307
 
274
308
  kwargs = {
@@ -334,6 +368,11 @@ class AnthropicBackend:
334
368
  self._model = model or "claude-haiku-4-20250514"
335
369
  self._api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
336
370
  self._client = None
371
+ self._rate_limiter = _RateLimiter()
372
+
373
+ def __repr__(self) -> str:
374
+ """Redact API key from repr to prevent exposure in logs/tracebacks."""
375
+ return f"AnthropicBackend(model={self._model!r}, api_key=***)"
337
376
 
338
377
  def _get_client(self):
339
378
  """Lazy-load the Anthropic client."""
@@ -351,6 +390,7 @@ class AnthropicBackend:
351
390
 
352
391
  def _chat(self, prompt: str) -> str:
353
392
  """Send a prompt to Claude and return the response text."""
393
+ self._rate_limiter.wait()
354
394
  client = self._get_client()
355
395
  response = client.messages.create(
356
396
  model=self._model,
@@ -402,15 +442,22 @@ class AnthropicBackend:
402
442
 
403
443
  # --- Registry ---
404
444
 
405
- PROVIDERS: dict[str, type] = {
445
+ _PROVIDERS: dict[str, type] = {
406
446
  "ollama": OllamaBackend,
407
447
  "anthropic": AnthropicBackend,
408
448
  }
449
+ # Public read-only view. Use register_provider() to add entries.
450
+ PROVIDERS: MappingProxyType[str, type] = MappingProxyType(_PROVIDERS)
409
451
 
410
452
 
411
453
  def register_provider(name: str, cls: type) -> None:
412
- """Register a new LLM provider backend."""
413
- PROVIDERS[name] = cls
454
+ """Register a new LLM provider backend.
455
+
456
+ This is the only sanctioned way to mutate the provider registry.
457
+ """
458
+ if not isinstance(name, str) or not name.strip():
459
+ raise ValueError("Provider name must be a non-empty string")
460
+ _PROVIDERS[name] = cls
414
461
 
415
462
 
416
463
  def get_llm_backend(
@@ -439,7 +486,7 @@ def get_llm_backend(
439
486
  logger.info("No LLM provider available, using regex fallback")
440
487
  return None
441
488
 
442
- provider_cls = PROVIDERS.get(config.provider)
489
+ provider_cls = _PROVIDERS.get(config.provider)
443
490
  if provider_cls is None:
444
491
  logger.warning("Unknown LLM provider: %s", config.provider)
445
492
  return None
buildlog/mcp/server.py CHANGED
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from mcp.server.fastmcp import FastMCP
6
6
 
7
7
  from buildlog.mcp.tools import (
8
+ buildlog_bandit_status,
8
9
  buildlog_diff,
9
10
  buildlog_experiment_end,
10
11
  buildlog_experiment_metrics,
@@ -43,6 +44,9 @@ mcp.tool()(buildlog_experiment_report)
43
44
  mcp.tool()(buildlog_gauntlet_issues)
44
45
  mcp.tool()(buildlog_gauntlet_accept_risk)
45
46
 
47
+ # Bandit tools
48
+ mcp.tool()(buildlog_bandit_status)
49
+
46
50
 
47
51
  def main() -> None:
48
52
  """Run the MCP server."""
@@ -33,6 +33,7 @@ from buildlog.seed_engine.categorizers import (
33
33
  )
34
34
  from buildlog.seed_engine.extractors import ManualExtractor, RuleExtractor
35
35
  from buildlog.seed_engine.generators import SeedGenerator
36
+ from buildlog.seed_engine.llm_extractor import LLMExtractor
36
37
  from buildlog.seed_engine.models import (
37
38
  CandidateRule,
38
39
  CategorizedRule,
@@ -59,6 +60,7 @@ __all__ = [
59
60
  # Extractors
60
61
  "RuleExtractor",
61
62
  "ManualExtractor",
63
+ "LLMExtractor",
62
64
  # Categorizers
63
65
  "Categorizer",
64
66
  "TagBasedCategorizer",
@@ -0,0 +1,121 @@
1
+ """LLM-backed rule extraction for the seed engine pipeline.
2
+
3
+ Adapts LLMBackend.extract_rules() into the RuleExtractor interface,
4
+ bridging the LLM module with the seed engine's 4-step pipeline.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from buildlog.seed_engine.extractors import RuleExtractor
13
+ from buildlog.seed_engine.models import CandidateRule, Source
14
+
15
+ if TYPE_CHECKING:
16
+ from buildlog.llm import LLMBackend
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ _PLACEHOLDER = "Not specified by LLM"
21
+
22
+
23
+ class LLMExtractor(RuleExtractor):
24
+ """LLM-backed rule extraction from source content.
25
+
26
+ Wraps any LLMBackend to produce CandidateRules with full
27
+ defensibility fields. Fields the LLM doesn't populate get
28
+ placeholder values so downstream validation passes.
29
+
30
+ Usage:
31
+ from buildlog.llm import OllamaBackend
32
+ from buildlog.seed_engine.llm_extractor import LLMExtractor
33
+
34
+ backend = OllamaBackend(model="llama3.2")
35
+ extractor = LLMExtractor(backend, source_content={"https://...": "..."})
36
+
37
+ rules = extractor.extract(source)
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ backend: LLMBackend,
43
+ source_content: dict[str, str] | None = None,
44
+ ) -> None:
45
+ """Initialize with an LLM backend.
46
+
47
+ Args:
48
+ backend: Any LLMBackend (Ollama, Anthropic, etc.).
49
+ source_content: Optional map of source.url → text content.
50
+ For sources that need pre-fetched content.
51
+ """
52
+ self._backend = backend
53
+ self._source_content = source_content or {}
54
+
55
+ def extract(self, source: Source) -> list[CandidateRule]:
56
+ """Extract candidate rules from a source via LLM.
57
+
58
+ Resolution for content:
59
+ 1. source_content dict (keyed by source.url)
60
+ 2. source.description as fallback
61
+
62
+ Returns empty list on LLM failure (logged, not raised).
63
+ """
64
+ content = self._source_content.get(source.url, "").strip()
65
+ if not content:
66
+ content = source.description.strip()
67
+ if not content:
68
+ logger.warning("No content for source %s, skipping", source.name)
69
+ return []
70
+
71
+ try:
72
+ extracted = self._backend.extract_rules(content)
73
+ except Exception:
74
+ logger.exception("LLM extraction failed for %s", source.name)
75
+ return []
76
+
77
+ candidates: list[CandidateRule] = []
78
+ for er in extracted:
79
+ if not er.rule.strip():
80
+ continue
81
+
82
+ metadata: dict[str, Any] = {
83
+ "extractor": "llm",
84
+ "severity": er.severity,
85
+ "scope": er.scope,
86
+ }
87
+ # Include backend class name (public info only)
88
+ metadata["backend_type"] = type(self._backend).__name__
89
+
90
+ candidates.append(
91
+ CandidateRule(
92
+ rule=er.rule,
93
+ context=er.context or _PLACEHOLDER,
94
+ antipattern=er.antipattern or _PLACEHOLDER,
95
+ rationale=er.rationale or _PLACEHOLDER,
96
+ source=source,
97
+ raw_tags=[er.category] + er.applicability,
98
+ confidence=0.7,
99
+ metadata=metadata,
100
+ )
101
+ )
102
+
103
+ logger.info("LLM extracted %d rules from %s", len(candidates), source.name)
104
+ return candidates
105
+
106
+ def validate(self, rule: CandidateRule) -> list[str]:
107
+ """Validate a candidate rule.
108
+
109
+ Warns on placeholder defensibility fields.
110
+ Requires non-empty rule text.
111
+ """
112
+ issues: list[str] = []
113
+ if not rule.rule.strip():
114
+ issues.append("Rule text is empty")
115
+ if rule.context == _PLACEHOLDER:
116
+ issues.append("Context is LLM placeholder — consider enriching")
117
+ if rule.antipattern == _PLACEHOLDER:
118
+ issues.append("Antipattern is LLM placeholder — consider enriching")
119
+ if rule.rationale == _PLACEHOLDER:
120
+ issues.append("Rationale is LLM placeholder — consider enriching")
121
+ return issues
@@ -12,13 +12,16 @@ from __future__ import annotations
12
12
  import logging
13
13
  from dataclasses import dataclass
14
14
  from pathlib import Path
15
- from typing import Any
15
+ from typing import TYPE_CHECKING, Any
16
16
 
17
17
  from buildlog.seed_engine.categorizers import Categorizer, TagBasedCategorizer
18
18
  from buildlog.seed_engine.extractors import ManualExtractor, RuleExtractor
19
19
  from buildlog.seed_engine.generators import SeedGenerator
20
20
  from buildlog.seed_engine.models import CandidateRule, CategorizedRule, Source
21
21
 
22
+ if TYPE_CHECKING:
23
+ from buildlog.llm import LLMBackend
24
+
22
25
  logger = logging.getLogger(__name__)
23
26
 
24
27
 
@@ -174,6 +177,7 @@ class Pipeline:
174
177
  Returns:
175
178
  List of validation issues (empty if valid).
176
179
  """
180
+ allowed_schemes = {"https", "http", "file"}
177
181
  issues = []
178
182
  for i, source in enumerate(sources):
179
183
  prefix = f"Source {i + 1} ({source.name})"
@@ -181,10 +185,50 @@ class Pipeline:
181
185
  issues.append(f"{prefix}: Missing name")
182
186
  if not source.url.strip():
183
187
  issues.append(f"{prefix}: Missing URL")
188
+ else:
189
+ # Validate URL scheme
190
+ scheme = (
191
+ source.url.split("://")[0].lower() if "://" in source.url else ""
192
+ )
193
+ if scheme not in allowed_schemes:
194
+ issues.append(
195
+ f"{prefix}: URL scheme '{scheme}' not in allowlist {allowed_schemes}"
196
+ )
184
197
  if not source.domain.strip():
185
198
  issues.append(f"{prefix}: Missing domain")
186
199
  return issues
187
200
 
201
+ @classmethod
202
+ def with_llm(
203
+ cls,
204
+ persona: str,
205
+ backend: LLMBackend,
206
+ source_content: dict[str, str] | None = None,
207
+ default_category: str = "general",
208
+ version: int = 1,
209
+ ) -> Pipeline:
210
+ """Convenience constructor wiring LLMExtractor + TagBasedCategorizer.
211
+
212
+ Args:
213
+ persona: Persona name for the seed file.
214
+ backend: Any LLMBackend implementation.
215
+ source_content: Optional pre-fetched content map.
216
+ default_category: Fallback category for uncategorized rules.
217
+ version: Seed file version.
218
+
219
+ Returns:
220
+ Pipeline configured with LLMExtractor.
221
+ """
222
+ from buildlog.seed_engine.llm_extractor import LLMExtractor
223
+
224
+ return cls(
225
+ persona=persona,
226
+ default_category=default_category,
227
+ version=version,
228
+ extractor=LLMExtractor(backend, source_content),
229
+ categorizer=TagBasedCategorizer(default_category=default_category),
230
+ )
231
+
188
232
  def dry_run(self, sources: list[Source]) -> dict[str, Any]:
189
233
  """Run pipeline without writing, returning preview.
190
234
 
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.4
2
+ Name: buildlog
3
+ Version: 0.9.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
+ ### A measurable learning loop for AI-assisted work
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
+ **Track what works. Prove it. Drop what doesn't.**
74
+
75
+ <img src="assets/hero-banner-perfectdeliberate.png" alt="buildlog - A measurable learning loop for AI-assisted work" 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
+ ## The Problem
86
+
87
+ Most AI agents do not learn. They execute without retaining context. You can bolt on memory stores and tool routers, but if the system cannot demonstrably improve its decision-making over time, you have a persistent memory store, not a learning system.
88
+
89
+ Every AI-assisted work session produces a trajectory: goals, decisions, tool uses, corrections, outcomes. Almost all of this is discarded. The next session starts from scratch with the same blind spots.
90
+
91
+ buildlog exists to close that gap. It captures structured trajectories from real work, extracts decision patterns, and uses statistical methods to select which patterns to surface in future sessions, then measures whether that selection actually reduced mistakes.
92
+
93
+ buildlog measures whether the system actually got better, and proves it.
94
+
95
+ ## How It Works
96
+
97
+ ### 1. Capture structured work trajectories
98
+
99
+ Each session is a dated entry documenting what you did, what went wrong, and what you learned. Each session is a structured record of decisions and outcomes, not a chat transcript.
100
+
101
+ ```bash
102
+ buildlog init # scaffold a project
103
+ buildlog new my-feature # start a session
104
+ # ... work ...
105
+ buildlog commit -m "feat: add auth"
106
+ ```
107
+
108
+ ### 2. Extract decision patterns as seeds
109
+
110
+ The seed engine watches your development patterns and extracts **seeds**: atomic observations about what works. A seed might be "always define interfaces before implementations" or "mock at the boundary, not the implementation." Each seed carries a category, a confidence score, and source provenance.
111
+
112
+ Extraction runs through a pipeline: `sources -> extractors -> categorizers -> generators`. Extractors range from regex-based (fast, cheap, brittle) to LLM-backed (accurate, expensive). The pipeline deduplicates semantically using embeddings.
113
+
114
+ ### 3. Select which patterns to surface using Thompson Sampling
115
+
116
+ Seeds compete for inclusion in your agent's instruction set. The system treats each seed as an arm in a contextual bandit and uses **Thompson Sampling** to balance exploration (trying under-tested rules) against exploitation (surfacing rules with strong track records).
117
+
118
+ Each seed maintains a Beta posterior updated by observed outcomes. Over time, the system converges on the rules that actually reduce mistakes in your specific codebase and workflow, not rules that sound good in the abstract.
119
+
120
+ ### 4. Render to every agent format
121
+
122
+ Selected rules are written into the instruction files your agents actually read:
123
+
124
+ - `CLAUDE.md` (Claude Code)
125
+ - `.cursorrules` (Cursor)
126
+ - `.github/copilot-instructions.md` (GitHub Copilot)
127
+ - Windsurf, Continue.dev, generic `settings.json`
128
+
129
+ The same knowledge base renders to every agent format.
130
+
131
+ ```bash
132
+ buildlog skills # render current policy to agent files
133
+ ```
134
+
135
+ ### 5. Close the loop with experiments
136
+
137
+ Track whether the selected rules are working. Run experiments, measure Repeated Mistake Rate (RMR) across sessions, and get statistical evidence, not feelings, about what improved.
138
+
139
+ ```bash
140
+ buildlog experiment start
141
+ # ... work across sessions ...
142
+ buildlog experiment end
143
+ buildlog experiment report
144
+ ```
145
+
146
+ ## What Else Is In the Box
147
+
148
+ - **Review gauntlet:** automated quality gate with curated reviewer personas. Runs on commits (via Claude Code hooks or CI) and files GitHub issues for findings, categorized by severity.
149
+ - **LLM-backed extraction:** when regex isn't enough, the seed engine can use OpenAI, Anthropic, or Ollama to extract patterns from code and logs. Metered backend tracks token usage and cost.
150
+ - **MCP server:** buildlog exposes itself as an MCP server so agents can query seeds, skills, and build history programmatically during sessions.
151
+ - **npm wrapper:** `npx @peleke.s/buildlog` for JS/TS projects. Thin shim that finds and invokes the Python CLI.
152
+
153
+ ## Current Limits
154
+
155
+ This is v0.8, not the end state.
156
+
157
+ - **Extraction quality is uneven.** Regex extractors miss nuance; LLM extractors are accurate but expensive. The middle ground is still being found.
158
+ - **Feedback signals are coarse.** Repeated Mistake Rate works but requires manual tagging. Richer automatic signals (test outcomes, review results, revision distance) are on the roadmap.
159
+ - **Credit assignment is limited.** When multiple rules are active, the system doesn't yet isolate which one was responsible for an outcome.
160
+ - **Single-agent only.** Multi-agent coordination (shared learning across agents) is designed but not implemented.
161
+ - **Long-horizon learning is not modeled.** The bandit operates per-session. Longer arcs of competence building need richer policy models.
162
+
163
+ The roadmap: contextual bandits (now) -> richer policy models -> longer-horizon RL -> multi-agent coordination. Each step builds on the same foundation: measuring whether rule changes actually reduce mistakes.
164
+
165
+ ## Installation
166
+
167
+ ### Global install (recommended)
168
+
169
+ ```bash
170
+ uv tool install "buildlog[mcp]" # or: pipx install "buildlog[mcp]"
171
+ ```
172
+
173
+ This puts `buildlog` and `buildlog-mcp` on your PATH. Works from any directory. The `[mcp]` extra is required for the MCP server.
174
+
175
+ ### Per-project (virtual environment)
176
+
177
+ ```bash
178
+ uv pip install "buildlog[mcp]" # or: pip install "buildlog[mcp]"
179
+ ```
180
+
181
+ Omit `[mcp]` if you only need the CLI.
182
+
183
+ ### For JS/TS projects
184
+
185
+ ```bash
186
+ npx @peleke.s/buildlog init
187
+ ```
188
+
189
+ ### MCP server for Claude Code
190
+
191
+ Add to `~/.claude/claude_code_config.json`:
192
+
193
+ ```json
194
+ {
195
+ "mcpServers": {
196
+ "buildlog": {
197
+ "command": "buildlog-mcp",
198
+ "args": []
199
+ }
200
+ }
201
+ }
202
+ ```
203
+
204
+ This exposes buildlog tools (seeds, skills, experiments, gauntlet, bandit status) to any Claude Code session.
205
+
206
+ ## Quick Start
207
+
208
+ ```bash
209
+ buildlog init # scaffold a project (run in any repo)
210
+ buildlog new my-feature # start a session
211
+ # ... work ...
212
+ buildlog distill && buildlog skills
213
+ buildlog experiment start
214
+ # ... work across sessions ...
215
+ buildlog experiment end
216
+ buildlog experiment report
217
+ ```
218
+
219
+ ## Documentation
220
+
221
+ | Section | Description |
222
+ |---------|------------|
223
+ | [Installation](https://peleke.github.io/buildlog-template/getting-started/installation/) | Setup, extras, and initialization |
224
+ | [Quick Start](https://peleke.github.io/buildlog-template/getting-started/quick-start/) | Full pipeline walkthrough |
225
+ | [Core Concepts](https://peleke.github.io/buildlog-template/getting-started/concepts/) | The problem, the claim, and the metric |
226
+ | [CLI Reference](https://peleke.github.io/buildlog-template/guides/cli-reference/) | Every command documented |
227
+ | [MCP Integration](https://peleke.github.io/buildlog-template/guides/mcp-integration/) | Claude Code setup and available tools |
228
+ | [Experiments](https://peleke.github.io/buildlog-template/guides/experiments/) | Running and measuring experiments |
229
+ | [Review Gauntlet](https://peleke.github.io/buildlog-template/guides/review-gauntlet/) | Reviewer personas and the gauntlet loop |
230
+ | [Multi-Agent Setup](https://peleke.github.io/buildlog-template/guides/multi-agent/) | Render rules to any AI coding agent |
231
+ | [Theory](https://peleke.github.io/buildlog-template/theory/00-background/) | The math behind Thompson Sampling |
232
+ | [Philosophy](https://peleke.github.io/buildlog-template/philosophy/) | Principles and honest limitations |
233
+
234
+ ## Contributing
235
+
236
+ ```bash
237
+ git clone https://github.com/Peleke/buildlog-template
238
+ cd buildlog-template
239
+ uv venv && source .venv/bin/activate
240
+ uv pip install -e ".[dev]"
241
+ pytest
242
+ ```
243
+
244
+ We're especially interested in better context representations, credit assignment approaches, statistical methodology improvements, and real-world experiment results (positive or negative).
245
+
246
+ ## License
247
+
248
+ MIT License. See [LICENSE](./LICENSE)
@@ -1,15 +1,15 @@
1
1
  buildlog/__init__.py,sha256=wxluyg3fDOiaKUAOJa0cav39hwtNJS5Y_0X20uL_yi4,90
2
- buildlog/cli.py,sha256=G2d2N2cmC3HEiYc6_mQhRyW8PLHzrHr2Kx38ooQsMlU,59456
2
+ buildlog/cli.py,sha256=CZUG3xYBfdyP47SDyvIgvjcfYDcHfV6ijGAr0B_7t1Y,66683
3
3
  buildlog/confidence.py,sha256=jPvN0_3drGHQG1C7iUxUtYjKC62nNKsHHxu6WXMfJFg,10137
4
4
  buildlog/distill.py,sha256=PL7UBToBb27BrCOTWGBTDIXGggtrUumHHBs0_MfG6vY,14166
5
5
  buildlog/embeddings.py,sha256=vPydWjJVkYp172zFou-lJ737qsu6vRMQAMs143RGIpA,12364
6
- buildlog/llm.py,sha256=vXFT03BZVI8GMX-ybSK18SoeABaZbr_B2KXm8WsNews,15278
6
+ buildlog/llm.py,sha256=t_3KnJ_eBmrtHaay4owG3EgGbJrVp_T379P0tTuyJf8,16986
7
7
  buildlog/seeds.py,sha256=L-lzO7rrDjMdVHcYKvEBm6scVDfCmFD0koiyafMZDLo,8596
8
8
  buildlog/skills.py,sha256=3CihyyAFfJh0w0VNXQauNhGm_RM8m_GIO1iiNLNPmsQ,33789
9
9
  buildlog/stats.py,sha256=2WdHdmzUNGobtWngmm9nA_UmqM7DQeAnZL8_rLQN8aw,13256
10
10
  buildlog/core/__init__.py,sha256=gPd6ava_sei0sYIS4NdeHgCVwJ2wEjyLt-Rxz_11IWU,1626
11
11
  buildlog/core/bandit.py,sha256=0z58s42jpQiBgXdjb465apenWdBVTvoc7bDOcnAcPLo,24565
12
- buildlog/core/operations.py,sha256=4gElPJBG9zlXoXO6CPTolwoRrd45DOIV6X1SAQbF3zE,70205
12
+ buildlog/core/operations.py,sha256=cL2w2Lrk7SBwha1aN9_t9WmQwi0OckCpAaGHCXDQztI,70249
13
13
  buildlog/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  buildlog/data/seeds/security_karen.yaml,sha256=Hjwp4j6FB-e0vCXp6AkyhAEkwASR4H1owdhPBID740E,8775
15
15
  buildlog/data/seeds/test_terrorist.yaml,sha256=-QY2x6d0LrlM6msMYaYwR43Wny9ZybQmU7pH_E7gSB0,11113
@@ -20,7 +20,7 @@ buildlog/engine/embeddings.py,sha256=78InITcBbaPZ29tUlb4hJNNVZ1fhxstvVa2fYvOh6QA
20
20
  buildlog/engine/experiments.py,sha256=38f_UsbY4TP3LTGM9yUsZxoXqxUGOxmLCoYrfZqjVks,20041
21
21
  buildlog/engine/types.py,sha256=W7U2B4TE79xACV0cuaidKevmTEskxqXfltH5hjIxV5k,734
22
22
  buildlog/mcp/__init__.py,sha256=jCLNUkYFrDcPd5dY9xbaaVANl-ZzdPim1BykgGY7f7U,334
23
- buildlog/mcp/server.py,sha256=vzgT_-SUWNNx9bvABJBgT7rHNGPx_nB7pVo-04WbwjI,1249
23
+ buildlog/mcp/server.py,sha256=IlE-xkW5gaOs5wkgusncRD9ugV70FRB6TKDanIqPaoc,1328
24
24
  buildlog/mcp/tools.py,sha256=k1f6R7Dgcx0VIKzTNezkcRc0UVkWXWkk-uGkRq1sIiw,17578
25
25
  buildlog/render/__init__.py,sha256=jj-o-vPdqpivdFKK85xskCOT8Zn_2tb3Q5Jb0gWLjAg,2567
26
26
  buildlog/render/base.py,sha256=gQfvOsH1zttAo10xtEyNsAbqZ4NRSPiDihO-aiGgTsw,533
@@ -32,23 +32,24 @@ buildlog/render/settings_json.py,sha256=4DS5OWksPrFCa7MIgWIu0t4rxYmItpMdGfTqMX3a
32
32
  buildlog/render/skill.py,sha256=_7umIS1Ms1oQ2_PopYueFjX41nMq1p28yJp6DhXFdgU,5981
33
33
  buildlog/render/tracking.py,sha256=1y_Mf1s3KblV8TC0W9x2a_ynSJHLwkVZJcW1gs1a6q4,1786
34
34
  buildlog/render/windsurf.py,sha256=jfJLb-luQLjdBM38_KKp0mZ08bUSDTBPxSLOIFWxawQ,3047
35
- buildlog/seed_engine/__init__.py,sha256=f2LYSAs_D1bK4CW_pt54UEmy_SB1u1MOEJlKqM7_R7E,1869
35
+ buildlog/seed_engine/__init__.py,sha256=QHo59cq-Ar_M6UthSOu_zpfYR1OF2ePu1qWrbgQqLak,1949
36
36
  buildlog/seed_engine/categorizers.py,sha256=uzatJQPG4Vf7z7ZAeyQbL-bVJ8cUKBHG-3U93-BFIRQ,4649
37
37
  buildlog/seed_engine/extractors.py,sha256=rkztg62LzFH018ob5Tl1eFp_yfPo8syn3Ib4h6bAyK4,4824
38
38
  buildlog/seed_engine/generators.py,sha256=T3N3uHg359xSBypIGmvVWYI0Qh8WLlGIFQWX0_gs0I4,4358
39
+ buildlog/seed_engine/llm_extractor.py,sha256=nfTmN4vuEiPQuhiL_2DX1QZ7TZlDR2vxYy0tVrkf2Vc,4124
39
40
  buildlog/seed_engine/models.py,sha256=qESSuoF3CJy8pp96E3Vmb38V-snzeaW0Xg0RfbH8v1U,3418
40
- buildlog/seed_engine/pipeline.py,sha256=wLdcjyA_B4wFEb2iKwRCUGkX-oaVu9OXIcmLcEsJN6s,6527
41
+ buildlog/seed_engine/pipeline.py,sha256=cyR2Vf32Hf0PK5DxnaFcq56ygjJIiJWiIkHY4Uz7AS8,8082
41
42
  buildlog/seed_engine/sources.py,sha256=8j9oUFZCSKMr5VpIuAxTPY3wTzfTEmw6M_41_aismiE,11184
42
- buildlog-0.8.0.data/data/share/buildlog/copier.yml,sha256=hD_UcLGB9eDxTGkWjHAvUoiqQKhLC0w3G-kPKX_p43I,802
43
- buildlog-0.8.0.data/data/share/buildlog/post_gen.py,sha256=XFlo40LuPpAsBhIRRRtHqvU3_5POss4L401hp35ijhw,1744
44
- buildlog-0.8.0.data/data/share/buildlog/template/buildlog/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
- buildlog-0.8.0.data/data/share/buildlog/template/buildlog/2026-01-01-example.md,sha256=7x9sKmydfmfKyNz9hV7MtYnQJuBwbxNanbPOcpQDDZQ,7040
46
- buildlog-0.8.0.data/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md,sha256=osclytWwl5jUiTgSpuT4cT3h3oPvCkZ5GPCnFuJZNcY,3802
47
- buildlog-0.8.0.data/data/share/buildlog/template/buildlog/_TEMPLATE.md,sha256=CUvxgcx1-9XT_EdQ8e_vnuPq_h-u1uhXJgForJU2Pso,2932
48
- buildlog-0.8.0.data/data/share/buildlog/template/buildlog/_TEMPLATE_QUICK.md,sha256=eUr5MiqLsM6drV7rAq53R1SLkK8G7LkMAUjWKXx81IA,409
49
- buildlog-0.8.0.data/data/share/buildlog/template/buildlog/assets/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
- buildlog-0.8.0.dist-info/METADATA,sha256=JNxWkxamVUmEeEQ9vz56j4lzZwex35sJyUngqytW6c4,7032
51
- buildlog-0.8.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
52
- buildlog-0.8.0.dist-info/entry_points.txt,sha256=BMFclPOomp_sgaa0OqBg6LfqCMlqzjZV88ww5TrPPoo,87
53
- buildlog-0.8.0.dist-info/licenses/LICENSE,sha256=fAgt-akug9nAwIj6M-SIf8u3ck-T7pJTwfmy9vWYASk,1074
54
- buildlog-0.8.0.dist-info/RECORD,,
43
+ buildlog-0.9.0.data/data/share/buildlog/copier.yml,sha256=hD_UcLGB9eDxTGkWjHAvUoiqQKhLC0w3G-kPKX_p43I,802
44
+ buildlog-0.9.0.data/data/share/buildlog/post_gen.py,sha256=XFlo40LuPpAsBhIRRRtHqvU3_5POss4L401hp35ijhw,1744
45
+ buildlog-0.9.0.data/data/share/buildlog/template/buildlog/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
+ buildlog-0.9.0.data/data/share/buildlog/template/buildlog/2026-01-01-example.md,sha256=7x9sKmydfmfKyNz9hV7MtYnQJuBwbxNanbPOcpQDDZQ,7040
47
+ buildlog-0.9.0.data/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md,sha256=osclytWwl5jUiTgSpuT4cT3h3oPvCkZ5GPCnFuJZNcY,3802
48
+ buildlog-0.9.0.data/data/share/buildlog/template/buildlog/_TEMPLATE.md,sha256=CUvxgcx1-9XT_EdQ8e_vnuPq_h-u1uhXJgForJU2Pso,2932
49
+ buildlog-0.9.0.data/data/share/buildlog/template/buildlog/_TEMPLATE_QUICK.md,sha256=eUr5MiqLsM6drV7rAq53R1SLkK8G7LkMAUjWKXx81IA,409
50
+ buildlog-0.9.0.data/data/share/buildlog/template/buildlog/assets/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
+ buildlog-0.9.0.dist-info/METADATA,sha256=uFrfGNpWCgBW7CYI3w6aUJDSHucCdVFpEoojWnsImA8,11341
52
+ buildlog-0.9.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
53
+ buildlog-0.9.0.dist-info/entry_points.txt,sha256=BMFclPOomp_sgaa0OqBg6LfqCMlqzjZV88ww5TrPPoo,87
54
+ buildlog-0.9.0.dist-info/licenses/LICENSE,sha256=fAgt-akug9nAwIj6M-SIf8u3ck-T7pJTwfmy9vWYASk,1074
55
+ buildlog-0.9.0.dist-info/RECORD,,
@@ -1,151 +0,0 @@
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>