loki-mode 6.77.2 → 6.80.0

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.
@@ -0,0 +1,220 @@
1
+ """Memory bridge for Magic Modules.
2
+
3
+ Feeds component-generation outcomes into Loki's memory system so agents
4
+ benefit from prior work across iterations and projects:
5
+
6
+ - Episodic memory: component generation events with debate results
7
+ - Semantic memory: stable tag clusters from repeated successful generations
8
+ - Retrieval: pull similar past generations during REASON phase
9
+
10
+ The memory system may or may not be available; every function here degrades
11
+ gracefully if memory imports fail.
12
+ """
13
+
14
+ import json
15
+ import uuid
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+ from typing import Optional
19
+
20
+
21
+ def _try_import_memory():
22
+ """Attempt to import the memory subsystem. Returns (engine_cls, trace_cls, pattern_cls, err)."""
23
+ try:
24
+ from memory.engine import MemoryEngine
25
+ from memory.schemas import EpisodeTrace, SemanticPattern
26
+ return MemoryEngine, EpisodeTrace, SemanticPattern, None
27
+ except Exception as exc:
28
+ return None, None, None, str(exc)
29
+
30
+
31
+ def _load_registry(project_dir: str) -> Optional[dict]:
32
+ reg = Path(project_dir) / ".loki" / "magic" / "registry.json"
33
+ if not reg.exists():
34
+ return None
35
+ try:
36
+ return json.loads(reg.read_text())
37
+ except Exception:
38
+ return None
39
+
40
+
41
+ def _engine_for(project_dir: str, MemoryEngine):
42
+ """Instantiate MemoryEngine rooted at project_dir/.loki/memory."""
43
+ base = str(Path(project_dir) / ".loki" / "memory")
44
+ return MemoryEngine(base_path=base)
45
+
46
+
47
+ def capture_component_generation(
48
+ project_dir: str,
49
+ component_name: str,
50
+ spec_path: str,
51
+ targets: list,
52
+ debate_result: Optional[dict] = None,
53
+ iteration: int = 0,
54
+ duration_seconds: float = 0,
55
+ ) -> dict:
56
+ """Record an episode for a single component generation event.
57
+
58
+ Returns summary dict {stored: bool, reason: str, episode_id: str?}.
59
+ """
60
+ ME, EpisodeTrace, _SemPat, err = _try_import_memory()
61
+ if ME is None:
62
+ return {"stored": False, "reason": f"memory unavailable: {err}"}
63
+
64
+ try:
65
+ engine = _engine_for(project_dir, ME)
66
+ except Exception as exc:
67
+ return {"stored": False, "reason": f"engine init failed: {exc}"}
68
+
69
+ # Build episode content
70
+ debate_summary = ""
71
+ if debate_result:
72
+ c_count = len(debate_result.get("critiques", []))
73
+ consensus = debate_result.get("consensus", False)
74
+ blocks = len(debate_result.get("blocks", []))
75
+ debate_summary = (
76
+ f" Debate: {c_count} personas, consensus={consensus}, "
77
+ f"blocks={blocks}"
78
+ )
79
+ goal = (
80
+ f"Generate magic component '{component_name}' "
81
+ f"(targets={','.join(targets) if targets else 'unknown'})."
82
+ f"{debate_summary}"
83
+ )
84
+ outcome = "success"
85
+ if debate_result and debate_result.get("blocks"):
86
+ outcome = "failure"
87
+
88
+ try:
89
+ ep_id = f"magic-{component_name.lower()}-{uuid.uuid4().hex[:8]}"
90
+ trace = EpisodeTrace(
91
+ id=ep_id,
92
+ task_id=f"magic-gen-{component_name}",
93
+ timestamp=datetime.now(timezone.utc),
94
+ duration_seconds=int(max(0, duration_seconds)),
95
+ agent="magic",
96
+ phase="ACT",
97
+ goal=goal,
98
+ outcome=outcome,
99
+ artifacts_produced=[spec_path] if spec_path else [],
100
+ files_modified=[spec_path] if spec_path else [],
101
+ importance=0.6 if outcome == "success" else 0.8,
102
+ )
103
+ stored_id = engine.store_episode(trace)
104
+ return {"stored": True, "reason": "episode saved", "episode_id": stored_id}
105
+ except Exception as exc:
106
+ return {"stored": False, "reason": f"store failed: {exc}"}
107
+
108
+
109
+ def capture_iteration_compound(project_dir: str, iteration: int = 0) -> dict:
110
+ """Called in COMPOUND phase: aggregate component stats and record semantic pattern.
111
+
112
+ Reads registry, computes per-tag pass rates, and stores as semantic memory
113
+ for tag clusters that hit the stability threshold.
114
+ """
115
+ data = _load_registry(project_dir)
116
+ if not data:
117
+ return {"recorded": False, "reason": "no registry"}
118
+
119
+ components = data.get("components", [])
120
+ if isinstance(components, dict):
121
+ components = [{"name": k, **(v or {})} for k, v in components.items()]
122
+
123
+ if not components:
124
+ return {"recorded": False, "reason": "no components"}
125
+
126
+ # Build per-tag pass rate
127
+ tag_stats = {}
128
+ for c in components:
129
+ tags = c.get("tags", []) or []
130
+ debate_passed = bool(c.get("debate_passed"))
131
+ for tag in tags:
132
+ bucket = tag_stats.setdefault(tag, {"total": 0, "passed": 0})
133
+ bucket["total"] += 1
134
+ if debate_passed:
135
+ bucket["passed"] += 1
136
+
137
+ # Derive patterns: tags with >=3 components and >=80% pass rate are "stable"
138
+ stable_tags = [
139
+ t for t, s in tag_stats.items()
140
+ if s["total"] >= 3 and s["passed"] / s["total"] >= 0.8
141
+ ]
142
+
143
+ ME, _Ep, SemanticPattern, err = _try_import_memory()
144
+ if ME is None:
145
+ return {
146
+ "recorded": False,
147
+ "reason": f"memory unavailable: {err}",
148
+ "stable_tags": stable_tags,
149
+ "tag_stats": tag_stats,
150
+ }
151
+
152
+ try:
153
+ engine = _engine_for(project_dir, ME)
154
+ except Exception as exc:
155
+ return {"recorded": False, "reason": f"engine init failed: {exc}"}
156
+
157
+ try:
158
+ stored = []
159
+ for tag in stable_tags:
160
+ s = tag_stats[tag]
161
+ pct = s["passed"] / s["total"]
162
+ pattern = SemanticPattern(
163
+ id=f"sem-magic-{tag}-{uuid.uuid4().hex[:6]}",
164
+ pattern=(
165
+ f"Magic components tagged '{tag}' pass debate reliably "
166
+ f"({s['passed']}/{s['total']}, {pct:.0%})."
167
+ ),
168
+ category="magic-components",
169
+ conditions=[f"component has tag '{tag}'"],
170
+ correct_approach=(
171
+ f"Follow prior successful '{tag}' specs; accessibility "
172
+ f"and design-token usage have been consistently present."
173
+ ),
174
+ confidence=min(0.95, 0.5 + pct * 0.5),
175
+ )
176
+ stored.append(engine.store_pattern(pattern))
177
+ return {
178
+ "recorded": True,
179
+ "stable_tags": stable_tags,
180
+ "component_count": len(components),
181
+ "patterns_stored": stored,
182
+ }
183
+ except Exception as exc:
184
+ return {"recorded": False, "reason": f"store failed: {exc}"}
185
+
186
+
187
+ def recall_similar_components(project_dir: str, tags: list = None, query: str = "") -> list:
188
+ """Query memory for similar prior components. Used at REASON phase.
189
+
190
+ Returns list of remembered components (possibly empty).
191
+ """
192
+ ME, _Ep, _SemPat, _err = _try_import_memory()
193
+ if ME is None:
194
+ return []
195
+ try:
196
+ engine = _engine_for(project_dir, ME)
197
+ context = {
198
+ "goal": query or "magic component generation",
199
+ "tags": tags or ["magic"],
200
+ "phase": "REASON",
201
+ }
202
+ results = engine.retrieve_relevant(context=context, top_k=5) or []
203
+ return [r if isinstance(r, dict) else {"content": str(r)} for r in results]
204
+ except Exception:
205
+ return []
206
+
207
+
208
+ def _format_tag_stats(stats: dict, limit: int = 8) -> str:
209
+ items = sorted(stats.items(), key=lambda x: x[1]["total"], reverse=True)[:limit]
210
+ parts = []
211
+ for tag, s in items:
212
+ pct = (s["passed"] / s["total"] * 100.0) if s["total"] else 0.0
213
+ parts.append(f"{tag}={s['passed']}/{s['total']} ({pct:.0f}%)")
214
+ return ", ".join(parts) if parts else "none"
215
+
216
+
217
+ if __name__ == "__main__":
218
+ import sys
219
+ project = sys.argv[1] if len(sys.argv) > 1 else "."
220
+ print(json.dumps(capture_iteration_compound(project), indent=2))
@@ -0,0 +1,265 @@
1
+ """PRD scanner for Magic Modules.
2
+
3
+ Runs during the REASON phase of a build. Reads the PRD, detects
4
+ UI component mentions (Button, Modal, Form, Table, Card, Nav, etc.),
5
+ and creates bare-minimum markdown specs at .loki/magic/specs/<Name>.md.
6
+
7
+ The generated specs are stubs -- placeholder for agents to refine
8
+ during the ACT phase. The goal is to make component needs explicit
9
+ and trackable from iteration 1, not discovered mid-build.
10
+ """
11
+
12
+ import re
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+ # Component keywords to detect in PRDs. Map lowercase keyword -> default PascalCase name.
17
+ UI_COMPONENT_VOCAB = {
18
+ "button": "Button",
19
+ "modal": "Modal",
20
+ "dialog": "Dialog",
21
+ "form": "Form",
22
+ "input": "Input",
23
+ "textarea": "Textarea",
24
+ "select": "Select",
25
+ "dropdown": "Dropdown",
26
+ "checkbox": "Checkbox",
27
+ "radio": "Radio",
28
+ "toggle": "Toggle",
29
+ "switch": "Switch",
30
+ "slider": "Slider",
31
+ "table": "Table",
32
+ "list": "List",
33
+ "card": "Card",
34
+ "tile": "Tile",
35
+ "navigation": "Navigation",
36
+ "navbar": "Navbar",
37
+ "sidebar": "Sidebar",
38
+ "header": "Header",
39
+ "footer": "Footer",
40
+ "tabs": "Tabs",
41
+ "accordion": "Accordion",
42
+ "badge": "Badge",
43
+ "avatar": "Avatar",
44
+ "toast": "Toast",
45
+ "notification": "Notification",
46
+ "tooltip": "Tooltip",
47
+ "popover": "Popover",
48
+ "menu": "Menu",
49
+ "breadcrumb": "Breadcrumb",
50
+ "pagination": "Pagination",
51
+ "progress bar": "ProgressBar",
52
+ "spinner": "Spinner",
53
+ "loader": "Loader",
54
+ "chart": "Chart",
55
+ "graph": "Graph",
56
+ "search bar": "SearchBar",
57
+ "filter": "FilterPanel",
58
+ "login": "LoginForm",
59
+ "signup": "SignupForm",
60
+ "profile": "ProfileCard",
61
+ "dashboard": "Dashboard",
62
+ }
63
+
64
+ # Patterns that strongly suggest component intent (vs incidental mentions)
65
+ INTENT_MARKERS = [
66
+ r"\badd\s+(?:a|an|the)\s+",
67
+ r"\bbuild\s+(?:a|an|the)\s+",
68
+ r"\bcreate\s+(?:a|an|the)\s+",
69
+ r"\bneed\s+(?:a|an|the)\s+",
70
+ r"\bwith\s+(?:a|an|the)\s+",
71
+ r"\binclude\s+(?:a|an|the)\s+",
72
+ r"\bshould\s+have\s+(?:a|an|the)\s+",
73
+ ]
74
+
75
+ INTENT_RE = re.compile("|".join(INTENT_MARKERS), re.IGNORECASE)
76
+
77
+ STUB_TEMPLATE = """# {name}
78
+
79
+ ## Description
80
+ Auto-seeded by the PRD scanner from phrase: "{evidence}".
81
+ Agents: refine this spec during the REASON/ACT phases. Fill in props,
82
+ behavior, visual details, and accessibility requirements based on the
83
+ PRD context. The spec is the source of truth; implementation regenerates
84
+ from it.
85
+
86
+ ## Props
87
+ - (to be determined by agent based on PRD context)
88
+
89
+ ## Behavior
90
+ (to be determined)
91
+
92
+ ## Visual / Styling
93
+ (to be determined -- use design tokens from .loki/magic/tokens.json)
94
+
95
+ ## Accessibility
96
+ - Keyboard navigation: (to be determined)
97
+ - Screen reader: (to be determined)
98
+ - Focus management: (to be determined)
99
+
100
+ ## Examples
101
+ ```tsx
102
+ <{name} />
103
+ ```
104
+ """
105
+
106
+
107
+ def scan_prd(prd_text: str, limit: int = 20) -> list:
108
+ """Scan a PRD and return a list of detected component intents.
109
+
110
+ Returns list of dicts: {name, keyword, evidence, confidence}
111
+ Confidence is 'high' when intent marker + keyword co-occur in same sentence,
112
+ 'medium' for keyword alone, 'low' for case-sensitive matches only.
113
+ """
114
+ if not prd_text:
115
+ return []
116
+ detected = []
117
+ seen_names = set()
118
+
119
+ # Split into sentences (approximate)
120
+ sentences = re.split(r"[.!?\n]+", prd_text)
121
+ for sent in sentences:
122
+ sent_clean = sent.strip()
123
+ if not sent_clean:
124
+ continue
125
+ sent_lower = sent_clean.lower()
126
+ has_intent = bool(INTENT_RE.search(sent_clean))
127
+
128
+ for keyword, default_name in UI_COMPONENT_VOCAB.items():
129
+ if keyword not in sent_lower:
130
+ continue
131
+ # Try to extract a more specific name from context, e.g.
132
+ # "add a Submit button" -> "SubmitButton"
133
+ name = _extract_compound_name(sent_clean, keyword, default_name)
134
+ if name in seen_names:
135
+ continue
136
+ seen_names.add(name)
137
+ confidence = "high" if has_intent else "medium"
138
+ detected.append({
139
+ "name": name,
140
+ "keyword": keyword,
141
+ "evidence": sent_clean[:200],
142
+ "confidence": confidence,
143
+ })
144
+ if len(detected) >= limit:
145
+ return detected
146
+ return detected
147
+
148
+
149
+ def _extract_compound_name(sentence: str, keyword: str, default: str) -> str:
150
+ """Try to derive a PascalCase compound name from context.
151
+
152
+ Examples:
153
+ "add a submit button" + "button" -> "SubmitButton"
154
+ "create a user profile card" + "card" -> "UserProfileCard"
155
+ "the search bar" + "search bar" -> "SearchBar"
156
+ Falls back to the default PascalCase form of the keyword.
157
+ """
158
+ # Find the keyword position
159
+ low = sentence.lower()
160
+ idx = low.find(keyword)
161
+ if idx < 0:
162
+ return default
163
+ # Look at 1-3 words before the keyword
164
+ before = sentence[:idx].strip()
165
+ tokens = re.findall(r"[A-Za-z]+", before)
166
+ # Filter out stop words. Verb forms of intent markers ("includes",
167
+ # "contains", "built", etc.) are stops -- otherwise phrases like
168
+ # "dashboard includes navigation" produce "DashboardIncludesNavigation".
169
+ stop = {
170
+ "a", "an", "the", "with", "and", "or", "of", "for", "to", "in",
171
+ "on", "at", "by", "from", "into", "as",
172
+ "add", "adds", "added", "adding",
173
+ "build", "builds", "built", "building",
174
+ "create", "creates", "created", "creating",
175
+ "need", "needs", "needed", "needing",
176
+ "include", "includes", "included", "including",
177
+ "contain", "contains", "contained", "containing",
178
+ "have", "has", "had", "having",
179
+ "require", "requires", "required", "requiring",
180
+ "should", "must", "will", "can", "may",
181
+ "our", "my", "their", "its", "this", "that", "these", "those",
182
+ "new", "simple", "basic", "complex", "main", "primary", "user",
183
+ "also", "plus", "along",
184
+ }
185
+ # Another UI component keyword in the modifier slot means we're spanning
186
+ # two separate components (e.g. "navigation sidebar search bar" produces
187
+ # name "NavigationSidebarSearchBar"). Stop before that token.
188
+ other_component_keywords = {
189
+ k.replace(" ", "") for k in UI_COMPONENT_VOCAB.keys() if k != keyword
190
+ } | set(UI_COMPONENT_VOCAB.keys())
191
+ # Take last 1-2 non-stop, non-component tokens
192
+ modifiers = []
193
+ for tok in reversed(tokens):
194
+ tok_low = tok.lower()
195
+ if tok_low in stop:
196
+ if modifiers:
197
+ break
198
+ continue
199
+ if tok_low in other_component_keywords:
200
+ # Another distinct component name -- stop scanning modifiers.
201
+ break
202
+ modifiers.append(tok)
203
+ if len(modifiers) >= 2:
204
+ break
205
+ modifiers.reverse()
206
+ if not modifiers:
207
+ return default
208
+ # PascalCase the modifiers + default
209
+ parts = [m.capitalize() for m in modifiers] + [default]
210
+ # Deduplicate adjacent parts (e.g. "UserUser")
211
+ out = [parts[0]]
212
+ for p in parts[1:]:
213
+ if p != out[-1]:
214
+ out.append(p)
215
+ name = "".join(out)
216
+ # Validate against name regex
217
+ if not re.match(r"^[A-Za-z][A-Za-z0-9_-]*$", name):
218
+ return default
219
+ return name
220
+
221
+
222
+ def seed_specs(detected: list, project_dir: str = ".", overwrite: bool = False) -> list:
223
+ """Write stub specs to .loki/magic/specs/. Returns list of paths written.
224
+
225
+ If overwrite=False (default), existing specs are never replaced.
226
+ """
227
+ specs_dir = Path(project_dir) / ".loki" / "magic" / "specs"
228
+ specs_dir.mkdir(parents=True, exist_ok=True)
229
+ written = []
230
+ for item in detected:
231
+ if item.get("confidence") == "low":
232
+ continue
233
+ name = item["name"]
234
+ spec_path = specs_dir / f"{name}.md"
235
+ if spec_path.exists() and not overwrite:
236
+ continue
237
+ stub = STUB_TEMPLATE.format(name=name, evidence=item["evidence"])
238
+ spec_path.write_text(stub)
239
+ written.append(str(spec_path))
240
+ return written
241
+
242
+
243
+ def scan_and_seed(prd_text: str, project_dir: str = ".", overwrite: bool = False) -> dict:
244
+ """Convenience: scan PRD, seed specs, return summary dict."""
245
+ detected = scan_prd(prd_text)
246
+ written = seed_specs(detected, project_dir=project_dir, overwrite=overwrite)
247
+ return {
248
+ "detected_count": len(detected),
249
+ "detected": detected,
250
+ "seeded_count": len(written),
251
+ "seeded_paths": written,
252
+ }
253
+
254
+
255
+ if __name__ == "__main__":
256
+ import sys
257
+ import json
258
+ if len(sys.argv) < 2:
259
+ print("Usage: python -m magic.core.prd_scanner <PRD_PATH> [project_dir]")
260
+ sys.exit(1)
261
+ prd_path = sys.argv[1]
262
+ project = sys.argv[2] if len(sys.argv) > 2 else "."
263
+ prd_text = Path(prd_path).read_text()
264
+ result = scan_and_seed(prd_text, project_dir=project)
265
+ print(json.dumps(result, indent=2))