@try-works/dsh-recursive-mode 0.1.5 → 0.1.7

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 (89) hide show
  1. package/lib/bootstrap.d.ts +6 -3
  2. package/lib/client/board.d.ts +4 -1
  3. package/lib/client/contract.d.ts +16 -13
  4. package/lib/client/index.d.ts +1 -1
  5. package/lib/client/open-state.d.ts +20 -0
  6. package/lib/client/slots.d.ts +13 -1
  7. package/lib/client.js +160 -71
  8. package/lib/index.js +3391 -906
  9. package/lib/runtime.d.ts +12 -5
  10. package/lib/ts-lint.d.ts +205 -0
  11. package/package.json +1 -1
  12. package/src/bootstrap.ts +338 -331
  13. package/src/client/board.tsx +0 -0
  14. package/src/client/contract.ts +19 -22
  15. package/src/client/index.ts +1 -1
  16. package/src/client/open-state.ts +46 -0
  17. package/src/client/slots.ts +67 -33
  18. package/src/commands.ts +11 -0
  19. package/src/index.ts +238 -228
  20. package/src/runtime.ts +19 -23
  21. package/src/ts-lint.ts +2110 -0
  22. package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  23. package/references/scripts/lint-recursive-run.ps1 +0 -25
  24. package/references/scripts/lint-recursive-run.py +0 -2870
  25. package/references/scripts/recursive-closeout.ps1 +0 -38
  26. package/references/scripts/recursive-closeout.py +0 -541
  27. package/references/scripts/recursive-init.ps1 +0 -515
  28. package/references/scripts/recursive-init.py +0 -356
  29. package/references/scripts/recursive-lock.ps1 +0 -26
  30. package/references/scripts/recursive-lock.py +0 -302
  31. package/references/scripts/recursive-review-bundle.ps1 +0 -58
  32. package/references/scripts/recursive-review-bundle.py +0 -503
  33. package/references/scripts/recursive-router-cli-configure.ps1 +0 -2
  34. package/references/scripts/recursive-router-cli-configure.py +0 -9
  35. package/references/scripts/recursive-router-cli-init.ps1 +0 -2
  36. package/references/scripts/recursive-router-cli-init.py +0 -9
  37. package/references/scripts/recursive-router-cli-invoke.ps1 +0 -2
  38. package/references/scripts/recursive-router-cli-invoke.py +0 -9
  39. package/references/scripts/recursive-router-cli-probe.ps1 +0 -2
  40. package/references/scripts/recursive-router-cli-probe.py +0 -9
  41. package/references/scripts/recursive-router-cli-resolve.ps1 +0 -2
  42. package/references/scripts/recursive-router-cli-resolve.py +0 -9
  43. package/references/scripts/recursive-router-cli-validate.ps1 +0 -2
  44. package/references/scripts/recursive-router-cli-validate.py +0 -9
  45. package/references/scripts/recursive-router-configure.ps1 +0 -27
  46. package/references/scripts/recursive-router-configure.py +0 -74
  47. package/references/scripts/recursive-router-init.ps1 +0 -17
  48. package/references/scripts/recursive-router-init.py +0 -29
  49. package/references/scripts/recursive-router-invoke.ps1 +0 -47
  50. package/references/scripts/recursive-router-invoke.py +0 -103
  51. package/references/scripts/recursive-router-probe.ps1 +0 -25
  52. package/references/scripts/recursive-router-probe.py +0 -44
  53. package/references/scripts/recursive-router-resolve.ps1 +0 -26
  54. package/references/scripts/recursive-router-resolve.py +0 -46
  55. package/references/scripts/recursive-router-validate.ps1 +0 -17
  56. package/references/scripts/recursive-router-validate.py +0 -27
  57. package/references/scripts/recursive-status.ps1 +0 -23
  58. package/references/scripts/recursive-status.py +0 -2124
  59. package/references/scripts/recursive-subagent-action.ps1 +0 -98
  60. package/references/scripts/recursive-subagent-action.py +0 -197
  61. package/references/scripts/recursive-training-extract.ps1 +0 -23
  62. package/references/scripts/recursive-training-extract.py +0 -99
  63. package/references/scripts/recursive-training-grpo.ps1 +0 -52
  64. package/references/scripts/recursive-training-grpo.py +0 -1341
  65. package/references/scripts/recursive-training-loader.ps1 +0 -78
  66. package/references/scripts/recursive-training-loader.py +0 -558
  67. package/references/scripts/recursive-training-mcp.ps1 +0 -29
  68. package/references/scripts/recursive-training-mcp.py +0 -261
  69. package/references/scripts/recursive-training-phase8-trigger.ps1 +0 -55
  70. package/references/scripts/recursive-training-phase8-trigger.py +0 -137
  71. package/references/scripts/recursive-training-sync.ps1 +0 -29
  72. package/references/scripts/recursive-training-sync.py +0 -183
  73. package/references/scripts/recursive_phase_rules.py +0 -367
  74. package/references/scripts/recursive_router_cli_lib.py +0 -2
  75. package/references/scripts/recursive_router_lib.py +0 -2282
  76. package/references/scripts/verify-locks.ps1 +0 -25
  77. package/references/scripts/verify-locks.py +0 -353
  78. package/scripts/__pycache__/lint-recursive-run.cpython-314.pyc +0 -0
  79. package/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
  80. package/scripts/install-recursive-mode.ps1 +0 -956
  81. package/scripts/install-recursive-mode.py +0 -750
  82. package/scripts/lint-recursive-run.py +0 -2870
  83. package/scripts/recursive-closeout.py +0 -541
  84. package/scripts/recursive-init.py +0 -356
  85. package/scripts/recursive-lock.py +0 -302
  86. package/scripts/recursive-status.py +0 -2124
  87. package/scripts/recursive_phase_rules.py +0 -367
  88. package/scripts/recursive_router_lib.py +0 -2282
  89. package/scripts/verify-locks.py +0 -353
