muscle-memory 0.2.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.
- muscle_memory/__init__.py +24 -0
- muscle_memory/__main__.py +6 -0
- muscle_memory/bootstrap.py +171 -0
- muscle_memory/cli.py +717 -0
- muscle_memory/config.py +165 -0
- muscle_memory/db.py +544 -0
- muscle_memory/dedup.py +153 -0
- muscle_memory/embeddings.py +136 -0
- muscle_memory/extractor.py +211 -0
- muscle_memory/hooks/__init__.py +5 -0
- muscle_memory/hooks/install.py +182 -0
- muscle_memory/hooks/stop.py +258 -0
- muscle_memory/hooks/user_prompt.py +238 -0
- muscle_memory/llm.py +278 -0
- muscle_memory/models.py +189 -0
- muscle_memory/outcomes.py +214 -0
- muscle_memory/prompts/__init__.py +1 -0
- muscle_memory/prompts/extract.md +113 -0
- muscle_memory/prompts/refine_gradient.md +87 -0
- muscle_memory/prompts/refine_judge.md +70 -0
- muscle_memory/prompts/refine_rewrite.md +53 -0
- muscle_memory/refine.py +535 -0
- muscle_memory/retriever.py +106 -0
- muscle_memory/scorer.py +94 -0
- muscle_memory-0.2.0.dist-info/METADATA +174 -0
- muscle_memory-0.2.0.dist-info/RECORD +29 -0
- muscle_memory-0.2.0.dist-info/WHEEL +4 -0
- muscle_memory-0.2.0.dist-info/entry_points.txt +3 -0
- muscle_memory-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""muscle-memory: procedural memory for coding agents."""
|
|
2
|
+
|
|
3
|
+
from muscle_memory.models import (
|
|
4
|
+
Episode,
|
|
5
|
+
Maturity,
|
|
6
|
+
Outcome,
|
|
7
|
+
Scope,
|
|
8
|
+
Skill,
|
|
9
|
+
ToolCall,
|
|
10
|
+
Trajectory,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__version__ = "0.2.0"
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Episode",
|
|
17
|
+
"Maturity",
|
|
18
|
+
"Outcome",
|
|
19
|
+
"Scope",
|
|
20
|
+
"Skill",
|
|
21
|
+
"ToolCall",
|
|
22
|
+
"Trajectory",
|
|
23
|
+
"__version__",
|
|
24
|
+
]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Seed the skill store from existing Claude Code session history.
|
|
2
|
+
|
|
3
|
+
Claude Code stores session JSONLs under `~/.claude/projects/<encoded-path>/`.
|
|
4
|
+
We walk those files, parse each into a Trajectory, infer outcomes,
|
|
5
|
+
and run the extractor to produce candidate Skills.
|
|
6
|
+
|
|
7
|
+
This lets new users get value from muscle-memory on day one.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import UTC, datetime, timedelta
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from muscle_memory.config import Config
|
|
17
|
+
from muscle_memory.db import Store
|
|
18
|
+
from muscle_memory.dedup import add_skill_with_dedup
|
|
19
|
+
from muscle_memory.embeddings import Embedder
|
|
20
|
+
from muscle_memory.extractor import ExtractionError, Extractor
|
|
21
|
+
from muscle_memory.hooks.stop import parse_transcript
|
|
22
|
+
from muscle_memory.llm import LLM
|
|
23
|
+
from muscle_memory.models import Episode, Skill
|
|
24
|
+
from muscle_memory.outcomes import infer_outcome
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class BootstrapReport:
|
|
29
|
+
sessions_considered: int = 0
|
|
30
|
+
sessions_parsed: int = 0
|
|
31
|
+
episodes_added: int = 0
|
|
32
|
+
skills_extracted: int = 0
|
|
33
|
+
errors: list[str] = field(default_factory=list)
|
|
34
|
+
aborted_reason: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def find_session_files(
|
|
38
|
+
*,
|
|
39
|
+
project_path: Path | None = None,
|
|
40
|
+
since: datetime | None = None,
|
|
41
|
+
claude_home: Path | None = None,
|
|
42
|
+
) -> list[Path]:
|
|
43
|
+
"""Return JSONL session files, optionally scoped to a project and time window.
|
|
44
|
+
|
|
45
|
+
Claude Code encodes project paths in the directory name by replacing
|
|
46
|
+
slashes with dashes. We detect that encoding if `project_path` is
|
|
47
|
+
given, otherwise we pick up every project.
|
|
48
|
+
"""
|
|
49
|
+
root = claude_home or (Path.home() / ".claude" / "projects")
|
|
50
|
+
if not root.exists():
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
if project_path is not None:
|
|
54
|
+
encoded = str(project_path.resolve()).replace("/", "-")
|
|
55
|
+
if encoded.startswith("-"):
|
|
56
|
+
encoded = encoded # already starts with - for absolute paths
|
|
57
|
+
candidates = [root / encoded]
|
|
58
|
+
# also try with leading dash (some versions include it)
|
|
59
|
+
alt = root / ("-" + encoded if not encoded.startswith("-") else encoded[1:])
|
|
60
|
+
candidates.append(alt)
|
|
61
|
+
dirs = [p for p in candidates if p.exists() and p.is_dir()]
|
|
62
|
+
else:
|
|
63
|
+
dirs = [p for p in root.iterdir() if p.is_dir()]
|
|
64
|
+
|
|
65
|
+
files: list[Path] = []
|
|
66
|
+
for d in dirs:
|
|
67
|
+
for jsonl in d.glob("*.jsonl"):
|
|
68
|
+
if since is not None:
|
|
69
|
+
mtime = datetime.fromtimestamp(jsonl.stat().st_mtime, tz=UTC)
|
|
70
|
+
if mtime < since:
|
|
71
|
+
continue
|
|
72
|
+
files.append(jsonl)
|
|
73
|
+
|
|
74
|
+
files.sort(key=lambda p: p.stat().st_mtime)
|
|
75
|
+
return files
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def bootstrap(
|
|
79
|
+
*,
|
|
80
|
+
config: Config,
|
|
81
|
+
store: Store,
|
|
82
|
+
embedder: Embedder,
|
|
83
|
+
llm: LLM,
|
|
84
|
+
days: int | None = 30,
|
|
85
|
+
project_only: bool = True,
|
|
86
|
+
max_sessions: int = 200,
|
|
87
|
+
) -> BootstrapReport:
|
|
88
|
+
report = BootstrapReport()
|
|
89
|
+
|
|
90
|
+
since: datetime | None = None
|
|
91
|
+
if days is not None:
|
|
92
|
+
since = datetime.now(UTC) - timedelta(days=days)
|
|
93
|
+
|
|
94
|
+
project_path = config.project_root if project_only else None
|
|
95
|
+
files = find_session_files(
|
|
96
|
+
project_path=project_path,
|
|
97
|
+
since=since,
|
|
98
|
+
)
|
|
99
|
+
files = files[-max_sessions:]
|
|
100
|
+
|
|
101
|
+
extractor = Extractor(llm, config)
|
|
102
|
+
|
|
103
|
+
for path in files:
|
|
104
|
+
report.sessions_considered += 1
|
|
105
|
+
try:
|
|
106
|
+
trajectory = parse_transcript(path)
|
|
107
|
+
except Exception as e: # noqa: BLE001
|
|
108
|
+
report.errors.append(f"{path.name}: parse failed: {e}")
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
if not trajectory.tool_calls:
|
|
112
|
+
continue
|
|
113
|
+
report.sessions_parsed += 1
|
|
114
|
+
|
|
115
|
+
# Bootstrap has no activation sidecars — it's processing historical
|
|
116
|
+
# sessions from before muscle-memory was installed. Falls back to
|
|
117
|
+
# keyword-only heuristic.
|
|
118
|
+
signal = infer_outcome(trajectory)
|
|
119
|
+
|
|
120
|
+
episode = Episode(
|
|
121
|
+
session_id=path.stem,
|
|
122
|
+
user_prompt=trajectory.user_prompt or "(bootstrap)",
|
|
123
|
+
trajectory=trajectory,
|
|
124
|
+
outcome=signal.outcome,
|
|
125
|
+
reward=signal.reward,
|
|
126
|
+
project_path=str(config.project_root) if config.project_root else None,
|
|
127
|
+
)
|
|
128
|
+
try:
|
|
129
|
+
store.add_episode(episode)
|
|
130
|
+
report.episodes_added += 1
|
|
131
|
+
except Exception as e: # noqa: BLE001
|
|
132
|
+
report.errors.append(f"{path.name}: episode insert failed: {e}")
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
skills = extractor.extract(episode)
|
|
137
|
+
except ExtractionError as e:
|
|
138
|
+
# Surface LLM errors loudly. If the first one fails for
|
|
139
|
+
# auth/credit/quota reasons, abort — every subsequent call
|
|
140
|
+
# will fail the same way and waste time.
|
|
141
|
+
msg = str(e)
|
|
142
|
+
report.errors.append(f"{path.name}: extraction failed: {msg}")
|
|
143
|
+
if _looks_fatal(msg) or report.skills_extracted == 0:
|
|
144
|
+
report.aborted_reason = msg
|
|
145
|
+
return report
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
for skill in skills:
|
|
149
|
+
added, _existing = add_skill_with_dedup(store, embedder, skill)
|
|
150
|
+
if added:
|
|
151
|
+
report.skills_extracted += 1
|
|
152
|
+
|
|
153
|
+
return report
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _looks_fatal(msg: str) -> bool:
|
|
157
|
+
"""Heuristic: does this LLM error look like it'll keep happening?"""
|
|
158
|
+
low = msg.lower()
|
|
159
|
+
fatal_markers = (
|
|
160
|
+
"credit",
|
|
161
|
+
"quota",
|
|
162
|
+
"billing",
|
|
163
|
+
"unauthorized",
|
|
164
|
+
"authentication",
|
|
165
|
+
"api key",
|
|
166
|
+
"not found", # wrong model name
|
|
167
|
+
"invalid_request_error",
|
|
168
|
+
)
|
|
169
|
+
return any(m in low for m in fatal_markers)
|
|
170
|
+
|
|
171
|
+
|