Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<!-- SPECKIT START -->` and `<!-- SPECKIT END -->` 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/<feature>/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.

Expand All @@ -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/<scope>/<feature>/plan.md` are found).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed that one, updated the Behavior section to the any-depth wording.

12 changes: 6 additions & 6 deletions extensions/agent-context/scripts/bash/update-agent-context.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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) {
Expand Down
56 changes: 56 additions & 0 deletions tests/extensions/test_extension_agent_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<scope>/<feature>/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."""
Expand Down
Loading