buildlog 0.8.0__py3-none-any.whl → 0.10.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 (28) hide show
  1. buildlog/cli.py +491 -30
  2. buildlog/constants.py +121 -0
  3. buildlog/core/__init__.py +44 -0
  4. buildlog/core/operations.py +1189 -13
  5. buildlog/data/seeds/bragi.yaml +61 -0
  6. buildlog/llm.py +51 -4
  7. buildlog/mcp/__init__.py +51 -3
  8. buildlog/mcp/server.py +40 -0
  9. buildlog/mcp/tools.py +526 -12
  10. buildlog/seed_engine/__init__.py +2 -0
  11. buildlog/seed_engine/llm_extractor.py +121 -0
  12. buildlog/seed_engine/pipeline.py +45 -1
  13. {buildlog-0.8.0.data → buildlog-0.10.0.data}/data/share/buildlog/post_gen.py +10 -5
  14. buildlog-0.10.0.data/data/share/buildlog/template/buildlog/.gitkeep +0 -0
  15. buildlog-0.10.0.data/data/share/buildlog/template/buildlog/assets/.gitkeep +0 -0
  16. buildlog-0.10.0.dist-info/METADATA +248 -0
  17. {buildlog-0.8.0.dist-info → buildlog-0.10.0.dist-info}/RECORD +27 -22
  18. buildlog-0.8.0.dist-info/METADATA +0 -151
  19. {buildlog-0.8.0.data → buildlog-0.10.0.data}/data/share/buildlog/copier.yml +0 -0
  20. {buildlog-0.8.0.data/data/share/buildlog/template/buildlog → buildlog-0.10.0.data/data/share/buildlog/template/buildlog/.buildlog}/.gitkeep +0 -0
  21. {buildlog-0.8.0.data/data/share/buildlog/template/buildlog/assets → buildlog-0.10.0.data/data/share/buildlog/template/buildlog/.buildlog/seeds}/.gitkeep +0 -0
  22. {buildlog-0.8.0.data → buildlog-0.10.0.data}/data/share/buildlog/template/buildlog/2026-01-01-example.md +0 -0
  23. {buildlog-0.8.0.data → buildlog-0.10.0.data}/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md +0 -0
  24. {buildlog-0.8.0.data → buildlog-0.10.0.data}/data/share/buildlog/template/buildlog/_TEMPLATE.md +0 -0
  25. {buildlog-0.8.0.data → buildlog-0.10.0.data}/data/share/buildlog/template/buildlog/_TEMPLATE_QUICK.md +0 -0
  26. {buildlog-0.8.0.dist-info → buildlog-0.10.0.dist-info}/WHEEL +0 -0
  27. {buildlog-0.8.0.dist-info → buildlog-0.10.0.dist-info}/entry_points.txt +0 -0
  28. {buildlog-0.8.0.dist-info → buildlog-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,61 @@
1
+ # Bragi - LLM Prose Pattern Detection (named for the skald of the Aesir)
2
+ # Detect and flag synthetic-sounding prose patterns common in LLM-generated text
3
+ # These patterns aren't inherently wrong, but their overuse signals machine authorship
4
+
5
+ persona: bragi
6
+ version: 1
7
+
8
+ rules:
9
+ - rule: "Em-dash usage"
10
+ category: prose
11
+ context: "Anywhere: headlines, parentheticals, contrasts, asides"
12
+ antipattern: "Any use of em dashes (—). Includes 'Not X — Y' pivots, parenthetical asides, and dramatic pauses."
13
+ rationale: "LLMs deploy em dashes constantly and poorly. Ban them outright. Use colons, semicolons, commas, parentheses, or separate sentences instead."
14
+
15
+ - rule: "Dismissive 'with' framing"
16
+ category: prose
17
+ context: "Feature descriptions, project summaries, demo introductions"
18
+ antipattern: "'A demo with persistence', 'a prototype with authentication' — reducing a significant feature to a prepositional afterthought"
19
+ rationale: "Makes complex work sound trivially assembled. Signals the author hasn't weighed the feature's actual cost."
20
+
21
+ - rule: "Tricolon and five-item enumerations"
22
+ category: prose
23
+ context: "Value propositions, team descriptions, capability lists"
24
+ antipattern: "'They execute, improvise, and forget' or 'fast, reliable, scalable, secure, and observable' — rhythmic clause lists"
25
+ rationale: "LLMs default to tricolons and pentalogies for rhetorical rhythm. Real technical writing itemizes what matters, not what sounds balanced."
26
+
27
+ - rule: "Performative honesty"
28
+ category: prose
29
+ context: "Status updates, retrospectives, project assessments"
30
+ antipattern: "'Being honest about where things are:', 'To be frank:', 'The truth is:' — flagging your own candor"
31
+ rationale: "If you have to announce you're being honest, the surrounding text is performing. Just state the thing."
32
+
33
+ - rule: "Self-referential pivot"
34
+ category: prose
35
+ context: "Comparative sections, differentiation arguments"
36
+ antipattern: "'This is where X diverges from...', 'This is where things get interesting' — narrating the structure of your own argument"
37
+ rationale: "Meta-commentary about your own text is a crutch. Make the point; don't announce you're about to make it."
38
+
39
+ - rule: "Punchy fragment marketing copy"
40
+ category: prose
41
+ context: "Taglines, hero sections, feature callouts"
42
+ antipattern: "'One knowledge base, every agent surface.' — sentence fragments styled as advertising slogans"
43
+ rationale: "Technical docs aren't ad copy. Fragment slogans trade precision for vibes."
44
+
45
+ - rule: "Colon-into-bold-statement"
46
+ category: prose
47
+ context: "Section transitions, key takeaways, framing statements"
48
+ antipattern: "'The question it answers: **How do you...**' — colon followed by a bolded rhetorical question or declaration"
49
+ rationale: "This is a formatting crutch that substitutes typographic emphasis for actual argumentative weight."
50
+
51
+ - rule: "Rhythmic parallel construction closers"
52
+ category: prose
53
+ context: "Paragraph endings, section closers, conclusions"
54
+ antipattern: "'decisions tied to outcomes, updated by evidence, grounded in practice' — parallel participial phrases as closers"
55
+ rationale: "These read as sermon cadences. They sound authoritative but say little. End with a concrete claim instead."
56
+
57
+ - rule: "Hedging with 'not' lists"
58
+ category: prose
59
+ context: "Definitions, scope statements, positioning"
60
+ antipattern: "'This is not X. Not Y. Not Z.' — defining something entirely by what it isn't, in staccato negation"
61
+ rationale: "Negative definition is evasive. Say what the thing is. Reserve negation for genuine disambiguation."
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/__init__.py CHANGED
@@ -1,17 +1,65 @@
1
1
  """MCP server for buildlog integration."""
2
2
 
3
3
  from buildlog.mcp.tools import (
4
+ buildlog_bandit_status,
5
+ buildlog_commit,
4
6
  buildlog_diff,
7
+ buildlog_distill,
8
+ buildlog_entry_list,
9
+ buildlog_entry_new,
10
+ buildlog_experiment_end,
11
+ buildlog_experiment_metrics,
12
+ buildlog_experiment_report,
13
+ buildlog_experiment_start,
14
+ buildlog_gauntlet_accept_risk,
15
+ buildlog_gauntlet_generate,
16
+ buildlog_gauntlet_issues,
17
+ buildlog_gauntlet_list_personas,
18
+ buildlog_gauntlet_loop,
19
+ buildlog_gauntlet_prompt,
20
+ buildlog_gauntlet_rules,
21
+ buildlog_init,
5
22
  buildlog_learn_from_review,
23
+ buildlog_log_mistake,
24
+ buildlog_log_reward,
25
+ buildlog_overview,
6
26
  buildlog_promote,
7
27
  buildlog_reject,
28
+ buildlog_rewards,
29
+ buildlog_skills,
30
+ buildlog_stats,
8
31
  buildlog_status,
32
+ buildlog_update,
9
33
  )
10
34
 
11
35
  __all__ = [
12
- "buildlog_status",
13
- "buildlog_promote",
14
- "buildlog_reject",
36
+ "buildlog_bandit_status",
37
+ "buildlog_commit",
15
38
  "buildlog_diff",
39
+ "buildlog_distill",
40
+ "buildlog_entry_list",
41
+ "buildlog_entry_new",
42
+ "buildlog_experiment_end",
43
+ "buildlog_experiment_metrics",
44
+ "buildlog_experiment_report",
45
+ "buildlog_experiment_start",
46
+ "buildlog_gauntlet_accept_risk",
47
+ "buildlog_gauntlet_generate",
48
+ "buildlog_gauntlet_issues",
49
+ "buildlog_gauntlet_list_personas",
50
+ "buildlog_gauntlet_loop",
51
+ "buildlog_gauntlet_prompt",
52
+ "buildlog_gauntlet_rules",
53
+ "buildlog_init",
16
54
  "buildlog_learn_from_review",
55
+ "buildlog_log_mistake",
56
+ "buildlog_log_reward",
57
+ "buildlog_overview",
58
+ "buildlog_promote",
59
+ "buildlog_reject",
60
+ "buildlog_rewards",
61
+ "buildlog_skills",
62
+ "buildlog_stats",
63
+ "buildlog_status",
64
+ "buildlog_update",
17
65
  ]
buildlog/mcp/server.py CHANGED
@@ -5,20 +5,35 @@ 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,
9
+ buildlog_commit,
8
10
  buildlog_diff,
11
+ buildlog_distill,
12
+ buildlog_entry_list,
13
+ buildlog_entry_new,
9
14
  buildlog_experiment_end,
10
15
  buildlog_experiment_metrics,
11
16
  buildlog_experiment_report,
12
17
  buildlog_experiment_start,
13
18
  buildlog_gauntlet_accept_risk,
19
+ buildlog_gauntlet_generate,
14
20
  buildlog_gauntlet_issues,
21
+ buildlog_gauntlet_list_personas,
22
+ buildlog_gauntlet_loop,
23
+ buildlog_gauntlet_prompt,
24
+ buildlog_gauntlet_rules,
25
+ buildlog_init,
15
26
  buildlog_learn_from_review,
16
27
  buildlog_log_mistake,
17
28
  buildlog_log_reward,
29
+ buildlog_overview,
18
30
  buildlog_promote,
19
31
  buildlog_reject,
20
32
  buildlog_rewards,
33
+ buildlog_skills,
34
+ buildlog_stats,
21
35
  buildlog_status,
36
+ buildlog_update,
22
37
  )
