From 887bab4d2e4cc16579e7bb1bed0e56b4db207ac7 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:08:38 -0700 Subject: [PATCH 1/2] ci: pick the e2e fixture with a glob, not ls | head Under set -o pipefail, head -1 exits after one line and ls dies of SIGPIPE once its output outgrows one write (89 TypeScript cases now do), failing the e2e step on whichever runner loses the race. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> (cherry picked from commit e55facefa867af880cf93ee0c828ce77339f10ae) --- .github/workflows/ci.yml | 5 ++++- .github/workflows/nightly.yml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 776ce8d4a..0c0003101 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -533,7 +533,10 @@ jobs: command -v souffle && { echo "::error::this runner has souffle; the gate would not prove anything"; exit 1; } export CODEGRAPH_TGZ="$(ls "$PWD"/artifacts/tgz/*.tgz)" for lang in java typescript python javascript csharp; do - case_dir="$(ls -d graph/test/$lang/cases/*/src | head -1)" + # A glob, not ls | head: under pipefail, head exiting early SIGPIPEs ls and fails the step. + cases=(graph/test/"$lang"/cases/*/src) + case_dir="${cases[0]}" + [ -d "$case_dir" ] || { echo "::error::no $lang case under graph/test/$lang/cases"; exit 1; } echo "::group::$lang ($case_dir)" bash .github/scripts/e2e-install.sh artifacts/engines "$PLATFORM" "$lang" "$case_dir" echo "::endgroup::" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 0e595e499..1eb9c60de 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -74,7 +74,10 @@ jobs: set -euo pipefail for pair in java:java typescript:typescript python:python javascript:javascript; do lang="${pair%%:*}" - case_dir="$(ls -d graph/test/$lang/cases/*/src | head -1)" + # A glob, not ls | head: under pipefail, head exiting early SIGPIPEs ls and fails the step. + cases=(graph/test/"$lang"/cases/*/src) + case_dir="${cases[0]}" + [ -d "$case_dir" ] || { echo "::error::no $lang case under graph/test/$lang/cases"; exit 1; } bash .github/scripts/e2e-install.sh artifacts/engines-linux-x64 linux-x64 "$lang" "$case_dir" done From cc37651fde9e3c0de5910fe08cbeb384d887dee0 Mon Sep 17 00:00:00 2001 From: swapnil Date: Tue, 29 Sep 2026 08:45:32 -0700 Subject: [PATCH 2/2] mcp: the verb dispatcher finds its folder when $0 mixes / and \ (Windows) On Windows every MCP tool call failed with "can't open file ...\code-graph\plugins\ax_grep.py" (or axiomcode-build, -context, -changed, -graph): bin/axiomcode exports AXIOMCODE_PLUGIN_ROOT from bash with '/', the MCP server joins the dispatcher's path onto it with os.path.join ('\'), and the dispatcher split $0 on '/' alone, landing on .../plugins. `dirname` split on either separator; the parameter expansion that replaced it for latency did not. The dispatcher now normalises '\' first, as bin/axiomcode already does. The CLI was unaffected. tests/mixed_separators.py builds the same mixed $0 off Windows and is red on the old dispatcher. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../skills/axiomcode/scripts/axiomcode | 5 +- tests/README.md | 2 + tests/mixed_separators.py | 54 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 tests/mixed_separators.py diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode index d753d02ed..770f54db9 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode @@ -53,7 +53,10 @@ # --grep (context, path, impact, test-impact) prints the answer's sites one per line, as grep does: `path:line: [resolved | one of a set | by name | text | hop N | test]`, the first 30 (--grep-limit N) and a count of # the rest, then whether it was verified and its bound. The MCP tools answer this way by default; full=True is the prose. -case "$0" in */*) H="${0%/*}";; *) H=.;; esac; H="$(cd "${H:-/}" && pwd)" # no `dirname`: see bin/axiomcode +# ON WINDOWS $0 CAN MIX SEPARATORS: the MCP server joins its script path onto AXIOMCODE_PLUGIN_ROOT with os.path.join, +# C:/.../plugins/axiomcode\skills\axiomcode\scripts\axiomcode. Splitting that on '/' alone lands on .../plugins, and +# every verb then runs a helper that is not there. `dirname` split on either; so does this, as bin/axiomcode does. +H="${0//\\//}"; case "$H" in */*) H="${H%/*}";; *) H=.;; esac; H="$(cd "${H:-/}" && pwd)" # no `dirname`: see bin/axiomcode # PYTHON UNDER ANOTHER NAME (#1331). Every verb runs `python3`, which a python.org install on Windows does not # provide, and which on a desktop Windows is the Store placeholder. Started from node (the `axiomcode` command, the diff --git a/tests/README.md b/tests/README.md index 08713cfb4..25889f3e3 100644 --- a/tests/README.md +++ b/tests/README.md @@ -33,6 +33,8 @@ One check needs no graph and is its own script: directly, through an npm-style symlink to bin/axiomcode.js, on the SDK-free fallback, and from .mcp.json, .codex-plugin/mcp.json and .cursor-plugin as each host starts it + python3 tests/mixed_separators.py the verb dispatcher finds its own folder when $0 mixes / and \, as the MCP + server starts it on Windows (every MCP tool call failed there from 0.1.3) python3 tests/manifests.py every agent's manifest (Claude, Codex, Cursor, Gemini) names the same plugin and points at files that exist, the way that agent resolves them, and Gemini's skill and Cursor's rule are current copies diff --git a/tests/mixed_separators.py b/tests/mixed_separators.py new file mode 100644 index 000000000..8f9f494d9 --- /dev/null +++ b/tests/mixed_separators.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""tests/mixed_separators.py — the verb dispatcher finds its own folder when $0 mixes '/' and '\\'. + +On Windows the MCP server starts the dispatcher as os.path.join(AXIOMCODE_PLUGIN_ROOT, 'skills', 'axiomcode', +'scripts', 'axiomcode'), and bin/axiomcode exports that root from bash with forward slashes, so bash sees +C:/.../plugins/axiomcode\\skills\\axiomcode\\scripts\\axiomcode. Splitting it on '/' alone gave .../plugins, and every +MCP tool call ran a helper that is not there ("can't open file ...\\plugins\\ax_grep.py"). + +Off Windows a backslash is an ordinary file-name character, so the same $0 is built for real: a directory whose +plugins/axiomcode links to the plugin, and beside it a link literally named axiomcode\\skills\\axiomcode\\scripts\\axiomcode +pointing at the dispatcher. `help impact` reads the verb's script from the dispatcher's folder, so it answers only +when that folder is the scripts folder. The plain path is the control. + + python3 tests/mixed_separators.py +""" +import os, subprocess, sys, tempfile + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PLUGIN = os.path.join(ROOT, 'plugins', 'axiomcode') +SCRIPTS = os.path.join(PLUGIN, 'skills', 'axiomcode', 'scripts') +MIXED = 'axiomcode\\skills\\axiomcode\\scripts\\axiomcode' + + +def main(): + fails = [] + def check(ok, why, detail=''): + print(('ok ' if ok else 'FAIL ') + why + ('' if ok else '\n ' + detail.strip().replace('\n', '\n '))) + if not ok: fails.append(why) + + bash = os.environ.get('AXIOMCODE_BASH') or 'bash' + def helps(dispatcher): + r = subprocess.run([bash, dispatcher, 'help', 'impact'], capture_output=True, text=True) + return r.returncode == 0 and 'impact' in r.stdout and 'no such verb' not in r.stderr, r.stdout[-300:] + r.stderr[-300:] + + ok, out = helps(os.path.join(SCRIPTS, 'axiomcode')) + check(ok, 'control: the dispatcher run by its own path finds its verbs', out) + + with tempfile.TemporaryDirectory(prefix='axiomcode-mixed-sep-') as t: + if os.name == 'nt': + dispatcher = PLUGIN.replace('\\', '/') + '\\' + MIXED.split('\\', 1)[1] + else: + os.makedirs(os.path.join(t, 'plugins')) + os.symlink(PLUGIN, os.path.join(t, 'plugins', 'axiomcode')) + os.symlink(os.path.join(SCRIPTS, 'axiomcode'), os.path.join(t, 'plugins', MIXED)) + dispatcher = os.path.join(t, 'plugins', MIXED) + ok, out = helps(dispatcher) + check(ok, 'a $0 that mixes / and \\ (how the MCP server starts it on Windows) finds the verbs', out) + + print(f"\n{'FAIL' if fails else 'ok'}: {len(fails)} failure(s)") + return 1 if fails else 0 + + +if __name__ == '__main__': + sys.exit(main())