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
9 changes: 9 additions & 0 deletions docs/integrations/junie.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ Invoke them in Junie's chat as `/speckit-specify` or
Handoff references and extension/preset command content are also normalized to
hyphenated names.

Commands that accept free-text input use Junie's `$prompt` placeholder with
`allowPromptArgument: true` in the installed command frontmatter. Specify
adapts `$ARGUMENTS` in core, extension, and preset commands during installation.
For example, enter this in Junie's chat to pass a feature description:

```text
/speckit-specify Add a search field to the dashboard
```

## Workflow Dispatch

Workflow steps use `junie --task <prompt>`, with `--model` when configured and
Expand Down
25 changes: 24 additions & 1 deletion src/specify_cli/integrations/junie/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,29 @@ def _rewrite_handoff_references(content: str) -> str:
lambda m: f"{m.group(1)}{format_junie_command_name(m.group(2))}",
content,
)

@staticmethod
def _adapt_argument_placeholders(content: str) -> str:
"""Stop Junie from auto-detecting required ``$identifier`` arguments.

Junie treats every ``$name`` token in a command body as a required
named argument. Map ``$ARGUMENTS`` to Junie's free-text ``$prompt``
(enabled via ``allowPromptArgument`` frontmatter) and drop the prose
example ``$speckit-...`` describing other agents' invocation syntax.
"""
content = content.replace(" or `$speckit-...`", "")
content = content.replace("$ARGUMENTS", "$prompt")
fm = re.match(r"\A---(\r?\n)(.*?)(\r?\n)---(?=\r?\n|\Z)", content, re.S)
if "$prompt" in content and fm:
Comment on lines +147 to +149
nl, block = fm.group(1), fm.group(2)
block, n = re.subn(
r"(?m)^allowPromptArgument:[^\r\n]*", "allowPromptArgument: true", block
)
if not n:
block = f"allowPromptArgument: true{nl}{block}"
content = f"---{nl}{block}{content[fm.end(2):]}"
return content

def post_process_command_content(self, content: str) -> str:
"""Apply Junie-specific transformations to command content.

Expand All @@ -143,7 +166,7 @@ def post_process_command_content(self, content: str) -> str:
"""
updated = self._inject_hook_command_note(content)
updated = self._rewrite_handoff_references(updated)
return updated
return self._adapt_argument_placeholders(updated)

def setup(
self,
Expand Down
37 changes: 37 additions & 0 deletions tests/integrations/test_integration_junie.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,43 @@ 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_argument_placeholders_adapted(self):
"""$ARGUMENTS / $speckit-... must not become required Junie arguments."""
junie = get_integration("junie")
content = (
"---\ndescription: x\n---\n$ARGUMENTS\n"
"run as `/skill:speckit-...` or `$speckit-...`).\n"
)
out = junie.post_process_command_content(content)
assert out.startswith("---\nallowPromptArgument: true\n")
assert "$prompt" in out
assert "$ARGUMENTS" not in out
assert "$speckit" not in out

def test_junie_allow_prompt_argument_false_is_enabled(self):
junie = get_integration("junie")
content = "---\nallowPromptArgument: false\n---\n$ARGUMENTS\n"
out = junie.post_process_command_content(content)
assert out == "---\nallowPromptArgument: true\n---\n$prompt\n"
body = "---\ndescription: x\n---\nallowPromptArgument: no\n$ARGUMENTS\n"
out = junie.post_process_command_content(body)
assert out.startswith("---\nallowPromptArgument: true\ndescription: x\n---\n")

def test_junie_setup_leaves_no_stray_dollar_arguments(self, tmp_path):
"""Installed commands only reference Junie's own ``$prompt`` token."""
import re
from specify_cli.integrations.manifest import IntegrationManifest

junie = get_integration("junie")
m = IntegrationManifest("junie", tmp_path)
junie.setup(tmp_path, m, script_type="sh")
files = list((tmp_path / ".junie" / "commands").glob("*.md"))
assert files
for f in files:
text = f.read_text(encoding="utf-8")
assert set(re.findall(r"\$[A-Za-z_][A-Za-z0-9_-]*", text)) <= {"$prompt"}, f
assert "allowPromptArgument: true" in text

def test_init_next_steps_show_hyphenated_commands(self, tmp_path):
"""Next Steps must list the `/speckit-<name>` commands Junie installs."""
from typer.testing import CliRunner
Expand Down