From 33bcc5c52331fa9cd759504faf6d8988ac1912a1 Mon Sep 17 00:00:00 2001 From: "mateus.cardoso" Date: Fri, 24 Jul 2026 21:43:12 -0300 Subject: [PATCH] fix(agent-context): discover nested plans in Python port mtime fallback The Python port of update-agent-context reintroduced a one-level plan scan (specs/*/plan.md) in its mtime fallback, while the Bash and PowerShell ports search recursively (specs/**/plan.md) per the fix for issue #3024. The three ports were therefore not in parity: for nested scoped layouts such as specs///plan.md, the Python port found no plan and omitted the plan link from the managed context section. Switch the fallback to `(root / "specs").rglob("plan.md")` and update the module docstring to match the documented recursive-discovery contract. Add a parity regression test covering the nested layout (it fails on the one-level glob and passes with the recursive scan). Co-Authored-By: Claude Opus 4.8 --- .../scripts/python/update_agent_context.py | 7 ++++--- ...test_update_agent_context_python_parity.py | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/extensions/agent-context/scripts/python/update_agent_context.py b/extensions/agent-context/scripts/python/update_agent_context.py index 15c0dceef9..fc8894ee14 100644 --- a/extensions/agent-context/scripts/python/update_agent_context.py +++ b/extensions/agent-context/scripts/python/update_agent_context.py @@ -11,8 +11,9 @@ 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. +recently modified ``plan.md`` anywhere under ``specs/`` (including nested scoped +layouts such as ``specs///plan.md``) only when feature.json is +absent or its plan does not exist yet. """ from __future__ import annotations @@ -173,7 +174,7 @@ def _resolve_plan_path(project_root: str) -> str: if not plan_path: root = Path(project_root).resolve() plans = sorted( - (root / "specs").glob("*/plan.md"), + (root / "specs").rglob("plan.md"), key=lambda p: p.stat().st_mtime, reverse=True, ) diff --git a/tests/extensions/test_update_agent_context_python_parity.py b/tests/extensions/test_update_agent_context_python_parity.py index 92e8e20f8b..51d1831662 100644 --- a/tests/extensions/test_update_agent_context_python_parity.py +++ b/tests/extensions/test_update_agent_context_python_parity.py @@ -317,6 +317,27 @@ def test_python_mtime_fallback_matching_bash(tmp_path: Path) -> None: assert b"at specs/001-new/plan.md" in content +@requires_posix_bash +def test_python_mtime_fallback_finds_nested_plan_matching_bash(tmp_path: Path) -> None: + # Regression: the mtime fallback must discover plan.md in nested scoped + # layouts (specs///plan.md), matching the Bash/PowerShell + # ports and the documented recursive-discovery contract (see #3024). A + # one-level scan (specs/*/plan.md) would miss this and omit the plan link. + repo_a, repo_b = twin_projects(tmp_path, context_file="AGENTS.md") + for repo in (repo_a, repo_b): + plan = repo / "specs" / "scope-a" / "002-nested" / "plan.md" + plan.parent.mkdir(parents=True, exist_ok=True) + plan.write_text("# plan\n", encoding="utf-8") + + bash = run_bash(repo_a) + py = run_python(repo_b) + + assert_parity(bash, py, repo_a, repo_b) + content = (repo_b / "AGENTS.md").read_bytes() + assert content == (repo_a / "AGENTS.md").read_bytes() + assert b"at specs/scope-a/002-nested/plan.md" in content + + @requires_posix_bash def test_python_prefers_feature_json_over_mtime_matching_bash(tmp_path: Path) -> None: repo_a, repo_b = twin_projects(tmp_path, context_file="AGENTS.md")