@@ -1,58 +0,0 @@
1
- [CmdletBinding()]
2
- param(
3
- [Parameter(Mandatory = $true)][string]$RunId,
4
- [Parameter(Mandatory = $true)][string]$Phase,
5
- [Parameter(Mandatory = $true)][string]$Role,
6
- [Parameter(Mandatory = $true)][string]$ArtifactPath,
7
- [string]$RepoRoot = (Get-Location).Path,
8
- [string[]]$UpstreamArtifact = @(),
9
- [string[]]$Addendum = @(),
10
- [string[]]$PriorRef = @(),
11
- [string[]]$ControlDoc = @(),
12
- [string[]]$CodeRef = @(),
13
- [string[]]$EvidenceRef = @(),
14
- [string[]]$AuditQuestion = @(),
15
- [string[]]$RequiredOutput = @(),
16
- [string]$RoutingConfigPath = "",
17
- [string]$RoutingDiscoveryPath = "",
18
- [string]$RoutedCli = "",
19
- [string]$RoutedModel = "",
20
- [string]$OutputName = "",
21
- [switch]$NoAutoAddenda
22
- )
23
-
24
- Set-StrictMode -Version Latest
25
- $ErrorActionPreference = "Stop"
26
-
27
- $python = if ($env:PYTHON) { $env:PYTHON } elseif (Get-Command python -ErrorAction SilentlyContinue) { "python" } else { $null }
28
- if (-not $python) {
29
- Write-Host "[FAIL] Python executable not found in PATH."
30
- exit 1
31
- }
32
-
33
- $scriptPath = Join-Path $PSScriptRoot "recursive-review-bundle.py"
34
- $argsList = @(
35
- $scriptPath,
36
- "--repo-root", $RepoRoot,
37
- "--run-id", $RunId,
38
- "--phase", $Phase,
39
- "--role", $Role,
40
- "--artifact-path", $ArtifactPath
41
- )
42
- if ($OutputName) { $argsList += @("--output-name", $OutputName) }
43
- if ($NoAutoAddenda.IsPresent) { $argsList += "--no-auto-addenda" }
44
- foreach ($value in @($UpstreamArtifact)) { foreach ($piece in ($value -split ",")) { if ($piece.Trim()) { $argsList += @("--upstream-artifact", $piece.Trim()) } } }
45
- foreach ($value in @($Addendum)) { foreach ($piece in ($value -split ",")) { if ($piece.Trim()) { $argsList += @("--addendum", $piece.Trim()) } } }
46
- foreach ($value in @($PriorRef)) { foreach ($piece in ($value -split ",")) { if ($piece.Trim()) { $argsList += @("--prior-ref", $piece.Trim()) } } }
47
- foreach ($value in @($ControlDoc)) { foreach ($piece in ($value -split ",")) { if ($piece.Trim()) { $argsList += @("--control-doc", $piece.Trim()) } } }
48
- foreach ($value in @($CodeRef)) { foreach ($piece in ($value -split ",")) { if ($piece.Trim()) { $argsList += @("--code-ref", $piece.Trim()) } } }
49
- foreach ($value in @($EvidenceRef)) { foreach ($piece in ($value -split ",")) { if ($piece.Trim()) { $argsList += @("--evidence-ref", $piece.Trim()) } } }
50
- foreach ($value in @($AuditQuestion)) { foreach ($piece in ($value -split ",")) { if ($piece.Trim()) { $argsList += @("--audit-question", $piece.Trim()) } } }
51
- foreach ($value in @($RequiredOutput)) { foreach ($piece in ($value -split ",")) { if ($piece.Trim()) { $argsList += @("--required-output", $piece.Trim()) } } }
52
- if ($RoutingConfigPath) { $argsList += @("--routing-config-path", $RoutingConfigPath) }
53
- if ($RoutingDiscoveryPath) { $argsList += @("--routing-discovery-path", $RoutingDiscoveryPath) }
54
- if ($RoutedCli) { $argsList += @("--routed-cli", $RoutedCli) }
55
- if ($RoutedModel) { $argsList += @("--routed-model", $RoutedModel) }
56
-
57
- & $python @argsList
58
- exit $LASTEXITCODE
@@ -1,503 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Generate a canonical recursive-mode review bundle for delegated audit or review.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- import argparse
9
- import hashlib
10
- import re
11
- import subprocess
12
- import sys
13
- from datetime import datetime, timezone
14
- from pathlib import Path
15
-
16
- TRANSIENT_RUNTIME_DIR_MARKERS = {"__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".hypothesis", ".tox", ".nox"}
17
- TRANSIENT_RUNTIME_FILE_NAMES = {".ds_store", "thumbs.db"}
18
- TRANSIENT_RUNTIME_SUFFIXES = (".pyc", ".pyo", ".pyd")
19
- DIFF_BASIS_ALLOWED_TYPES = {"local commit", "local branch", "remote ref", "merge-base derived"}
20
- WORKING_TREE_COMPARISON_REFS = {"working-tree", "working-tree@head", "worktree", "working-tree+head"}
21
-
22
-
23
- def trim_md_value(value: str) -> str:
24
- return value.strip().strip("`\"'")
25
-
26
-
27
- def get_md_field_value(content: str, field_name: str) -> str | None:
28
- pattern = re.compile(rf"(?m)^[ \t]*(?:[-*][ \t]+)?{re.escape(field_name)}:\s*(.+?)\s*$")
29
- match = pattern.search(content)
30
- if not match:
31
- return None
32
- return trim_md_value(match.group(1))
33
-
34
-
35
- def content_sha256(content: str) -> str:
36
- normalized = content.replace("\r\n", "\n").replace("\r", "\n")
37
- return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
38
-
39
-
40
- def run_git(repo_root: Path, *args: str) -> tuple[str | None, str | None]:
41
- try:
42
- result = subprocess.run(
43
- ["git", "-C", str(repo_root), *args],
44
- check=False,
45
- capture_output=True,
46
- text=True,
47
- )
48
- except OSError as exc:
49
- return None, f"Unable to execute git: {exc}"
50
- if result.returncode != 0:
51
- message = result.stderr.strip() or result.stdout.strip() or f"git {' '.join(args)} failed"
52
- return None, message
53
- return result.stdout.strip(), None
54
-
55
-
56
- def normalize_baseline_type(value: str | None) -> str | None:
57
- if value is None:
58
- return None
59
- compact = trim_md_value(value).strip().lower().replace("-", " ")
60
- compact = re.sub(r"\s+", " ", compact)
61
- if compact in DIFF_BASIS_ALLOWED_TYPES:
62
- return compact
63
- aliases = {
64
- "commit": "local commit",
65
- "branch": "local branch",
66
- "remote": "remote ref",
67
- "remote branch": "remote ref",
68
- "merge base": "merge-base derived",
69
- }
70
- return aliases.get(compact)
71
-
72
-
73
- def normalize_comparison_reference(value: str | None) -> str | None:
74
- if value is None:
75
- return None
76
- compact = trim_md_value(value).strip()
77
- if not compact:
78
- return None
79
- if compact.lower() in WORKING_TREE_COMPARISON_REFS:
80
- return "working-tree"
81
- return compact
82
-
83
-
84
- def parse_diff_basis_source(content: str) -> str:
85
- diff_body = re.search(r"(?ms)^##\s+Diff Basis For Later Audits\s*$\n?(.*?)(?=^##\s+|\Z)", content)
86
- if diff_body:
87
- return diff_body.group(1)
88
- diff_body = re.search(r"(?ms)^##\s+Diff Basis\s*$\n?(.*?)(?=^##\s+|\Z)", content)
89
- if diff_body:
90
- return diff_body.group(1)
91
- return content
92
-
93
-
94
- def get_run_diff_basis(run_dir: Path) -> dict[str, str | None]:
95
- worktree_path = run_dir / "00-worktree.md"
96
- if not worktree_path.exists():
97
- return {
98
- "baseline_type": None,
99
- "baseline_reference": None,
100
- "comparison_reference": None,
101
- "normalized_baseline": None,
102
- "normalized_comparison": None,
103
- "normalized_diff_command": None,
104
- "base_branch": None,
105
- "worktree_branch": None,
106
- "notes": None,
107
- }
108
-
109
- content = worktree_path.read_text(encoding="utf-8")
110
- source = parse_diff_basis_source(content)
111
- return {
112
- "baseline_type": get_md_field_value(source, "Baseline type"),
113
- "baseline_reference": get_md_field_value(source, "Baseline reference"),
114
- "comparison_reference": get_md_field_value(source, "Comparison reference"),
115
- "normalized_baseline": (
116
- get_md_field_value(source, "Normalized baseline")
117
- or get_md_field_value(source, "Normalized baseline commit")
118
- or get_md_field_value(source, "Base commit")
119
- ),
120
- "normalized_comparison": (
121
- get_md_field_value(source, "Normalized comparison")
122
- or get_md_field_value(source, "Normalized comparison reference")
123
- or get_md_field_value(source, "Worktree branch")
124
- ),
125
- "normalized_diff_command": (
126
- get_md_field_value(source, "Normalized diff command")
127
- or get_md_field_value(source, "Diff command convention")
128
- ),
129
- "base_branch": get_md_field_value(source, "Base branch"),
130
- "worktree_branch": get_md_field_value(source, "Worktree branch"),
131
- "notes": get_md_field_value(source, "Diff basis notes") or get_md_field_value(source, "Notes"),
132
- }
133
-
134
-
135
- def normalize_diff_basis(repo_root: Path, diff_basis: dict[str, str | None]) -> tuple[dict[str, str] | None, str | None]:
136
- baseline_type = normalize_baseline_type(diff_basis.get("baseline_type"))
137
- baseline_reference = trim_md_value(diff_basis.get("baseline_reference") or "")
138
- comparison_reference = normalize_comparison_reference(diff_basis.get("comparison_reference"))
139
- normalized_baseline = trim_md_value(diff_basis.get("normalized_baseline") or "")
140
- normalized_comparison = normalize_comparison_reference(diff_basis.get("normalized_comparison"))
141
- normalized_diff_command = trim_md_value(diff_basis.get("normalized_diff_command") or "")
142
-
143
- missing_fields = []
144
- if not baseline_type:
145
- missing_fields.append("Baseline type")
146
- if not baseline_reference:
147
- missing_fields.append("Baseline reference")
148
- if not comparison_reference:
149
- missing_fields.append("Comparison reference")
150
- if not normalized_baseline:
151
- missing_fields.append("Normalized baseline")
152
- if not normalized_comparison:
153
- missing_fields.append("Normalized comparison")
154
- if not normalized_diff_command:
155
- missing_fields.append("Normalized diff command")
156
- if missing_fields:
157
- return None, f"Diff basis is missing required field(s): {', '.join(missing_fields)}"
158
-
159
- comparison_git_ref = "HEAD" if normalized_comparison == "working-tree" else normalized_comparison
160
- if baseline_type == "merge-base derived":
161
- computed_baseline, error = run_git(repo_root, "merge-base", comparison_git_ref, baseline_reference)
162
- if error:
163
- return None, f"Unable to compute merge-base for diff basis: {error}"
164
- else:
165
- computed_baseline, error = run_git(repo_root, "rev-parse", "--verify", f"{baseline_reference}^{{commit}}")
166
- if error:
167
- return None, f"Unable to resolve baseline reference '{baseline_reference}': {error}"
168
-
169
- if normalized_baseline != computed_baseline:
170
- return None, (
171
- "Recorded Normalized baseline does not match the executable diff basis "
172
- f"({normalized_baseline} != {computed_baseline})"
173
- )
174
-
175
- if normalized_comparison == "working-tree":
176
- expected_command = f"git diff --name-only {computed_baseline}"
177
- git_args = ["diff", "--name-only", computed_baseline]
178
- else:
179
- computed_comparison, error = run_git(repo_root, "rev-parse", "--verify", f"{normalized_comparison}^{{commit}}")
180
- if error:
181
- return None, f"Unable to resolve comparison reference '{normalized_comparison}': {error}"
182
- expected_command = f"git diff --name-only {computed_baseline}..{computed_comparison}"
183
- git_args = ["diff", "--name-only", f"{computed_baseline}..{computed_comparison}"]
184
- if normalized_comparison != computed_comparison:
185
- return None, (
186
- "Recorded Normalized comparison does not match the executable diff basis "
187
- f"({normalized_comparison} != {computed_comparison})"
188
- )
189
-
190
- if normalized_diff_command != expected_command:
191
- return None, (
192
- "Recorded Normalized diff command does not match the executable diff basis "
193
- f"({normalized_diff_command} != {expected_command})"
194
- )
195
-
196
- return {
197
- "baseline_type": baseline_type,
198
- "baseline_reference": baseline_reference,
199
- "comparison_reference": comparison_reference,
200
- "normalized_baseline": computed_baseline,
201
- "normalized_comparison": normalized_comparison,
202
- "normalized_diff_command": expected_command,
203
- "comparison_git_ref": comparison_git_ref,
204
- "git_args": git_args,
205
- }, None
206
-
207
-
208
- def get_git_changed_files(repo_root: Path, diff_basis: dict[str, str | None]) -> tuple[list[str] | None, str | None]:
209
- normalized_basis, basis_error = normalize_diff_basis(repo_root, diff_basis)
210
- if basis_error:
211
- return None, basis_error
212
-
213
- try:
214
- diff_result = subprocess.run(
215
- ["git", "-C", str(repo_root), *normalized_basis["git_args"]],
216
- check=False,
217
- capture_output=True,
218
- text=True,
219
- )
220
- untracked_result = subprocess.run(
221
- ["git", "-C", str(repo_root), "ls-files", "--others", "--exclude-standard"],
222
- check=False,
223
- capture_output=True,
224
- text=True,
225
- )
226
- except OSError as exc:
227
- return None, f"Unable to execute git: {exc}"
228
-
229
- if diff_result.returncode != 0:
230
- message = diff_result.stderr.strip() or diff_result.stdout.strip() or "git diff failed"
231
- return None, message
232
- if untracked_result.returncode != 0:
233
- message = untracked_result.stderr.strip() or untracked_result.stdout.strip() or "git ls-files failed"
234
- return None, message
235
-
236
- tracked_changed = [path.strip() for path in diff_result.stdout.splitlines() if path.strip()]
237
- untracked_changed = [
238
- path.strip()
239
- for path in untracked_result.stdout.splitlines()
240
- if path.strip() and not is_transient_runtime_path(path.strip())
241
- ]
242
- changed = tracked_changed + untracked_changed
243
- return sorted(set(path.strip() for path in changed if path.strip())), None
244
-
245
-
246
- def normalize_repo_path(raw_path: str) -> str:
247
- return raw_path.replace("\\", "/").strip().lstrip("/")
248
-
249
-
250
- def is_transient_runtime_path(normalized_path: str) -> bool:
251
- candidate = normalize_repo_path(normalized_path)
252
- if not candidate:
253
- return False
254
- parts = Path(candidate).parts
255
- if any(part in TRANSIENT_RUNTIME_DIR_MARKERS for part in parts):
256
- return True
257
- file_name = Path(candidate).name.lower()
258
- if file_name in TRANSIENT_RUNTIME_FILE_NAMES:
259
- return True
260
- return file_name.endswith(TRANSIENT_RUNTIME_SUFFIXES)
261
-
262
-
263
- def filter_runtime_changed_files(paths: list[str], run_id: str) -> list[str]:
264
- filtered: list[str] = []
265
- for raw_path in paths:
266
- normalized = normalize_repo_path(raw_path)
267
- if not normalized:
268
- continue
269
- if normalized.startswith(f".recursive/run/{run_id}/"):
270
- continue
271
- if is_transient_runtime_path(normalized):
272
- continue
273
- filtered.append(normalized)
274
- return sorted(set(filtered))
275
-
276
-
277
- def get_stage_local_addenda_paths(run_dir: Path, artifact_name: str) -> list[Path]:
278
- addenda_dir = run_dir / "addenda"
279
- if not addenda_dir.exists():
280
- return []
281
- base_name = artifact_name[:-3] if artifact_name.endswith(".md") else artifact_name
282
- return sorted(addenda_dir.glob(f"{base_name}.addendum-*.md"))
283
-
284
-
285
- def get_current_phase_addenda_paths(run_dir: Path, artifact_name: str) -> list[Path]:
286
- addenda_dir = run_dir / "addenda"
287
- if not addenda_dir.exists():
288
- return []
289
- base_name = artifact_name[:-3] if artifact_name.endswith(".md") else artifact_name
290
- matches = list(get_stage_local_addenda_paths(run_dir, artifact_name))
291
- matches.extend(sorted(addenda_dir.glob(f"{base_name}.upstream-gap.*.addendum-*.md")))
292
- unique: dict[str, Path] = {}
293
- for path in matches:
294
- unique[str(path.resolve())] = path
295
- return [unique[key] for key in sorted(unique)]
296
-
297
-
298
- def auto_discover_addenda(run_dir: Path, run_id: str, artifact_path: str, upstream_artifacts: list[str]) -> list[str]:
299
- run_prefix = f".recursive/run/{run_id}/"
300
- discovered: list[str] = []
301
-
302
- for artifact in upstream_artifacts:
303
- normalized = normalize_repo_path(artifact)
304
- if not normalized.startswith(run_prefix):
305
- continue
306
- relative = normalized[len(run_prefix):]
307
- if relative.startswith("addenda/"):
308
- continue
309
- for addendum_path in get_stage_local_addenda_paths(run_dir, Path(relative).name):
310
- discovered.append(f"{run_prefix}addenda/{addendum_path.name}")
311
-
312
- normalized_artifact_path = normalize_repo_path(artifact_path)
313
- if normalized_artifact_path.startswith(run_prefix):
314
- artifact_name = Path(normalized_artifact_path[len(run_prefix):]).name
315
- for addendum_path in get_current_phase_addenda_paths(run_dir, artifact_name):
316
- discovered.append(f"{run_prefix}addenda/{addendum_path.name}")
317
-
318
- return sorted(set(discovered))
319
-
320
-
321
- def auto_discover_skill_memory_refs(repo_root: Path, phase: str, role: str) -> list[str]:
322
- skills_router = ".recursive/memory/skills/SKILLS.md"
323
- discovered: list[str] = []
324
- if (repo_root / skills_router).exists():
325
- discovered.append(skills_router)
326
-
327
- skills_root = repo_root / ".recursive" / "memory" / "skills"
328
- if not skills_root.exists():
329
- return discovered
330
-
331
- query_tokens = set(re.findall(r"[a-z0-9]+", f"{phase} {role}".lower()))
332
- if not query_tokens:
333
- return discovered
334
-
335
- for path in sorted(skills_root.rglob("*.md")):
336
- relative = normalize_repo_path(str(path.relative_to(repo_root)))
337
- if relative == skills_router:
338
- continue
339
- stem_tokens = set(re.findall(r"[a-z0-9]+", path.stem.lower()))
340
- if query_tokens & stem_tokens:
341
- discovered.append(relative)
342
- return sorted(set(discovered))
343
-
344
-
345
- def slugify(value: str) -> str:
346
- lowered = value.strip().lower()
347
- slug = re.sub(r"[^a-z0-9]+", "-", lowered).strip("-")
348
- return slug or "bundle"
349
-
350
-
351
- def render_list(title: str, values: list[str], *, indent: str = "- ") -> list[str]:
352
- lines = [title]
353
- if not values:
354
- lines.append(f"{indent}none")
355
- return lines
356
- lines.extend(f"{indent}`{value}`" for value in values)
357
- return lines
358
-
359
-
360
- def main() -> int:
361
- parser = argparse.ArgumentParser(description="Generate a canonical recursive-mode review bundle.")
362
- parser.add_argument("--repo-root", default=".", help="Repository root path.")
363
- parser.add_argument("--run-id", required=True, help="Run ID under .recursive/run/.")
364
- parser.add_argument("--phase", required=True, help="Phase name for the bundle, e.g. '03.5 Code Review'.")
365
- parser.add_argument("--role", required=True, help="Delegated role, e.g. analyst, code-reviewer, tester.")
366
- parser.add_argument("--artifact-path", required=True, help="Repo-relative artifact path the review is for.")
367
- parser.add_argument("--upstream-artifact", action="append", default=[], help="Repo-relative upstream artifact path. Repeat as needed.")
368
- parser.add_argument("--addendum", action="append", default=[], help="Repo-relative addendum path. Repeat as needed.")
369
- parser.add_argument("--prior-ref", action="append", default=[], help="Repo-relative prior run or recursive memory path. Repeat as needed.")
370
- parser.add_argument("--control-doc", action="append", default=[], help="Repo-relative control-plane doc path. Repeat as needed.")
371
- parser.add_argument("--code-ref", action="append", default=[], help="Repo-relative code file to inspect. Repeat as needed.")
372
- parser.add_argument("--evidence-ref", action="append", default=[], help="Repo-relative evidence artifact path. Repeat as needed.")
373
- parser.add_argument("--audit-question", action="append", default=[], help="Audit or review question. Repeat as needed.")
374
- parser.add_argument("--required-output", action="append", default=[], help="Required output bullet. Repeat as needed.")
375
- parser.add_argument("--routing-config-path", default="", help="Repo-relative routing policy path for routed delegation.")
376
- parser.add_argument("--routing-discovery-path", default="", help="Repo-relative routing discovery path for routed delegation.")
377
- parser.add_argument("--routed-cli", default="", help="Resolved routed CLI id if applicable.")
378
- parser.add_argument("--routed-model", default="", help="Resolved routed model id if applicable.")
379
- parser.add_argument("--output-name", default="", help="Optional bundle filename under evidence/review-bundles/.")
380
- parser.add_argument("--no-auto-addenda", action="store_true", help="Disable automatic addenda discovery for upstream artifacts and the current phase.")
381
- args = parser.parse_args()
382
-
383
- repo_root = Path(args.repo_root).resolve()
384
- run_dir = repo_root / ".recursive" / "run" / args.run_id.strip()
385
- if not run_dir.exists():
386
- print(f"[FAIL] Run directory not found: {run_dir}")
387
- return 1
388
-
389
- diff_basis = get_run_diff_basis(run_dir)
390
- normalized_diff_basis, diff_basis_error = normalize_diff_basis(repo_root, diff_basis)
391
- if diff_basis_error:
392
- print(f"[FAIL] {diff_basis_error}")
393
- return 1
394
- changed_files, diff_error = get_git_changed_files(repo_root, diff_basis)
395
- if diff_error:
396
- print(f"[FAIL] {diff_error}")
397
- return 1
398
-
399
- filtered_changed_files = filter_runtime_changed_files(changed_files or [], run_dir.name)
400
-
401
- artifact_path = normalize_repo_path(args.artifact_path)
402
- upstream_artifacts = [normalize_repo_path(item) for item in args.upstream_artifact]
403
- addenda = [normalize_repo_path(item) for item in args.addendum]
404
- if not (repo_root / artifact_path).exists():
405
- print(f"[FAIL] Artifact path not found: /{artifact_path}")
406
- return 1
407
- if not args.no_auto_addenda:
408
- addenda.extend(auto_discover_addenda(run_dir, run_dir.name, artifact_path, upstream_artifacts))
409
- addenda = sorted(set(addenda))
410
- prior_refs = sorted(set(normalize_repo_path(item) for item in args.prior_ref if item.strip()))
411
- prior_refs.extend(auto_discover_skill_memory_refs(repo_root, args.phase, args.role))
412
- prior_refs = sorted(set(prior_refs))
413
- control_docs = [normalize_repo_path(item) for item in args.control_doc]
414
- code_refs = [normalize_repo_path(item) for item in args.code_ref]
415
- evidence_refs = [normalize_repo_path(item) for item in args.evidence_ref]
416
- routing_config_path = normalize_repo_path(args.routing_config_path) if args.routing_config_path.strip() else ""
417
- routing_discovery_path = normalize_repo_path(args.routing_discovery_path) if args.routing_discovery_path.strip() else ""
418
- audit_questions = [item.strip() for item in args.audit_question if item.strip()]
419
- required_output = [item.strip() for item in args.required_output if item.strip()]
420
-
421
- if not required_output:
422
- required_output = [
423
- "Findings ordered by severity",
424
- "Requirement and plan alignment assessment",
425
- "Diff reconciliation summary",
426
- "Explicit verdict and repair recommendation",
427
- ]
428
-
429
- bundle_name = args.output_name.strip()
430
- if not bundle_name:
431
- bundle_name = f"{slugify(args.phase)}-{slugify(args.role)}.md"
432
- if not bundle_name.lower().endswith(".md"):
433
- bundle_name = f"{bundle_name}.md"
434
-
435
- bundle_dir = run_dir / "evidence" / "review-bundles"
436
- bundle_dir.mkdir(parents=True, exist_ok=True)
437
- bundle_path = bundle_dir / bundle_name
438
- bundle_rel = normalize_repo_path(str(bundle_path.relative_to(repo_root)))
439
- artifact_content_hash = content_sha256((repo_root / artifact_path).read_text(encoding="utf-8"))
440
-
441
- generated_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
442
- lines: list[str] = [
443
- f"Run: `/.recursive/run/{run_dir.name}/`",
444
- f"Phase: `{args.phase.strip()}`",
445
- f"Role: `{args.role.strip()}`",
446
- f"Bundle Path: `/{bundle_rel}`",
447
- f"Artifact Path: `/{artifact_path}`",
448
- f"Artifact Content Hash: `{artifact_content_hash}`",
449
- f"GeneratedAt: `{generated_at}`",
450
- "",
451
- "## Bundle Scope",
452
- "- Canonical delegated review bundle for recursive-mode audit/review work.",
453
- "- Regenerate this bundle if the draft, changed files, or required evidence changes materially before review.",
454
- "",
455
- "## Routing",
456
- f"- Routed CLI: `{args.routed_cli.strip() or 'none'}`",
457
- f"- Routed Model: `{args.routed_model.strip() or 'none'}`",
458
- f"- Routing Config Path: `{('/' + routing_config_path) if routing_config_path else 'none'}`",
459
- f"- Routing Discovery Path: `{('/' + routing_discovery_path) if routing_discovery_path else 'none'}`",
460
- "",
461
- "## Diff Basis",
462
- f"- Baseline type: `{normalized_diff_basis['baseline_type']}`",
463
- f"- Baseline reference: `{diff_basis['baseline_reference'] or 'UNKNOWN'}`",
464
- f"- Comparison reference: `{normalized_diff_basis['comparison_reference']}`",
465
- f"- Normalized baseline: `{normalized_diff_basis['normalized_baseline']}`",
466
- f"- Normalized comparison: `{normalized_diff_basis['normalized_comparison']}`",
467
- f"- Normalized diff command: `{normalized_diff_basis['normalized_diff_command']}`",
468
- "",
469
- ]
470
- lines.extend(render_list("## Changed Files Reviewed", filtered_changed_files))
471
- lines.append("")
472
- lines.extend(render_list("## Upstream Artifacts To Re-read", upstream_artifacts))
473
- lines.append("")
474
- lines.extend(render_list("## Relevant Addenda", addenda))
475
- lines.append("")
476
- lines.extend(render_list("## Prior Recursive Evidence", prior_refs))
477
- lines.append("")
478
- lines.extend(render_list("## Control-Plane Docs", control_docs))
479
- lines.append("")
480
- lines.extend(render_list("## Targeted Code References", code_refs))
481
- lines.append("")
482
- lines.extend(render_list("## Evidence References", evidence_refs))
483
- lines.append("")
484
- lines.extend(render_list("## Audit Questions", audit_questions))
485
- lines.append("")
486
- lines.extend(render_list("## Required Output", required_output))
487
- lines.append("")
488
- lines.append("## Notes")
489
- lines.append("- Review output is invalid if it does not cite the upstream artifacts, diff basis, changed files, and final verdict.")
490
- lines.append("- If this bundle is incomplete, reject delegation and perform the audit as self-audit.")
491
- lines.append("")
492
-
493
- bundle_path.write_text("\n".join(lines), encoding="utf-8", newline="\n")
494
-
495
- print(f"[OK] Wrote review bundle: /{bundle_rel}")
496
- print(f"Changed files: {len(filtered_changed_files)}")
497
- print(f"Role: {args.role.strip()}")
498
- print(f"Phase: {args.phase.strip()}")
499
- return 0
500
-
501
-
502
- if __name__ == "__main__":
503
- raise SystemExit(main())
@@ -1,2 +0,0 @@
1
- & (Join-Path $PSScriptRoot "recursive-router-configure.ps1") @args
2
- exit $LASTEXITCODE
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import runpy
5
- from pathlib import Path
6
-
7
-
8
- if __name__ == "__main__":
9
- runpy.run_path(str(Path(__file__).with_name("recursive-router-configure.py")), run_name="__main__")
@@ -1,2 +0,0 @@
1
- & (Join-Path $PSScriptRoot "recursive-router-init.ps1") @args
2
- exit $LASTEXITCODE
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import runpy
5
- from pathlib import Path
6
-
7
-
8
- if __name__ == "__main__":
9
- runpy.run_path(str(Path(__file__).with_name("recursive-router-init.py")), run_name="__main__")
@@ -1,2 +0,0 @@
1
- & (Join-Path $PSScriptRoot "recursive-router-invoke.ps1") @args
2
- exit $LASTEXITCODE
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import runpy
5
- from pathlib import Path
6
-
7
-
8
- if __name__ == "__main__":
9
- runpy.run_path(str(Path(__file__).with_name("recursive-router-invoke.py")), run_name="__main__")
@@ -1,2 +0,0 @@
1
- & (Join-Path $PSScriptRoot "recursive-router-probe.ps1") @args
2
- exit $LASTEXITCODE
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import runpy
5
- from pathlib import Path
6
-
7
-
8
- if __name__ == "__main__":
9
- runpy.run_path(str(Path(__file__).with_name("recursive-router-probe.py")), run_name="__main__")
@@ -1,2 +0,0 @@
1
- & (Join-Path $PSScriptRoot "recursive-router-resolve.ps1") @args
2
- exit $LASTEXITCODE
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import runpy
5
- from pathlib import Path
6
-
7
-
8
- if __name__ == "__main__":
9
- runpy.run_path(str(Path(__file__).with_name("recursive-router-resolve.py")), run_name="__main__")
@@ -1,2 +0,0 @@
1
- & (Join-Path $PSScriptRoot "recursive-router-validate.ps1") @args
2
- exit $LASTEXITCODE
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import runpy
5
- from pathlib import Path
6
-
7
-
8
- if __name__ == "__main__":
9
- runpy.run_path(str(Path(__file__).with_name("recursive-router-validate.py")), run_name="__main__")