From 2c1ed919c5ce066f0a6635ae62046e663ae3fa27 Mon Sep 17 00:00:00 2001 From: Didier Cherix Date: Wed, 30 Sep 2026 09:04:43 +0200 Subject: [PATCH 1/2] Enhance Junie integration Enhance Junie integration Modify the Junie integration to dynamically inject the `allowPromptArgument` frontmatter key based on the presence of the `$ARGUMENTS` variable. This ensures that the Junie agent is correctly configured to accept user input only when the command template supports it. The transformation logic now also converts `$ARGUMENTS` to `$prompt` while escaping other template variables (e.g., `$$variable`) to prevent accidental expansion by the Junie agent. Changes: - Inject `allowPromptArgument: true` in Junie frontmatter if `$ARGUMENTS` is detected; set to `false` otherwise. - Map `$ARGUMENTS` to `$prompt` and double-escape other `$` variables. - Update template script and memory paths to use `.specify/` prefix. - Refine clarification questioning hierarchy and state management. - Add comprehensive tests for Junie frontmatter and variable transforms. --- CHANGELOG.md | 5 +- .../integrations/junie/__init__.py | 66 +++++++++++++++++++ tests/integrations/test_integration_junie.py | 57 ++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6c2d0ec65..2dc1d1ee1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,7 +100,7 @@ - fix(agy): support --model, --add-dir workspace, and pre-prompt extra args (#4481) - Remove a dead case pattern in common.sh (SC2221/SC2222) (#4605) - fix: support SKILL.md rendering for the generic integration (#4562) -- chore: release 1.0.8, begin 1.0.9.dev0 development (#4619) + - chore: release 1.0.8, begin 1.0.9.dev0 development (#4619) ## [1.0.8] - 2026-09-17 @@ -126,6 +126,9 @@ - Update adrkit extension to v0.1.4 (#4600) - chore: upgrade feature-assess to gh-aw v0.88.7 (#4546) - chore: release 1.0.7, begin 1.0.8.dev0 development (#4592) +- feat(integrations): enhance Junie integration with prompt arguments and variable escaping (#4600) +- fix(junie): ensure `allowPromptArgument` is unique and correctly set in command frontmatter +- feat(junie): map `$ARGUMENTS` to `$prompt` and generically escape other variables with `$$` ## [1.0.7] - 2026-09-15 diff --git a/src/specify_cli/integrations/junie/__init__.py b/src/specify_cli/integrations/junie/__init__.py index 2d4a6b32d9..5dc35f9035 100644 --- a/src/specify_cli/integrations/junie/__init__.py +++ b/src/specify_cli/integrations/junie/__init__.py @@ -132,6 +132,66 @@ def _rewrite_handoff_references(content: str) -> str: lambda m: f"{m.group(1)}{format_junie_command_name(m.group(2))}", content, ) + + @staticmethod + def _inject_allow_prompt_argument(content: str, allow_prompt: bool = True) -> str: + """Inject allowPromptArgument: true/false into the YAML frontmatter. + + If frontmatter exists, it ensures the key is set to the desired value (overwriting if needed). + If not, it creates a minimal frontmatter. + """ + value = "true" if allow_prompt else "false" + if not content.startswith("---"): + # No frontmatter at all? Create one. + return f"---\nallowPromptArgument: {value}\n---\n\n" + content + + parts = content.split("---", 2) + if len(parts) < 3: + # Malformed frontmatter? + return content + + frontmatter = parts[1] + body = parts[2] + + if "allowPromptArgument:" in frontmatter: + # Overwrite existing key + frontmatter = re.sub( + r"(?m)^(\s*allowPromptArgument:\s*).*", + fr"\1{value}", + frontmatter + ) + else: + # Append to frontmatter. Ensure it ends with newline. + # Check if the last line of frontmatter is a key-value pair. + lines = frontmatter.splitlines() + if lines and not lines[-1].strip(): + # Remove trailing empty lines in frontmatter + while lines and not lines[-1].strip(): + lines.pop() + frontmatter = "\n".join(lines) + "\n" + elif not frontmatter.endswith("\n"): + frontmatter += "\n" + + frontmatter += f"allowPromptArgument: {value}\n" + + return f"---{frontmatter}---{body}" + + @staticmethod + def _transform_body_variables(content: str) -> str: + """Transform $ARGUMENTS to $prompt and escape other $word by doubling $.""" + # 1. $ARGUMENTS -> $prompt + # We do this before regex so we can exclude $prompt from doubling. + content = content.replace("$ARGUMENTS", "$prompt") + + # 2. Double $ for other variables: $[A-Za-z_][A-Za-z0-9_-]* + def double_dollar(match: re.Match[str]) -> str: + word = match.group(1) + if word == "prompt": + return "$prompt" + return "$$" + word + + return re.sub(r"\$([A-Za-z_][A-Za-z0-9_-]*)", double_dollar, content) + def post_process_command_content(self, content: str) -> str: """Apply Junie-specific transformations to command content. @@ -140,8 +200,14 @@ def post_process_command_content(self, content: str) -> str: ``post_process_command_content``) applies these transforms to extension/preset command files too, not just core commands. """ + # FR-001: Detect $ARGUMENTS before transformation + has_arguments = "$ARGUMENTS" in content + updated = self._inject_hook_command_note(content) updated = self._rewrite_handoff_references(updated) + # FR-002, FR-003: Set allowPromptArgument based on $ARGUMENTS presence + updated = self._inject_allow_prompt_argument(updated, allow_prompt=has_arguments) + updated = self._transform_body_variables(updated) return updated def setup( diff --git a/tests/integrations/test_integration_junie.py b/tests/integrations/test_integration_junie.py index a6234ba734..18c93eb3c1 100644 --- a/tests/integrations/test_integration_junie.py +++ b/tests/integrations/test_integration_junie.py @@ -109,6 +109,63 @@ def test_junie_hook_instruction_injection_no_trailing_newline(self): # Instruction stays on its own line rather than being mashed onto the note. assert "\n- For each executable hook, output the following:" in injected + def test_junie_transformations(self): + """Verify junie-specific transformations for frontmatter and body.""" + junie = get_integration("junie") + content = "---\ndescription: Test\n---\n$ARGUMENTS\n$speckit-plan\n" + updated = junie.post_process_command_content(content) + assert "allowPromptArgument: true" in updated + assert "$prompt" in updated + assert "$ARGUMENTS" not in updated + assert "$$speckit-plan" in updated + + def test_junie_frontmatter_uniqueness(self): + """Verify allowPromptArgument is unique and overwritten. (US1)""" + junie = get_integration("junie") + # With $ARGUMENTS -> true + content = "---\nallowPromptArgument: false\ndescription: Test\n---\n$ARGUMENTS" + updated = junie.post_process_command_content(content) + assert updated.count("allowPromptArgument:") == 1 + assert "allowPromptArgument: true" in updated + + # Without $ARGUMENTS -> false + content = "---\nallowPromptArgument: true\ndescription: Test\n---\nBody" + updated = junie.post_process_command_content(content) + assert updated.count("allowPromptArgument:") == 1 + assert "allowPromptArgument: false" in updated + + def test_junie_variable_escaping_regex(self): + """Verify generic variable escaping using regex. (US2)""" + junie = get_integration("junie") + content = "Hook: $speckit-git-branch, User: $user_name, Prompt: $prompt" + # Note: $prompt should stay $prompt + updated = junie.post_process_command_content(content) + assert "$$speckit-git-branch" in updated + assert "$$user_name" in updated + assert "$prompt" in updated + assert "$$prompt" not in updated + + def test_junie_missing_frontmatter(self): + """Verify frontmatter is created if missing.""" + junie = get_integration("junie") + # With $ARGUMENTS + content = "$ARGUMENTS" + updated = junie.post_process_command_content(content) + assert updated.startswith("---\nallowPromptArgument: true\n---\n\n") + assert "$prompt" in updated + + # Without $ARGUMENTS + content = "Body" + updated = junie.post_process_command_content(content) + assert updated.startswith("---\nallowPromptArgument: false\n---\n\n") + + def test_junie_malformed_frontmatter(self): + """Verify malformed frontmatter is handled gracefully (returned as is).""" + junie = get_integration("junie") + content = "---\nMalformed frontmatter (missing closing dashes)\nBody" + updated = junie.post_process_command_content(content) + assert updated == content + # -- Overrides for MarkdownIntegrationTests --------------------------- def test_setup_creates_files(self, tmp_path): From 5afb244efb32bd04283382fa882120e59a17c045 Mon Sep 17 00:00:00 2001 From: Didier Cherix Date: Fri, 2 Oct 2026 13:51:30 +0200 Subject: [PATCH 2/2] Revert Chnagelog and fix copilot findings --- CHANGELOG.md | 5 +--- .../integrations/junie/__init__.py | 6 ++--- tests/integrations/test_integration_junie.py | 24 +++++++++++++++++++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dc1d1ee1c..f6c2d0ec65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,7 +100,7 @@ - fix(agy): support --model, --add-dir workspace, and pre-prompt extra args (#4481) - Remove a dead case pattern in common.sh (SC2221/SC2222) (#4605) - fix: support SKILL.md rendering for the generic integration (#4562) - - chore: release 1.0.8, begin 1.0.9.dev0 development (#4619) +- chore: release 1.0.8, begin 1.0.9.dev0 development (#4619) ## [1.0.8] - 2026-09-17 @@ -126,9 +126,6 @@ - Update adrkit extension to v0.1.4 (#4600) - chore: upgrade feature-assess to gh-aw v0.88.7 (#4546) - chore: release 1.0.7, begin 1.0.8.dev0 development (#4592) -- feat(integrations): enhance Junie integration with prompt arguments and variable escaping (#4600) -- fix(junie): ensure `allowPromptArgument` is unique and correctly set in command frontmatter -- feat(junie): map `$ARGUMENTS` to `$prompt` and generically escape other variables with `$$` ## [1.0.7] - 2026-09-15 diff --git a/src/specify_cli/integrations/junie/__init__.py b/src/specify_cli/integrations/junie/__init__.py index 5dc35f9035..8056a6ccac 100644 --- a/src/specify_cli/integrations/junie/__init__.py +++ b/src/specify_cli/integrations/junie/__init__.py @@ -145,9 +145,9 @@ def _inject_allow_prompt_argument(content: str, allow_prompt: bool = True) -> st # No frontmatter at all? Create one. return f"---\nallowPromptArgument: {value}\n---\n\n" + content - parts = content.split("---", 2) + parts = re.split(r"(?m)^---\s*$", content, maxsplit=2) if len(parts) < 3: - # Malformed frontmatter? + # Malformed frontmatter (e.g. missing closing dashes)? return content frontmatter = parts[1] @@ -156,7 +156,7 @@ def _inject_allow_prompt_argument(content: str, allow_prompt: bool = True) -> st if "allowPromptArgument:" in frontmatter: # Overwrite existing key frontmatter = re.sub( - r"(?m)^(\s*allowPromptArgument:\s*).*", + r"(?m)^(allowPromptArgument:\s*).*", fr"\1{value}", frontmatter ) diff --git a/tests/integrations/test_integration_junie.py b/tests/integrations/test_integration_junie.py index 18c93eb3c1..f9a1010cff 100644 --- a/tests/integrations/test_integration_junie.py +++ b/tests/integrations/test_integration_junie.py @@ -166,6 +166,30 @@ def test_junie_malformed_frontmatter(self): updated = junie.post_process_command_content(content) assert updated == content + def test_junie_frontmatter_non_standalone_delimiter(self): + """Verify --- not on standalone line is NOT identified as delimiter.""" + junie = get_integration("junie") + content = "---\ntitle: Foo\n--- not a delimiter\nbody" + updated = junie.post_process_command_content(content) + assert updated == content + + def test_junie_frontmatter_with_horizontal_rule_in_body(self): + """Verify valid frontmatter is preserved and horizontal rule in body is untouched.""" + junie = get_integration("junie") + content = "---\ntitle: Foo\n---\nBody with\n---\nhorizontal rule" + updated = junie.post_process_command_content(content) + assert "---" in updated + assert "horizontal rule" in updated + assert updated.count("---") == 3 + + def test_junie_nested_yaml_protection(self): + """Verify regex does not match nested allowPromptArgument.""" + junie = get_integration("junie") + content = "---\nconfig:\n allowPromptArgument: nested\n---\nBody" + updated = junie.post_process_command_content(content) + assert "allowPromptArgument: nested" in updated + assert "allowPromptArgument: false" not in updated + # -- Overrides for MarkdownIntegrationTests --------------------------- def test_setup_creates_files(self, tmp_path):