Skip to content

Commit 8834e1c

Browse files
Merge branch 'main' into fix/slack-message-tool-durable-fallback
2 parents 6e138bf + 4dad7bd commit 8834e1c

3,936 files changed

Lines changed: 105557 additions & 36931 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/autoreview/SKILL.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ Format first if formatting can change line locations. Then it is OK to run tests
100100
scripts/autoreview --parallel-tests "<focused test command>"
101101
```
102102

103+
On Windows, the default `--parallel-tests` shell preserves the platform `cmd.exe`
104+
semantics used by Python `shell=True`. Use `--parallel-tests-shell powershell`
105+
or `--parallel-tests-shell pwsh` when the focused test command is PowerShell-specific.
106+
103107
Tradeoff: tests may force code changes that stale the review. If tests or review lead to code edits, rerun the affected tests and rerun review until no accepted/actionable findings remain. Once that rerun exits cleanly, stop; do not spend another long review cycle on redundant confirmation.
104108

105109
## Review Panels
@@ -144,6 +148,22 @@ OpenClaw repo-local helper:
144148
.agents/skills/autoreview/scripts/autoreview --help
145149
```
146150

151+
On native Windows, invoke the extensionless Python helper through Python:
152+
153+
```powershell
154+
python .agents\skills\autoreview\scripts\autoreview --help
155+
```
156+
157+
The smoke harness has thin shell wrappers over a shared Python implementation:
158+
159+
```bash
160+
.agents/skills/autoreview/scripts/test-review-harness --fixture benign --engine codex
161+
```
162+
163+
```powershell
164+
.agents\skills\autoreview\scripts\test-review-harness.ps1 -Fixture benign -Engine codex
165+
```
166+
147167
`agent-scripts` checkout helper:
148168

