@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,261 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Optional MCP (Model Context Protocol) server for repository experiences.
|
|
4
|
+
|
|
5
|
+
Provides a get_repo_experiences tool that any MCP-compatible client can call
|
|
6
|
+
to dynamically retrieve ReasoningBank-structured experiences relevant to the
|
|
7
|
+
current task, file paths, and task type.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python recursive-training-mcp.py --repo-root /path/to/repo
|
|
11
|
+
|
|
12
|
+
Compatible clients: Claude Desktop, Cline, Cursor (with MCP), Continue.dev.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Dict, List
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ReasoningBankReader:
|
|
24
|
+
"""Reads ReasoningBank-structured memory from the memory plane."""
|
|
25
|
+
|
|
26
|
+
DOMAINS_DIR = ".recursive/memory/domains"
|
|
27
|
+
TRAINING_DIR = ".recursive/memory/training"
|
|
28
|
+
|
|
29
|
+
def __init__(self, repo_root: str):
|
|
30
|
+
self.root = Path(repo_root)
|
|
31
|
+
self.domains_dir = self.root / self.DOMAINS_DIR
|
|
32
|
+
self.training_dir = self.root / self.TRAINING_DIR
|
|
33
|
+
|
|
34
|
+
def _parse_items_from_file(self, filepath: Path) -> List[dict]:
|
|
35
|
+
if not filepath.exists():
|
|
36
|
+
return []
|
|
37
|
+
content = filepath.read_text(encoding="utf-8")
|
|
38
|
+
if "Status: CURRENT" not in content and "Status: SUSPECT" not in content:
|
|
39
|
+
return []
|
|
40
|
+
|
|
41
|
+
items = []
|
|
42
|
+
pattern = re.compile(
|
|
43
|
+
r'### (RB-\d+):\s*(.+?)\n\n'
|
|
44
|
+
r'\*\*Description:\*\*\s*(.+?)\n\n'
|
|
45
|
+
r'\*\*Content:\*\*\s*(.+?)(?=\n\n```|\n### |\Z)',
|
|
46
|
+
re.DOTALL
|
|
47
|
+
)
|
|
48
|
+
for m in pattern.finditer(content):
|
|
49
|
+
items.append({
|
|
50
|
+
"rb_id": m.group(1),
|
|
51
|
+
"title": m.group(2).strip(),
|
|
52
|
+
"description": m.group(3).strip(),
|
|
53
|
+
"content": m.group(4).strip(),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
# Enrich with schema metadata
|
|
57
|
+
schema_pattern = re.compile(
|
|
58
|
+
r'```yaml\n'
|
|
59
|
+
r'rb_id: "(RB-\d+)"\n'
|
|
60
|
+
r'title: "([^"]+)"\n'
|
|
61
|
+
r'description: "([^"]+)"\n'
|
|
62
|
+
r'task_type: "([^"]*)"\n'
|
|
63
|
+
r'subsystem: "([^"]*)"\n'
|
|
64
|
+
r'source_runs: (.+?)\n'
|
|
65
|
+
r'applies_to: (.+?)\n'
|
|
66
|
+
r'success_rate: ([\d.]+)\n'
|
|
67
|
+
r'status: (\w+)\n'
|
|
68
|
+
r'created_at: "([^"]+)"\n'
|
|
69
|
+
r'```',
|
|
70
|
+
re.DOTALL
|
|
71
|
+
)
|
|
72
|
+
for m in schema_pattern.finditer(content):
|
|
73
|
+
rb_id = m.group(1)
|
|
74
|
+
for item in items:
|
|
75
|
+
if item["rb_id"] == rb_id:
|
|
76
|
+
item["task_type"] = m.group(4)
|
|
77
|
+
item["subsystem"] = m.group(5)
|
|
78
|
+
item["success_rate"] = float(m.group(8))
|
|
79
|
+
item["status"] = m.group(9)
|
|
80
|
+
item["created_at"] = m.group(10)
|
|
81
|
+
# Parse JSON arrays
|
|
82
|
+
try:
|
|
83
|
+
sr = m.group(6)
|
|
84
|
+
item["source_runs"] = json.loads(sr) if sr.strip() else []
|
|
85
|
+
except Exception:
|
|
86
|
+
item["source_runs"] = []
|
|
87
|
+
try:
|
|
88
|
+
at = m.group(7)
|
|
89
|
+
item["applies_to"] = json.loads(at) if at.strip() else []
|
|
90
|
+
except Exception:
|
|
91
|
+
item["applies_to"] = []
|
|
92
|
+
break
|
|
93
|
+
return items
|
|
94
|
+
|
|
95
|
+
def load_all_items(self) -> List[dict]:
|
|
96
|
+
items = []
|
|
97
|
+
for d in (self.domains_dir, self.training_dir):
|
|
98
|
+
if not d.exists():
|
|
99
|
+
continue
|
|
100
|
+
for f in sorted(d.glob("*.md")):
|
|
101
|
+
items.extend(self._parse_items_from_file(f))
|
|
102
|
+
return [item for item in items if item.get("status") != "deprecated"]
|
|
103
|
+
|
|
104
|
+
def get_relevant(self, task_type: str = "", file_paths: List[str] = None, subsystem: str = "") -> List[dict]:
|
|
105
|
+
"""Score and filter items by relevance. Returns sorted list with scores."""
|
|
106
|
+
all_items = self.load_all_items()
|
|
107
|
+
file_paths = file_paths or []
|
|
108
|
+
scored = []
|
|
109
|
+
|
|
110
|
+
for item in all_items:
|
|
111
|
+
score = 0
|
|
112
|
+
applies_to = item.get("applies_to", [])
|
|
113
|
+
item_task = item.get("task_type", "")
|
|
114
|
+
item_subsystem = item.get("subsystem", "")
|
|
115
|
+
|
|
116
|
+
# Task type match (strong signal)
|
|
117
|
+
if task_type and item_task and task_type.lower() in item_task.lower():
|
|
118
|
+
score += 5
|
|
119
|
+
|
|
120
|
+
# Subsystem match (strong signal)
|
|
121
|
+
if subsystem and item_subsystem and subsystem.lower() in item_subsystem.lower():
|
|
122
|
+
score += 4
|
|
123
|
+
|
|
124
|
+
# File path overlap (medium signal)
|
|
125
|
+
for f in file_paths:
|
|
126
|
+
parts = f.split("/")
|
|
127
|
+
for part in parts:
|
|
128
|
+
if len(part) > 2:
|
|
129
|
+
for tag in applies_to:
|
|
130
|
+
if part.lower() in tag.lower():
|
|
131
|
+
score += 2
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
# Content overlap (weak signal)
|
|
135
|
+
for f in file_paths:
|
|
136
|
+
content = f"{item.get('title', '')} {item.get('description', '')} {item.get('content', '')}"
|
|
137
|
+
if f.lower() in content.lower():
|
|
138
|
+
score += 1
|
|
139
|
+
|
|
140
|
+
# Success rate bonus
|
|
141
|
+
sr = item.get("success_rate", 0)
|
|
142
|
+
if isinstance(sr, (int, float)) and sr > 0.8:
|
|
143
|
+
score += 1
|
|
144
|
+
|
|
145
|
+
if score > 0:
|
|
146
|
+
item["_relevance_score"] = score
|
|
147
|
+
scored.append(item)
|
|
148
|
+
|
|
149
|
+
scored.sort(key=lambda x: x["_relevance_score"], reverse=True)
|
|
150
|
+
return scored
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def main():
|
|
154
|
+
parser = argparse.ArgumentParser(description="MCP server for repo experiences")
|
|
155
|
+
parser.add_argument("--repo-root", type=str, required=True)
|
|
156
|
+
args = parser.parse_args()
|
|
157
|
+
|
|
158
|
+
repo_root = Path(args.repo_root).resolve()
|
|
159
|
+
rb = ReasoningBankReader(repo_root)
|
|
160
|
+
|
|
161
|
+
print("MCP server (ReasoningBank) starting...", file=sys.stderr)
|
|
162
|
+
print(f"Repo root: {repo_root}", file=sys.stderr)
|
|
163
|
+
|
|
164
|
+
while True:
|
|
165
|
+
try:
|
|
166
|
+
line = input()
|
|
167
|
+
msg = json.loads(line)
|
|
168
|
+
method = msg.get("method", "")
|
|
169
|
+
msg_id = msg.get("id")
|
|
170
|
+
|
|
171
|
+
if method == "initialize":
|
|
172
|
+
print(json.dumps({
|
|
173
|
+
"jsonrpc": "2.0", "id": msg_id,
|
|
174
|
+
"result": {
|
|
175
|
+
"protocolVersion": "2024-11-05",
|
|
176
|
+
"capabilities": {"tools": {}},
|
|
177
|
+
"serverInfo": {"name": "recursive-training-mcp", "version": "2.0.0"},
|
|
178
|
+
},
|
|
179
|
+
}), flush=True)
|
|
180
|
+
|
|
181
|
+
elif method == "tools/list":
|
|
182
|
+
print(json.dumps({
|
|
183
|
+
"jsonrpc": "2.0", "id": msg_id,
|
|
184
|
+
"result": {
|
|
185
|
+
"tools": [
|
|
186
|
+
{
|
|
187
|
+
"name": "get_repo_experiences",
|
|
188
|
+
"description": "Get ReasoningBank-structured experiences for the current repository",
|
|
189
|
+
"inputSchema": {
|
|
190
|
+
"type": "object",
|
|
191
|
+
"properties": {
|
|
192
|
+
"task_type": {
|
|
193
|
+
"type": "string",
|
|
194
|
+
"description": "Type of task (e.g., commit-remediation, add-api-endpoint)",
|
|
195
|
+
},
|
|
196
|
+
"file_paths": {
|
|
197
|
+
"type": "array",
|
|
198
|
+
"items": {"type": "string"},
|
|
199
|
+
"description": "List of file paths being modified",
|
|
200
|
+
},
|
|
201
|
+
"subsystem": {
|
|
202
|
+
"type": "string",
|
|
203
|
+
"description": "Target subsystem (e.g., packages/artifacts)",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
}), flush=True)
|
|
211
|
+
|
|
212
|
+
elif method == "tools/call":
|
|
213
|
+
params = msg.get("params", {})
|
|
214
|
+
tool_name = params.get("name", "")
|
|
215
|
+
arguments = params.get("arguments", {})
|
|
216
|
+
|
|
217
|
+
if tool_name == "get_repo_experiences":
|
|
218
|
+
task_type = arguments.get("task_type", "")
|
|
219
|
+
file_paths = arguments.get("file_paths", [])
|
|
220
|
+
subsystem = arguments.get("subsystem", "")
|
|
221
|
+
items = rb.get_relevant(task_type, file_paths, subsystem)
|
|
222
|
+
|
|
223
|
+
if items:
|
|
224
|
+
lines = ["# Repository Experiential Knowledge (ReasoningBank)\n"]
|
|
225
|
+
for item in items[:20]: # top 20
|
|
226
|
+
rb_id = item["rb_id"]
|
|
227
|
+
title = item["title"]
|
|
228
|
+
desc = item["description"]
|
|
229
|
+
content = item["content"]
|
|
230
|
+
score = item.get("_relevance_score", 0)
|
|
231
|
+
sr = item.get("success_rate")
|
|
232
|
+
sr_str = f" [success: {sr:.0%}]" if isinstance(sr, float) else ""
|
|
233
|
+
applies = ", ".join(item.get("applies_to", []))
|
|
234
|
+
lines.append(
|
|
235
|
+
f"## [{rb_id}] {title}{sr_str} (score: {score})\n"
|
|
236
|
+
f"{desc}\n\n{content}\n"
|
|
237
|
+
f"*Applies to: {applies}*\n"
|
|
238
|
+
)
|
|
239
|
+
result_text = "\n".join(lines)
|
|
240
|
+
else:
|
|
241
|
+
result_text = "No relevant experiences found for this task and file set."
|
|
242
|
+
|
|
243
|
+
print(json.dumps({
|
|
244
|
+
"jsonrpc": "2.0", "id": msg_id,
|
|
245
|
+
"result": {"content": [{"type": "text", "text": result_text}]},
|
|
246
|
+
}), flush=True)
|
|
247
|
+
|
|
248
|
+
else:
|
|
249
|
+
print(json.dumps({
|
|
250
|
+
"jsonrpc": "2.0", "id": msg_id,
|
|
251
|
+
"error": {"code": -32601, "message": f"Unknown tool: {tool_name}"},
|
|
252
|
+
}), flush=True)
|
|
253
|
+
|
|
254
|
+
except EOFError:
|
|
255
|
+
break
|
|
256
|
+
except Exception as e:
|
|
257
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
if __name__ == "__main__":
|
|
261
|
+
main()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
Post-Phase 8 training trigger for recursive-mode.
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Thin wrapper around recursive-training-phase8-trigger.py.
|
|
8
|
+
Extraction transport is owned by recursive-training-extract.py, not this wrapper.
|
|
9
|
+
|
|
10
|
+
.PARAMETER RepoRoot
|
|
11
|
+
Path to the git repository root.
|
|
12
|
+
|
|
13
|
+
.PARAMETER RunId
|
|
14
|
+
The run that just completed Phase 8.
|
|
15
|
+
|
|
16
|
+
.PARAMETER Auto
|
|
17
|
+
Skip user confirmation and run training immediately.
|
|
18
|
+
|
|
19
|
+
.PARAMETER GrpoArgs
|
|
20
|
+
Extra arguments passed through to recursive-training-grpo.py
|
|
21
|
+
(for example: '--winner-only-threshold 3').
|
|
22
|
+
|
|
23
|
+
.EXAMPLE
|
|
24
|
+
.\recursive-training-phase8-trigger.ps1 -RepoRoot . -RunId phase25 -Auto
|
|
25
|
+
#>
|
|
26
|
+
param(
|
|
27
|
+
[Parameter(Mandatory)]
|
|
28
|
+
[string]$RepoRoot,
|
|
29
|
+
|
|
30
|
+
[Parameter(Mandatory)]
|
|
31
|
+
[string]$RunId,
|
|
32
|
+
|
|
33
|
+
[switch]$Auto,
|
|
34
|
+
|
|
35
|
+
[string]$GrpoArgs = ""
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
$python = Get-Command python -ErrorAction SilentlyContinue
|
|
39
|
+
if (-not $python) {
|
|
40
|
+
$python = Get-Command python3 -ErrorAction SilentlyContinue
|
|
41
|
+
}
|
|
42
|
+
if (-not $python) {
|
|
43
|
+
Write-Error "Python is required but not found."
|
|
44
|
+
exit 1
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
48
|
+
$pyScript = Join-Path $scriptDir "recursive-training-phase8-trigger.py"
|
|
49
|
+
|
|
50
|
+
$argsList = @("--repo-root", $RepoRoot, "--run-id", $RunId)
|
|
51
|
+
if ($Auto) { $argsList += "--auto" }
|
|
52
|
+
if ($GrpoArgs) { $argsList += @("--grpo-args", $GrpoArgs) }
|
|
53
|
+
|
|
54
|
+
& $python.Source $pyScript @argsList
|
|
55
|
+
exit $LASTEXITCODE
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Post-Phase 8 training trigger for recursive-mode.
|
|
4
|
+
|
|
5
|
+
Run this immediately after Phase 8 (08-memory-impact.md) is locked.
|
|
6
|
+
Thin wrapper: checks preconditions, then delegates to recursive-training-grpo.py.
|
|
7
|
+
|
|
8
|
+
Usage (after Phase 8 lock):
|
|
9
|
+
python .recursive/scripts/recursive-training-phase8-trigger.py \
|
|
10
|
+
--repo-root . \
|
|
11
|
+
--run-id phase25-organizations-teams-rbac \
|
|
12
|
+
[--auto] \
|
|
13
|
+
[--grpo-args "--winner-only-threshold 3"]
|
|
14
|
+
|
|
15
|
+
Arguments:
|
|
16
|
+
--repo-root: Path to git repository root
|
|
17
|
+
--run-id: The run that just completed Phase 8
|
|
18
|
+
--auto: Skip the "Train now?" prompt and run immediately
|
|
19
|
+
--grpo-args: Extra arguments passed through to recursive-training-grpo.py
|
|
20
|
+
|
|
21
|
+
Exit codes:
|
|
22
|
+
0: OK (training wrote memory items)
|
|
23
|
+
1: Error (missing scripts, bridge failure, etc.)
|
|
24
|
+
2: User declined (or --auto not set and user not prompted) / extractor unavailable
|
|
25
|
+
3: Training ran but wrote zero items (insufficient groups / empty extraction)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import shlex
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def count_completed_runs(runs_dir: Path) -> int:
|
|
36
|
+
"""Count runs with a locked 08-memory-impact.md artifact."""
|
|
37
|
+
if not runs_dir.exists():
|
|
38
|
+
return 0
|
|
39
|
+
count = 0
|
|
40
|
+
for run_dir in runs_dir.iterdir():
|
|
41
|
+
if run_dir.is_dir():
|
|
42
|
+
phase8 = run_dir / "08-memory-impact.md"
|
|
43
|
+
if phase8.exists():
|
|
44
|
+
text = phase8.read_text(encoding="utf-8")
|
|
45
|
+
if "LockedAt" in text or "Status: LOCKED" in text:
|
|
46
|
+
count += 1
|
|
47
|
+
return count
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main() -> int:
|
|
51
|
+
parser = argparse.ArgumentParser(
|
|
52
|
+
description="Post-Phase 8 training trigger for recursive-mode"
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument("--repo-root", type=str, required=True)
|
|
55
|
+
parser.add_argument("--run-id", type=str, required=True)
|
|
56
|
+
parser.add_argument("--auto", action="store_true",
|
|
57
|
+
help="Run training immediately without prompting")
|
|
58
|
+
parser.add_argument("--grpo-args", type=str, default="",
|
|
59
|
+
help='Extra args for recursive-training-grpo.py, e.g. "--winner-only-threshold 3"')
|
|
60
|
+
args = parser.parse_args()
|
|
61
|
+
|
|
62
|
+
repo_root = Path(args.repo_root).resolve()
|
|
63
|
+
scripts_dir = repo_root / ".recursive" / "scripts"
|
|
64
|
+
runs_dir = repo_root / ".recursive" / "run"
|
|
65
|
+
grpo_script = scripts_dir / "recursive-training-grpo.py"
|
|
66
|
+
|
|
67
|
+
# --- Check scripts exist ---
|
|
68
|
+
if not grpo_script.exists():
|
|
69
|
+
print("ERROR: recursive-training-grpo.py not found.")
|
|
70
|
+
print("Run: python <SKILL_DIR>/scripts/install-recursive-mode.py --repo-root .")
|
|
71
|
+
return 1
|
|
72
|
+
|
|
73
|
+
# --- Check enough runs ---
|
|
74
|
+
completed = count_completed_runs(runs_dir)
|
|
75
|
+
if completed < 2:
|
|
76
|
+
print(f"Only {completed} completed run(s). Need 2+ for training.")
|
|
77
|
+
print("Training skipped. Complete more runs and try again.")
|
|
78
|
+
return 3
|
|
79
|
+
|
|
80
|
+
# --- Prompt or auto-run ---
|
|
81
|
+
if not args.auto:
|
|
82
|
+
print(f"Run '{args.run_id}' completed Phase 8.")
|
|
83
|
+
print(f"{completed} completed runs available.")
|
|
84
|
+
print("")
|
|
85
|
+
print("To extract learnings, run:")
|
|
86
|
+
print(f" python .recursive/scripts/recursive-training-grpo.py \\")
|
|
87
|
+
print(f" --repo-root . --incremental --run-id {args.run_id}")
|
|
88
|
+
if args.grpo_args:
|
|
89
|
+
print(f" {args.grpo_args}")
|
|
90
|
+
print("")
|
|
91
|
+
print("Or re-run this script with --auto to skip confirmation.")
|
|
92
|
+
return 2
|
|
93
|
+
|
|
94
|
+
# --- Run training ---
|
|
95
|
+
print(f"Auto-triggering training for {args.run_id} ({completed} runs)...")
|
|
96
|
+
|
|
97
|
+
def run_grpo(extra: list[str]) -> subprocess.CompletedProcess:
|
|
98
|
+
cmd = [sys.executable, str(grpo_script), "--repo-root", str(repo_root), *extra]
|
|
99
|
+
if args.grpo_args:
|
|
100
|
+
cmd.extend(shlex.split(args.grpo_args))
|
|
101
|
+
return subprocess.run(cmd)
|
|
102
|
+
|
|
103
|
+
result = run_grpo(["--incremental", "--run-id", args.run_id])
|
|
104
|
+
if result.returncode == 3:
|
|
105
|
+
print(
|
|
106
|
+
"Incremental subsystem group wrote 0 items; "
|
|
107
|
+
"falling back to full Phase-8-locked training..."
|
|
108
|
+
)
|
|
109
|
+
result = run_grpo([])
|
|
110
|
+
|
|
111
|
+
if result.returncode == 0:
|
|
112
|
+
print("Training extraction updated the memory plane.")
|
|
113
|
+
print(
|
|
114
|
+
"Future runs should read .recursive/memory/MEMORY.md and use "
|
|
115
|
+
"recursive-training-loader.py when experiential memory is relevant."
|
|
116
|
+
)
|
|
117
|
+
print("Done.")
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
if result.returncode == 2:
|
|
121
|
+
print("ERROR: Training extractor is unavailable. Memory plane was not updated.")
|
|
122
|
+
print(
|
|
123
|
+
"Wire RECURSIVE_TRAINING_EXTRACTOR_CMD, provide a response file, "
|
|
124
|
+
"or run agent-operated extraction."
|
|
125
|
+
)
|
|
126
|
+
return 2
|
|
127
|
+
|
|
128
|
+
if result.returncode == 3:
|
|
129
|
+
print("Training ran but extracted 0 items. Memory plane was not updated.")
|
|
130
|
+
return 3
|
|
131
|
+
|
|
132
|
+
print("ERROR: Training extraction failed.")
|
|
133
|
+
return result.returncode or 1
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
sys.exit(main())
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
Sync .recursive/memory/ to tool instruction files (PowerShell wrapper).
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Thin wrapper that calls recursive-training-sync.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-sync.py"
|
|
28
|
+
|
|
29
|
+
& $python.Source $pyScript @("--repo-root", $RepoRoot)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Read-only startup guidance for recursive-mode experiential memory.
|
|
4
|
+
|
|
5
|
+
Reads ReasoningBank-structured memory from .recursive/memory/domains/
|
|
6
|
+
and .recursive/memory/training/, then prints what an agent should read or load
|
|
7
|
+
at the start of a run.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python recursive-training-sync.py --repo-root .
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, List
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ReasoningBankReader:
|
|
20
|
+
"""Reads ReasoningBank-structured memory from the memory plane."""
|
|
21
|
+
|
|
22
|
+
MEMORY_ROOT = ".recursive/memory"
|
|
23
|
+
DOMAINS_DIR = ".recursive/memory/domains"
|
|
24
|
+
TRAINING_DIR = ".recursive/memory/training"
|
|
25
|
+
|
|
26
|
+
def __init__(self, repo_root: str):
|
|
27
|
+
self.root = Path(repo_root)
|
|
28
|
+
self.memory_root = self.root / self.MEMORY_ROOT
|
|
29
|
+
self.domains_dir = self.root / self.DOMAINS_DIR
|
|
30
|
+
self.training_dir = self.root / self.TRAINING_DIR
|
|
31
|
+
|
|
32
|
+
def _parse_items_from_file(self, filepath: Path) -> List[dict]:
|
|
33
|
+
"""Parse ReasoningBank items from a memory markdown file."""
|
|
34
|
+
if not filepath.exists():
|
|
35
|
+
return []
|
|
36
|
+
content = filepath.read_text(encoding="utf-8")
|
|
37
|
+
if "Status: CURRENT" not in content:
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
items = []
|
|
41
|
+
# Match ReasoningBank item blocks
|
|
42
|
+
pattern = re.compile(
|
|
43
|
+
r'### (RB-\d+):\s*(.+?)\n\n'
|
|
44
|
+
r'\*\*Description:\*\*\s*(.+?)\n\n'
|
|
45
|
+
r'\*\*Content:\*\*\s*(.+?)(?=\n\n```|\n### |\Z)',
|
|
46
|
+
re.DOTALL
|
|
47
|
+
)
|
|
48
|
+
for m in pattern.finditer(content):
|
|
49
|
+
items.append({
|
|
50
|
+
"rb_id": m.group(1),
|
|
51
|
+
"title": m.group(2).strip(),
|
|
52
|
+
"description": m.group(3).strip(),
|
|
53
|
+
"content": m.group(4).strip(),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
# Also parse YAML schema blocks for metadata
|
|
57
|
+
schema_pattern = re.compile(
|
|
58
|
+
r'```yaml\n'
|
|
59
|
+
r'rb_id: "(RB-\d+)"\n'
|
|
60
|
+
r'title: "([^"]+)"\n'
|
|
61
|
+
r'description: "([^"]+)"\n'
|
|
62
|
+
r'task_type: "([^"]*)"\n'
|
|
63
|
+
r'subsystem: "([^"]*)"\n'
|
|
64
|
+
r'source_runs: (.+?)\n'
|
|
65
|
+
r'applies_to: (.+?)\n'
|
|
66
|
+
r'success_rate: ([\d.]+)\n'
|
|
67
|
+
r'status: (\w+)\n'
|
|
68
|
+
r'created_at: "([^"]+)"\n'
|
|
69
|
+
r'```',
|
|
70
|
+
re.DOTALL
|
|
71
|
+
)
|
|
72
|
+
for m in schema_pattern.finditer(content):
|
|
73
|
+
rb_id = m.group(1)
|
|
74
|
+
for item in items:
|
|
75
|
+
if item["rb_id"] == rb_id:
|
|
76
|
+
item["task_type"] = m.group(4)
|
|
77
|
+
item["subsystem"] = m.group(5)
|
|
78
|
+
item["success_rate"] = float(m.group(8))
|
|
79
|
+
item["status"] = m.group(9)
|
|
80
|
+
item["created_at"] = m.group(10)
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
return items
|
|
84
|
+
|
|
85
|
+
def load_all_items(self) -> List[dict]:
|
|
86
|
+
"""Load all ReasoningBank items from domain + training memory."""
|
|
87
|
+
items = []
|
|
88
|
+
for d in (self.domains_dir, self.training_dir):
|
|
89
|
+
if not d.exists():
|
|
90
|
+
continue
|
|
91
|
+
for f in sorted(d.glob("*.md")):
|
|
92
|
+
items.extend(self._parse_items_from_file(f))
|
|
93
|
+
return items
|
|
94
|
+
|
|
95
|
+
def list_training_docs(self) -> Dict[str, dict]:
|
|
96
|
+
"""List all training memory docs with metadata."""
|
|
97
|
+
docs = {}
|
|
98
|
+
for d in (self.domains_dir, self.training_dir):
|
|
99
|
+
if not d.exists():
|
|
100
|
+
continue
|
|
101
|
+
for f in sorted(d.glob("*.md")):
|
|
102
|
+
content = f.read_text(encoding="utf-8")
|
|
103
|
+
if "Status: CURRENT" not in content:
|
|
104
|
+
continue
|
|
105
|
+
# Count items
|
|
106
|
+
item_count = len(re.findall(r'### RB-\d+:', content))
|
|
107
|
+
# Extract scope from frontmatter if present
|
|
108
|
+
scope_match = re.search(r'Scope:\s*(.+)', content)
|
|
109
|
+
scope = scope_match.group(1).strip() if scope_match else f.name.replace('.md', '')
|
|
110
|
+
docs[f.name] = {
|
|
111
|
+
"path": str(f.relative_to(self.memory_root)).replace('\\', '/'),
|
|
112
|
+
"type": "domain" if "domains" in str(f) else "training",
|
|
113
|
+
"scope": scope,
|
|
114
|
+
"items": item_count,
|
|
115
|
+
}
|
|
116
|
+
return docs
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ExperienceSync:
|
|
120
|
+
def __init__(self, repo_root: str):
|
|
121
|
+
self.repo_root = Path(repo_root)
|
|
122
|
+
self.rb_reader = ReasoningBankReader(repo_root)
|
|
123
|
+
|
|
124
|
+
def build_startup_guidance(self) -> str:
|
|
125
|
+
"""Render non-mutating startup guidance for agents."""
|
|
126
|
+
docs = self.rb_reader.list_training_docs()
|
|
127
|
+
lines = ["## Recursive Training Startup Guidance\n"]
|
|
128
|
+
lines.append(
|
|
129
|
+
"At the start of a run, read `/.recursive/memory/MEMORY.md` first.\n"
|
|
130
|
+
)
|
|
131
|
+
lines.append(
|
|
132
|
+
"If the task may benefit from prior experiential memory, run the loader with the current task context:\n\n"
|
|
133
|
+
"```bash\n"
|
|
134
|
+
"python .recursive/scripts/recursive-training-loader.py --repo-root . \\\n"
|
|
135
|
+
" --query \"your task description\" \\\n"
|
|
136
|
+
" --files \"path/to/file1.ts,path/to/file2.ts\"\n"
|
|
137
|
+
"```\n"
|
|
138
|
+
)
|
|
139
|
+
lines.append("The loader resolves relevant memory docs from the memory plane and returns the most applicable learnings.\n")
|
|
140
|
+
|
|
141
|
+
if not docs:
|
|
142
|
+
lines.append("No current domain or training memory docs were found under `/.recursive/memory/`.\n")
|
|
143
|
+
return "\n".join(lines)
|
|
144
|
+
|
|
145
|
+
# Group by type
|
|
146
|
+
domains = {k: v for k, v in docs.items() if v["type"] == "domain"}
|
|
147
|
+
training = {k: v for k, v in docs.items() if v["type"] == "training"}
|
|
148
|
+
|
|
149
|
+
if domains:
|
|
150
|
+
lines.append("### Domain Memory (subsystem-specific)\n")
|
|
151
|
+
lines.append("| Doc | Scope | Items | Description |")
|
|
152
|
+
lines.append("|-----|-------|-------|-------------|")
|
|
153
|
+
for name, info in sorted(domains.items()):
|
|
154
|
+
lines.append(f"| `{info['path']}` | {info['scope']} | {info['items']} items | subsystem-specific learnings |")
|
|
155
|
+
lines.append("")
|
|
156
|
+
|
|
157
|
+
if training:
|
|
158
|
+
lines.append("### Training Memory (task-type patterns)\n")
|
|
159
|
+
lines.append("| Doc | Scope | Items | Description |")
|
|
160
|
+
lines.append("|-----|-------|-------|-------------|")
|
|
161
|
+
for name, info in sorted(training.items()):
|
|
162
|
+
lines.append(f"| `{info['path']}` | {info['scope']} | {info['items']} items | cross-subsystem task patterns |")
|
|
163
|
+
lines.append("")
|
|
164
|
+
|
|
165
|
+
lines.append("---\n")
|
|
166
|
+
lines.append("This output is advisory only. The canonical memory remains under `/.recursive/memory/`.\n")
|
|
167
|
+
return "\n".join(lines)
|
|
168
|
+
|
|
169
|
+
def sync_all(self):
|
|
170
|
+
print(self.build_startup_guidance().strip())
|
|
171
|
+
|
|
172
|
+
def main():
|
|
173
|
+
parser = argparse.ArgumentParser(description="Print startup guidance for recursive-mode experiential memory")
|
|
174
|
+
parser.add_argument("--repo-root", type=str, required=True, help="Path to git repository root")
|
|
175
|
+
args = parser.parse_args()
|
|
176
|
+
|
|
177
|
+
sync = ExperienceSync(args.repo_root)
|
|
178
|
+
print("Training sync is read-only. Startup guidance:")
|
|
179
|
+
sync.sync_all()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__":
|
|
183
|
+
main()
|