From fd587ac8d9ed6a5e8ff00e657e3acf99981dd772 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 15:45:11 +0000 Subject: [PATCH 1/6] Initial plan From cc364a8663c8fb62f81f16842782e2bed05c6e5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 15:57:28 +0000 Subject: [PATCH 2/6] chore: planning git detection fix for macOS ARM64 Agent-Logs-Url: https://github.com/github/gh-aw/sessions/82628923-30ee-4061-af73-61b141aba2e4 Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- .github/workflows/copilot-pr-nlp-analysis.lock.yml | 8 +++++--- .github/workflows/spec-librarian.lock.yml | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/copilot-pr-nlp-analysis.lock.yml b/.github/workflows/copilot-pr-nlp-analysis.lock.yml index 8b486a80fae..98173fc844d 100644 --- a/.github/workflows/copilot-pr-nlp-analysis.lock.yml +++ b/.github/workflows/copilot-pr-nlp-analysis.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"e680d07d85ca99dc6dc3ee4be59cdb3c9d1c49e7bf28efa85f1641c408445d8c","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/setup-python","sha":"a309ff8b426b58ec0e2a45f0f869d46889d02405","version":"v6.2.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41","digest":"sha256:cb2b565d070116d4b67e355775340528b5a2c3cb18b2c9049638bcc2df681770","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41@sha256:cb2b565d070116d4b67e355775340528b5a2c3cb18b2c9049638bcc2df681770"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41","digest":"sha256:fadd0de387209f69a9a7a1b8722bb5e7fdfb80ba9749a5c60f0e4cd7582a74d0","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41@sha256:fadd0de387209f69a9a7a1b8722bb5e7fdfb80ba9749a5c60f0e4cd7582a74d0"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41","digest":"sha256:1260445d25968dbf3ae70143964177a0e5914cf2ce07a6117f7d3caec6c3e3c4","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41@sha256:1260445d25968dbf3ae70143964177a0e5914cf2ce07a6117f7d3caec6c3e3c4"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/setup-python","sha":"a309ff8b426b58ec0e2a45f0f869d46889d02405","version":"v6.2.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41","digest":"sha256:cb2b565d070116d4b67e355775340528b5a2c3cb18b2c9049638bcc2df681770","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41@sha256:cb2b565d070116d4b67e355775340528b5a2c3cb18b2c9049638bcc2df681770"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41","digest":"sha256:fadd0de387209f69a9a7a1b8722bb5e7fdfb80ba9749a5c60f0e4cd7582a74d0","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41@sha256:fadd0de387209f69a9a7a1b8722bb5e7fdfb80ba9749a5c60f0e4cd7582a74d0"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41","digest":"sha256:1260445d25968dbf3ae70143964177a0e5914cf2ce07a6117f7d3caec6c3e3c4","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41@sha256:1260445d25968dbf3ae70143964177a0e5914cf2ce07a6117f7d3caec6c3e3c4"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -49,7 +49,8 @@ # - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 # - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -518,7 +519,7 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.41 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} @@ -1785,3 +1786,4 @@ jobs: actions/setup sparse-checkout-cone-mode: true persist-credentials: false + diff --git a/.github/workflows/spec-librarian.lock.yml b/.github/workflows/spec-librarian.lock.yml index 6d120855950..555c32f53c5 100644 --- a/.github/workflows/spec-librarian.lock.yml +++ b/.github/workflows/spec-librarian.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"b21c866096e7bf37ad8390647a4e60737201a07a362b4a013fe73ebdfe63b049","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["GH_AW_AGENT_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41","digest":"sha256:cb2b565d070116d4b67e355775340528b5a2c3cb18b2c9049638bcc2df681770","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41@sha256:cb2b565d070116d4b67e355775340528b5a2c3cb18b2c9049638bcc2df681770"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41","digest":"sha256:fadd0de387209f69a9a7a1b8722bb5e7fdfb80ba9749a5c60f0e4cd7582a74d0","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41@sha256:fadd0de387209f69a9a7a1b8722bb5e7fdfb80ba9749a5c60f0e4cd7582a74d0"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.41","digest":"sha256:62171f2fa508667b8b0a9e096f826983f312e3da0ce894f80c0f83a875af60fe","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.41@sha256:62171f2fa508667b8b0a9e096f826983f312e3da0ce894f80c0f83a875af60fe"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41","digest":"sha256:1260445d25968dbf3ae70143964177a0e5914cf2ce07a6117f7d3caec6c3e3c4","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41@sha256:1260445d25968dbf3ae70143964177a0e5914cf2ce07a6117f7d3caec6c3e3c4"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_AGENT_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41","digest":"sha256:cb2b565d070116d4b67e355775340528b5a2c3cb18b2c9049638bcc2df681770","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41@sha256:cb2b565d070116d4b67e355775340528b5a2c3cb18b2c9049638bcc2df681770"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41","digest":"sha256:fadd0de387209f69a9a7a1b8722bb5e7fdfb80ba9749a5c60f0e4cd7582a74d0","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41@sha256:fadd0de387209f69a9a7a1b8722bb5e7fdfb80ba9749a5c60f0e4cd7582a74d0"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.41","digest":"sha256:62171f2fa508667b8b0a9e096f826983f312e3da0ce894f80c0f83a875af60fe","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.41@sha256:62171f2fa508667b8b0a9e096f826983f312e3da0ce894f80c0f83a875af60fe"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41","digest":"sha256:1260445d25968dbf3ae70143964177a0e5914cf2ce07a6117f7d3caec6c3e3c4","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41@sha256:1260445d25968dbf3ae70143964177a0e5914cf2ce07a6117f7d3caec6c3e3c4"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -46,7 +46,8 @@ # Custom actions used: # - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 # @@ -466,7 +467,7 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.41 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} @@ -1567,3 +1568,4 @@ jobs: /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore + From 8b1d706f834fdd6d2ed46a0a84d3abde252a0096 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 16:01:03 +0000 Subject: [PATCH 3/6] fix: replace exec.Command git detection with pure Go filesystem traversal Fixes `gh aw init` failing with "not in a git repository" on macOS ARM64 (Apple Silicon) when the x86_64 binary runs under Rosetta 2. Previously, `isGitRepo()` and `FindGitRoot()` used `exec.Command("git", ...)` to detect git repositories. Under Rosetta 2 (x86_64 emulation on ARM64), this subprocess call can silently fail due to PATH differences or OS-level subprocess restrictions. Replace with pure Go filesystem traversal: walk up from the current directory (or a given starting directory) looking for a `.git` entry (directory or file, handling both normal repos and worktrees). This eliminates the dependency on the `git` executable for repository detection, making it work regardless of architecture, Rosetta translation, or git installation state. Changes: - `pkg/gitutil/gitutil.go`: `FindGitRoot()` now uses `os.Stat`/`filepath.Dir` traversal; new exported `FindGitRootFrom(startDir)` helper for callers that start from a specific directory - `pkg/cli/git.go`: `isGitRepo()` delegates to `gitutil.FindGitRoot()`; `findGitRootForPath()` uses `gitutil.FindGitRootFrom(dir)` instead of exec - `pkg/gitutil/gitutil_test.go`: add `TestFindGitRootFrom` tests - `pkg/gitutil/spec_test.go`: update doc comment to reflect new implementation Agent-Logs-Url: https://github.com/github/gh-aw/sessions/82628923-30ee-4061-af73-61b141aba2e4 Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/cli/git.go | 10 ++++----- pkg/gitutil/gitutil.go | 43 +++++++++++++++++++++++++++++++------ pkg/gitutil/gitutil_test.go | 38 ++++++++++++++++++++++++++++++++ pkg/gitutil/spec_test.go | 5 +++-- 4 files changed, 81 insertions(+), 15 deletions(-) diff --git a/pkg/cli/git.go b/pkg/cli/git.go index 527b2105e97..953405525b7 100644 --- a/pkg/cli/git.go +++ b/pkg/cli/git.go @@ -18,8 +18,8 @@ import ( var gitLog = logger.New("cli:git") func isGitRepo() bool { - cmd := exec.Command("git", "rev-parse", "--git-dir") - return cmd.Run() == nil + _, err := gitutil.FindGitRoot() + return err == nil } // findGitRootForPath finds the root directory of the git repository containing the specified path @@ -41,13 +41,11 @@ func findGitRootForPath(path string) (string, error) { // Use the directory containing the file dir := filepath.Dir(absPath) - // Run git command in the file's directory - cmd := exec.Command("git", "-C", dir, "rev-parse", "--show-toplevel") - output, err := cmd.Output() + // Find git root using filesystem traversal from the file's directory + gitRoot, err := gitutil.FindGitRootFrom(dir) if err != nil { return "", fmt.Errorf("failed to get repository root for path %s: %w", path, err) } - gitRoot := strings.TrimSpace(string(output)) gitLog.Printf("Found git root for path: %s", gitRoot) return gitRoot, nil } diff --git a/pkg/gitutil/gitutil.go b/pkg/gitutil/gitutil.go index 5ffb76626ea..e9aadb5a6db 100644 --- a/pkg/gitutil/gitutil.go +++ b/pkg/gitutil/gitutil.go @@ -1,7 +1,9 @@ package gitutil import ( + "errors" "fmt" + "os" "os/exec" "path/filepath" "regexp" @@ -75,18 +77,45 @@ func ExtractBaseRepo(repoPath string) string { } // FindGitRoot finds the root directory of the git repository. -// Returns an error if not in a git repository or if the git command fails. +// Uses pure Go filesystem traversal to avoid requiring the git executable, +// which can fail when the binary runs under Rosetta 2 on macOS ARM64 or in +// environments where git is not on PATH. +// Returns an error if not in a git repository. func FindGitRoot() (string, error) { log.Print("Finding git root directory") - cmd := exec.Command("git", "rev-parse", "--show-toplevel") - output, err := cmd.Output() + + dir, err := os.Getwd() if err != nil { - log.Printf("Failed to find git root: %v", err) + log.Printf("Failed to get current directory: %v", err) return "", fmt.Errorf("not in a git repository or git command failed: %w", err) } - gitRoot := strings.TrimSpace(string(output)) - log.Printf("Found git root: %s", gitRoot) - return gitRoot, nil + + root, err := FindGitRootFrom(dir) + if err != nil { + log.Printf("Failed to find git root: %v", err) + return "", err + } + + log.Printf("Found git root: %s", root) + return root, nil +} + +// FindGitRootFrom finds the root directory of the git repository starting from +// the given directory. It traverses upward until it finds a .git entry (file or +// directory) or reaches the filesystem root. +// Returns an error if not in a git repository. +func FindGitRootFrom(startDir string) (string, error) { + dir := startDir + for { + if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil { + return dir, nil + } + parent := filepath.Dir(dir) + if parent == dir { + return "", errors.New("not in a git repository") + } + dir = parent + } } // ReadFileFromHEADWithRoot is like ReadFileFromHEAD but accepts a pre-computed git diff --git a/pkg/gitutil/gitutil_test.go b/pkg/gitutil/gitutil_test.go index bfb87542295..76e2a88e2c1 100644 --- a/pkg/gitutil/gitutil_test.go +++ b/pkg/gitutil/gitutil_test.go @@ -3,6 +3,7 @@ package gitutil import ( + "os" "path/filepath" "testing" @@ -293,6 +294,43 @@ func TestFindGitRoot(t *testing.T) { }) } +func TestFindGitRootFrom(t *testing.T) { + t.Run("returns git root from the repository root itself", func(t *testing.T) { + gitRoot, err := FindGitRoot() + require.NoError(t, err, "must be inside a git repository") + + root, err := FindGitRootFrom(gitRoot) + require.NoError(t, err, "FindGitRootFrom should succeed when starting from the git root") + assert.Equal(t, gitRoot, root, "FindGitRootFrom from git root should return git root") + }) + + t.Run("returns git root from a subdirectory", func(t *testing.T) { + gitRoot, err := FindGitRoot() + require.NoError(t, err, "must be inside a git repository") + + // Use a known subdirectory within the repo + subDir := filepath.Join(gitRoot, "pkg") + if _, statErr := os.Stat(subDir); os.IsNotExist(statErr) { + t.Skip("pkg/ subdirectory not found, skipping subdirectory test") + } + + root, err := FindGitRootFrom(subDir) + require.NoError(t, err, "FindGitRootFrom should succeed from a subdirectory") + assert.Equal(t, gitRoot, root, "FindGitRootFrom from subdirectory should return the git root") + }) + + t.Run("returns error when starting outside any git repository", func(t *testing.T) { + tmpDir := t.TempDir() + // Create a nested directory that is definitely not a git repo + nonRepoDir := filepath.Join(tmpDir, "not-a-git-repo", "subdir") + require.NoError(t, os.MkdirAll(nonRepoDir, 0755), "should create nested temp dir") + + _, err := FindGitRootFrom(nonRepoDir) + require.Error(t, err, "FindGitRootFrom should return error outside a git repository") + assert.Contains(t, err.Error(), "not in a git repository", "error should mention not in git repository") + }) +} + func TestReadFileFromHEADWithRoot(t *testing.T) { t.Run("reads a committed file with pre-computed root", func(t *testing.T) { gitRoot, err := FindGitRoot() diff --git a/pkg/gitutil/spec_test.go b/pkg/gitutil/spec_test.go index a397821337f..170ef35c161 100644 --- a/pkg/gitutil/spec_test.go +++ b/pkg/gitutil/spec_test.go @@ -270,8 +270,9 @@ func TestSpec_PublicAPI_IsValidFullSHA(t *testing.T) { // FindGitRoot as described in the package README.md. // // Specification: Returns the absolute path of the root directory of the current -// Git repository by running `git rev-parse --show-toplevel`. Returns an error -// if the working directory is not inside a Git repository. +// Git repository using pure Go filesystem traversal (looks for .git in the +// current directory and its parents). Returns an error if the working directory +// is not inside a Git repository. func TestSpec_PublicAPI_FindGitRoot(t *testing.T) { t.Run("returns non-empty absolute path when in git repository", func(t *testing.T) { root, err := FindGitRoot() From fe2f830ba403b41697e47f39fbb507381bc04e72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 16:06:26 +0000 Subject: [PATCH 4/6] fix: address code review feedback - fix error message and improve test robustness Agent-Logs-Url: https://github.com/github/gh-aw/sessions/82628923-30ee-4061-af73-61b141aba2e4 Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/gitutil/gitutil.go | 2 +- pkg/gitutil/gitutil_test.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/gitutil/gitutil.go b/pkg/gitutil/gitutil.go index e9aadb5a6db..4a441f99adb 100644 --- a/pkg/gitutil/gitutil.go +++ b/pkg/gitutil/gitutil.go @@ -87,7 +87,7 @@ func FindGitRoot() (string, error) { dir, err := os.Getwd() if err != nil { log.Printf("Failed to get current directory: %v", err) - return "", fmt.Errorf("not in a git repository or git command failed: %w", err) + return "", fmt.Errorf("failed to get current directory: %w", err) } root, err := FindGitRootFrom(dir) diff --git a/pkg/gitutil/gitutil_test.go b/pkg/gitutil/gitutil_test.go index 76e2a88e2c1..d46120ac201 100644 --- a/pkg/gitutil/gitutil_test.go +++ b/pkg/gitutil/gitutil_test.go @@ -308,11 +308,11 @@ func TestFindGitRootFrom(t *testing.T) { gitRoot, err := FindGitRoot() require.NoError(t, err, "must be inside a git repository") - // Use a known subdirectory within the repo - subDir := filepath.Join(gitRoot, "pkg") - if _, statErr := os.Stat(subDir); os.IsNotExist(statErr) { - t.Skip("pkg/ subdirectory not found, skipping subdirectory test") - } + // Create a temporary subdirectory inside the repo to avoid depending on + // specific repo layout (e.g. pkg/ may not exist in all test environments). + subDir, mkdirErr := os.MkdirTemp(gitRoot, "test-subdir-*") + require.NoError(t, mkdirErr, "should create temp subdir inside git repo") + defer os.RemoveAll(subDir) root, err := FindGitRootFrom(subDir) require.NoError(t, err, "FindGitRootFrom should succeed from a subdirectory") From 5e5e8c151bdc880508af1c572502758d0adf8561 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 19:19:16 +0000 Subject: [PATCH 5/6] chore: merge main and recompile workflow lock files Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/spec-librarian.lock.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/spec-librarian.lock.yml b/.github/workflows/spec-librarian.lock.yml index a40020bed82..f1f5cd29767 100644 --- a/.github/workflows/spec-librarian.lock.yml +++ b/.github/workflows/spec-librarian.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"b21c866096e7bf37ad8390647a4e60737201a07a362b4a013fe73ebdfe63b049","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["GH_AW_AGENT_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41","digest":"sha256:cb2b565d070116d4b67e355775340528b5a2c3cb18b2c9049638bcc2df681770","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41@sha256:cb2b565d070116d4b67e355775340528b5a2c3cb18b2c9049638bcc2df681770"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41","digest":"sha256:fadd0de387209f69a9a7a1b8722bb5e7fdfb80ba9749a5c60f0e4cd7582a74d0","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41@sha256:fadd0de387209f69a9a7a1b8722bb5e7fdfb80ba9749a5c60f0e4cd7582a74d0"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.41","digest":"sha256:62171f2fa508667b8b0a9e096f826983f312e3da0ce894f80c0f83a875af60fe","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.41@sha256:62171f2fa508667b8b0a9e096f826983f312e3da0ce894f80c0f83a875af60fe"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41","digest":"sha256:1260445d25968dbf3ae70143964177a0e5914cf2ce07a6117f7d3caec6c3e3c4","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41@sha256:1260445d25968dbf3ae70143964177a0e5914cf2ce07a6117f7d3caec6c3e3c4"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_AGENT_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41","digest":"sha256:cb2b565d070116d4b67e355775340528b5a2c3cb18b2c9049638bcc2df681770","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41@sha256:cb2b565d070116d4b67e355775340528b5a2c3cb18b2c9049638bcc2df681770"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41","digest":"sha256:fadd0de387209f69a9a7a1b8722bb5e7fdfb80ba9749a5c60f0e4cd7582a74d0","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41@sha256:fadd0de387209f69a9a7a1b8722bb5e7fdfb80ba9749a5c60f0e4cd7582a74d0"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.41","digest":"sha256:62171f2fa508667b8b0a9e096f826983f312e3da0ce894f80c0f83a875af60fe","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.41@sha256:62171f2fa508667b8b0a9e096f826983f312e3da0ce894f80c0f83a875af60fe"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41","digest":"sha256:1260445d25968dbf3ae70143964177a0e5914cf2ce07a6117f7d3caec6c3e3c4","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41@sha256:1260445d25968dbf3ae70143964177a0e5914cf2ce07a6117f7d3caec6c3e3c4"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -467,7 +467,7 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.41 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} From c2d3db364acfa007fa5617dc39fccca054379ebc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 19:36:30 +0000 Subject: [PATCH 6/6] fix: improve FindGitRootFrom robustness and add worktree/edge-case tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/gitutil/gitutil.go | 29 ++++++++++++++++++++--- pkg/gitutil/gitutil_test.go | 47 +++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/pkg/gitutil/gitutil.go b/pkg/gitutil/gitutil.go index 4a441f99adb..497d9c92b17 100644 --- a/pkg/gitutil/gitutil.go +++ b/pkg/gitutil/gitutil.go @@ -105,10 +105,33 @@ func FindGitRoot() (string, error) { // directory) or reaches the filesystem root. // Returns an error if not in a git repository. func FindGitRootFrom(startDir string) (string, error) { - dir := startDir + dir, err := filepath.Abs(startDir) + if err != nil { + return "", fmt.Errorf("failed to resolve absolute path for %q: %w", startDir, err) + } + dir = filepath.Clean(dir) for { - if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil { - return dir, nil + gitPath := filepath.Join(dir, ".git") + info, err := os.Stat(gitPath) + if err == nil { + // .git exists — accept if it's a directory (normal repo) or a + // regular file (worktree / git-submodule pointer). + if info.IsDir() { + return dir, nil + } + // Worktree marker: must be a regular file beginning with "gitdir:" + if info.Mode().IsRegular() { + data, readErr := os.ReadFile(gitPath) + if readErr != nil { + return "", fmt.Errorf("failed to read .git file at %q: %w", gitPath, readErr) + } + if strings.HasPrefix(strings.TrimSpace(string(data)), "gitdir:") { + return dir, nil + } + } + } else if !errors.Is(err, os.ErrNotExist) { + // Unexpected error (e.g. permission denied) — surface it. + return "", fmt.Errorf("failed to stat %q: %w", gitPath, err) } parent := filepath.Dir(dir) if parent == dir { diff --git a/pkg/gitutil/gitutil_test.go b/pkg/gitutil/gitutil_test.go index d46120ac201..ec542ac4e57 100644 --- a/pkg/gitutil/gitutil_test.go +++ b/pkg/gitutil/gitutil_test.go @@ -329,6 +329,53 @@ func TestFindGitRootFrom(t *testing.T) { require.Error(t, err, "FindGitRootFrom should return error outside a git repository") assert.Contains(t, err.Error(), "not in a git repository", "error should mention not in git repository") }) + + t.Run("returns git root when .git is a worktree marker file", func(t *testing.T) { + // Simulate a git worktree: the repo root has a .git *file* (not dir) + // whose content begins with "gitdir: /some/path" + tmpDir := t.TempDir() + repoRoot := filepath.Join(tmpDir, "worktree-repo") + require.NoError(t, os.MkdirAll(repoRoot, 0755)) + + // Write a valid worktree .git file + gitFile := filepath.Join(repoRoot, ".git") + require.NoError(t, os.WriteFile(gitFile, []byte("gitdir: /tmp/real-repo/.git/worktrees/myworktree\n"), 0644)) + + // Start from the root itself + root, err := FindGitRootFrom(repoRoot) + require.NoError(t, err, "FindGitRootFrom should detect a worktree .git file") + assert.Equal(t, repoRoot, root) + + // Start from a subdirectory inside the worktree + subDir := filepath.Join(repoRoot, "pkg", "sub") + require.NoError(t, os.MkdirAll(subDir, 0755)) + root, err = FindGitRootFrom(subDir) + require.NoError(t, err, "FindGitRootFrom should detect worktree root from a subdirectory") + assert.Equal(t, repoRoot, root) + }) + + t.Run("ignores non-worktree .git files without gitdir prefix", func(t *testing.T) { + // A plain file named .git that does NOT start with "gitdir:" should not + // be treated as a valid repo root. + tmpDir := t.TempDir() + repoRoot := filepath.Join(tmpDir, "fake-git-file") + require.NoError(t, os.MkdirAll(repoRoot, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(repoRoot, ".git"), []byte("not a valid git file\n"), 0644)) + + _, err := FindGitRootFrom(repoRoot) + require.Error(t, err, "FindGitRootFrom should not accept a .git file without gitdir: prefix") + assert.Contains(t, err.Error(), "not in a git repository") + }) + + t.Run("handles relative path input", func(t *testing.T) { + // "." should resolve to os.Getwd(). Skip gracefully if the working + // directory is not inside a git repository (e.g. some CI containers). + root, err := FindGitRootFrom(".") + if err != nil { + t.Skipf("skipping: working directory is not inside a git repository (%v)", err) + } + assert.NotEmpty(t, root) + }) } func TestReadFileFromHEADWithRoot(t *testing.T) {