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.
- buildlog/cli.py +491 -30
- buildlog/constants.py +121 -0
- buildlog/core/__init__.py +44 -0
- buildlog/core/operations.py +1189 -13
- buildlog/data/seeds/bragi.yaml +61 -0
- buildlog/llm.py +51 -4
- buildlog/mcp/__init__.py +51 -3
- buildlog/mcp/server.py +40 -0
- buildlog/mcp/tools.py +526 -12
- buildlog/seed_engine/__init__.py +2 -0
- buildlog/seed_engine/llm_extractor.py +121 -0
- buildlog/seed_engine/pipeline.py +45 -1
- {buildlog-0.8.0.data → buildlog-0.10.0.data}/data/share/buildlog/post_gen.py +10 -5
- buildlog-0.10.0.data/data/share/buildlog/template/buildlog/.gitkeep +0 -0
- buildlog-0.10.0.data/data/share/buildlog/template/buildlog/assets/.gitkeep +0 -0
- buildlog-0.10.0.dist-info/METADATA +248 -0
- {buildlog-0.8.0.dist-info → buildlog-0.10.0.dist-info}/RECORD +27 -22
- buildlog-0.8.0.dist-info/METADATA +0 -151
- {buildlog-0.8.0.data → buildlog-0.10.0.data}/data/share/buildlog/copier.yml +0 -0
- {buildlog-0.8.0.data/data/share/buildlog/template/buildlog → buildlog-0.10.0.data/data/share/buildlog/template/buildlog/.buildlog}/.gitkeep +0 -0
- {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
- {buildlog-0.8.0.data → buildlog-0.10.0.data}/data/share/buildlog/template/buildlog/2026-01-01-example.md +0 -0
- {buildlog-0.8.0.data → buildlog-0.10.0.data}/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md +0 -0
- {buildlog-0.8.0.data → buildlog-0.10.0.data}/data/share/buildlog/template/buildlog/_TEMPLATE.md +0 -0
- {buildlog-0.8.0.data → buildlog-0.10.0.data}/data/share/buildlog/template/buildlog/_TEMPLATE_QUICK.md +0 -0
- {buildlog-0.8.0.dist-info → buildlog-0.10.0.dist-info}/WHEEL +0 -0
- {buildlog-0.8.0.dist-info → buildlog-0.10.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
"
|
|
13
|
-
"
|
|
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."""
|