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
2 changes: 1 addition & 1 deletion docs/reference/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
| [Qoder CLI](https://qoder.com/cli) | `qodercli` | |
| [Qwen Code](https://github.com/QwenLM/qwen-code) | `qwen` | |
| [RovoDev](https://www.atlassian.com/software/rovo-dev) | `rovodev` | Generates `.rovodev/skills/`, prompt wrappers, and `prompts.yml`; runtime dispatch uses `acli rovodev` |
| [SHAI (OVHcloud)](https://github.com/ovh/shai) | `shai` | |
| [SHAI (OVHcloud)](https://github.com/ovh/shai) | `shai` | Workflow `command` and `prompt` steps can't run through SHAI: its CLI accepts prompt text (as arguments, on stdin, or through `shai agent`), but hands it to its auto-fix agent and never loads `.shai/commands`, so those steps fail |
| [Tabnine CLI](https://docs.tabnine.com/main/getting-started/tabnine-cli) | `tabnine` | |
| [Trae](https://www.trae.ai/) | `trae` | Skills-based integration; skills are installed automatically |
| [ZCode](https://zcode.z.ai/) | `zcode` | Skills-based integration; installs skills into `.zcode/skills/` and invokes them as `$speckit-<command>` |
Expand Down
29 changes: 29 additions & 0 deletions src/specify_cli/integrations/shai/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
"""SHAI CLI integration."""

from __future__ import annotations

from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Any

from ..base import MarkdownIntegration


Expand All @@ -19,3 +25,26 @@ class ShaiIntegration(MarkdownIntegration):
"extension": ".md",
}
multi_install_safe = True

def build_exec_args(
self,
prompt: str,
*,
model: str | None = None,
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
# SHAI takes headless prompt text from positional arguments, stdin or
# `shai agent <name> <prompt>`, but every route goes to its auto-fix
# agent (`handle_fix` in shai-cli/src/main.rs of ovh/shai), and
# `.shai/commands` is never loaded, so no route runs an installed
# Spec Kit command. Positional arguments collect `-p` and `--model`
# too (`trailing_var_arg`/`allow_hyphen_values`), so the inherited
# `shai -p <prompt>` handed that literal text to the auto-fix agent,
# which exits 0, and workflow steps reported success without running
# the command (#2416). Opt out of CLI dispatch and let those steps
# fail instead.
self.validate_runtime_config(integration_args, integration_options)
return None
8 changes: 6 additions & 2 deletions src/specify_cli/workflows/step/command/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,12 @@ def execute(self, config: dict[str, Any], context: StepContext) -> StepResult:
output=output,
error=(
f"Cannot dispatch command {command!r}: "
f"integration {integration!r} CLI not found or not installed. "
f"Install the CLI tool or check 'specify integration list'."
f"integration {integration!r} CLI not found or not installed, "
f"or the integration does not support CLI dispatch. "
f"If the CLI is missing, install it or check "
f"'specify integration list'; if the integration does not "
f"support CLI dispatch, set the step's 'integration' to one "
f"that does."
),
)

Expand Down
6 changes: 5 additions & 1 deletion src/specify_cli/workflows/step/prompt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,11 @@ def execute(self, config: dict[str, Any], context: StepContext) -> StepResult:
error=(
f"Cannot dispatch prompt: "
f"integration {integration!r} "
f"CLI not found or not installed."
f"CLI not found or not installed, "
f"or the integration does not support CLI dispatch. "
f"If the CLI is missing, install it; if the integration "
f"does not support CLI dispatch, set the step's "
f"'integration' to one that does."
),
)

Expand Down
49 changes: 49 additions & 0 deletions tests/integrations/test_integration_shai.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
"""Tests for ShaiIntegration."""

from unittest.mock import MagicMock, patch

from specify_cli.integrations import get_integration
from specify_cli.workflows.base import StepContext, StepStatus
from specify_cli.workflows.step.command import CommandStep
from specify_cli.workflows.step.prompt import PromptStep

from .test_integration_base_markdown import MarkdownIntegrationTests


Expand All @@ -8,3 +15,45 @@ class TestShaiIntegration(MarkdownIntegrationTests):
FOLDER = ".shai/"
COMMANDS_SUBDIR = "commands"
REGISTRAR_DIR = ".shai/commands"


class TestShaiCliDispatch:
"""SHAI's CLI can't run an installed Spec Kit command (#2416).

SHAI's argument, stdin and `shai agent <name> <prompt>` routes all pass
the text to its auto-fix agent, which exits 0, and `.shai/commands` is
never loaded, so dispatching `shai -p <prompt>` marked workflow steps
completed without running the command.
"""

def test_build_exec_args_opts_out(self):
integration = get_integration("shai")
assert integration.build_exec_args("/speckit.plan", model="m") is None

def _run(self, step, config, tmp_path):
ctx = StepContext(default_integration="shai", project_root=str(tmp_path))
exited_ok = MagicMock(returncode=0, stdout="", stderr="")
with patch("shutil.which", return_value="/usr/local/bin/shai"), \
patch("subprocess.run", return_value=exited_ok) as run:
result = step.execute(config, ctx)
return result, run

def test_command_step_fails_instead_of_running_shai(self, tmp_path):
result, run = self._run(
CommandStep(), {"id": "plan", "command": "speckit.plan"}, tmp_path
)
assert result.status == StepStatus.FAILED
assert result.output["dispatched"] is False
assert "does not support CLI dispatch" in result.error
assert "set the step's 'integration'" in result.error
run.assert_not_called()

def test_prompt_step_fails_instead_of_running_shai(self, tmp_path):
result, run = self._run(
PromptStep(), {"id": "ask", "prompt": "Summarize the spec"}, tmp_path
)
assert result.status == StepStatus.FAILED
assert result.output["dispatched"] is False
assert "does not support CLI dispatch" in result.error
assert "set the step's 'integration'" in result.error
run.assert_not_called()