From 4eb5061e789e55b1a2d1f5c8d3ced0818c5f8f45 Mon Sep 17 00:00:00 2001 From: Aleksej Vasilev Date: Tue, 22 Sep 2026 12:46:17 +0200 Subject: [PATCH 1/3] Resolve npm .cmd-shim backends on Windows The backend contract resolves CLI paths with Python's shutil.which. Under native Windows Python that walks PATHEXT and returns the `foo.CMD` sibling spelled with backslashes. The value is then consumed by bash, which can neither `command -v` that path nor exec-test it: npm writes the .cmd shim without the executable bit, and the launcher bash can use is the extensionless name on its own PATH. Every npm-installed backend was therefore rejected with a false "backend CLI not found", which also masked the real backend error. Fall back to the extensionless basename when the resolved value looks like a path and is unusable. A bare name that resolves to nothing still fails where it did before. Fixes #1 Co-Authored-By: Claude Opus 5 (1M context) --- .../scripts/lib/backend_run.sh | 25 +++++++++++-- .../scripts/tests/run.sh | 35 +++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/skills/pragmatic-orchestration/scripts/lib/backend_run.sh b/skills/pragmatic-orchestration/scripts/lib/backend_run.sh index 57808c2..da065b8 100755 --- a/skills/pragmatic-orchestration/scripts/lib/backend_run.sh +++ b/skills/pragmatic-orchestration/scripts/lib/backend_run.sh @@ -293,9 +293,28 @@ if [[ -z "$BIN" ]]; then echo "Error: unknown backend '$BACKEND' for agent $AGENT_ID" >&2 exit $EXIT_CONFIG_ERROR fi -if ! command -v "$BIN" &>/dev/null && [[ ! -x "$BIN" ]]; then - echo "Error: backend CLI not found: $BIN (backend=$BACKEND)" >&2 - exit $EXIT_CONFIG_ERROR +bin_usable() { + command -v "$1" &>/dev/null || [[ -x "$1" ]] +} +if ! bin_usable "$BIN"; then + # Windows fallback. The contract resolves binaries with Python's + # shutil.which, which walks PATHEXT and returns the `foo.CMD` sibling with + # backslashes. MSYS bash can neither `command -v` that path nor exec-test + # it (npm writes the .cmd shim without the executable bit); the launcher + # it can use is the extensionless name on its own PATH. Only retry when + # $BIN looks like a resolved path, so a missing bare name still fails here. + _bin_fallback="" + if [[ "$BIN" == *[/\\]* ]]; then + _bin_fallback="${BIN##*[/\\]}" + _bin_fallback="${_bin_fallback%.*}" + fi + if [[ -n "$_bin_fallback" ]] && bin_usable "$_bin_fallback"; then + BIN="$_bin_fallback" + else + echo "Error: backend CLI not found: $BIN (backend=$BACKEND)" >&2 + exit $EXIT_CONFIG_ERROR + fi + unset _bin_fallback fi # Codex model alias + default efforts are already applied by backend_contract diff --git a/skills/pragmatic-orchestration/scripts/tests/run.sh b/skills/pragmatic-orchestration/scripts/tests/run.sh index 3bdb4ec..5581560 100755 --- a/skills/pragmatic-orchestration/scripts/tests/run.sh +++ b/skills/pragmatic-orchestration/scripts/tests/run.sh @@ -895,6 +895,41 @@ xml=$(CLAUDE_MODEL="claude-runtime" CLAUDE_EFFORT="max" \ assert_contains "ask XML uses resolved model" "$xml" 'model="claude-runtime"' assert_contains "ask XML uses resolved effort" "$xml" 'effort="max"' +echo "=== Windows .cmd shim binary resolution ===" +# On Windows the contract resolves binaries with Python's shutil.which, which +# walks PATHEXT and returns the `foo.CMD` sibling spelled with backslashes. npm +# writes that shim without the executable bit, so bash can neither `command -v` +# the path nor exec-test it, and every npm-installed backend was rejected with a +# false "backend CLI not found". Reproduced here on any platform by pointing the +# override at such a path; the extensionless launcher is the usable one. +SHIM_DIR="$TMP/cmd-shim" +mkdir -p "$SHIM_DIR" +cp "$FAKES/fake-codex" "$SHIM_DIR/shimcodex" +chmod +x "$SHIM_DIR/shimcodex" +printf '@echo off\r\n' > "$SHIM_DIR/shimcodex.cmd" +chmod 644 "$SHIM_DIR/shimcodex.cmd" + +export PORCH_RUN_DIR="$TMP/run-ask-cmd-shim" +mkdir -p "$PORCH_RUN_DIR" +set +e +out=$(PATH="$SHIM_DIR:$PATH" PORCH_BIN_CODEX="$SHIM_DIR\\shimcodex.CMD" \ + "$PORCH" review ask --progress none -a codex "review this" \ + 2>"$TMP/ask-cmd-shim.err") +set -e +assert_contains "windows .cmd path falls back to extensionless launcher" "$out" "FAKE_CODEX_OK" +assert_not_contains "windows .cmd path is not reported missing" \ + "$(cat "$TMP/ask-cmd-shim.err")" "backend CLI not found" + +# A bare name that resolves to nothing must still fail; the fallback only +# applies to values that look like a resolved path. +set +e +PATH="$SHIM_DIR:$PATH" PORCH_BIN_CODEX="definitely-absent-cli" \ + "$PORCH" review ask --progress none -a codex "review this" \ + >"$TMP/ask-absent-bin.out" 2>"$TMP/ask-absent-bin.err" +absent_bin_rc=$? +set -e +assert_eq "missing bare binary still fails" "$absent_bin_rc" "3" + echo "=== Delegate with fake ===" export PORCH_RUN_DIR="$TMP/run-del" mkdir -p "$PORCH_RUN_DIR" From 3050a60eac424feabd420d16a9b96802412d2db3 Mon Sep 17 00:00:00 2001 From: Aleksej Vasilev Date: Tue, 22 Sep 2026 14:53:49 +0200 Subject: [PATCH 2/3] Keep Windows shim fallback in the resolved directory --- .claude-plugin/marketplace.json | 4 ++-- .claude-plugin/plugin.json | 2 +- .../scripts/lib/backend_run.sh | 20 ++++++++-------- .../scripts/tests/run.sh | 24 +++++++++++++++++++ 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 206a8ad..885ea26 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -13,7 +13,7 @@ { "name": "pragmatic-orchestration", "description": "Multi-agent orchestration for coding agents: run external agent CLIs (Codex, Claude Code, OpenCode, Grok Build, Gemini, Devin CLI) as independent reviewers, stateful repository researchers, or steerable single-agent implementers via the `porch` CLI Also ships remote-agents: dedicated remote Linux/Windows agent hosts (SSH-over-SSM, headless desktop, work bridge).", - "version": "1.3.4", + "version": "1.3.5", "author": { "name": "CodeAlive-AI", "email": "contact@codealive.ai" @@ -61,5 +61,5 @@ ] } ], - "version": "1.3.4" + "version": "1.3.5" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 2987106..ecfd20f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "pragmatic-orchestration", "description": "Multi-agent orchestration for coding agents: run external agent CLIs (Codex, Claude Code, OpenCode, Grok Build, Gemini, Devin CLI) as independent reviewers, stateful repository researchers, or steerable single-agent implementers via the `porch` CLI. Review ask/code modes (read-only), delegate mode (full-access, steerable, detachable), session-history search, and subscription quota checks Ships a second skill, remote-agents, for provisioning and operating dedicated remote Linux/Windows agent hosts.", - "version": "1.3.4", + "version": "1.3.5", "author": { "name": "CodeAlive-AI", "email": "contact@codealive.ai" diff --git a/skills/pragmatic-orchestration/scripts/lib/backend_run.sh b/skills/pragmatic-orchestration/scripts/lib/backend_run.sh index da065b8..897c9f9 100755 --- a/skills/pragmatic-orchestration/scripts/lib/backend_run.sh +++ b/skills/pragmatic-orchestration/scripts/lib/backend_run.sh @@ -297,18 +297,18 @@ bin_usable() { command -v "$1" &>/dev/null || [[ -x "$1" ]] } if ! bin_usable "$BIN"; then - # Windows fallback. The contract resolves binaries with Python's - # shutil.which, which walks PATHEXT and returns the `foo.CMD` sibling with - # backslashes. MSYS bash can neither `command -v` that path nor exec-test - # it (npm writes the .cmd shim without the executable bit); the launcher - # it can use is the extensionless name on its own PATH. Only retry when - # $BIN looks like a resolved path, so a missing bare name still fails here. + # Native Windows Python can resolve an npm .CMD shim that MSYS bash cannot + # execute. Use its extensionless sibling, keeping the resolved directory + # (and any explicit binary override) authoritative. _bin_fallback="" - if [[ "$BIN" == *[/\\]* ]]; then - _bin_fallback="${BIN##*[/\\]}" - _bin_fallback="${_bin_fallback%.*}" + if [[ "$BIN" == *[/\\]* && "$BIN" == *.[cC][mM][dD] ]]; then + _bin_fallback="${BIN%.*}" + _bin_fallback="${_bin_fallback//\\//}" + if command -v cygpath &>/dev/null; then + _bin_fallback="$(cygpath -u "$_bin_fallback" 2>/dev/null)" || _bin_fallback="" + fi fi - if [[ -n "$_bin_fallback" ]] && bin_usable "$_bin_fallback"; then + if [[ -n "$_bin_fallback" && -x "$_bin_fallback" ]]; then BIN="$_bin_fallback" else echo "Error: backend CLI not found: $BIN (backend=$BACKEND)" >&2 diff --git a/skills/pragmatic-orchestration/scripts/tests/run.sh b/skills/pragmatic-orchestration/scripts/tests/run.sh index 5581560..ac56dd6 100755 --- a/skills/pragmatic-orchestration/scripts/tests/run.sh +++ b/skills/pragmatic-orchestration/scripts/tests/run.sh @@ -930,6 +930,30 @@ absent_bin_rc=$? set -e assert_eq "missing bare binary still fails" "$absent_bin_rc" "3" +# A missing .CMD path must not pick a different installation from PATH. +set +e +PATH="$SHIM_DIR:$PATH" PORCH_BIN_CODEX="$TMP/missing/shimcodex.CMD" \ + PORCH_RUN_DIR="$TMP/run-ask-missing-cmd" \ + "$PORCH" review ask --progress none -a codex "review this" \ + >"$TMP/ask-missing-cmd.out" 2>"$TMP/ask-missing-cmd.err" +missing_cmd_rc=$? +set -e +assert_eq "missing .CMD sibling fails instead of searching PATH" "$missing_cmd_rc" "3" +assert_not_contains "missing .CMD sibling does not run another CLI" \ + "$(cat "$TMP/ask-missing-cmd.out")" "FAKE_CODEX_OK" + +# Other extensions do not imply an npm shim, even when a sibling exists. +set +e +PATH="$SHIM_DIR:$PATH" PORCH_BIN_CODEX="$SHIM_DIR/shimcodex.exe" \ + PORCH_RUN_DIR="$TMP/run-ask-missing-exe" \ + "$PORCH" review ask --progress none -a codex "review this" \ + >"$TMP/ask-missing-exe.out" 2>"$TMP/ask-missing-exe.err" +missing_exe_rc=$? +set -e +assert_eq "missing non-.CMD binary still fails" "$missing_exe_rc" "3" +assert_not_contains "missing non-.CMD binary does not run sibling" \ + "$(cat "$TMP/ask-missing-exe.out")" "FAKE_CODEX_OK" + echo "=== Delegate with fake ===" export PORCH_RUN_DIR="$TMP/run-del" mkdir -p "$PORCH_RUN_DIR" From 7e4e68545b89d9aaa1dbb8c341f0e14e2fb2f55c Mon Sep 17 00:00:00 2001 From: Aleksej Vasilev Date: Wed, 30 Sep 2026 16:08:38 +0200 Subject: [PATCH 3/3] chore: retrigger CI Re-run smoke tests after a flaky ubuntu failure caused by interleaved writes to the fake backend argv log. Co-Authored-By: Claude Opus 5.5