@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.
- package/cordis.patch.yml +15 -10
- package/lib/bootstrap.d.ts +9 -2
- package/lib/client/apply-guard.d.ts +14 -0
- package/lib/client/board.d.ts +5 -4
- package/lib/client/contract.d.ts +57 -24
- package/lib/client/host-api.d.ts +26 -0
- package/lib/client/index.d.ts +16 -9
- package/lib/client/inspector.d.ts +3 -3
- package/lib/client/slots.d.ts +4 -6
- package/lib/client/strip.d.ts +4 -3
- package/lib/client/use-live.d.ts +9 -0
- package/lib/client.js +252 -174
- package/lib/index.d.ts +11 -11
- package/lib/index.js +972 -4504
- package/lib/init-templates.d.ts +41 -0
- package/lib/live-route.d.ts +69 -0
- package/lib/phase-rules.d.ts +34 -0
- package/lib/runtime.d.ts +24 -17
- package/lib/snapshot.d.ts +11 -0
- package/package.json +4 -3
- package/preset/recursive/agent.cordis.yml +7 -4
- package/preset/recursive/preset.yml +1 -1
- package/references/agents-block.md +95 -0
- package/references/artifact-template.md +2574 -0
- package/references/bodies/claude.md +7 -0
- package/references/bodies/codex-agents.md +95 -0
- package/references/bodies/copilot.md +7 -0
- package/references/bodies/cursorrules.md +7 -0
- package/references/bodies/decisions.md +5 -0
- package/references/bodies/delegated-verification.md +49 -0
- package/references/bodies/memory-router.md +45 -0
- package/references/bodies/phase8-skill-memory.md +48 -0
- package/references/bodies/plans-bridge.md +43 -0
- package/references/bodies/recursive-agents-router.md +64 -0
- package/references/bodies/skill-discovery.md +51 -0
- package/references/bodies/skill-memory-router.md +42 -0
- package/references/bodies/state.md +5 -0
- package/references/bootstrap/RECURSIVE.md +2362 -0
- package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
- package/references/scripts/lint-recursive-run.ps1 +25 -0
- package/references/scripts/lint-recursive-run.py +2870 -0
- package/references/scripts/recursive-closeout.ps1 +38 -0
- package/references/scripts/recursive-closeout.py +541 -0
- package/references/scripts/recursive-init.ps1 +515 -0
- package/references/scripts/recursive-init.py +356 -0
- package/references/scripts/recursive-lock.ps1 +26 -0
- package/references/scripts/recursive-lock.py +302 -0
- package/references/scripts/recursive-review-bundle.ps1 +58 -0
- package/references/scripts/recursive-review-bundle.py +503 -0
- package/references/scripts/recursive-router-cli-configure.ps1 +2 -0
- package/references/scripts/recursive-router-cli-configure.py +9 -0
- package/references/scripts/recursive-router-cli-init.ps1 +2 -0
- package/references/scripts/recursive-router-cli-init.py +9 -0
- package/references/scripts/recursive-router-cli-invoke.ps1 +2 -0
- package/references/scripts/recursive-router-cli-invoke.py +9 -0
- package/references/scripts/recursive-router-cli-probe.ps1 +2 -0
- package/references/scripts/recursive-router-cli-probe.py +9 -0
- package/references/scripts/recursive-router-cli-resolve.ps1 +2 -0
- package/references/scripts/recursive-router-cli-resolve.py +9 -0
- package/references/scripts/recursive-router-cli-validate.ps1 +2 -0
- package/references/scripts/recursive-router-cli-validate.py +9 -0
- package/references/scripts/recursive-router-configure.ps1 +27 -0
- package/references/scripts/recursive-router-configure.py +74 -0
- package/references/scripts/recursive-router-init.ps1 +17 -0
- package/references/scripts/recursive-router-init.py +29 -0
- package/references/scripts/recursive-router-invoke.ps1 +47 -0
- package/references/scripts/recursive-router-invoke.py +103 -0
- package/references/scripts/recursive-router-probe.ps1 +25 -0
- package/references/scripts/recursive-router-probe.py +44 -0
- package/references/scripts/recursive-router-resolve.ps1 +26 -0
- package/references/scripts/recursive-router-resolve.py +46 -0
- package/references/scripts/recursive-router-validate.ps1 +17 -0
- package/references/scripts/recursive-router-validate.py +27 -0
- package/references/scripts/recursive-status.ps1 +23 -0
- package/references/scripts/recursive-status.py +2124 -0
- package/references/scripts/recursive-subagent-action.ps1 +98 -0
- package/references/scripts/recursive-subagent-action.py +197 -0
- package/references/scripts/recursive-training-extract.ps1 +23 -0
- package/references/scripts/recursive-training-extract.py +99 -0
- package/references/scripts/recursive-training-grpo.ps1 +52 -0
- package/references/scripts/recursive-training-grpo.py +1341 -0
- package/references/scripts/recursive-training-loader.ps1 +78 -0
- package/references/scripts/recursive-training-loader.py +558 -0
- package/references/scripts/recursive-training-mcp.ps1 +29 -0
- package/references/scripts/recursive-training-mcp.py +261 -0
- package/references/scripts/recursive-training-phase8-trigger.ps1 +55 -0
- package/references/scripts/recursive-training-phase8-trigger.py +137 -0
- package/references/scripts/recursive-training-sync.ps1 +29 -0
- package/references/scripts/recursive-training-sync.py +183 -0
- package/references/scripts/recursive_phase_rules.py +367 -0
- package/references/scripts/recursive_router_cli_lib.py +2 -0
- package/references/scripts/recursive_router_lib.py +2282 -0
- package/references/scripts/verify-locks.ps1 +25 -0
- package/references/scripts/verify-locks.py +353 -0
- package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
- package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
- package/scripts/install-preset.cmd +7 -0
- package/scripts/install-preset.js +101 -0
- package/scripts/lint-recursive-run.py +7 -5
- package/scripts/test-recursive-mode-smoke.ts +29 -28
- package/src/bootstrap.ts +253 -39
- package/src/client/apply-guard.ts +23 -0
- package/src/client/board.tsx +0 -0
- package/src/client/contract.ts +72 -24
- package/src/client/host-api.ts +90 -0
- package/src/client/index.ts +22 -11
- package/src/client/inspector.tsx +7 -9
- package/src/client/slots.ts +70 -26
- package/src/client/strip.tsx +9 -9
- package/src/client/use-live.ts +52 -0
- package/src/index.ts +89 -146
- package/src/init-templates.ts +276 -0
- package/src/live-route.ts +163 -0
- package/src/phase-rules.ts +262 -0
- package/src/policy.ts +28 -4
- package/src/runtime.ts +129 -77
- package/src/snapshot.ts +104 -0
- package/lib/client/node.d.ts +0 -71
- package/lib/events.d.ts +0 -173
- package/lib/projection.d.ts +0 -29
- package/src/client/node.ts +0 -156
- package/src/events.ts +0 -173
- 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)
|