23
38
 
24
39
  mcp = FastMCP("buildlog")
@@ -43,6 +58,31 @@ mcp.tool()(buildlog_experiment_report)
43
58
  mcp.tool()(buildlog_gauntlet_issues)
44
59
  mcp.tool()(buildlog_gauntlet_accept_risk)
45
60
 
61
+ # Bandit tools
62
+ mcp.tool()(buildlog_bandit_status)
63
+
64
+ # Entry & overview tools
65
+ mcp.tool()(buildlog_gauntlet_rules)
66
+ mcp.tool()(buildlog_overview)
67
+ mcp.tool()(buildlog_entry_new)
68
+ mcp.tool()(buildlog_entry_list)
69
+
70
+ # P0: Gauntlet loop completion
71
+ mcp.tool()(buildlog_commit)
72
+ mcp.tool()(buildlog_gauntlet_prompt)
73
+ mcp.tool()(buildlog_gauntlet_loop)
74
+
75
+ # P1: Learning pipeline
76
+ mcp.tool()(buildlog_distill)
77
+ mcp.tool()(buildlog_skills)
78
+ mcp.tool()(buildlog_stats)
79
+ mcp.tool()(buildlog_gauntlet_list_personas)
80
+
81
+ # P2: Nice-to-have
82
+ mcp.tool()(buildlog_gauntlet_generate)
83
+ mcp.tool()(buildlog_init)
84
+ mcp.tool()(buildlog_update)
85
+
46
86
 
47
87
  def main() -> None:
48
88
  """Run the MCP server."""