@try-works/dsh-recursive-mode 0.1.2 → 0.1.4

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.
Files changed (123) hide show
  1. package/cordis.patch.yml +15 -10
  2. package/lib/bootstrap.d.ts +9 -2
  3. package/lib/client/apply-guard.d.ts +14 -0
  4. package/lib/client/board.d.ts +5 -4
  5. package/lib/client/contract.d.ts +57 -24
  6. package/lib/client/host-api.d.ts +26 -0
  7. package/lib/client/index.d.ts +16 -9
  8. package/lib/client/inspector.d.ts +3 -3
  9. package/lib/client/slots.d.ts +4 -6
  10. package/lib/client/strip.d.ts +4 -3
  11. package/lib/client/use-live.d.ts +9 -0
  12. package/lib/client.js +252 -174
  13. package/lib/index.d.ts +11 -11
  14. package/lib/index.js +972 -4504
  15. package/lib/init-templates.d.ts +41 -0
  16. package/lib/live-route.d.ts +69 -0
  17. package/lib/phase-rules.d.ts +34 -0
  18. package/lib/runtime.d.ts +24 -17
  19. package/lib/snapshot.d.ts +11 -0
  20. package/package.json +4 -3
  21. package/preset/recursive/agent.cordis.yml +7 -4
  22. package/preset/recursive/preset.yml +1 -1
  23. package/references/agents-block.md +95 -0
  24. package/references/artifact-template.md +2574 -0
  25. package/references/bodies/claude.md +7 -0
  26. package/references/bodies/codex-agents.md +95 -0
  27. package/references/bodies/copilot.md +7 -0
  28. package/references/bodies/cursorrules.md +7 -0
  29. package/references/bodies/decisions.md +5 -0
  30. package/references/bodies/delegated-verification.md +49 -0
  31. package/references/bodies/memory-router.md +45 -0
  32. package/references/bodies/phase8-skill-memory.md +48 -0
  33. package/references/bodies/plans-bridge.md +43 -0
  34. package/references/bodies/recursive-agents-router.md +64 -0
  35. package/references/bodies/skill-discovery.md +51 -0
  36. package/references/bodies/skill-memory-router.md +42 -0
  37. package/references/bodies/state.md +5 -0
  38. package/references/bootstrap/RECURSIVE.md +2362 -0
  39. package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  40. package/references/scripts/lint-recursive-run.ps1 +25 -0
  41. package/references/scripts/lint-recursive-run.py +2870 -0
  42. package/references/scripts/recursive-closeout.ps1 +38 -0
  43. package/references/scripts/recursive-closeout.py +541 -0
  44. package/references/scripts/recursive-init.ps1 +515 -0
  45. package/references/scripts/recursive-init.py +356 -0
  46. package/references/scripts/recursive-lock.ps1 +26 -0
  47. package/references/scripts/recursive-lock.py +302 -0
  48. package/references/scripts/recursive-review-bundle.ps1 +58 -0
  49. package/references/scripts/recursive-review-bundle.py +503 -0
  50. package/references/scripts/recursive-router-cli-configure.ps1 +2 -0
  51. package/references/scripts/recursive-router-cli-configure.py +9 -0
  52. package/references/scripts/recursive-router-cli-init.ps1 +2 -0
  53. package/references/scripts/recursive-router-cli-init.py +9 -0
  54. package/references/scripts/recursive-router-cli-invoke.ps1 +2 -0
  55. package/references/scripts/recursive-router-cli-invoke.py +9 -0
  56. package/references/scripts/recursive-router-cli-probe.ps1 +2 -0
  57. package/references/scripts/recursive-router-cli-probe.py +9 -0
  58. package/references/scripts/recursive-router-cli-resolve.ps1 +2 -0
  59. package/references/scripts/recursive-router-cli-resolve.py +9 -0
  60. package/references/scripts/recursive-router-cli-validate.ps1 +2 -0
  61. package/references/scripts/recursive-router-cli-validate.py +9 -0
  62. package/references/scripts/recursive-router-configure.ps1 +27 -0
  63. package/references/scripts/recursive-router-configure.py +74 -0
  64. package/references/scripts/recursive-router-init.ps1 +17 -0
  65. package/references/scripts/recursive-router-init.py +29 -0
  66. package/references/scripts/recursive-router-invoke.ps1 +47 -0
  67. package/references/scripts/recursive-router-invoke.py +103 -0
  68. package/references/scripts/recursive-router-probe.ps1 +25 -0
  69. package/references/scripts/recursive-router-probe.py +44 -0
  70. package/references/scripts/recursive-router-resolve.ps1 +26 -0
  71. package/references/scripts/recursive-router-resolve.py +46 -0
  72. package/references/scripts/recursive-router-validate.ps1 +17 -0
  73. package/references/scripts/recursive-router-validate.py +27 -0
  74. package/references/scripts/recursive-status.ps1 +23 -0
  75. package/references/scripts/recursive-status.py +2124 -0
  76. package/references/scripts/recursive-subagent-action.ps1 +98 -0
  77. package/references/scripts/recursive-subagent-action.py +197 -0
  78. package/references/scripts/recursive-training-extract.ps1 +23 -0
  79. package/references/scripts/recursive-training-extract.py +99 -0
  80. package/references/scripts/recursive-training-grpo.ps1 +52 -0
  81. package/references/scripts/recursive-training-grpo.py +1341 -0
  82. package/references/scripts/recursive-training-loader.ps1 +78 -0
  83. package/references/scripts/recursive-training-loader.py +558 -0
  84. package/references/scripts/recursive-training-mcp.ps1 +29 -0
  85. package/references/scripts/recursive-training-mcp.py +261 -0
  86. package/references/scripts/recursive-training-phase8-trigger.ps1 +55 -0
  87. package/references/scripts/recursive-training-phase8-trigger.py +137 -0
  88. package/references/scripts/recursive-training-sync.ps1 +29 -0
  89. package/references/scripts/recursive-training-sync.py +183 -0
  90. package/references/scripts/recursive_phase_rules.py +367 -0
  91. package/references/scripts/recursive_router_cli_lib.py +2 -0
  92. package/references/scripts/recursive_router_lib.py +2282 -0
  93. package/references/scripts/verify-locks.ps1 +25 -0
  94. package/references/scripts/verify-locks.py +353 -0
  95. package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
  96. package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  97. package/scripts/install-preset.cmd +7 -0
  98. package/scripts/install-preset.js +101 -0
  99. package/scripts/lint-recursive-run.py +7 -5
  100. package/scripts/test-recursive-mode-smoke.ts +29 -28
  101. package/src/bootstrap.ts +253 -39
  102. package/src/client/apply-guard.ts +23 -0
  103. package/src/client/board.tsx +0 -0
  104. package/src/client/contract.ts +72 -24
  105. package/src/client/host-api.ts +90 -0
  106. package/src/client/index.ts +22 -11
  107. package/src/client/inspector.tsx +7 -9
  108. package/src/client/slots.ts +70 -26
  109. package/src/client/strip.tsx +9 -9
  110. package/src/client/use-live.ts +52 -0
  111. package/src/index.ts +89 -146
  112. package/src/init-templates.ts +276 -0
  113. package/src/live-route.ts +163 -0
  114. package/src/phase-rules.ts +262 -0
  115. package/src/policy.ts +28 -4
  116. package/src/runtime.ts +129 -77
  117. package/src/snapshot.ts +104 -0
  118. package/lib/client/node.d.ts +0 -71
  119. package/lib/events.d.ts +0 -173
  120. package/lib/projection.d.ts +0 -29
  121. package/src/client/node.ts +0 -156
  122. package/src/events.ts +0 -173
  123. package/src/projection.ts +0 -237