149169
```bash
@@ -169,10 +189,11 @@ The helper:
169189
- otherwise uses current PR base if `gh pr view` works
170190
- otherwise uses `origin/main` for non-main branches
171191
- supports `--engine codex`, `claude`, `droid`, and `copilot`; default is `AUTOREVIEW_ENGINE` or `codex`; Codex should remain the default when nothing is set
192+
- resolves bare `git`, `gh`, reviewer, and PowerShell shell commands from absolute `PATH` entries only, never from the reviewed checkout; explicit relative `--*-bin` paths are resolved from the reviewed repository root
172193
- use `--mode commit --commit <ref>` for already-committed work, especially clean `main` after landing
173194
- should be left in `--mode auto` or forced to `--mode branch` for PR/branch work; do not force `--mode local` after committing
174195
- writes only to stdout unless `--output`, `--json-output`, or live streamed engine stderr is set
175-
- supports `--dry-run`, `--parallel-tests`, `--prompt`, `--prompt-file`, `--dataset`, `--no-tools`, `--no-web-search`, and commit refs
196+
- supports `--dry-run`, `--parallel-tests`, `--parallel-tests-shell`, `--prompt`, `--prompt-file`, `--dataset`, `--no-tools`, `--no-web-search`, and commit refs
176197
- supports `--stream-engine-output` or `AUTOREVIEW_STREAM_ENGINE_OUTPUT=1` for live engine text while preserving structured validation; Codex and Claude hide tool/file event details, emit compact activity summaries, and report usage at turn completion
177198
- supports opt-in review panels with `--panel` / `--reviewers`, plus per-engine `--model` and `--thinking`
178199
- allows read-only tools and web search by default where the selected CLI supports them; forbids nested review in the prompt; Codex is run through `codex exec` with read-only sandbox and structured output

.agents/skills/autoreview/scripts/autoreview

Lines changed: 102 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -214,12 +214,17 @@ def run_with_stream(
214214

215215

216216
def git(repo: Path, *args: str, check: bool = True) -> str:
217-
return run(["git", *args], repo, check=check).stdout
217+
return run([resolve_command("git", repo), *args], repo, check=check).stdout
218218

219219

220220
def repo_root() -> Path:
221+
start = Path.cwd().resolve()
222+
unsafe_root = discover_repo_root(start) or start
223+
git_bin = find_command("git", unsafe_root)
224+
if not git_bin:
225+
raise SystemExit("git executable not found. Install Git or add it to PATH.")
221226
result = subprocess.run(
222-
["git", "rev-parse", "--show-toplevel"],
227+
[git_bin, "rev-parse", "--show-toplevel"],
223228
text=True,
224229
stdout=subprocess.PIPE,
225230
stderr=subprocess.PIPE,
@@ -229,6 +234,16 @@ def repo_root() -> Path:
229234
return Path(result.stdout.strip()).resolve()
230235

231236

237+
def discover_repo_root(start: Path) -> Path | None:
238+
current = start
239+
while True:
240+
if (current / ".git").exists():
241+
return current
242+
if current.parent == current:
243+
return None
244+
current = current.parent
245+
246+
232247
def current_branch(repo: Path) -> str:
233248
return git(repo, "branch", "--show-current", check=False).strip() or "detached"
234249

@@ -250,17 +265,70 @@ def choose_target(repo: Path, mode: str, base_ref: str | None) -> tuple[str, str
250265

251266

252267
def detect_pr_base(repo: Path) -> str | None:
253-
if not shutil_which("gh"):
268+
gh_bin = find_command("gh", repo)
269+
if not gh_bin:
254270
return None
255-
result = run(["gh", "pr", "view", "--json", "baseRefName", "--jq", ".baseRefName"], repo, check=False)
271+
result = run([gh_bin, "pr", "view", "--json", "baseRefName", "--jq", ".baseRefName"], repo, check=False)
256272
base = result.stdout.strip()
257273
return f"origin/{base}" if result.returncode == 0 and base else None
258274

259275

260-
def shutil_which(name: str) -> str | None:
276+
def resolve_command(name: str, repo: Path) -> str:
277+
resolved = find_command(name, repo)
278+
if resolved:
279+
return resolved
280+
raise SystemExit(f"executable not found: {name}. Install it or pass an explicit trusted path when supported.")
281+
282+
283+
def find_command(name: str, repo: Path) -> str | None:
284+
command = Path(name)
285+
if has_directory_component(name, command):
286+
base = command if command.is_absolute() else repo / command
287+
return first_executable_candidate(base)
261288
for part in os.environ.get("PATH", "").split(os.pathsep):
262-
candidate = Path(part) / name
263-
if candidate.exists() and os.access(candidate, os.X_OK):
289+
if not part or part == ".":
290+
continue
291+
path_part = Path(part)
292+
if not path_part.is_absolute():
293+
continue
294+
try:
295+
resolved_part = path_part.resolve()
296+
resolved_repo = repo.resolve()
297+
except OSError:
298+
continue
299+
if is_within(resolved_part, resolved_repo):
300+
continue
301+
found = first_executable_candidate(resolved_part / name, reject_root=resolved_repo)
302+
if found:
303+
return found
304+
return None
305+
306+
307+
def is_within(path: Path, root: Path) -> bool:
308+
return path == root or path.is_relative_to(root)
309+
310+
311+
def has_directory_component(name: str, command: Path) -> bool:
312+
separators = [separator for separator in (os.sep, os.altsep) if separator]
313+
return command.is_absolute() or bool(command.drive) or any(separator in name for separator in separators)
314+
315+
316+
def first_executable_candidate(path: Path, *, reject_root: Path | None = None) -> str | None:
317+
if os.name == "nt" and not path.suffix:
318+
extensions = [ext for ext in os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";") if ext]
319+
candidates = [path.with_suffix(ext.lower()) for ext in extensions]
320+
candidates.extend(path.with_suffix(ext.upper()) for ext in extensions)
321+
candidates.append(path)
322+
else:
323+
candidates = [path]
324+
for candidate in candidates:
325+
if candidate.is_file() and os.access(candidate, os.X_OK):
326+
if reject_root is not None:
327+
try:
328+
if is_within(candidate.resolve(), reject_root):
329+
continue
330+
except OSError:
331+
continue
264332
return str(candidate)
265333
return None
266334

@@ -419,7 +487,7 @@ def run_codex(args: argparse.Namespace, repo: Path, prompt: str) -> str:
419487
raise SystemExit("--no-tools is not supported by the Codex engine; use --engine claude --no-tools for a no-tools run")
420488
schema_path = write_json_temp(SCHEMA)
421489
output_path = Path(tempfile.NamedTemporaryFile("w", suffix=".json", delete=False).name)
422-
cmd = [args.codex_bin, "--ask-for-approval", "never"]
490+
cmd = [resolve_command(args.codex_bin, repo), "--ask-for-approval", "never"]
423491
if args.web_search:
424492
cmd.append("--search")
425493
if args.model:
@@ -463,7 +531,7 @@ def run_codex(args: argparse.Namespace, repo: Path, prompt: str) -> str:
463531

464532
def run_claude(args: argparse.Namespace, repo: Path, prompt: str) -> str:
465533
cmd = [
466-
args.claude_bin,
534+
resolve_command(args.claude_bin, repo),
467535
"--print",
468536
"--no-session-persistence",
469537
"--output-format",
@@ -500,7 +568,7 @@ def run_droid(args: argparse.Namespace, repo: Path, prompt: str) -> str:
500568
prompt_path = Path(tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False).name)
501569
prompt_path.write_text(prompt)
502570
cmd = [
503-
args.droid_bin,
571+
resolve_command(args.droid_bin, repo),
504572
"exec",
505573
"--cwd",
506574
str(repo),
@@ -530,7 +598,7 @@ def run_copilot(args: argparse.Namespace, repo: Path, prompt: str) -> str:
530598
prompt_path.write_text(prompt)
531599
os.chmod(prompt_path, 0o600)
532600
cmd = [
533-
args.copilot_bin,
601+
resolve_command(args.copilot_bin, repo),
534602
"-C",
535603
tempdir,
536604
"-p",
@@ -877,9 +945,23 @@ def print_report(report: dict[str, Any], *, label: str = "autoreview") -> None:
877945
print(report["overall_explanation"])
878946

879947

880-
def start_parallel_tests(command: str, repo: Path) -> tuple[subprocess.Popen, float]:
948+
def start_parallel_tests(command: str, repo: Path, shell_kind: str) -> tuple[subprocess.Popen, float]:
881949
print(f"tests: {command}")
882-
return subprocess.Popen(command, cwd=repo, shell=True), time.time()
950+
if shell_kind == "default" or shell_kind == "cmd":
951+
return subprocess.Popen(command, cwd=repo, shell=True), time.time()
952+
if shell_kind == "powershell":
953+
powershell = resolve_command("powershell", repo)
954+
return subprocess.Popen(
955+
[powershell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command],
956+
cwd=repo,
957+
), time.time()
958+
if shell_kind == "pwsh":
959+
pwsh = resolve_command("pwsh", repo)
960+
return subprocess.Popen(
961+
[pwsh, "-NoProfile", "-Command", command],
962+
cwd=repo,
963+
), time.time()
964+
raise SystemExit(f"invalid --parallel-tests-shell/AUTOREVIEW_PARALLEL_TESTS_SHELL: {shell_kind}")
883965

884966

885967
def finish_parallel_tests(proc: subprocess.Popen, started: float) -> int:
@@ -924,6 +1006,12 @@ def parse_args() -> argparse.Namespace:
9241006
help="Stream review engine output while preserving buffered output for validation. Codex output is filtered to hide tool/file chatter.",
9251007
)
9261008
parser.add_argument("--parallel-tests", help="Run a test command concurrently with review; failure fails the helper.")
1009+
parser.add_argument(
1010+
"--parallel-tests-shell",
1011+
choices=["default", "cmd", "powershell", "pwsh"],
1012+
default=os.environ.get("AUTOREVIEW_PARALLEL_TESTS_SHELL", "default"),
1013+
help="Shell for --parallel-tests. Default preserves Python shell=True platform behavior; use powershell or pwsh for PowerShell-specific commands.",
1014+
)
9271015
parser.add_argument("--require-finding", action="append", default=[], help="Require finding text to contain this substring.")
9281016
parser.add_argument("--expect-findings", action="store_true", help="Treat findings as success; for harness acceptance tests.")
9291017
parser.add_argument("--dry-run", action="store_true")
@@ -1129,7 +1217,7 @@ def main() -> int:
11291217

11301218
tests_proc: tuple[subprocess.Popen, float] | None = None
11311219
if args.parallel_tests:
1132-
tests_proc = start_parallel_tests(args.parallel_tests, repo)
1220+
tests_proc = start_parallel_tests(args.parallel_tests, repo, args.parallel_tests_shell)
11331221
try:
11341222
if len(reviewers) == 1:
11351223
report = run_reviewer(reviewers[0], repo, prompt, changed_paths, args.require_finding)

0 commit comments

Comments
 (0)