fix(agent-context): discover nested plan.md in scoped layouts (#3024) (#3301)

* fix(agent-context): discover nested plan.md in scoped layouts (#3024)

The agent-context updater only looked for plan.md one level deep
(specs/*/plan.md), so scoped layouts created via SPECIFY_FEATURE_DIRECTORY
(specs/<scope>/<feature>/plan.md) were never picked up and no plan
reference was written into the context file.

Recurse into specs/ in both the bash (rglob) and PowerShell (-Recurse)
scripts. In the PowerShell script, also replace
[System.IO.Path]::GetRelativePath, which is .NET Core 2.1+ only and throws
under Windows PowerShell 5.1 (.NET Framework); the exception was swallowed
by the surrounding try/catch, leaving the plan path empty on 5.1 even when
a plan was found. Compute the project-relative path by stripping the root
prefix instead.

Add regression tests for both scripts covering nested discovery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(agent-context): guard mtime plan discovery against symlink escape

Address Copilot review feedback on #3301:

- bash updater: the mtime fallback filtered candidates lexically via
  relative_to() on the *unresolved* path, so a plan reached through a
  specs/ symlink pointing outside the project could be selected and emit
  an in-project-looking path. Resolve each candidate and keep only those
  whose resolved path stays under root before picking the newest.
- test: the nested-plan PowerShell regression targets a Windows
  PowerShell 5.1 (.NET Framework) failure mode, but ran whatever
  POWERSHELL resolved to (prefers pwsh). Prefer powershell.exe on Windows
  so the 5.1-only compat fix is actually exercised.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(agent-context): note recursive plan.md discovery in update command

Auto-detection now recurses (`specs/**/plan.md`) to support nested scoped layouts created via SPECIFY_FEATURE_DIRECTORY (#3024). The update command doc still described the old one-level `specs/*/plan.md` glob, which could mislead users troubleshooting plan detection. Addresses Copilot review feedback on PR #3301.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Noor ul ain
2026-07-11 00:21:43 +05:00
committed by GitHub
parent 74d03a2814
commit 126e56882b
4 changed files with 90 additions and 14 deletions

View File

@@ -25,6 +25,14 @@ BASH = shutil.which("bash")
POWERSHELL = (
shutil.which("pwsh") or shutil.which("powershell.exe") or shutil.which("powershell")
)
# On Windows, prefer the built-in Windows PowerShell 5.1 (.NET Framework) when a
# test needs to exercise a 5.1-specific code path; fall back to whatever
# POWERSHELL resolves to elsewhere.
WINDOWS_POWERSHELL = (
(shutil.which("powershell.exe") or shutil.which("powershell") or POWERSHELL)
if os.name == "nt"
else POWERSHELL
)
def _write_ext_config(project_root: Path, **overrides: object) -> None:
@@ -279,12 +287,14 @@ def shlex_quote(value: str) -> str:
return "'" + value.replace("'", "'\"'\"'") + "'"
def _run_powershell_agent_context_script(project_root: Path) -> subprocess.CompletedProcess:
def _run_powershell_agent_context_script(
project_root: Path, powershell: str | None = None
) -> subprocess.CompletedProcess:
script = EXT_DIR / "scripts" / "powershell" / "update-agent-context.ps1"
env = _bundled_script_env(project_root)
return subprocess.run(
[
POWERSHELL,
powershell or POWERSHELL,
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
@@ -412,6 +422,29 @@ class TestBundledUpdaterPathValidation:
assert output.count("agent-context: updated CLAUDE.md") == 1
assert "agent-context: updated agents.md" not in output
@requires_bash
def test_bash_script_discovers_nested_plan(self, tmp_path):
"""Plan discovery recurses into scoped layouts (#3024)."""
project = tmp_path / "project"
project.mkdir()
_install_agent_context_config(
project,
context_file="AGENTS.md",
context_files=[],
)
plan = project / "specs" / "scope" / "001-feature" / "plan.md"
plan.parent.mkdir(parents=True)
plan.write_text("# Plan\n", encoding="utf-8")
result = _run_bash_agent_context_script(project)
assert result.returncode == 0, result.stderr + result.stdout
text = (project / "AGENTS.md").read_text(encoding="utf-8")
# The old one-level glob (specs/*/plan.md) would find nothing here, so no
# "at" line would be emitted. Normalize separators before matching: on
# MSYS bash the emitted path may be absolute with backslashes.
assert "specs/scope/001-feature/plan.md" in text.replace("\\", "/")
@requires_bash
def test_bash_script_falls_back_from_invalid_speckit_python(self, tmp_path):
project = tmp_path / "project"
@@ -484,6 +517,33 @@ class TestBundledUpdaterPathValidation:
assert output.count("agent-context: updated CLAUDE.md") == 1
assert "agent-context: updated agents.md" not in output
@pytest.mark.skipif(WINDOWS_POWERSHELL is None, reason="PowerShell not available")
def test_powershell_script_discovers_nested_plan(self, tmp_path):
"""Plan discovery recurses into scoped layouts (#3024).
The relative-path fix this covers is specific to Windows PowerShell 5.1
(.NET Framework), so prefer ``powershell.exe`` over ``pwsh`` here to
actually exercise that failure mode on Windows.
"""
project = tmp_path / "project"
project.mkdir()
_install_agent_context_config(
project,
context_file="AGENTS.md",
context_files=[],
)
plan = project / "specs" / "scope" / "001-feature" / "plan.md"
plan.parent.mkdir(parents=True)
plan.write_text("# Plan\n", encoding="utf-8")
result = _run_powershell_agent_context_script(
project, powershell=WINDOWS_POWERSHELL
)
assert result.returncode == 0, result.stderr + result.stdout
text = (project / "AGENTS.md").read_text(encoding="utf-8")
assert "at specs/scope/001-feature/plan.md" in text
@pytest.mark.skipif(POWERSHELL is None, reason="PowerShell not available")
def test_powershell_script_falls_back_from_invalid_speckit_python(self, tmp_path):
project = tmp_path / "project"