@@ -0,0 +1,78 @@
1
+ #Requires -Version 5.1
2
+ <#
3
+ .SYNOPSIS
4
+ Progressive memory loader for recursive-mode experiential knowledge.
5
+
6
+ .DESCRIPTION
7
+ Reads the memory router plus the repository memory plane, scores memory docs
8
+ by relevance to the current task, reads the most relevant docs, scores
9
+ individual items, and returns formatted context for the agent.
10
+
11
+ This is the canonical way to load repo-specific experiential knowledge.
12
+ Any harness can call it — no MCP required.
13
+
14
+ .PARAMETER RepoRoot
15
+ Path to the git repository root.
16
+
17
+ .PARAMETER Query
18
+ Description of the current task (e.g., "implementing frontend feature").
19
+
20
+ .PARAMETER Files
21
+ Comma-separated list of file paths being modified.
22
+
23
+ .PARAMETER Subsystem
24
+ Target subsystem (e.g., "web", "artifacts", "api-worker").
25
+
26
+ .PARAMETER TaskType
27
+ Target task type (e.g., "commit-workflow", "test-validation").
28
+
29
+ .PARAMETER MaxDocs
30
+ Maximum memory docs to load (default: 3).
31
+
32
+ .PARAMETER MaxItems
33
+ Maximum items to return (default: 10).
34
+
35
+ .PARAMETER DryRun
36
+ Show what would be loaded without returning content.
37
+
38
+ .PARAMETER All
39
+ Load all items as flat list.
40
+
41
+ .EXAMPLE
42
+ .\recursive-training-loader.ps1 -RepoRoot . -Query "frontend feature" -Files "apps/web/src/App.tsx"
43
+
44
+ .EXAMPLE
45
+ .\recursive-training-loader.ps1 -RepoRoot . -Subsystem "web" -MaxDocs 5
46
+
47
+ .EXAMPLE
48
+ .\recursive-training-loader.ps1 -RepoRoot . -TaskType "commit-workflow" -MaxItems 20
49
+ #>
50
+ param(
51
+ [Parameter(Mandatory)]
52
+ [string]$RepoRoot,
53
+
54
+ [string]$Query = "",
55
+ [string]$Files = "",
56
+ [string]$Subsystem = "",
57
+ [string]$TaskType = "",
58
+ [int]$MaxDocs = 3,
59
+ [int]$MaxItems = 10,
60
+ [switch]$DryRun,
61
+ [switch]$All
62
+ )
63
+
64
+ $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
65
+ $pyScript = Join-Path $scriptDir "recursive-training-loader.py"
66
+
67
+ $argsList = @("--repo-root", $RepoRoot)
68
+
69
+ if ($Query) { $argsList += @("--query", $Query) }
70
+ if ($Files) { $argsList += @("--files", $Files) }
71
+ if ($Subsystem) { $argsList += @("--subsystem", $Subsystem) }
72
+ if ($TaskType) { $argsList += @("--task-type", $TaskType) }
73
+ if ($MaxDocs -ne 3) { $argsList += @("--max-docs", $MaxDocs) }
74
+ if ($MaxItems -ne 10) { $argsList += @("--max-items", $MaxItems) }
75
+ if ($DryRun) { $argsList += "--dry-run" }
76
+ if ($All) { $argsList += "--all" }
77
+
78
+ & python $pyScript @argsList
@@ -0,0 +1,558 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Progressive memory loader for recursive-mode experiential knowledge.
4
+
5
+ Reads the memory router plus the repository memory plane, scores memory docs by
6
+ relevance to the current task, reads the most relevant docs, scores individual
7
+ items, and returns formatted context for the agent.
8
+
9
+ This script lives at `.recursive/scripts/recursive-training-loader.py` in the
10
+ target repository. It is copied there during recursive-mode installation.
11
+
12
+ This is the canonical way to load repo-specific experiential knowledge.
13
+ Run it directly when a task may benefit from repo-specific experiential memory.
14
+
15
+ Usage:
16
+ # From repo root
17
+ python .recursive/scripts/recursive-training-loader.py --repo-root . \
18
+ --query "implementing frontend feature with react" \
19
+ --files "apps/web/src/App.tsx,apps/web/src/stores/ui-store.ts"
20
+
21
+ # Load all memories for a subsystem
22
+ python .recursive/scripts/recursive-training-loader.py --repo-root . \
23
+ --subsystem "web" --max-docs 5
24
+
25
+ # Load specific task-type memories
26
+ python .recursive/scripts/recursive-training-loader.py --repo-root . \
27
+ --task-type "commit-workflow" --max-items 20
28
+
29
+ # Dry run: show what would be loaded without returning content
30
+ python .recursive/scripts/recursive-training-loader.py --repo-root . \
31
+ --query "frontend rebuild" --dry-run
32
+
33
+ Progressive disclosure levels:
34
+ Level 1: MEMORY.md (lightweight registry)
35
+ Level 2: This loader + filesystem discovery (selective doc loading based on task context)
36
+ Level 3: Agent applies specific items from loaded docs
37
+ """
38
+
39
+ import argparse
40
+ import json
41
+ import re
42
+ import sys
43
+ from collections import defaultdict
44
+ from pathlib import Path
45
+ from typing import Dict, List, Optional, Tuple
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Memory doc discovery
50
+ # ---------------------------------------------------------------------------
51
+
52
+ class MemoryDoc:
53
+ """Represents a single memory document (domain or training)."""
54
+
55
+ def __init__(self, rel_path: str, doc_type: str, scope: str, item_count: int,
56
+ description: str = "", full_path: Optional[Path] = None):
57
+ self.rel_path = rel_path
58
+ self.doc_type = doc_type # "domain" or "training"
59
+ self.scope = scope
60
+ self.item_count = item_count
61
+ self.description = description
62
+ self.full_path = full_path
63
+ self._items: Optional[List[dict]] = None
64
+
65
+ @property
66
+ def items(self) -> List[dict]:
67
+ """Lazy-load items from disk."""
68
+ if self._items is None and self.full_path:
69
+ self._items = self._parse_items()
70
+ return self._items or []
71
+
72
+ def _parse_items(self) -> List[dict]:
73
+ """Parse ReasoningBank items from the markdown file."""
74
+ if not self.full_path or not self.full_path.exists():
75
+ return []
76
+ content = self.full_path.read_text(encoding="utf-8")
77
+
78
+ items = []
79
+ # Match item blocks: ### RB-N: Title
80
+ item_pattern = re.compile(
81
+ r'### (RB-\d+):\s*(.+?)\n\n'
82
+ r'\*\*Description:\*\*\s*(.+?)\n\n'
83
+ r'\*\*Content:\*\*\s*(.+?)(?=\n\n```|\n### |\Z)',
84
+ re.DOTALL
85
+ )
86
+
87
+ # Also parse schema metadata
88
+ schema_pattern = re.compile(
89
+ r'```yaml\n'
90
+ r'rb_id: "(RB-\d+)"\n'
91
+ r'title: "([^"]+)"\n'
92
+ r'description: "([^"]+)"\n'
93
+ r'task_type: "([^"]*)"\n'
94
+ r'subsystem: "([^"]*)"\n'
95
+ r'source_runs: (.+?)\n'
96
+ r'applies_to: (.+?)\n'
97
+ r'success_rate: ([\d.]+)\n'
98
+ r'status: (\w+)\n'
99
+ r'created_at: "([^"]+)"\n'
100
+ r'```',
101
+ re.DOTALL
102
+ )
103
+
104
+ # Build a map of schema metadata by rb_id
105
+ schema_by_id: Dict[str, dict] = {}
106
+ for m in schema_pattern.finditer(content):
107
+ rb_id = m.group(1)
108
+ schema_by_id[rb_id] = {
109
+ "task_type": m.group(4),
110
+ "subsystem": m.group(5),
111
+ "success_rate": float(m.group(8)),
112
+ "status": m.group(9),
113
+ "source_runs": _parse_json_array(m.group(6)),
114
+ "applies_to": _parse_json_array(m.group(7)),
115
+ }
116
+
117
+ for m in item_pattern.finditer(content):
118
+ rb_id = m.group(1)
119
+ item = {
120
+ "rb_id": rb_id,
121
+ "title": m.group(2).strip(),
122
+ "description": m.group(3).strip(),
123
+ "content": m.group(4).strip(),
124
+ }
125
+ # Merge schema metadata if available
126
+ if rb_id in schema_by_id:
127
+ item.update(schema_by_id[rb_id])
128
+ else:
129
+ item.update({
130
+ "task_type": "",
131
+ "subsystem": self.scope,
132
+ "success_rate": 0.0,
133
+ "status": "active",
134
+ "source_runs": [],
135
+ "applies_to": [],
136
+ })
137
+
138
+ if item.get("status") != "deprecated":
139
+ items.append(item)
140
+
141
+ return items
142
+
143
+
144
+ def _parse_json_array(text: str) -> List[str]:
145
+ """Parse a JSON array string like ['a', 'b'] or ["a", "b"]."""
146
+ text = text.strip()
147
+ if not text or text == "[]":
148
+ return []
149
+ try:
150
+ return json.loads(text)
151
+ except json.JSONDecodeError:
152
+ # Fallback: parse comma-separated list
153
+ items = re.findall(r'["\']([^"\']+)["\']', text)
154
+ return items if items else []
155
+
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # Registry discovery
159
+ # ---------------------------------------------------------------------------
160
+
161
+ class MemoryRegistry:
162
+ """Discovers memory docs from the filesystem."""
163
+
164
+ DOMAINS_DIR = ".recursive/memory/domains"
165
+ TRAINING_DIR = ".recursive/memory/training"
166
+
167
+ def __init__(self, repo_root: str):
168
+ self.root = Path(repo_root).resolve()
169
+ self.docs: List[MemoryDoc] = []
170
+
171
+ def discover(self) -> List[MemoryDoc]:
172
+ """Discover all memory docs from the filesystem."""
173
+ self.docs = sorted(self._scan_filesystem().values(), key=lambda d: (d.doc_type, d.scope))
174
+ return self.docs
175
+
176
+ def _scan_filesystem(self) -> Dict[str, MemoryDoc]:
177
+ """Scan .recursive/memory/ and exclude stale/deprecated docs by default."""
178
+ docs: Dict[str, MemoryDoc] = {}
179
+
180
+ for base_dir, doc_type in (
181
+ (self.root / self.DOMAINS_DIR, "domain"),
182
+ (self.root / self.TRAINING_DIR, "training"),
183
+ ):
184
+ if not base_dir.exists():
185
+ continue
186
+ for f in sorted(base_dir.glob("*.md")):
187
+ content = f.read_text(encoding="utf-8")
188
+ if "Status: STALE" in content or "Status: DEPRECATED" in content:
189
+ continue
190
+ rel_path = str(f.relative_to(self.root / ".recursive/memory")).replace("\\", "/")
191
+ if rel_path in docs:
192
+ continue # Already discovered from registry
193
+ item_count = len(re.findall(r'### RB-\d+:', content))
194
+ docs[rel_path] = MemoryDoc(
195
+ rel_path=rel_path,
196
+ doc_type=doc_type,
197
+ scope=f.stem,
198
+ item_count=item_count,
199
+ full_path=f,
200
+ )
201
+
202
+ return docs
203
+
204
+ def get_doc_by_scope(self, scope: str) -> Optional[MemoryDoc]:
205
+ """Find a doc by its scope name (exact match)."""
206
+ for doc in self.docs:
207
+ if doc.scope.lower() == scope.lower():
208
+ return doc
209
+ return None
210
+
211
+
212
+ # ---------------------------------------------------------------------------
213
+ # Relevance scoring
214
+ # ---------------------------------------------------------------------------
215
+
216
+ class RelevanceScorer:
217
+ """Scores memory docs and items by relevance to the current task."""
218
+
219
+ def __init__(self, query: str, file_paths: List[str], subsystem: str = "",
220
+ task_type: str = ""):
221
+ self.query = query.lower()
222
+ self.query_parts = [p for p in re.split(r'[^a-zA-Z0-9]', self.query) if len(p) > 2]
223
+ self.file_paths = [p.lower() for p in file_paths]
224
+ self.subsystem = subsystem.lower()
225
+ self.task_type = task_type.lower()
226
+
227
+ def score_doc(self, doc: MemoryDoc) -> float:
228
+ """Score a memory doc by relevance (0-100)."""
229
+ score = 0.0
230
+ scope_lower = doc.scope.lower()
231
+
232
+ # Direct subsystem match (strong signal)
233
+ if self.subsystem and self.subsystem in scope_lower:
234
+ score += 30
235
+ if self.subsystem and scope_lower in self.subsystem:
236
+ score += 25
237
+
238
+ # Direct task_type match for training docs
239
+ if self.task_type and doc.doc_type == "training":
240
+ if self.task_type in scope_lower:
241
+ score += 25
242
+ if scope_lower in self.task_type:
243
+ score += 20
244
+
245
+ # Query keyword overlap with scope
246
+ for part in self.query_parts:
247
+ if part in scope_lower:
248
+ score += 8
249
+
250
+ # File path overlap with doc scope (for domain docs)
251
+ if doc.doc_type == "domain":
252
+ for fp in self.file_paths:
253
+ parts = fp.split("/")
254
+ for part in parts:
255
+ if len(part) > 2 and part in scope_lower:
256
+ score += 10
257
+ break
258
+
259
+ # Query keyword overlap with doc description
260
+ desc_lower = doc.description.lower()
261
+ for part in self.query_parts:
262
+ if part in desc_lower:
263
+ score += 5
264
+
265
+ # Prefer docs with more items (more signal)
266
+ score += min(doc.item_count * 0.5, 5)
267
+
268
+ return score
269
+
270
+ def score_item(self, item: dict) -> float:
271
+ """Score an individual memory item by relevance (0-100)."""
272
+ score = 0.0
273
+ title = item.get("title", "").lower()
274
+ desc = item.get("description", "").lower()
275
+ content = item.get("content", "").lower()
276
+ applies_to = [a.lower() for a in item.get("applies_to", [])]
277
+ item_task = item.get("task_type", "").lower()
278
+ item_subsystem = item.get("subsystem", "").lower()
279
+ success_rate = item.get("success_rate", 0.0)
280
+
281
+ all_text = f"{title} {desc} {content}"
282
+
283
+ # Direct task_type match
284
+ if self.task_type:
285
+ if self.task_type in item_task:
286
+ score += 20
287
+ if item_task in self.task_type:
288
+ score += 15
289
+
290
+ # Direct subsystem match
291
+ if self.subsystem:
292
+ if self.subsystem in item_subsystem:
293
+ score += 15
294
+ if item_subsystem in self.subsystem:
295
+ score += 10
296
+
297
+ # Query keyword overlap
298
+ for part in self.query_parts:
299
+ if part in title:
300
+ score += 10
301
+ if part in desc:
302
+ score += 5
303
+ if part in content:
304
+ score += 3
305
+
306
+ # File path overlap with applies_to
307
+ for fp in self.file_paths:
308
+ fp_parts = fp.split("/")
309
+ for fp_part in fp_parts:
310
+ if len(fp_part) <= 2:
311
+ continue
312
+ for tag in applies_to:
313
+ if fp_part in tag or tag in fp_part:
314
+ score += 8
315
+ break
316
+
317
+ # Success rate bonus (prefer proven items)
318
+ if isinstance(success_rate, float) and success_rate > 0.8:
319
+ score += 3
320
+ if isinstance(success_rate, float) and success_rate > 0.95:
321
+ score += 2
322
+
323
+ # Penalize very long content (less focused)
324
+ if len(content) > 300:
325
+ score -= 2
326
+
327
+ return score
328
+
329
+
330
+ # ---------------------------------------------------------------------------
331
+ # Memory loader
332
+ # ---------------------------------------------------------------------------
333
+
334
+ class MemoryLoader:
335
+ """Main loader: discovers docs, scores relevance, returns formatted context."""
336
+
337
+ def __init__(self, repo_root: str):
338
+ self.repo_root = Path(repo_root).resolve()
339
+ self.registry = MemoryRegistry(repo_root)
340
+ self.docs = self.registry.discover()
341
+
342
+ def load(self, query: str = "", file_paths: List[str] = None,
343
+ subsystem: str = "", task_type: str = "",
344
+ max_docs: int = 3, max_items: int = 10,
345
+ dry_run: bool = False) -> str:
346
+ """Load the most relevant memory items for the given task context.
347
+
348
+ Args:
349
+ query: Description of the current task
350
+ file_paths: Files being modified
351
+ subsystem: Target subsystem (optional, stronger signal)
352
+ task_type: Target task type (optional, stronger signal)
353
+ max_docs: Max memory docs to read
354
+ max_items: Max items to return
355
+ dry_run: If True, only report what would be loaded
356
+
357
+ Returns:
358
+ Formatted memory context string ready to inject into agent prompt
359
+ """
360
+ file_paths = file_paths or []
361
+ scorer = RelevanceScorer(query, file_paths, subsystem, task_type)
362
+
363
+ # --- Level 2: Score and select docs ---
364
+ scored_docs = []
365
+ for doc in self.docs:
366
+ score = scorer.score_doc(doc)
367
+ if score > 0:
368
+ scored_docs.append((score, doc))
369
+
370
+ scored_docs.sort(key=lambda x: x[0], reverse=True)
371
+ selected_docs = scored_docs[:max_docs]
372
+
373
+ if dry_run:
374
+ return self._format_dry_run(selected_docs, max_items)
375
+
376
+ # --- Level 3: Read selected docs, score items ---
377
+ all_items: List[Tuple[float, dict, str]] = [] # (score, item, doc_path)
378
+ for doc_score, doc in selected_docs:
379
+ for item in doc.items:
380
+ item_score = scorer.score_item(item)
381
+ # Blend doc relevance with item relevance
382
+ blended = item_score + (doc_score * 0.3)
383
+ all_items.append((blended, item, doc.rel_path))
384
+
385
+ all_items.sort(key=lambda x: x[0], reverse=True)
386
+ top_items = all_items[:max_items]
387
+
388
+ return self._format_output(top_items, selected_docs, query)
389
+
390
+ def _format_output(self, items: List[Tuple[float, dict, str]],
391
+ docs: List[Tuple[float, MemoryDoc]],
392
+ query: str) -> str:
393
+ """Format the loaded items as context for the agent."""
394
+ lines = [
395
+ "=" * 60,
396
+ "REPOSITORY EXPERIENTIAL KNOWLEDGE (Loaded via Memory Loader)",
397
+ "=" * 60,
398
+ "",
399
+ f"Query: {query or '(none)'}" if query else "",
400
+ ]
401
+
402
+ if docs:
403
+ lines.append("Sources loaded:")
404
+ for score, doc in docs:
405
+ lines.append(f" - {doc.rel_path} ({doc.item_count} items, relevance: {score:.1f})")
406
+ lines.append("")
407
+
408
+ if not items:
409
+ lines.append("No relevant experiences found for this task and file set.")
410
+ lines.append("")
411
+ lines.append("Tip: If you're starting a new task type, run training first:")
412
+ lines.append(" python .recursive/scripts/recursive-training-grpo.py --repo-root .")
413
+ return "\n".join(lines)
414
+
415
+ lines.append(f"Top {len(items)} relevant learnings (ordered by relevance):\n")
416
+
417
+ for idx, (score, item, doc_path) in enumerate(items, 1):
418
+ rb_id = item["rb_id"]
419
+ title = item["title"]
420
+ desc = item["description"]
421
+ content = item["content"]
422
+ task_type = item.get("task_type", "")
423
+ applies_to = item.get("applies_to", [])
424
+ success = item.get("success_rate", 0.0)
425
+ source_runs = item.get("source_runs", [])
426
+
427
+ sr_str = f" [success: {success:.0%}]" if isinstance(success, float) and success > 0 else ""
428
+ tt_str = f" [{task_type}]" if task_type else ""
429
+ applies_str = ", ".join(applies_to) if applies_to else ""
430
+ runs_str = f" (from: {', '.join(source_runs[:3])}{'...' if len(source_runs) > 3 else ''})" if source_runs else ""
431
+
432
+ lines.append(f"{idx}. [{rb_id}{tt_str}]{sr_str} {title}")
433
+ lines.append(f" {desc}")
434
+ lines.append(f" {content}")
435
+ if applies_str:
436
+ lines.append(f" Applies to: {applies_str}")
437
+ if runs_str:
438
+ lines.append(f" Source runs{runs_str}")
439
+ lines.append("")
440
+
441
+ lines.append("-" * 60)
442
+ lines.append("Apply these learnings when relevant. They are scoped to THIS repository.")
443
+ lines.append("=" * 60)
444
+
445
+ return "\n".join(lines)
446
+
447
+ def _format_dry_run(self, docs: List[Tuple[float, MemoryDoc]], max_items: int) -> str:
448
+ """Format dry-run output showing what would be loaded."""
449
+ lines = [
450
+ "=" * 60,
451
+ "DRY RUN: Memory Loader Preview",
452
+ "=" * 60,
453
+ "",
454
+ ]
455
+
456
+ if not docs:
457
+ lines.append("No memory docs match the current task context.")
458
+ return "\n".join(lines)
459
+
460
+ lines.append(f"Would load top {len(docs)} memory doc(s):")
461
+ for score, doc in docs:
462
+ lines.append(f" - {doc.rel_path} ({doc.item_count} items, score: {score:.1f})")
463
+
464
+ lines.append("")
465
+ lines.append(f"Would return top {max_items} items across all selected docs.")
466
+ lines.append("Run without --dry-run to load the actual content.")
467
+ return "\n".join(lines)
468
+
469
+ def load_all_flat(self) -> str:
470
+ """Load ALL memory items as a flat list (for README/IDE files)."""
471
+ items = []
472
+ for doc in self.docs:
473
+ for item in doc.items:
474
+ items.append(item)
475
+
476
+ if not items:
477
+ return ""
478
+
479
+ lines = ["## Repository Experiential Knowledge (ReasoningBank)\n"]
480
+ lines.append("Lessons learned from prior recursive-mode runs in THIS repository:\n")
481
+ for item in sorted(items, key=lambda x: x.get("rb_id", "")):
482
+ rb_id = item["rb_id"]
483
+ title = item["title"]
484
+ desc = item["description"]
485
+ content = item["content"]
486
+ applies = ", ".join(item.get("applies_to", []))
487
+ lines.append(f"- [{rb_id}] **{title}**: {desc} {content}")
488
+ if applies:
489
+ lines.append(f" (applies to: {applies})")
490
+ return "\n".join(lines)
491
+
492
+
493
+ # ---------------------------------------------------------------------------
494
+ # CLI
495
+ # ---------------------------------------------------------------------------
496
+
497
+ def _configure_utf8_stdout() -> None:
498
+ """Avoid Windows cp1252 crashes when memory items contain arrows/unicode."""
499
+ try:
500
+ if hasattr(sys.stdout, "reconfigure"):
501
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
502
+ if hasattr(sys.stderr, "reconfigure"):
503
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
504
+ except Exception:
505
+ pass
506
+
507
+
508
+ def main():
509
+ _configure_utf8_stdout()
510
+ parser = argparse.ArgumentParser(
511
+ description="Progressive memory loader for recursive-mode experiential knowledge"
512
+ )
513
+ parser.add_argument("--repo-root", type=str, required=True,
514
+ help="Path to git repository root")
515
+ parser.add_argument("--query", type=str, default="",
516
+ help="Description of the current task (e.g., 'implementing frontend feature')")
517
+ parser.add_argument("--files", type=str, default="",
518
+ help="Comma-separated list of file paths being modified")
519
+ parser.add_argument("--subsystem", type=str, default="",
520
+ help="Target subsystem (e.g., 'web', 'artifacts', 'api-worker')")
521
+ parser.add_argument("--task-type", type=str, default="",
522
+ help="Target task type (e.g., 'commit-workflow', 'test-validation')")
523
+ parser.add_argument("--max-docs", type=int, default=3,
524
+ help="Maximum memory docs to load (default: 3)")
525
+ parser.add_argument("--max-items", type=int, default=10,
526
+ help="Maximum items to return (default: 10)")
527
+ parser.add_argument("--dry-run", action="store_true",
528
+ help="Show what would be loaded without returning content")
529
+ parser.add_argument("--all", action="store_true",
530
+ help="Load all items as flat list (for README generation)")
531
+ args = parser.parse_args()
532
+
533
+ loader = MemoryLoader(args.repo_root)
534
+
535
+ if args.all:
536
+ output = loader.load_all_flat()
537
+ if output:
538
+ print(output)
539
+ else:
540
+ print("No memory items found.")
541
+ return
542
+
543
+ file_paths = [f.strip() for f in args.files.split(",") if f.strip()] if args.files else []
544
+
545
+ output = loader.load(
546
+ query=args.query,
547
+ file_paths=file_paths,
548
+ subsystem=args.subsystem,
549
+ task_type=args.task_type,
550
+ max_docs=args.max_docs,
551
+ max_items=args.max_items,
552
+ dry_run=args.dry_run,
553
+ )
554
+ print(output)
555
+
556
+
557
+ if __name__ == "__main__":
558
+ main()
@@ -0,0 +1,29 @@
1
+ #Requires -Version 5.1
2
+ <#
3
+ .SYNOPSIS
4
+ MCP server for repository experiences (PowerShell wrapper).
5
+
6
+ .DESCRIPTION
7
+ Thin wrapper that calls recursive-training-mcp.py with the same arguments.
8
+ #>
9
+
10
+ [CmdletBinding()]
11
+ param(
12
+ [Parameter(Mandatory = $true)]
13
+ [string]$RepoRoot
14
+ )
15
+
16
+ $python = Get-Command python -ErrorAction SilentlyContinue
17
+ if (-not $python) {
18
+ $python = Get-Command python3 -ErrorAction SilentlyContinue
19
+ }
20
+
21
+ if (-not $python) {
22
+ Write-Error "Python is required but not found. Please install Python 3.10+."
23
+ exit 1
24
+ }
25
+
26
+ $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
27
+ $pyScript = Join-Path $scriptDir "recursive-training-mcp.py"
28
+
29
+ & $python.Source $pyScript @("--repo-root", $RepoRoot)