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/cli.py
CHANGED
|
@@ -8,7 +8,8 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
import click
|
|
10
10
|
|
|
11
|
-
from buildlog.core import
|
|
11
|
+
from buildlog.core import diff as core_diff
|
|
12
|
+
from buildlog.core import get_rewards, log_reward, promote, reject, status
|
|
12
13
|
from buildlog.distill import CATEGORIES, distill_all, format_output
|
|
13
14
|
from buildlog.skills import format_skills, generate_skills
|
|
14
15
|
from buildlog.stats import calculate_stats, format_dashboard, format_json
|
|
@@ -49,11 +50,18 @@ def main():
|
|
|
49
50
|
|
|
50
51
|
@main.command()
|
|
51
52
|
@click.option("--no-claude-md", is_flag=True, help="Don't update CLAUDE.md")
|
|
52
|
-
|
|
53
|
+
@click.option(
|
|
54
|
+
"--defaults",
|
|
55
|
+
is_flag=True,
|
|
56
|
+
help="Use default values for all prompts (non-interactive)",
|
|
57
|
+
)
|
|
58
|
+
def init(no_claude_md: bool, defaults: bool):
|
|
53
59
|
"""Initialize buildlog in the current directory.
|
|
54
60
|
|
|
55
61
|
Sets up the buildlog/ directory with templates and optionally
|
|
56
62
|
adds instructions to CLAUDE.md.
|
|
63
|
+
|
|
64
|
+
Use --defaults for non-interactive environments (CI, scripts, agents).
|
|
57
65
|
"""
|
|
58
66
|
buildlog_dir = Path("buildlog")
|
|
59
67
|
|
|
@@ -66,46 +74,186 @@ def init(no_claude_md: bool):
|
|
|
66
74
|
if template_dir:
|
|
67
75
|
# Use local template
|
|
68
76
|
click.echo("Initializing buildlog from local template...")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
)
|
|
83
|
-
except subprocess.CalledProcessError:
|
|
84
|
-
click.echo("Failed to initialize buildlog.", err=True)
|
|
85
|
-
raise SystemExit(1)
|
|
77
|
+
subprocess.run(
|
|
78
|
+
[
|
|
79
|
+
sys.executable,
|
|
80
|
+
"-m",
|
|
81
|
+
"copier",
|
|
82
|
+
"copy",
|
|
83
|
+
"--trust",
|
|
84
|
+
*(["--defaults"] if defaults else []),
|
|
85
|
+
*(["--data", "update_claude_md=false"] if no_claude_md else []),
|
|
86
|
+
str(template_dir),
|
|
87
|
+
".",
|
|
88
|
+
],
|
|
89
|
+
)
|
|
86
90
|
else:
|
|
87
91
|
# Fall back to GitHub
|
|
88
92
|
click.echo("Initializing buildlog from GitHub...")
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
93
|
+
subprocess.run(
|
|
94
|
+
[
|
|
95
|
+
sys.executable,
|
|
96
|
+
"-m",
|
|
97
|
+
"copier",
|
|
98
|
+
"copy",
|
|
99
|
+
"--trust",
|
|
100
|
+
*(["--defaults"] if defaults else []),
|
|
101
|
+
*(["--data", "update_claude_md=false"] if no_claude_md else []),
|
|
102
|
+
"gh:Peleke/buildlog-template",
|
|
103
|
+
".",
|
|
104
|
+
],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Verify the buildlog directory was actually created
|
|
108
|
+
if not buildlog_dir.exists():
|
|
109
|
+
click.echo("Failed to initialize buildlog.", err=True)
|
|
110
|
+
raise SystemExit(1)
|
|
111
|
+
|
|
112
|
+
# Update CLAUDE.md if it exists and user didn't opt out
|
|
113
|
+
if not no_claude_md:
|
|
114
|
+
claude_md = Path("CLAUDE.md")
|
|
115
|
+
if claude_md.exists():
|
|
116
|
+
content = claude_md.read_text()
|
|
117
|
+
if "## Build Journal" not in content:
|
|
118
|
+
section = (
|
|
119
|
+
"\n## Build Journal\n\n"
|
|
120
|
+
"After completing significant work (features, debugging sessions, "
|
|
121
|
+
"deployments,\n"
|
|
122
|
+
"2+ hour focused sessions), write a build journal entry.\n\n"
|
|
123
|
+
"**Location:** `buildlog/YYYY-MM-DD-{slug}.md`\n"
|
|
124
|
+
"**Template:** `buildlog/_TEMPLATE.md`\n"
|
|
125
|
+
)
|
|
126
|
+
with open(claude_md, "a") as f:
|
|
127
|
+
f.write(section)
|
|
128
|
+
click.echo("Added Build Journal section to CLAUDE.md")
|
|
106
129
|
|
|
107
130
|
click.echo("\n✓ buildlog initialized!")
|
|
108
|
-
click.echo(
|
|
131
|
+
click.echo()
|
|
132
|
+
click.echo("How it works:")
|
|
133
|
+
click.echo(" 1. Write entries buildlog new my-feature (or --quick)")
|
|
134
|
+
click.echo(" 2. Extract rules buildlog skills")
|
|
135
|
+
click.echo(" 3. Promote to agent buildlog promote <id> --target cursor")
|
|
136
|
+
click.echo(" 4. Measure learning buildlog overview")
|
|
137
|
+
click.echo()
|
|
138
|
+
click.echo(
|
|
139
|
+
"Targets: claude_md, cursor, copilot, windsurf, continue_dev, settings_json, skill"
|
|
140
|
+
)
|
|
141
|
+
click.echo()
|
|
142
|
+
click.echo("Start now: buildlog new my-first-task --quick")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@main.command()
|
|
146
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
147
|
+
def overview(output_json: bool):
|
|
148
|
+
"""Show the full state of your buildlog at a glance.
|
|
149
|
+
|
|
150
|
+
Entries, skills, promoted rules, experiments — everything in one view.
|
|
151
|
+
|
|
152
|
+
Examples:
|
|
153
|
+
|
|
154
|
+
buildlog overview
|
|
155
|
+
buildlog overview --json
|
|
156
|
+
"""
|
|
157
|
+
import json as json_module
|
|
158
|
+
|
|
159
|
+
buildlog_dir = Path("buildlog")
|
|
160
|
+
|
|
161
|
+
if not buildlog_dir.exists():
|
|
162
|
+
click.echo("No buildlog/ directory found. Run 'buildlog init' first.", err=True)
|
|
163
|
+
raise SystemExit(1)
|
|
164
|
+
|
|
165
|
+
# Count entries
|
|
166
|
+
entries = sorted(buildlog_dir.glob("20??-??-??-*.md"))
|
|
167
|
+
|
|
168
|
+
# Try to get skills
|
|
169
|
+
try:
|
|
170
|
+
skill_set = generate_skills(buildlog_dir)
|
|
171
|
+
total_skills = skill_set.total_skills
|
|
172
|
+
by_confidence = {"high": 0, "medium": 0, "low": 0}
|
|
173
|
+
for cat_skills in skill_set.skills.values():
|
|
174
|
+
for s in cat_skills:
|
|
175
|
+
by_confidence[s.confidence] += 1
|
|
176
|
+
except Exception:
|
|
177
|
+
total_skills = 0
|
|
178
|
+
by_confidence = {"high": 0, "medium": 0, "low": 0}
|
|
179
|
+
|
|
180
|
+
# Promoted/rejected counts
|
|
181
|
+
promoted_path = buildlog_dir / ".buildlog" / "promoted.json"
|
|
182
|
+
rejected_path = buildlog_dir / ".buildlog" / "rejected.json"
|
|
183
|
+
promoted_count = 0
|
|
184
|
+
rejected_count = 0
|
|
185
|
+
if promoted_path.exists():
|
|
186
|
+
try:
|
|
187
|
+
data = json_module.loads(promoted_path.read_text())
|
|
188
|
+
promoted_count = len(data.get("skill_ids", []))
|
|
189
|
+
except (json_module.JSONDecodeError, OSError):
|
|
190
|
+
pass
|
|
191
|
+
if rejected_path.exists():
|
|
192
|
+
try:
|
|
193
|
+
data = json_module.loads(rejected_path.read_text())
|
|
194
|
+
rejected_count = len(data.get("skill_ids", []))
|
|
195
|
+
except (json_module.JSONDecodeError, OSError):
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
# Active session?
|
|
199
|
+
active_session_path = buildlog_dir / ".buildlog" / "active_session.json"
|
|
200
|
+
active_session = None
|
|
201
|
+
if active_session_path.exists():
|
|
202
|
+
try:
|
|
203
|
+
active_session = json_module.loads(active_session_path.read_text())
|
|
204
|
+
except (json_module.JSONDecodeError, OSError):
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
# Render targets with files
|
|
208
|
+
from buildlog.render import RENDERERS
|
|
209
|
+
|
|
210
|
+
result = {
|
|
211
|
+
"entries": len(entries),
|
|
212
|
+
"skills": {
|
|
213
|
+
"total": total_skills,
|
|
214
|
+
"by_confidence": by_confidence,
|
|
215
|
+
"promoted": promoted_count,
|
|
216
|
+
"rejected": rejected_count,
|
|
217
|
+
"pending": total_skills - promoted_count - rejected_count,
|
|
218
|
+
},
|
|
219
|
+
"active_session": active_session.get("id") if active_session else None,
|
|
220
|
+
"render_targets": list(RENDERERS.keys()),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if output_json:
|
|
224
|
+
click.echo(json_module.dumps(result, indent=2))
|
|
225
|
+
else:
|
|
226
|
+
click.echo("buildlog overview")
|
|
227
|
+
click.echo("=" * 40)
|
|
228
|
+
click.echo(f" Entries: {len(entries)}")
|
|
229
|
+
click.echo(f" Skills: {total_skills}")
|
|
230
|
+
if total_skills > 0:
|
|
231
|
+
conf_parts = [f"{k}={v}" for k, v in by_confidence.items() if v > 0]
|
|
232
|
+
click.echo(f" confidence: {', '.join(conf_parts)}")
|
|
233
|
+
click.echo(f" Promoted: {promoted_count}")
|
|
234
|
+
click.echo(f" Rejected: {rejected_count}")
|
|
235
|
+
pending = total_skills - promoted_count - rejected_count
|
|
236
|
+
if pending > 0:
|
|
237
|
+
click.echo(f" Pending: {pending}")
|
|
238
|
+
if active_session:
|
|
239
|
+
click.echo(f" Session: {active_session.get('id', '?')} (active)")
|
|
240
|
+
click.echo()
|
|
241
|
+
|
|
242
|
+
if len(entries) == 0:
|
|
243
|
+
click.echo("Get started:")
|
|
244
|
+
click.echo(" buildlog new my-first-task # Full template")
|
|
245
|
+
click.echo(" buildlog new my-first-task --quick # Short template")
|
|
246
|
+
elif total_skills == 0:
|
|
247
|
+
click.echo("Next steps:")
|
|
248
|
+
click.echo(
|
|
249
|
+
" buildlog skills # Extract rules from entries"
|
|
250
|
+
)
|
|
251
|
+
elif promoted_count == 0:
|
|
252
|
+
click.echo("Next steps:")
|
|
253
|
+
click.echo(" buildlog status # See extracted skills")
|
|
254
|
+
click.echo(" buildlog promote <id> --target cursor # Push to your agent")
|
|
255
|
+
else:
|
|
256
|
+
click.echo("Targets: " + ", ".join(RENDERERS.keys()))
|
|
109
257
|
|
|
110
258
|
|
|
111
259
|
@main.command()
|
|
@@ -113,7 +261,12 @@ def init(no_claude_md: bool):
|
|
|
113
261
|
@click.option(
|
|
114
262
|
"--date", "-d", "entry_date", default=None, help="Date for entry (YYYY-MM-DD)"
|
|
115
263
|
)
|
|
116
|
-
|
|
264
|
+
@click.option(
|
|
265
|
+
"--quick",
|
|
266
|
+
is_flag=True,
|
|
267
|
+
help="Use the short template (good for small tasks)",
|
|
268
|
+
)
|
|
269
|
+
def new(slug: str, entry_date: str | None, quick: bool):
|
|
117
270
|
"""Create a new buildlog entry.
|
|
118
271
|
|
|
119
272
|
SLUG is a short identifier for the entry (e.g., 'auth-api', 'bugfix-login').
|
|
@@ -121,10 +274,16 @@ def new(slug: str, entry_date: str | None):
|
|
|
121
274
|
Examples:
|
|
122
275
|
|
|
123
276
|
buildlog new auth-api
|
|
277
|
+
buildlog new bugfix-login --quick
|
|
124
278
|
buildlog new runpod-deploy --date 2026-01-15
|
|
125
279
|
"""
|
|
126
280
|
buildlog_dir = Path("buildlog")
|
|
127
|
-
|
|
281
|
+
template_name = "_TEMPLATE_QUICK.md" if quick else "_TEMPLATE.md"
|
|
282
|
+
template_file = buildlog_dir / template_name
|
|
283
|
+
|
|
284
|
+
# Fall back to full template if quick template doesn't exist
|
|
285
|
+
if quick and not template_file.exists():
|
|
286
|
+
template_file = buildlog_dir / "_TEMPLATE.md"
|
|
128
287
|
|
|
129
288
|
if not buildlog_dir.exists():
|
|
130
289
|
click.echo("No buildlog/ directory found. Run 'buildlog init' first.", err=True)
|
|
@@ -257,7 +416,18 @@ def update():
|
|
|
257
416
|
type=click.Choice(CATEGORIES),
|
|
258
417
|
help="Filter to a specific category",
|
|
259
418
|
)
|
|
260
|
-
|
|
419
|
+
@click.option(
|
|
420
|
+
"--llm",
|
|
421
|
+
is_flag=True,
|
|
422
|
+
help="Use LLM-backed extraction (Ollama/Anthropic, falls back to regex)",
|
|
423
|
+
)
|
|
424
|
+
def distill(
|
|
425
|
+
output: str | None,
|
|
426
|
+
fmt: str,
|
|
427
|
+
since: datetime | None,
|
|
428
|
+
category: str | None,
|
|
429
|
+
llm: bool,
|
|
430
|
+
):
|
|
261
431
|
"""Extract patterns from all buildlog entries.
|
|
262
432
|
|
|
263
433
|
Parses the Improvements section of each buildlog entry and aggregates
|
|
@@ -282,7 +452,9 @@ def distill(output: str | None, fmt: str, since: datetime | None, category: str
|
|
|
282
452
|
|
|
283
453
|
# Run distillation
|
|
284
454
|
try:
|
|
285
|
-
result = distill_all(
|
|
455
|
+
result = distill_all(
|
|
456
|
+
buildlog_dir, since=since_date, category_filter=category, llm=llm
|
|
457
|
+
)
|
|
286
458
|
except Exception as e:
|
|
287
459
|
click.echo(f"Failed to distill entries: {e}", err=True)
|
|
288
460
|
raise SystemExit(1)
|
|
@@ -383,12 +555,18 @@ def stats(output_json: bool, detailed: bool, since_date: str | None):
|
|
|
383
555
|
default=None,
|
|
384
556
|
help="Embedding backend for semantic deduplication",
|
|
385
557
|
)
|
|
558
|
+
@click.option(
|
|
559
|
+
"--llm",
|
|
560
|
+
is_flag=True,
|
|
561
|
+
help="Use LLM for extraction, canonical selection, and scoring (Ollama/Anthropic)",
|
|
562
|
+
)
|
|
386
563
|
def skills(
|
|
387
564
|
output: str | None,
|
|
388
565
|
fmt: str,
|
|
389
566
|
min_frequency: int,
|
|
390
567
|
since: datetime | None,
|
|
391
568
|
embeddings: str | None,
|
|
569
|
+
llm: bool = False,
|
|
392
570
|
):
|
|
393
571
|
"""Generate agent-consumable skills from buildlog patterns.
|
|
394
572
|
|
|
@@ -424,6 +602,7 @@ def skills(
|
|
|
424
602
|
min_frequency=min_frequency,
|
|
425
603
|
since_date=since_date,
|
|
426
604
|
embedding_backend=embeddings,
|
|
605
|
+
llm=llm,
|
|
427
606
|
)
|
|
428
607
|
except ImportError as e:
|
|
429
608
|
click.echo(f"Missing dependency: {e}", err=True)
|
|
@@ -584,6 +763,219 @@ def rewards(limit: int | None, output_json: bool):
|
|
|
584
763
|
click.echo("Log your first with: buildlog reward accepted")
|
|
585
764
|
|
|
586
765
|
|
|
766
|
+
# -----------------------------------------------------------------------------
|
|
767
|
+
# Skill Management Commands (status, promote, reject, diff)
|
|
768
|
+
# -----------------------------------------------------------------------------
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
@main.command()
|
|
772
|
+
@click.option(
|
|
773
|
+
"--min-confidence",
|
|
774
|
+
type=click.Choice(["low", "medium", "high"]),
|
|
775
|
+
default="low",
|
|
776
|
+
help="Minimum confidence level to include",
|
|
777
|
+
)
|
|
778
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
779
|
+
def status_cmd(min_confidence: str, output_json: bool):
|
|
780
|
+
"""Show extracted skills by category and confidence.
|
|
781
|
+
|
|
782
|
+
Displays all skills extracted from buildlog entries, grouped by category,
|
|
783
|
+
with confidence levels and promotion status.
|
|
784
|
+
|
|
785
|
+
Examples:
|
|
786
|
+
|
|
787
|
+
buildlog status
|
|
788
|
+
buildlog status --min-confidence medium
|
|
789
|
+
buildlog status --json
|
|
790
|
+
"""
|
|
791
|
+
import json as json_module
|
|
792
|
+
from dataclasses import asdict
|
|
793
|
+
|
|
794
|
+
buildlog_dir = Path("buildlog")
|
|
795
|
+
|
|
796
|
+
if not buildlog_dir.exists():
|
|
797
|
+
click.echo("No buildlog/ directory found. Run 'buildlog init' first.", err=True)
|
|
798
|
+
raise SystemExit(1)
|
|
799
|
+
|
|
800
|
+
result = status(buildlog_dir, min_confidence=min_confidence) # type: ignore[arg-type]
|
|
801
|
+
|
|
802
|
+
if result.error:
|
|
803
|
+
click.echo(f"Error: {result.error}", err=True)
|
|
804
|
+
raise SystemExit(1)
|
|
805
|
+
|
|
806
|
+
if output_json:
|
|
807
|
+
click.echo(json_module.dumps(asdict(result), indent=2))
|
|
808
|
+
else:
|
|
809
|
+
click.echo(
|
|
810
|
+
f"Skills: {result.total_skills} total from {result.total_entries} entries"
|
|
811
|
+
)
|
|
812
|
+
conf_str = ", ".join(
|
|
813
|
+
f"{k}={v}" for k, v in result.by_confidence.items() if v > 0
|
|
814
|
+
)
|
|
815
|
+
click.echo(f" By confidence: {conf_str}")
|
|
816
|
+
click.echo()
|
|
817
|
+
for category, skills in result.skills.items():
|
|
818
|
+
if not skills:
|
|
819
|
+
continue
|
|
820
|
+
click.echo(f" {category} ({len(skills)})")
|
|
821
|
+
for s in skills:
|
|
822
|
+
conf = s.get("confidence", "?")
|
|
823
|
+
click.echo(f" [{conf}] {s['id']}: {s['rule'][:70]}")
|
|
824
|
+
if result.promotable_ids:
|
|
825
|
+
click.echo(f"\nPromotable: {', '.join(result.promotable_ids)}")
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
# Register with the name "status" (avoiding collision with Python builtin)
|
|
829
|
+
status_cmd.name = "status"
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
@main.command()
|
|
833
|
+
@click.argument("skill_ids", nargs=-1, required=True)
|
|
834
|
+
@click.option(
|
|
835
|
+
"--target",
|
|
836
|
+
type=click.Choice(
|
|
837
|
+
[
|
|
838
|
+
"claude_md",
|
|
839
|
+
"settings_json",
|
|
840
|
+
"skill",
|
|
841
|
+
"cursor",
|
|
842
|
+
"copilot",
|
|
843
|
+
"windsurf",
|
|
844
|
+
"continue_dev",
|
|
845
|
+
]
|
|
846
|
+
),
|
|
847
|
+
default="claude_md",
|
|
848
|
+
help="Where to write promoted rules",
|
|
849
|
+
)
|
|
850
|
+
@click.option(
|
|
851
|
+
"--target-path", type=click.Path(), help="Custom path for the target file"
|
|
852
|
+
)
|
|
853
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
854
|
+
def promote_cmd(
|
|
855
|
+
skill_ids: tuple[str, ...], target: str, target_path: str | None, output_json: bool
|
|
856
|
+
):
|
|
857
|
+
"""Promote skills to agent rules.
|
|
858
|
+
|
|
859
|
+
Surface high-confidence skills to your agent via CLAUDE.md, settings.json,
|
|
860
|
+
or Agent Skills.
|
|
861
|
+
|
|
862
|
+
Examples:
|
|
863
|
+
|
|
864
|
+
buildlog promote arch-b0fcb62a1e
|
|
865
|
+
buildlog promote arch-123 wf-456 --target skill
|
|
866
|
+
buildlog promote arch-123 --target settings_json --target-path .claude/settings.json
|
|
867
|
+
"""
|
|
868
|
+
import json as json_module
|
|
869
|
+
from dataclasses import asdict
|
|
870
|
+
|
|
871
|
+
buildlog_dir = Path("buildlog")
|
|
872
|
+
|
|
873
|
+
if not buildlog_dir.exists():
|
|
874
|
+
click.echo("No buildlog/ directory found. Run 'buildlog init' first.", err=True)
|
|
875
|
+
raise SystemExit(1)
|
|
876
|
+
|
|
877
|
+
result = promote(
|
|
878
|
+
buildlog_dir,
|
|
879
|
+
skill_ids=list(skill_ids),
|
|
880
|
+
target=target, # type: ignore[arg-type]
|
|
881
|
+
target_path=Path(target_path) if target_path else None,
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
if result.error:
|
|
885
|
+
click.echo(f"Error: {result.error}", err=True)
|
|
886
|
+
raise SystemExit(1)
|
|
887
|
+
|
|
888
|
+
if output_json:
|
|
889
|
+
click.echo(json_module.dumps(asdict(result), indent=2))
|
|
890
|
+
else:
|
|
891
|
+
click.echo(f"✓ {result.message}")
|
|
892
|
+
if result.not_found_ids:
|
|
893
|
+
click.echo(f" Not found: {', '.join(result.not_found_ids)}")
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
promote_cmd.name = "promote"
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
@main.command("reject")
|
|
900
|
+
@click.argument("skill_ids", nargs=-1, required=True)
|
|
901
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
902
|
+
def reject_cmd(skill_ids: tuple[str, ...], output_json: bool):
|
|
903
|
+
"""Mark skills as rejected (false positives).
|
|
904
|
+
|
|
905
|
+
Rejected skills won't be suggested for promotion again.
|
|
906
|
+
|
|
907
|
+
Examples:
|
|
908
|
+
|
|
909
|
+
buildlog reject arch-b0fcb62a1e
|
|
910
|
+
buildlog reject dk-123 wf-456
|
|
911
|
+
"""
|
|
912
|
+
import json as json_module
|
|
913
|
+
from dataclasses import asdict
|
|
914
|
+
|
|
915
|
+
buildlog_dir = Path("buildlog")
|
|
916
|
+
|
|
917
|
+
if not buildlog_dir.exists():
|
|
918
|
+
click.echo("No buildlog/ directory found. Run 'buildlog init' first.", err=True)
|
|
919
|
+
raise SystemExit(1)
|
|
920
|
+
|
|
921
|
+
result = reject(buildlog_dir, skill_ids=list(skill_ids))
|
|
922
|
+
|
|
923
|
+
if result.error:
|
|
924
|
+
click.echo(f"Error: {result.error}", err=True)
|
|
925
|
+
raise SystemExit(1)
|
|
926
|
+
|
|
927
|
+
if output_json:
|
|
928
|
+
click.echo(json_module.dumps(asdict(result), indent=2))
|
|
929
|
+
else:
|
|
930
|
+
click.echo(f"✓ Rejected {len(result.rejected_ids)} skills")
|
|
931
|
+
click.echo(f" Total rejected: {result.total_rejected}")
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
@main.command("diff")
|
|
935
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
936
|
+
def diff_cmd(output_json: bool):
|
|
937
|
+
"""Show skills pending review (not yet promoted or rejected).
|
|
938
|
+
|
|
939
|
+
Useful for seeing what's new since the last time you reviewed skills.
|
|
940
|
+
|
|
941
|
+
Examples:
|
|
942
|
+
|
|
943
|
+
buildlog diff
|
|
944
|
+
buildlog diff --json
|
|
945
|
+
"""
|
|
946
|
+
import json as json_module
|
|
947
|
+
from dataclasses import asdict
|
|
948
|
+
|
|
949
|
+
buildlog_dir = Path("buildlog")
|
|
950
|
+
|
|
951
|
+
if not buildlog_dir.exists():
|
|
952
|
+
click.echo("No buildlog/ directory found. Run 'buildlog init' first.", err=True)
|
|
953
|
+
raise SystemExit(1)
|
|
954
|
+
|
|
955
|
+
result = core_diff(buildlog_dir)
|
|
956
|
+
|
|
957
|
+
if result.error:
|
|
958
|
+
click.echo(f"Error: {result.error}", err=True)
|
|
959
|
+
raise SystemExit(1)
|
|
960
|
+
|
|
961
|
+
if output_json:
|
|
962
|
+
click.echo(json_module.dumps(asdict(result), indent=2))
|
|
963
|
+
else:
|
|
964
|
+
click.echo(
|
|
965
|
+
f"Pending: {result.total_pending} | "
|
|
966
|
+
f"Promoted: {result.already_promoted} | "
|
|
967
|
+
f"Rejected: {result.already_rejected}"
|
|
968
|
+
)
|
|
969
|
+
click.echo()
|
|
970
|
+
for category, skills in result.pending.items():
|
|
971
|
+
if not skills:
|
|
972
|
+
continue
|
|
973
|
+
click.echo(f" {category} ({len(skills)})")
|
|
974
|
+
for s in skills:
|
|
975
|
+
conf = s.get("confidence", "?")
|
|
976
|
+
click.echo(f" [{conf}] {s['id']}: {s['rule'][:70]}")
|
|
977
|
+
|
|
978
|
+
|
|
587
979
|
# -----------------------------------------------------------------------------
|
|
588
980
|
# Experiment Commands (Session Tracking for Issue #21)
|
|
589
981
|
# -----------------------------------------------------------------------------
|
|
@@ -708,7 +1100,7 @@ def experiment_end(
|
|
|
708
1100
|
|
|
709
1101
|
@experiment.command("log-mistake")
|
|
710
1102
|
@click.option(
|
|
711
|
-
"--class",
|
|
1103
|
+
"--error-class",
|
|
712
1104
|
"error_class",
|
|
713
1105
|
required=True,
|
|
714
1106
|
help="Error class (e.g., 'missing_test', 'validation_boundary')",
|
|
@@ -739,8 +1131,8 @@ def experiment_log_mistake(
|
|
|
739
1131
|
|
|
740
1132
|
Examples:
|
|
741
1133
|
|
|
742
|
-
buildlog experiment log-mistake --class missing_test -d "Forgot tests"
|
|
743
|
-
buildlog experiment log-mistake --class validation -d "No max length" -r val-123
|
|
1134
|
+
buildlog experiment log-mistake --error-class missing_test -d "Forgot tests"
|
|
1135
|
+
buildlog experiment log-mistake --error-class validation -d "No max length" -r val-123
|
|
744
1136
|
"""
|
|
745
1137
|
import json as json_module
|
|
746
1138
|
from dataclasses import asdict
|
|
@@ -1264,5 +1656,158 @@ def gauntlet_learn(issues_file: str, source: str | None, output_json: bool):
|
|
|
1264
1656
|
click.echo(f" Total processed: {result.total_issues_processed}")
|
|
1265
1657
|
|
|
1266
1658
|
|
|
1659
|
+
@gauntlet.command("loop")
|
|
1660
|
+
@click.argument("target", type=click.Path(exists=True))
|
|
1661
|
+
@click.option(
|
|
1662
|
+
"--persona",
|
|
1663
|
+
"-p",
|
|
1664
|
+
multiple=True,
|
|
1665
|
+
help="Personas to run (default: all)",
|
|
1666
|
+
)
|
|
1667
|
+
@click.option(
|
|
1668
|
+
"--max-iterations",
|
|
1669
|
+
"-n",
|
|
1670
|
+
default=10,
|
|
1671
|
+
help="Maximum iterations to prevent infinite loops (default: 10)",
|
|
1672
|
+
)
|
|
1673
|
+
@click.option(
|
|
1674
|
+
"--stop-at",
|
|
1675
|
+
type=click.Choice(["criticals", "majors", "minors"]),
|
|
1676
|
+
default="minors",
|
|
1677
|
+
help="Stop after clearing this severity level (default: minors)",
|
|
1678
|
+
)
|
|
1679
|
+
@click.option(
|
|
1680
|
+
"--auto-gh-issues",
|
|
1681
|
+
is_flag=True,
|
|
1682
|
+
help="Create GitHub issues for remaining items when accepting risk",
|
|
1683
|
+
)
|
|
1684
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
1685
|
+
def gauntlet_loop(
|
|
1686
|
+
target: str,
|
|
1687
|
+
persona: tuple[str, ...],
|
|
1688
|
+
max_iterations: int,
|
|
1689
|
+
stop_at: str,
|
|
1690
|
+
auto_gh_issues: bool,
|
|
1691
|
+
output_json: bool,
|
|
1692
|
+
):
|
|
1693
|
+
"""Run the gauntlet loop: review, fix, repeat until clean.
|
|
1694
|
+
|
|
1695
|
+
This command orchestrates the gauntlet loop workflow:
|
|
1696
|
+
|
|
1697
|
+
1. Generate review prompt for target code
|
|
1698
|
+
2. Process issues and determine action
|
|
1699
|
+
3. On criticals: output fix instructions, expect re-run
|
|
1700
|
+
4. On majors only: checkpoint (ask to continue)
|
|
1701
|
+
5. On minors only: checkpoint (accept risk?)
|
|
1702
|
+
6. Optionally create GitHub issues for remaining items
|
|
1703
|
+
|
|
1704
|
+
The loop is designed to be run interactively with an agent
|
|
1705
|
+
(Claude Code, Cursor, etc.) that does the actual fixing.
|
|
1706
|
+
|
|
1707
|
+
Examples:
|
|
1708
|
+
|
|
1709
|
+
buildlog gauntlet loop src/
|
|
1710
|
+
buildlog gauntlet loop tests/ --stop-at majors
|
|
1711
|
+
buildlog gauntlet loop . --auto-gh-issues
|
|
1712
|
+
"""
|
|
1713
|
+
import json as json_module
|
|
1714
|
+
|
|
1715
|
+
from buildlog.seeds import get_default_seeds_dir, load_all_seeds
|
|
1716
|
+
|
|
1717
|
+
# Find seeds directory
|
|
1718
|
+
seeds_dir = get_default_seeds_dir()
|
|
1719
|
+
|
|
1720
|
+
if seeds_dir is None:
|
|
1721
|
+
click.echo("No seed files found.", err=True)
|
|
1722
|
+
raise SystemExit(1)
|
|
1723
|
+
|
|
1724
|
+
seeds = load_all_seeds(seeds_dir)
|
|
1725
|
+
|
|
1726
|
+
if not seeds:
|
|
1727
|
+
click.echo("No seed files found in directory.", err=True)
|
|
1728
|
+
raise SystemExit(1)
|
|
1729
|
+
|
|
1730
|
+
# Filter personas
|
|
1731
|
+
if persona:
|
|
1732
|
+
seeds = {k: v for k, v in seeds.items() if k in persona}
|
|
1733
|
+
if not seeds:
|
|
1734
|
+
click.echo(f"No matching personas: {', '.join(persona)}", err=True)
|
|
1735
|
+
raise SystemExit(1)
|
|
1736
|
+
|
|
1737
|
+
target_path = Path(target)
|
|
1738
|
+
|
|
1739
|
+
# Generate persona rules summary
|
|
1740
|
+
rules_by_persona: dict[str, list[dict[str, str]]] = {}
|
|
1741
|
+
for name, sf in seeds.items():
|
|
1742
|
+
rules_by_persona[name] = [
|
|
1743
|
+
{"rule": r.rule, "antipattern": r.antipattern, "category": r.category}
|
|
1744
|
+
for r in sf.rules
|
|
1745
|
+
]
|
|
1746
|
+
|
|
1747
|
+
# Loop instructions
|
|
1748
|
+
instructions = [
|
|
1749
|
+
"1. Review the target code using the rules from each persona",
|
|
1750
|
+
"2. Report all violations as JSON issues with: severity, category, description, rule_learned, location",
|
|
1751
|
+
"3. Call `buildlog_gauntlet_issues` with the issues list to determine next action",
|
|
1752
|
+
"4. If action='fix_criticals': Fix critical+major issues, then re-run gauntlet",
|
|
1753
|
+
"5. If action='checkpoint_majors': Ask user whether to continue fixing majors",
|
|
1754
|
+
"6. If action='checkpoint_minors': Ask user whether to accept risk or continue",
|
|
1755
|
+
"7. If user accepts risk and --auto-gh-issues: Call `buildlog_gauntlet_accept_risk` with remaining issues",
|
|
1756
|
+
"8. Repeat until action='clean' or max_iterations reached",
|
|
1757
|
+
]
|
|
1758
|
+
|
|
1759
|
+
# Expected issue format
|
|
1760
|
+
issue_format = {
|
|
1761
|
+
"severity": "critical|major|minor|nitpick",
|
|
1762
|
+
"category": "security|testing|architectural|workflow|...",
|
|
1763
|
+
"description": "Concrete description of what's wrong",
|
|
1764
|
+
"rule_learned": "Generalizable rule for the future",
|
|
1765
|
+
"location": "file:line (optional)",
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
# Build the loop output
|
|
1769
|
+
output = {
|
|
1770
|
+
"command": "gauntlet_loop",
|
|
1771
|
+
"target": str(target_path),
|
|
1772
|
+
"personas": list(seeds.keys()),
|
|
1773
|
+
"max_iterations": max_iterations,
|
|
1774
|
+
"stop_at": stop_at,
|
|
1775
|
+
"auto_gh_issues": auto_gh_issues,
|
|
1776
|
+
"rules_by_persona": rules_by_persona,
|
|
1777
|
+
"instructions": instructions,
|
|
1778
|
+
"issue_format": issue_format,
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
if output_json:
|
|
1782
|
+
click.echo(json_module.dumps(output, indent=2))
|
|
1783
|
+
else:
|
|
1784
|
+
# Human-readable output
|
|
1785
|
+
click.echo("=" * 60)
|
|
1786
|
+
click.echo("GAUNTLET LOOP")
|
|
1787
|
+
click.echo("=" * 60)
|
|
1788
|
+
click.echo(f"\nTarget: {target_path}")
|
|
1789
|
+
click.echo(f"Personas: {', '.join(seeds.keys())}")
|
|
1790
|
+
click.echo(f"Max iterations: {max_iterations}")
|
|
1791
|
+
click.echo(f"Stop at: {stop_at}")
|
|
1792
|
+
click.echo(f"Auto GH issues: {auto_gh_issues}")
|
|
1793
|
+
|
|
1794
|
+
click.echo("\n--- RULES ---")
|
|
1795
|
+
for name, rules in rules_by_persona.items():
|
|
1796
|
+
click.echo(f"\n## {name.replace('_', ' ').title()}")
|
|
1797
|
+
for r in rules:
|
|
1798
|
+
click.echo(f" • {r['rule']}")
|
|
1799
|
+
|
|
1800
|
+
click.echo("\n--- LOOP WORKFLOW ---")
|
|
1801
|
+
for instruction in instructions:
|
|
1802
|
+
click.echo(f" {instruction}")
|
|
1803
|
+
|
|
1804
|
+
click.echo("\n--- ISSUE FORMAT ---")
|
|
1805
|
+
click.echo(json_module.dumps(issue_format, indent=2))
|
|
1806
|
+
|
|
1807
|
+
click.echo("\n" + "=" * 60)
|
|
1808
|
+
click.echo("Ready. Run gauntlet review and process issues.")
|
|
1809
|
+
click.echo("=" * 60)
|
|
1810
|
+
|
|
1811
|
+
|
|
1267
1812
|
if __name__ == "__main__":
|
|
1268
1813
|
main()
|