fix(workflows): auto-detect project integration instead of hardcoding copilot default#2408
fix(workflows): auto-detect project integration instead of hardcoding copilot default#2408markuswondrak wants to merge 13 commits intogithub:mainfrom
Conversation
… copilot (github#2406) The workflow engine hardcoded 'copilot' as the default integration input, ignoring the project's configured integration in .specify/integration.json. This caused workflows to fail when a project was initialized with a different integration (e.g. opencode) but the copilot CLI happened to be installed. Changes: - Change workflow.yml integration default from 'copilot' to 'auto' - Add _resolve_integration_auto() to WorkflowEngine that reads .specify/integration.json and falls back to 'copilot' if absent - Post-process resolved inputs to replace 'auto' with detected value - Add 4 regression tests for auto-detection scenarios Fixes github#2406 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Updates the built-in speckit workflow and workflow engine to avoid hardcoding "copilot" as the default integration by introducing an "auto" integration mode that resolves from project configuration.
Changes:
- Set
workflows/speckit/workflow.ymlintegration input default to"auto"and updated the prompt to document it. - Added
WorkflowEngine._resolve_integration_auto()and post-processing in_resolve_inputs()to read.specify/integration.json, with fallback to"copilot". - Added regression tests for auto-detection, fallback behavior, explicit override precedence, and empty JSON handling; added an Unreleased changelog entry.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| workflows/speckit/workflow.yml | Switches workflow input default integration from "copilot" to "auto" so runs honor project configuration. |
| src/specify_cli/workflows/engine.py | Implements "auto" integration resolution by reading .specify/integration.json with a backward-compatible fallback. |
| tests/test_workflows.py | Adds regression tests to ensure "auto" resolves correctly and preserves override behavior. |
| CHANGELOG.md | Notes the fix in the Unreleased section. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
mnriem
left a comment
There was a problem hiding this comment.
Thank you for looking into this. Please address Copilot feedback. If not applicable, please explain why
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 4/4 changed files
- Comments generated: 4
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback
- Add tests for JSONDecodeError and OSError fallback paths in _resolve_integration_auto() - Centralize INTEGRATION_JSON constant in workflows/constants.py (zero-dependency module) - Change changelog reference to (Fixes github#2406) for auto-close semantics - Remove static requires.integrations.any from speckit workflow (incomplete with 'auto') - Add opencode to integration prompt examples
|
Thanks for the thorough review! I've addressed all four findings:
|
mnriem
left a comment
There was a problem hiding this comment.
Please resolve conflicts
There was a problem hiding this comment.
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 5/6 changed files
- Comments generated: 3
|
Please revert changes to CHANGELOG.md as it is now auto-generated (still need to update docs to no longer require it) and resolve the other conflict |
- CHANGELOG.md: take released [0.8.3] block from origin, discard auto-generated [Unreleased] section that shouldn't have been committed - __init__.py: add devin_skill_mode from origin, preserve multi-line native_skill_mode format from HEAD Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- engine.py: strip whitespace from integration value in _resolve_integration_auto(); whitespace-only strings like ' ' now fall back to 'copilot' instead of being returned as-is - __init__.py: define INTEGRATION_JSON locally instead of importing from workflows.constants; avoids executing the workflows package __init__ (which registers all step types) during CLI startup for commands that don't use workflows - tests: add test for whitespace-only integration value fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 5 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Move INTEGRATION_JSON from specify_cli.workflows.constants to specify_cli.constants to avoid importing the workflows package when the main CLI module loads. Update imports in __init__.py and workflows/engine.py accordingly, and remove the now-empty workflows/constants.py. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 5 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 5 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 5 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… config - Add _INTEGRATION_JSON local constant in engine.py - Replace post-processing _resolve_integration_auto() with inline _resolve_default() + _load_project_integration() methods - Revert all Black style-only changes to engine.py, __init__.py - Delete unnecessary constants.py - Add TestIntegrationAutoDetect with 6 tests (no changes to existing tests) - Update workflow.yml: default 'auto', remove static integrations.any list Agent-Logs-Url: https://github.com/markuswondrak/spec-kit/sessions/4cb75741-9272-4a13-a90b-cc6cad486967 Co-authored-by: markuswondrak <245696895+markuswondrak@users.noreply.github.com>
…ion-auto-detect fix(workflows): minimal-invasive auto-detect integration from project config
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Use suggested type-safe strip call Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (1)
src/specify_cli/workflows/engine.py:747
- The docstring says the method falls back when the file “does not contain a valid” key, but the implementation only checks for a non-empty string (and not a known integration key). Either validate the value against the registered integrations (and fall back if unknown) or adjust the docstring wording to match the current behavior.
def _load_project_integration(self) -> str:
"""Read the active integration key from ``.specify/integration.json``.
Returns the stored integration string, or ``"copilot"`` when the file is
missing, unreadable, or does not contain a valid non-empty key.
The ``"copilot"`` fallback preserves backwards compatibility for projects
that predate the introduction of ``.specify/integration.json``.
"""
- Files reviewed: 3/3 changed files
- Comments generated: 2
| recorded in ``.specify/integration.json`` so workflows dispatch to the | ||
| AI the project was actually initialized with. | ||
| """ | ||
| if name == "integration" and default == "auto": | ||
| return self._load_project_integration() | ||
| return default | ||
|
|
||
| def _load_project_integration(self) -> str: | ||
| """Read the active integration key from ``.specify/integration.json``. | ||
|
|
||
| Returns the stored integration string, or ``"copilot"`` when the file is | ||
| missing, unreadable, or does not contain a valid non-empty key. | ||
| The ``"copilot"`` fallback preserves backwards compatibility for projects | ||
| that predate the introduction of ``.specify/integration.json``. | ||
| """ | ||
| path = self.project_root / _INTEGRATION_JSON | ||
| if not path.is_file(): | ||
| return "copilot" | ||
| try: | ||
| data = json.loads(path.read_text(encoding="utf-8")) | ||
| except (OSError, UnicodeDecodeError, json.JSONDecodeError): | ||
| return "copilot" | ||
| if isinstance(data, dict): | ||
| value = data.get("integration") | ||
| if isinstance(value, str): | ||
| value = value.strip() | ||
| if value and value != "auto": | ||
| return value |
There was a problem hiding this comment.
_load_project_integration() only consults .specify/integration.json. If that file is missing but .specify/init-options.json still exists (e.g., older projects or users deleting integration.json), integration=auto will incorrectly fall back to copilot instead of the project’s configured integration. Consider adding a secondary lookup (read init-options.json’s integration/ai key) before falling back to copilot.
This issue also appears on line 740 of the same file.
| recorded in ``.specify/integration.json`` so workflows dispatch to the | |
| AI the project was actually initialized with. | |
| """ | |
| if name == "integration" and default == "auto": | |
| return self._load_project_integration() | |
| return default | |
| def _load_project_integration(self) -> str: | |
| """Read the active integration key from ``.specify/integration.json``. | |
| Returns the stored integration string, or ``"copilot"`` when the file is | |
| missing, unreadable, or does not contain a valid non-empty key. | |
| The ``"copilot"`` fallback preserves backwards compatibility for projects | |
| that predate the introduction of ``.specify/integration.json``. | |
| """ | |
| path = self.project_root / _INTEGRATION_JSON | |
| if not path.is_file(): | |
| return "copilot" | |
| try: | |
| data = json.loads(path.read_text(encoding="utf-8")) | |
| except (OSError, UnicodeDecodeError, json.JSONDecodeError): | |
| return "copilot" | |
| if isinstance(data, dict): | |
| value = data.get("integration") | |
| if isinstance(value, str): | |
| value = value.strip() | |
| if value and value != "auto": | |
| return value | |
| recorded in project metadata so workflows dispatch to the AI the project | |
| was actually initialized with. | |
| """ | |
| if name == "integration" and default == "auto": | |
| return self._load_project_integration() | |
| return default | |
| def _load_project_integration(self) -> str: | |
| """Read the active integration key from project metadata. | |
| The primary source is ``.specify/integration.json``. If that file is | |
| missing or invalid, fall back to ``.specify/init-options.json`` for | |
| older projects or partially migrated state, checking ``integration`` | |
| first and then ``ai``. Returns ``"copilot"`` only when neither source | |
| contains a valid non-empty integration key. | |
| """ | |
| def _read_integration(path: Path, *keys: str) -> str | None: | |
| if not path.is_file(): | |
| return None | |
| try: | |
| data = json.loads(path.read_text(encoding="utf-8")) | |
| except (OSError, UnicodeDecodeError, json.JSONDecodeError): | |
| return None | |
| if not isinstance(data, dict): | |
| return None | |
| for key in keys: | |
| value = data.get(key) | |
| if isinstance(value, str): | |
| value = value.strip() | |
| if value and value != "auto": | |
| return value | |
| return None | |
| integration = _read_integration( | |
| self.project_root / _INTEGRATION_JSON, "integration" | |
| ) | |
| if integration is not None: | |
| return integration | |
| integration = _read_integration( | |
| self.project_root / ".specify" / "init-options.json", | |
| "integration", | |
| "ai", | |
| ) | |
| if integration is not None: | |
| return integration |
| # ID format: lowercase alphanumeric with hyphens | ||
| _ID_PATTERN = re.compile(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$") | ||
|
|
||
| _INTEGRATION_JSON = ".specify/integration.json" |
There was a problem hiding this comment.
_INTEGRATION_JSON duplicates the existing INTEGRATION_JSON = ".specify/integration.json" constant in src/specify_cli/__init__.py. To avoid string drift, consider centralizing this path constant in a small shared module (e.g., specify_cli/paths.py) and importing it from both places (avoiding importing the CLI module here).
| _INTEGRATION_JSON = ".specify/integration.json" | |
| _SPECIFY_DIR = ".specify" | |
| _INTEGRATION_JSON_FILENAME = "integration.json" | |
| _INTEGRATION_JSON = f"{_SPECIFY_DIR}/{_INTEGRATION_JSON_FILENAME}" |
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback. If not applicable, please explain why
Description
Fixes #2406
The built-in
speckitworkflow used"copilot"as the defaultintegrationinput. In projects initialized with a different integration such asopencode, workflows could dispatch to the wrong agent and fail with"No such agent: speckit.specify"when thecopilotCLI happened to be installed locally.Changes
workflows/speckit/workflow.ymlintegrationinput default from"copilot"to"auto"opencodeand the explicitautooptionrequires.integrations.anyallowlistsrc/specify_cli/workflows/engine.py_resolve_default()and_load_project_integration()to read.specify/integration.json"copilot"fallback for missing, unreadable, invalid, or empty config_resolve_inputs()so both defaulted and explicit--input integration=autovalues resolve through the same pathtests/test_workflows.pyintegration=auto, explicit override precedence, missing file fallback, invalid JSON fallback, OSError fallback, and whitespace-only valuesHow it works
integrationto"auto""auto", the engine reads.specify/integration.jsonand uses the stored integration key"copilot"for backwards compatibilityTesting
uvx ruff check src/ tests/test_workflows.pyuv run pytest tests/test_workflows.pyuv run pytest tests/test_workflows.py -k TestIntegrationAutoDetect -vAI Disclosure