diff --git a/extensions/agent-context/commands/speckit.agent-context.update.md b/extensions/agent-context/commands/speckit.agent-context.update.md index a654eb5a0e..67700b8f6c 100644 --- a/extensions/agent-context/commands/speckit.agent-context.update.md +++ b/extensions/agent-context/commands/speckit.agent-context.update.md @@ -15,7 +15,7 @@ The script reads the agent-context extension config at - `context_files` — optional project-relative paths for multiple coding agent context files. When non-empty, the script updates each listed file and the list takes precedence over `context_file`. - `context_markers.start` / `.end` — the delimiters surrounding the managed section. Defaults to `` and `` when the field is missing. -It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (`specs//plan.md`). +It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (`specs/**/plan.md`, any depth). If `context_files` and `context_file` are empty, the command reports nothing to do and exits successfully. Context file paths must stay project-relative; absolute paths, Windows drive paths, backslash separators, and `..` path segments are rejected. @@ -24,4 +24,4 @@ If `context_files` and `context_file` are empty, the command reports nothing to - **Bash**: `.specify/extensions/agent-context/scripts/bash/update-agent-context.sh [plan_path]` - **PowerShell**: `.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 [plan_path]` -When `plan_path` is omitted, the script auto-detects the most recently modified `specs/*/plan.md`. +When `plan_path` is omitted, the script auto-detects the most recently modified `specs/**/plan.md` (any depth, so scoped layouts like `specs///plan.md` are found). diff --git a/extensions/agent-context/scripts/bash/update-agent-context.sh b/extensions/agent-context/scripts/bash/update-agent-context.sh index b7121a2f64..7ce3d42402 100755 --- a/extensions/agent-context/scripts/bash/update-agent-context.sh +++ b/extensions/agent-context/scripts/bash/update-agent-context.sh @@ -12,7 +12,7 @@ # # When `plan_path` is omitted, the script derives it from `.specify/feature.json` # (written by /speckit-specify). Falls back to the most recently modified -# `specs/*/plan.md` only when feature.json is absent or its plan does not exist yet. +# `specs/**/plan.md` only when feature.json is absent or its plan does not exist yet. set -euo pipefail @@ -307,14 +307,14 @@ import sys from pathlib import Path root = Path(sys.argv[1]).resolve() specs = root / "specs" -plans = sorted( - specs.glob("*/plan.md"), +plan = max( + specs.glob("**/plan.md"), key=lambda p: p.stat().st_mtime, - reverse=True, + default=None, ) -if plans: +if plan: try: - print(plans[0].relative_to(root).as_posix()) + print(plan.relative_to(root).as_posix()) except ValueError: print("") else: diff --git a/extensions/agent-context/scripts/powershell/update-agent-context.ps1 b/extensions/agent-context/scripts/powershell/update-agent-context.ps1 index 98a55c55fd..e04c104612 100644 --- a/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +++ b/extensions/agent-context/scripts/powershell/update-agent-context.ps1 @@ -12,7 +12,7 @@ # # When `plan_path` is omitted, the script derives it from `.specify/feature.json` # (written by /speckit-specify). Falls back to the most recently modified -# `specs/*/plan.md` only when feature.json is absent or its plan does not exist yet. +# `specs/**/plan.md` only when feature.json is absent or its plan does not exist yet. [CmdletBinding()] param( @@ -426,9 +426,7 @@ if (-not $PlanPath) { if (-not $PlanPath) { try { $specsDir = Join-Path $ProjectRoot 'specs' - $candidate = Get-ChildItem -Path $specsDir -Directory -ErrorAction SilentlyContinue | - ForEach-Object { Get-Item -LiteralPath (Join-Path $_.FullName 'plan.md') -ErrorAction SilentlyContinue } | - Where-Object { $_ } | + $candidate = Get-ChildItem -Path $specsDir -Recurse -File -Filter 'plan.md' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($candidate) { diff --git a/tests/extensions/test_extension_agent_context.py b/tests/extensions/test_extension_agent_context.py index f99d449401..e82e53284e 100644 --- a/tests/extensions/test_extension_agent_context.py +++ b/tests/extensions/test_extension_agent_context.py @@ -688,6 +688,62 @@ def test_bash_script_nothing_to_do_without_integration(self, tmp_path): _MDC_CONTEXT_FILE = ".cursor/rules/specify-rules.mdc" +class TestPlanDiscovery: + """Mtime fallback must find plans in nested spec layouts (#3024). + + Repos using SPECIFY_FEATURE_DIRECTORY place plans at + ``specs///plan.md``; a one-level ``specs/*/plan.md`` + glob never matches those. + """ + + @staticmethod + def _make_plans(project: Path) -> Path: + # Older flat plan plus a newer nested plan: recursive discovery + # must pick the nested one by mtime. + flat = project / "specs" / "old-feature" / "plan.md" + flat.parent.mkdir(parents=True) + flat.write_text("flat plan\n", encoding="utf-8") + os.utime(flat, (1_000_000_000, 1_000_000_000)) + nested = project / "specs" / "scope" / "new-feature" / "plan.md" + nested.parent.mkdir(parents=True) + nested.write_text("nested plan\n", encoding="utf-8") + return nested + + @requires_bash + def test_bash_script_finds_nested_plan(self, tmp_path): + project = tmp_path / "project" + project.mkdir() + _install_agent_context_config( + project, + context_file="AGENTS.md", + context_files=["AGENTS.md"], + ) + self._make_plans(project) + + result = _run_bash_agent_context_script(project) + + assert result.returncode == 0, result.stderr + result.stdout + content = (project / "AGENTS.md").read_text(encoding="utf-8") + assert "specs/scope/new-feature/plan.md" in content + + @pytest.mark.skipif(POWERSHELL is None, reason="PowerShell not available") + def test_powershell_script_finds_nested_plan(self, tmp_path): + project = tmp_path / "project" + project.mkdir() + _install_agent_context_config( + project, + context_file="AGENTS.md", + context_files=["AGENTS.md"], + ) + self._make_plans(project) + + result = _run_powershell_agent_context_script(project) + + assert result.returncode == 0, result.stderr + result.stdout + content = (project / "AGENTS.md").read_text(encoding="utf-8") + assert "specs/scope/new-feature/plan.md" in content + + class TestMdcFrontmatter: """Cursor-style ``.mdc`` targets must carry ``alwaysApply: true`` frontmatter so the rule file is auto-loaded; non-``.mdc`` targets must not gain any."""