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.
- buildlog/__init__.py +1 -1
- buildlog/cli.py +589 -44
- buildlog/confidence.py +27 -0
- buildlog/core/__init__.py +12 -0
- buildlog/core/bandit.py +699 -0
- buildlog/core/operations.py +499 -11
- buildlog/distill.py +80 -1
- buildlog/engine/__init__.py +61 -0
- buildlog/engine/bandit.py +23 -0
- buildlog/engine/confidence.py +28 -0
- buildlog/engine/embeddings.py +28 -0
- buildlog/engine/experiments.py +619 -0
- buildlog/engine/types.py +31 -0
- buildlog/llm.py +461 -0
- buildlog/mcp/server.py +12 -6
- buildlog/mcp/tools.py +166 -13
- buildlog/render/__init__.py +19 -2
- buildlog/render/claude_md.py +74 -26
- buildlog/render/continue_dev.py +102 -0
- buildlog/render/copilot.py +100 -0
- buildlog/render/cursor.py +105 -0
- buildlog/render/tracking.py +20 -1
- buildlog/render/windsurf.py +95 -0
- buildlog/seeds.py +41 -0
- buildlog/skills.py +69 -6
- {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/copier.yml +0 -4
- buildlog-0.8.0.data/data/share/buildlog/template/buildlog/_TEMPLATE_QUICK.md +21 -0
- buildlog-0.8.0.dist-info/METADATA +151 -0
- buildlog-0.8.0.dist-info/RECORD +54 -0
- buildlog-0.6.1.dist-info/METADATA +0 -490
- buildlog-0.6.1.dist-info/RECORD +0 -41
- {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/post_gen.py +0 -0
- {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/template/buildlog/.gitkeep +0 -0
- {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/template/buildlog/2026-01-01-example.md +0 -0
- {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md +0 -0
- {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/template/buildlog/_TEMPLATE.md +0 -0
- {buildlog-0.6.1.data → buildlog-0.8.0.data}/data/share/buildlog/template/buildlog/assets/.gitkeep +0 -0
- {buildlog-0.6.1.dist-info → buildlog-0.8.0.dist-info}/WHEEL +0 -0
- {buildlog-0.6.1.dist-info → buildlog-0.8.0.dist-info}/entry_points.txt +0 -0
- {buildlog-0.6.1.dist-info → buildlog-0.8.0.dist-info}/licenses/LICENSE +0 -0
buildlog/mcp/tools.py
CHANGED
|
@@ -12,6 +12,7 @@ from typing import Literal
|
|
|
12
12
|
from buildlog.core import (
|
|
13
13
|
diff,
|
|
14
14
|
end_session,
|
|
15
|
+
get_bandit_status,
|
|
15
16
|
get_experiment_report,
|
|
16
17
|
get_rewards,
|
|
17
18
|
get_session_metrics,
|
|
@@ -52,17 +53,17 @@ def buildlog_status(
|
|
|
52
53
|
|
|
53
54
|
def buildlog_promote(
|
|
54
55
|
skill_ids: list[str],
|
|
55
|
-
target:
|
|
56
|
+
target: str = "claude_md",
|
|
56
57
|
buildlog_dir: str = "buildlog",
|
|
57
58
|
) -> dict:
|
|
58
59
|
"""Promote skills to your agent's rules.
|
|
59
60
|
|
|
60
|
-
Writes selected skills to
|
|
61
|
-
.claude/skills/buildlog-learned/SKILL.md (Anthropic Agent Skills format).
|
|
61
|
+
Writes selected skills to agent-specific rule files.
|
|
62
62
|
|
|
63
63
|
Args:
|
|
64
64
|
skill_ids: List of skill IDs to promote (e.g., ["arch-b0fcb62a1e"])
|
|
65
|
-
target: Where to write rules
|
|
65
|
+
target: Where to write rules. One of: claude_md, settings_json,
|
|
66
|
+
skill, cursor, copilot, windsurf, continue_dev.
|
|
66
67
|
buildlog_dir: Path to buildlog directory
|
|
67
68
|
|
|
68
69
|
Returns:
|
|
@@ -262,36 +263,47 @@ def buildlog_rewards(
|
|
|
262
263
|
# -----------------------------------------------------------------------------
|
|
263
264
|
|
|
264
265
|
|
|
265
|
-
def
|
|
266
|
+
def buildlog_experiment_start(
|
|
266
267
|
error_class: str | None = None,
|
|
267
268
|
notes: str | None = None,
|
|
269
|
+
select_k: int = 3,
|
|
268
270
|
buildlog_dir: str = "buildlog",
|
|
269
271
|
) -> dict:
|
|
270
|
-
"""Start a new experiment session.
|
|
272
|
+
"""Start a new experiment session with Thompson Sampling rule selection.
|
|
271
273
|
|
|
272
|
-
Begins tracking for a learning experiment.
|
|
273
|
-
|
|
274
|
+
Begins tracking for a learning experiment. Uses Thompson Sampling
|
|
275
|
+
to select which rules will be "active" for this session based on
|
|
276
|
+
the error class context.
|
|
277
|
+
|
|
278
|
+
The selected rules will receive feedback:
|
|
279
|
+
- Negative feedback (reward=0) when log_mistake() is called
|
|
280
|
+
- Explicit feedback when log_reward() is called
|
|
281
|
+
|
|
282
|
+
This teaches the bandit which rules are effective for which contexts.
|
|
274
283
|
|
|
275
284
|
Args:
|
|
276
|
-
error_class: Error class being targeted (e.g., "missing_test")
|
|
285
|
+
error_class: Error class being targeted (e.g., "missing_test").
|
|
286
|
+
This is the CONTEXT for contextual bandits.
|
|
277
287
|
notes: Notes about this session
|
|
288
|
+
select_k: Number of rules to select via Thompson Sampling
|
|
278
289
|
buildlog_dir: Path to buildlog directory
|
|
279
290
|
|
|
280
291
|
Returns:
|
|
281
|
-
Dict with session_id, error_class, rules_count, message
|
|
292
|
+
Dict with session_id, error_class, rules_count, selected_rules, message
|
|
282
293
|
|
|
283
294
|
Example:
|
|
284
|
-
buildlog_start_session(error_class="
|
|
295
|
+
buildlog_start_session(error_class="type-errors", select_k=5)
|
|
285
296
|
"""
|
|
286
297
|
result = start_session(
|
|
287
298
|
Path(buildlog_dir),
|
|
288
299
|
error_class=error_class,
|
|
289
300
|
notes=notes,
|
|
301
|
+
select_k=select_k,
|
|
290
302
|
)
|
|
291
303
|
return asdict(result)
|
|
292
304
|
|
|
293
305
|
|
|
294
|
-
def
|
|
306
|
+
def buildlog_experiment_end(
|
|
295
307
|
entry_file: str | None = None,
|
|
296
308
|
notes: str | None = None,
|
|
297
309
|
buildlog_dir: str = "buildlog",
|
|
@@ -358,7 +370,7 @@ def buildlog_log_mistake(
|
|
|
358
370
|
return asdict(result)
|
|
359
371
|
|
|
360
372
|
|
|
361
|
-
def
|
|
373
|
+
def buildlog_experiment_metrics(
|
|
362
374
|
session_id: str | None = None,
|
|
363
375
|
buildlog_dir: str = "buildlog",
|
|
364
376
|
) -> dict:
|
|
@@ -405,3 +417,144 @@ def buildlog_experiment_report(
|
|
|
405
417
|
buildlog_experiment_report()
|
|
406
418
|
"""
|
|
407
419
|
return get_experiment_report(Path(buildlog_dir))
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def buildlog_bandit_status(
|
|
423
|
+
buildlog_dir: str = "buildlog",
|
|
424
|
+
context: str | None = None,
|
|
425
|
+
top_k: int = 10,
|
|
426
|
+
) -> dict:
|
|
427
|
+
"""Get Thompson Sampling bandit status and rule rankings.
|
|
428
|
+
|
|
429
|
+
Shows the bandit's learned beliefs about which rules are effective
|
|
430
|
+
for each error class context. Higher mean = bandit believes rule
|
|
431
|
+
is more effective.
|
|
432
|
+
|
|
433
|
+
The bandit uses Beta distributions to model uncertainty:
|
|
434
|
+
- High variance (wide CI) = uncertain, will explore more
|
|
435
|
+
- Low variance (narrow CI) = confident, will exploit
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
buildlog_dir: Path to buildlog directory
|
|
439
|
+
context: Specific error class to filter by (optional)
|
|
440
|
+
top_k: Number of top rules to show per context
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Dict with:
|
|
444
|
+
- summary: Total contexts, arms, observations
|
|
445
|
+
- top_rules: Best rules per context by expected value
|
|
446
|
+
- all_rules: Full stats if filtering by context
|
|
447
|
+
|
|
448
|
+
Example:
|
|
449
|
+
# See all bandit state
|
|
450
|
+
buildlog_bandit_status()
|
|
451
|
+
|
|
452
|
+
# See state for specific error class
|
|
453
|
+
buildlog_bandit_status(context="type-errors")
|
|
454
|
+
"""
|
|
455
|
+
return get_bandit_status(Path(buildlog_dir), context, top_k)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# -----------------------------------------------------------------------------
|
|
459
|
+
# Gauntlet Loop MCP Tools
|
|
460
|
+
# -----------------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def buildlog_gauntlet_issues(
|
|
464
|
+
issues: list[dict],
|
|
465
|
+
iteration: int = 1,
|
|
466
|
+
source: str | None = None,
|
|
467
|
+
buildlog_dir: str = "buildlog",
|
|
468
|
+
) -> dict:
|
|
469
|
+
"""Process gauntlet review issues and determine next action.
|
|
470
|
+
|
|
471
|
+
Call this after running a gauntlet review. It categorizes issues by
|
|
472
|
+
severity, persists learnings, and returns the appropriate next action.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
issues: List of issues from the gauntlet review, each with:
|
|
476
|
+
{
|
|
477
|
+
"severity": "critical|major|minor|nitpick",
|
|
478
|
+
"category": "security|testing|architectural|...",
|
|
479
|
+
"description": "What's wrong",
|
|
480
|
+
"rule_learned": "Generalizable rule",
|
|
481
|
+
"location": "file:line (optional)"
|
|
482
|
+
}
|
|
483
|
+
iteration: Current iteration number (for tracking loops)
|
|
484
|
+
source: Optional source identifier for learnings
|
|
485
|
+
buildlog_dir: Path to buildlog directory
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
Dict with:
|
|
489
|
+
- action: What to do next:
|
|
490
|
+
- "fix_criticals": Criticals remain, auto-fix and loop
|
|
491
|
+
- "checkpoint_majors": No criticals, majors remain (ask user)
|
|
492
|
+
- "checkpoint_minors": Only minors remain (ask user)
|
|
493
|
+
- "clean": No issues remain
|
|
494
|
+
- criticals: List of critical issues
|
|
495
|
+
- majors: List of major issues
|
|
496
|
+
- minors: List of minor/nitpick issues
|
|
497
|
+
- iteration: Current iteration number
|
|
498
|
+
- learnings_persisted: Number of learnings saved
|
|
499
|
+
- message: Human-readable summary
|
|
500
|
+
|
|
501
|
+
Example:
|
|
502
|
+
# After running gauntlet review
|
|
503
|
+
result = buildlog_gauntlet_issues(
|
|
504
|
+
issues=[
|
|
505
|
+
{"severity": "critical", "category": "security", ...},
|
|
506
|
+
{"severity": "major", "category": "testing", ...},
|
|
507
|
+
],
|
|
508
|
+
iteration=1
|
|
509
|
+
)
|
|
510
|
+
# result["action"] tells you what to do next
|
|
511
|
+
"""
|
|
512
|
+
from buildlog.core import gauntlet_process_issues
|
|
513
|
+
|
|
514
|
+
result = gauntlet_process_issues(
|
|
515
|
+
Path(buildlog_dir),
|
|
516
|
+
issues=issues,
|
|
517
|
+
iteration=iteration,
|
|
518
|
+
source=source,
|
|
519
|
+
)
|
|
520
|
+
return asdict(result)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def buildlog_gauntlet_accept_risk(
|
|
524
|
+
remaining_issues: list[dict],
|
|
525
|
+
create_github_issues: bool = False,
|
|
526
|
+
repo: str | None = None,
|
|
527
|
+
) -> dict:
|
|
528
|
+
"""Accept risk for remaining issues, optionally creating GitHub issues.
|
|
529
|
+
|
|
530
|
+
Call this when the user decides to accept remaining issues as risk
|
|
531
|
+
(e.g., only minors remain and they want to move on).
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
remaining_issues: Issues being accepted as risk
|
|
535
|
+
create_github_issues: Whether to create GitHub issues for tracking
|
|
536
|
+
repo: Repository for GitHub issues (uses current repo if None)
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
Dict with:
|
|
540
|
+
- accepted_issues: Number of issues accepted
|
|
541
|
+
- github_issues_created: Number of GitHub issues created
|
|
542
|
+
- github_issue_urls: URLs of created issues
|
|
543
|
+
- message: Human-readable summary
|
|
544
|
+
- error: Error message if GitHub issue creation failed
|
|
545
|
+
|
|
546
|
+
Example:
|
|
547
|
+
# User accepts risk with minors, wants GitHub issues
|
|
548
|
+
result = buildlog_gauntlet_accept_risk(
|
|
549
|
+
remaining_issues=[...],
|
|
550
|
+
create_github_issues=True
|
|
551
|
+
)
|
|
552
|
+
"""
|
|
553
|
+
from buildlog.core import gauntlet_accept_risk
|
|
554
|
+
|
|
555
|
+
result = gauntlet_accept_risk(
|
|
556
|
+
remaining_issues=remaining_issues,
|
|
557
|
+
create_github_issues=create_github_issues,
|
|
558
|
+
repo=repo,
|
|
559
|
+
)
|
|
560
|
+
return asdict(result)
|
buildlog/render/__init__.py
CHANGED
|
@@ -7,8 +7,12 @@ from typing import TYPE_CHECKING, Literal
|
|
|
7
7
|
|
|
8
8
|
from buildlog.render.base import RenderTarget
|
|
9
9
|
from buildlog.render.claude_md import ClaudeMdRenderer
|
|
10
|
+
from buildlog.render.continue_dev import ContinueRenderer
|
|
11
|
+
from buildlog.render.copilot import CopilotRenderer
|
|
12
|
+
from buildlog.render.cursor import CursorRenderer
|
|
10
13
|
from buildlog.render.settings_json import SettingsJsonRenderer
|
|
11
14
|
from buildlog.render.skill import SkillRenderer
|
|
15
|
+
from buildlog.render.windsurf import WindsurfRenderer
|
|
12
16
|
|
|
13
17
|
if TYPE_CHECKING:
|
|
14
18
|
from typing import Any
|
|
@@ -18,8 +22,13 @@ __all__ = [
|
|
|
18
22
|
"ClaudeMdRenderer",
|
|
19
23
|
"SettingsJsonRenderer",
|
|
20
24
|
"SkillRenderer",
|
|
25
|
+
"CursorRenderer",
|
|
26
|
+
"CopilotRenderer",
|
|
27
|
+
"WindsurfRenderer",
|
|
28
|
+
"ContinueRenderer",
|
|
21
29
|
"get_renderer",
|
|
22
30
|
"RENDERERS",
|
|
31
|
+
"RENDER_TARGETS",
|
|
23
32
|
]
|
|
24
33
|
|
|
25
34
|
# Registry of available renderers
|
|
@@ -28,18 +37,26 @@ RENDERERS: dict[str, type[RenderTarget]] = {
|
|
|
28
37
|
"claude_md": ClaudeMdRenderer,
|
|
29
38
|
"settings_json": SettingsJsonRenderer,
|
|
30
39
|
"skill": SkillRenderer,
|
|
40
|
+
"cursor": CursorRenderer,
|
|
41
|
+
"copilot": CopilotRenderer,
|
|
42
|
+
"windsurf": WindsurfRenderer,
|
|
43
|
+
"continue_dev": ContinueRenderer,
|
|
31
44
|
}
|
|
32
45
|
|
|
46
|
+
# Valid target names (useful for CLI choices and type hints)
|
|
47
|
+
RENDER_TARGETS = list(RENDERERS.keys())
|
|
48
|
+
|
|
33
49
|
|
|
34
50
|
def get_renderer(
|
|
35
|
-
target:
|
|
51
|
+
target: str,
|
|
36
52
|
path: Path | None = None,
|
|
37
53
|
**kwargs: Any,
|
|
38
54
|
) -> RenderTarget:
|
|
39
55
|
"""Get renderer for target.
|
|
40
56
|
|
|
41
57
|
Args:
|
|
42
|
-
target: Target format -
|
|
58
|
+
target: Target format - one of: claude_md, settings_json, skill,
|
|
59
|
+
cursor, copilot, windsurf, continue_dev.
|
|
43
60
|
path: Optional custom path for the target file.
|
|
44
61
|
**kwargs: Additional arguments passed to the renderer constructor.
|
|
45
62
|
Common kwargs (accepted by all renderers):
|
buildlog/render/claude_md.py
CHANGED
|
@@ -6,15 +6,24 @@ from datetime import datetime
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
|
-
from buildlog.render.tracking import track_promoted
|
|
9
|
+
from buildlog.render.tracking import get_promoted_ids, track_promoted
|
|
10
10
|
from buildlog.skills import _to_imperative
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from buildlog.skills import Skill
|
|
14
14
|
|
|
15
|
+
# Markers to identify the buildlog-managed section in CLAUDE.md
|
|
16
|
+
_SECTION_START = "<!-- buildlog:rules:start -->"
|
|
17
|
+
_SECTION_END = "<!-- buildlog:rules:end -->"
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
class ClaudeMdRenderer:
|
|
17
|
-
"""
|
|
21
|
+
"""Manages a dedicated rules section in CLAUDE.md.
|
|
22
|
+
|
|
23
|
+
Uses HTML comment markers to identify the buildlog-managed section.
|
|
24
|
+
On each promote, the section is replaced (not appended) with ALL
|
|
25
|
+
currently promoted rules, preventing duplicates.
|
|
26
|
+
"""
|
|
18
27
|
|
|
19
28
|
def __init__(self, path: Path | None = None, tracking_path: Path | None = None):
|
|
20
29
|
"""Initialize renderer.
|
|
@@ -31,10 +40,14 @@ class ClaudeMdRenderer:
|
|
|
31
40
|
self.tracking_path = tracking_path
|
|
32
41
|
|
|
33
42
|
def render(self, skills: list[Skill]) -> str:
|
|
34
|
-
"""
|
|
43
|
+
"""Write skills to CLAUDE.md, replacing the buildlog-managed section.
|
|
44
|
+
|
|
45
|
+
On first run, appends a marked section. On subsequent runs, finds
|
|
46
|
+
and replaces the marked section with updated rules. This prevents
|
|
47
|
+
the duplicate accumulation that append-only causes.
|
|
35
48
|
|
|
36
49
|
Args:
|
|
37
|
-
skills: List of skills to
|
|
50
|
+
skills: List of skills to write.
|
|
38
51
|
|
|
39
52
|
Returns:
|
|
40
53
|
Confirmation message.
|
|
@@ -42,25 +55,71 @@ class ClaudeMdRenderer:
|
|
|
42
55
|
if not skills:
|
|
43
56
|
return "No skills to promote"
|
|
44
57
|
|
|
58
|
+
# Filter out already-promoted skills for tracking purposes,
|
|
59
|
+
# but we still rebuild the full section from ALL promoted skills
|
|
60
|
+
already_promoted = get_promoted_ids(self.tracking_path)
|
|
61
|
+
new_skills = [s for s in skills if s.id not in already_promoted]
|
|
62
|
+
|
|
63
|
+
if not new_skills:
|
|
64
|
+
return f"All {len(skills)} skills already promoted"
|
|
65
|
+
|
|
66
|
+
# Track the new skills first so the section includes them
|
|
67
|
+
track_promoted(new_skills, self.tracking_path)
|
|
68
|
+
|
|
69
|
+
# Build the section content from ALL skills being promoted now
|
|
70
|
+
# (not just new ones — we replace the entire section)
|
|
71
|
+
all_skills = skills # All skills passed in this call
|
|
72
|
+
section = self._build_section(all_skills)
|
|
73
|
+
|
|
74
|
+
# Read existing file
|
|
75
|
+
if self.path.exists():
|
|
76
|
+
existing = self.path.read_text()
|
|
77
|
+
else:
|
|
78
|
+
existing = ""
|
|
79
|
+
|
|
80
|
+
# Replace or append the buildlog section
|
|
81
|
+
if _SECTION_START in existing and _SECTION_END in existing:
|
|
82
|
+
# Replace existing section
|
|
83
|
+
start_idx = existing.index(_SECTION_START)
|
|
84
|
+
end_idx = existing.index(_SECTION_END) + len(_SECTION_END)
|
|
85
|
+
updated = existing[:start_idx] + section + existing[end_idx:]
|
|
86
|
+
elif _SECTION_START in existing:
|
|
87
|
+
# Malformed: start marker but no end. Replace from start to EOF.
|
|
88
|
+
start_idx = existing.index(_SECTION_START)
|
|
89
|
+
updated = existing[:start_idx] + section
|
|
90
|
+
else:
|
|
91
|
+
# No existing section — append
|
|
92
|
+
updated = existing.rstrip() + "\n\n" + section + "\n"
|
|
93
|
+
|
|
94
|
+
self.path.write_text(updated)
|
|
95
|
+
|
|
96
|
+
skipped = len(skills) - len(new_skills)
|
|
97
|
+
msg = f"Wrote {len(new_skills)} new rules to {self.path} ({len(all_skills)} total in section)"
|
|
98
|
+
if skipped > 0:
|
|
99
|
+
msg += f" ({skipped} already tracked)"
|
|
100
|
+
return msg
|
|
101
|
+
|
|
102
|
+
def _build_section(self, skills: list[Skill]) -> str:
|
|
103
|
+
"""Build the marked section content."""
|
|
104
|
+
category_titles = {
|
|
105
|
+
"architectural": "Architectural",
|
|
106
|
+
"workflow": "Workflow",
|
|
107
|
+
"tool_usage": "Tool Usage",
|
|
108
|
+
"domain_knowledge": "Domain Knowledge",
|
|
109
|
+
}
|
|
110
|
+
|
|
45
111
|
# Group by category
|
|
46
112
|
by_category: dict[str, list[Skill]] = {}
|
|
47
113
|
for skill in skills:
|
|
48
114
|
by_category.setdefault(skill.category, []).append(skill)
|
|
49
115
|
|
|
50
|
-
# Build section
|
|
51
116
|
lines = [
|
|
117
|
+
_SECTION_START,
|
|
52
118
|
"",
|
|
53
|
-
f"## Learned Rules (
|
|
119
|
+
f"## Learned Rules (buildlog, updated {datetime.now().strftime('%Y-%m-%d')})",
|
|
54
120
|
"",
|
|
55
121
|
]
|
|
56
122
|
|
|
57
|
-
category_titles = {
|
|
58
|
-
"architectural": "Architectural",
|
|
59
|
-
"workflow": "Workflow",
|
|
60
|
-
"tool_usage": "Tool Usage",
|
|
61
|
-
"domain_knowledge": "Domain Knowledge",
|
|
62
|
-
}
|
|
63
|
-
|
|
64
123
|
for category, cat_skills in by_category.items():
|
|
65
124
|
title = category_titles.get(category, category.replace("_", " ").title())
|
|
66
125
|
lines.append(f"### {title}")
|
|
@@ -70,16 +129,5 @@ class ClaudeMdRenderer:
|
|
|
70
129
|
lines.append(f"- {rule}")
|
|
71
130
|
lines.append("")
|
|
72
131
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
# Append to file
|
|
76
|
-
if self.path.exists():
|
|
77
|
-
existing = self.path.read_text()
|
|
78
|
-
self.path.write_text(existing + content)
|
|
79
|
-
else:
|
|
80
|
-
self.path.write_text(content)
|
|
81
|
-
|
|
82
|
-
# Track promoted skill IDs using shared utility
|
|
83
|
-
track_promoted(skills, self.tracking_path)
|
|
84
|
-
|
|
85
|
-
return f"Appended {len(skills)} rules to {self.path}"
|
|
132
|
+
lines.append(_SECTION_END)
|
|
133
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Render skills to Continue.dev rules format.
|
|
2
|
+
|
|
3
|
+
Creates .continue/rules/buildlog-rules.md 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
|
+
from buildlog.skills import _to_imperative
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from buildlog.skills import Skill
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ContinueRenderer:
|
|
21
|
+
"""Creates .continue/rules/buildlog-rules.md for Continue.dev."""
|
|
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.
|
|
28
|
+
Defaults to .continue/rules/buildlog-rules.md.
|
|
29
|
+
tracking_path: Path to promoted.json tracking file.
|
|
30
|
+
Defaults to .buildlog/promoted.json.
|
|
31
|
+
"""
|
|
32
|
+
self.path = path or Path(".continue/rules/buildlog-rules.md")
|
|
33
|
+
self.tracking_path = tracking_path or Path(".buildlog/promoted.json")
|
|
34
|
+
|
|
35
|
+
def render(self, skills: list[Skill]) -> str:
|
|
36
|
+
"""Render skills to Continue.dev rules file.
|
|
37
|
+
|
|
38
|
+
Overwrites the file with all promoted skills.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
skills: List of skills to render.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Confirmation message.
|
|
45
|
+
"""
|
|
46
|
+
if not skills:
|
|
47
|
+
return "No skills to promote"
|
|
48
|
+
|
|
49
|
+
# Filter out already-promoted skills
|
|
50
|
+
already_promoted = get_promoted_ids(self.tracking_path)
|
|
51
|
+
new_skills = [s for s in skills if s.id not in already_promoted]
|
|
52
|
+
|
|
53
|
+
if not new_skills:
|
|
54
|
+
return f"All {len(skills)} skills already promoted"
|
|
55
|
+
|
|
56
|
+
# Group by category
|
|
57
|
+
by_category: dict[str, list[Skill]] = {}
|
|
58
|
+
for skill in new_skills:
|
|
59
|
+
by_category.setdefault(skill.category, []).append(skill)
|
|
60
|
+
|
|
61
|
+
category_titles = {
|
|
62
|
+
"architectural": "Architectural",
|
|
63
|
+
"workflow": "Workflow",
|
|
64
|
+
"tool_usage": "Tool Usage",
|
|
65
|
+
"domain_knowledge": "Domain Knowledge",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Build content with YAML frontmatter
|
|
69
|
+
lines = [
|
|
70
|
+
"---",
|
|
71
|
+
"name: buildlog-learned-rules",
|
|
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
|
+
rule = _to_imperative(skill.rule, skill.confidence)
|
|
86
|
+
lines.append(f"- {rule}")
|
|
87
|
+
lines.append("")
|
|
88
|
+
|
|
89
|
+
content = "\n".join(lines)
|
|
90
|
+
|
|
91
|
+
# Write file
|
|
92
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
self.path.write_text(content)
|
|
94
|
+
|
|
95
|
+
# Track promoted skill IDs
|
|
96
|
+
track_promoted(new_skills, self.tracking_path)
|
|
97
|
+
|
|
98
|
+
skipped = len(skills) - len(new_skills)
|
|
99
|
+
msg = f"Wrote {len(new_skills)} rules to {self.path}"
|
|
100
|
+
if skipped > 0:
|
|
101
|
+
msg += f" ({skipped} already promoted, skipped)"
|
|
102
|
+
return msg
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Render skills to GitHub Copilot instructions format.
|
|
2
|
+
|
|
3
|
+
Appends a learned rules section to .github/copilot-instructions.md.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from buildlog.render.tracking import get_promoted_ids, track_promoted
|
|
13
|
+
from buildlog.skills import _to_imperative
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from buildlog.skills import Skill
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CopilotRenderer:
|
|
20
|
+
"""Appends promoted skills to .github/copilot-instructions.md."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, path: Path | None = None, tracking_path: Path | None = None):
|
|
23
|
+
"""Initialize renderer.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
path: Path to copilot instructions file.
|
|
27
|
+
Defaults to .github/copilot-instructions.md.
|
|
28
|
+
tracking_path: Path to promoted.json tracking file.
|
|
29
|
+
Defaults to .buildlog/promoted.json.
|
|
30
|
+
"""
|
|
31
|
+
self.path = path or Path(".github/copilot-instructions.md")
|
|
32
|
+
self.tracking_path = tracking_path or Path(".buildlog/promoted.json")
|
|
33
|
+
|
|
34
|
+
def render(self, skills: list[Skill]) -> str:
|
|
35
|
+
"""Append skills to copilot-instructions.md.
|
|
36
|
+
|
|
37
|
+
Filters out already-promoted skills and appends a new section.
|
|
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 section
|
|
68
|
+
lines = [
|
|
69
|
+
"",
|
|
70
|
+
f"## Learned Rules (buildlog {datetime.now().strftime('%Y-%m-%d')})",
|
|
71
|
+
"",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
for category, cat_skills in by_category.items():
|
|
75
|
+
title = category_titles.get(category, category.replace("_", " ").title())
|
|
76
|
+
lines.append(f"### {title}")
|
|
77
|
+
lines.append("")
|
|
78
|
+
for skill in cat_skills:
|
|
79
|
+
rule = _to_imperative(skill.rule, skill.confidence)
|
|
80
|
+
lines.append(f"- {rule}")
|
|
81
|
+
lines.append("")
|
|
82
|
+
|
|
83
|
+
content = "\n".join(lines)
|
|
84
|
+
|
|
85
|
+
# Append to file (create if needed)
|
|
86
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
if self.path.exists():
|
|
88
|
+
existing = self.path.read_text()
|
|
89
|
+
self.path.write_text(existing + content)
|
|
90
|
+
else:
|
|
91
|
+
self.path.write_text(content)
|
|
92
|
+
|
|
93
|
+
# Track promoted skill IDs
|
|
94
|
+
track_promoted(new_skills, self.tracking_path)
|
|
95
|
+
|
|
96
|
+
skipped = len(skills) - len(new_skills)
|
|
97
|
+
msg = f"Appended {len(new_skills)} rules to {self.path}"
|
|
98
|
+
if skipped > 0:
|
|
99
|
+
msg += f" ({skipped} already promoted, skipped)"
|
|
100
|
+
return msg
|