@try-works/dsh-recursive-mode 0.1.5 → 0.1.6
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/lib/bootstrap.d.ts +6 -3
- package/lib/client/board.d.ts +4 -1
- package/lib/client/open-state.d.ts +20 -0
- package/lib/client/slots.d.ts +7 -1
- package/lib/client.js +135 -47
- package/lib/index.js +3391 -906
- package/lib/runtime.d.ts +12 -5
- package/lib/ts-lint.d.ts +205 -0
- package/package.json +1 -1
- package/src/bootstrap.ts +338 -331
- package/src/client/board.tsx +0 -0
- package/src/client/open-state.ts +46 -0
- package/src/client/slots.ts +41 -20
- package/src/commands.ts +11 -0
- package/src/index.ts +238 -228
- package/src/runtime.ts +19 -23
- package/src/ts-lint.ts +2110 -0
- package/references/scripts/__pycache__/recursive_phase_rules.cpython-314.pyc +0 -0
- package/references/scripts/lint-recursive-run.ps1 +0 -25
- package/references/scripts/lint-recursive-run.py +0 -2870
- package/references/scripts/recursive-closeout.ps1 +0 -38
- package/references/scripts/recursive-closeout.py +0 -541
- package/references/scripts/recursive-init.ps1 +0 -515
- package/references/scripts/recursive-init.py +0 -356
- package/references/scripts/recursive-lock.ps1 +0 -26
- package/references/scripts/recursive-lock.py +0 -302
- package/references/scripts/recursive-review-bundle.ps1 +0 -58
- package/references/scripts/recursive-review-bundle.py +0 -503
- package/references/scripts/recursive-router-cli-configure.ps1 +0 -2
- package/references/scripts/recursive-router-cli-configure.py +0 -9
- package/references/scripts/recursive-router-cli-init.ps1 +0 -2
- package/references/scripts/recursive-router-cli-init.py +0 -9
- package/references/scripts/recursive-router-cli-invoke.ps1 +0 -2
- package/references/scripts/recursive-router-cli-invoke.py +0 -9
- package/references/scripts/recursive-router-cli-probe.ps1 +0 -2
- package/references/scripts/recursive-router-cli-probe.py +0 -9
- package/references/scripts/recursive-router-cli-resolve.ps1 +0 -2
- package/references/scripts/recursive-router-cli-resolve.py +0 -9
- package/references/scripts/recursive-router-cli-validate.ps1 +0 -2
- package/references/scripts/recursive-router-cli-validate.py +0 -9
- package/references/scripts/recursive-router-configure.ps1 +0 -27
- package/references/scripts/recursive-router-configure.py +0 -74
- package/references/scripts/recursive-router-init.ps1 +0 -17
- package/references/scripts/recursive-router-init.py +0 -29
- package/references/scripts/recursive-router-invoke.ps1 +0 -47
- package/references/scripts/recursive-router-invoke.py +0 -103
- package/references/scripts/recursive-router-probe.ps1 +0 -25
- package/references/scripts/recursive-router-probe.py +0 -44
- package/references/scripts/recursive-router-resolve.ps1 +0 -26
- package/references/scripts/recursive-router-resolve.py +0 -46
- package/references/scripts/recursive-router-validate.ps1 +0 -17
- package/references/scripts/recursive-router-validate.py +0 -27
- package/references/scripts/recursive-status.ps1 +0 -23
- package/references/scripts/recursive-status.py +0 -2124
- package/references/scripts/recursive-subagent-action.ps1 +0 -98
- package/references/scripts/recursive-subagent-action.py +0 -197
- package/references/scripts/recursive-training-extract.ps1 +0 -23
- package/references/scripts/recursive-training-extract.py +0 -99
- package/references/scripts/recursive-training-grpo.ps1 +0 -52
- package/references/scripts/recursive-training-grpo.py +0 -1341
- package/references/scripts/recursive-training-loader.ps1 +0 -78
- package/references/scripts/recursive-training-loader.py +0 -558
- package/references/scripts/recursive-training-mcp.ps1 +0 -29
- package/references/scripts/recursive-training-mcp.py +0 -261
- package/references/scripts/recursive-training-phase8-trigger.ps1 +0 -55
- package/references/scripts/recursive-training-phase8-trigger.py +0 -137
- package/references/scripts/recursive-training-sync.ps1 +0 -29
- package/references/scripts/recursive-training-sync.py +0 -183
- package/references/scripts/recursive_phase_rules.py +0 -367
- package/references/scripts/recursive_router_cli_lib.py +0 -2
- package/references/scripts/recursive_router_lib.py +0 -2282
- package/references/scripts/verify-locks.ps1 +0 -25
- package/references/scripts/verify-locks.py +0 -353
- 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-recursive-mode.ps1 +0 -956
- package/scripts/install-recursive-mode.py +0 -750
- package/scripts/lint-recursive-run.py +0 -2870
- package/scripts/recursive-closeout.py +0 -541
- package/scripts/recursive-init.py +0 -356
- package/scripts/recursive-lock.py +0 -302
- package/scripts/recursive-status.py +0 -2124
- package/scripts/recursive_phase_rules.py +0 -367
- package/scripts/recursive_router_lib.py +0 -2282
- 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())
|