fix(workflow): support integration: auto to follow project's initialized AI#2421
fix(workflow): support integration: auto to follow project's initialized AI#2421Quratulain-bilal wants to merge 2 commits intogithub:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adjusts the speckit workflow so its integration input defaults to the project’s initialized AI integration (via .specify/integration.json) instead of being hardcoded to Copilot, aligning workflow execution with project initialization state.
Changes:
- Updated
workflows/speckit/workflow.ymlto usedefault: "auto"forinputs.integrationand clarified the prompt text. - Enhanced
WorkflowEngineinput resolution to interpret the"auto"default sentinel forintegrationby reading.specify/integration.json. - Added regression tests covering auto-resolution, explicit override behavior, missing file fallback, and malformed JSON fallback.
Show a summary per file
| File | Description |
|---|---|
| workflows/speckit/workflow.yml | Switches the workflow’s integration default to auto and documents the behavior in the input prompt. |
| src/specify_cli/workflows/engine.py | Implements "auto" default resolution for the integration input by reading .specify/integration.json. |
| tests/test_workflows.py | Adds tests validating the new default resolution and fallback/override behavior. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 3/3 changed files
- Comments generated: 3
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback
- Centralize the integration.json path as a module-level INTEGRATION_JSON constant in workflows/engine.py (mirrors specify_cli.INTEGRATION_JSON; cannot be imported directly without a circular dependency). - Catch UnicodeDecodeError alongside JSONDecodeError/OSError so a non-UTF8 integration.json falls back to the literal default instead of crashing the workflow engine. Adds a regression test. - Drop the narrow requires.integrations.any allowlist in the speckit workflow YAML; the four core commands (specify/plan/tasks/implement) are provided by every integration, so the previous list was always inaccurate (e.g. opencode, codex, etc. were excluded).
|
Thanks for the review @mnriem and @copilot — addressed all three points in 74dcaf3: 1. Centralized path constant — added a module-level 2. UnicodeDecodeError fallback — extended the 3. All 131 workflow tests pass locally. |
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: 3/3 changed files
- Comments generated: 3
| requires: | ||
| speckit_version: ">=0.7.2" | ||
| integrations: | ||
| any: ["copilot", "claude", "gemini"] | ||
| # The four commands used below (specify, plan, tasks, implement) are core | ||
| # spec-kit commands provided by every integration, so this workflow runs | ||
| # against whichever integration the project was initialized with. |
There was a problem hiding this comment.
The requires.integrations constraint was removed here, but the PR description only mentions changing the integration input default/prompt. If this removal is intentional, please update the requires metadata to explicitly reflect the intended compatibility (e.g., broaden the allowed integrations) rather than dropping the field entirely, since other tooling/docs may rely on it as a declarative compatibility signal.
| ) | ||
| elif "default" in input_def: | ||
| resolved[name] = input_def["default"] | ||
| resolved[name] = self._resolve_default(name, input_def["default"]) |
There was a problem hiding this comment.
_resolve_inputs() coerces/validates provided values via _coerce_input(), but defaults (including the newly project-resolved integration default) bypass type coercion and enum validation. This can yield invalid inputs values even when an input definition declares an enum/type. Consider running the resolved default through _coerce_input() as well so defaults are validated consistently with user-provided inputs.
| resolved[name] = self._resolve_default(name, input_def["default"]) | |
| default_value = self._resolve_default(name, input_def["default"]) | |
| resolved[name] = self._coerce_input( | |
| name, default_value, input_def | |
| ) |
|
Please address Copilot feedback. If not applicable, please explain why |
- Move INTEGRATION_JSON to a dedicated specify_cli._paths module so the CLI entrypoint and the workflows engine share a single source of truth instead of each carrying its own constant. specify_cli still re-exports INTEGRATION_JSON for backward compatibility. - Run the resolved default value through _coerce_input() so workflow defaults (including the project-resolved integration default) are validated against declared type/enum constraints, just like user-provided inputs. Adds two regression tests covering an enum violation and numeric coercion of a string default. - Restore requires.integrations as an explicit declarative compatibility signal, using the wildcard form ``any: "*"`` to accurately reflect that the workflow runs against any registered integration.
|
Round 2 — addressed in e9d946e: 1. Shared path constant (no duplication) — moved 2. Defaults now run through 3. Test results: |
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/5 changed files
- Comments generated: 5
|
Please address Copilot feedback. If not applicable, please explain why |
- workflows/speckit/workflow.yml: replace the wildcard ``any: "*"`` with an explicit list ``[claude, copilot, gemini, opencode]`` matching the documented schema for ``requires.integrations.any``. Comment notes the list is a non-exhaustive compatibility hint. - engine.validate_workflow: validate input defaults at authoring time so enum/type mismatches in workflow YAML surface during install/validation instead of at execution. The ``integration: auto`` sentinel is exempt because it is resolved against project state at runtime. - Extract a shared low-level helper ``try_read_integration_json`` into ``specify_cli._paths`` and use it from the workflow engine. Keeps parsing rules consistent between the CLI's loud loader and the engine's silent loader; CLI ``_read_integration_json`` retains its diagnostic semantics layered on top of the same parsing surface. - Drop the now-unused INTEGRATION_JSON import in engine.py. - Update test docstring to reflect actual engine behavior on missing integration state (no special-cased dispatcher error message exists). - Add 2 regression tests for validate_workflow's new default checks.
|
Round 3 — addressed in 67e4d1f. Walking through each Copilot point: 1. 2. 3. 4. Test docstring overstates dispatcher behavior — applied the suggested wording change. The docstring now accurately states that the engine must not invent an integration; downstream resolution handles an unresolved 5. PR description test count drifted — updated the PR body to match the actual coverage (8 new regression tests, full breakdown in the "Fix" section). Test results: 135 passed in |
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:741
_resolve_inputs()always runs resolved defaults through_coerce_input(). Forintegration: autowith anenumthat does not include"auto", if.specify/integration.jsonis missing/malformed then_resolve_default()returns the literal"auto"and_coerce_input()will raise a ValueError during input resolution. This is inconsistent with the validation-time exemption forintegration: autoand with the intended "fall back to literal default" behavior. Consider exempting enum validation for the"auto"sentinel at runtime (or skipping coercion until afterautohas resolved to a concrete integration).
elif "default" in input_def:
default_value = self._resolve_default(name, input_def["default"])
resolved[name] = self._coerce_input(name, default_value, input_def)
elif input_def.get("required", False):
- Files reviewed: 5/5 changed files
- Comments generated: 1
|
Please address Copilot feedback. If not applicable, please explain wh |
…#2421 Previously the ``integration: auto`` exemption skipped default validation entirely, which meant a workflow could declare an incompatible type (e.g. ``type: number`` with ``default: "auto"``) and still pass validation, only to fail later at runtime. Both the install-time check in ``validate_workflow()`` and the runtime coercion in ``_resolve_inputs()`` now exempt only the enum-membership check for the auto sentinel; the declared type is still enforced. Adds a regression test that asserts a ``type: number`` + ``default: "auto"`` workflow is rejected at validation time.
|
Round 4 — addressed in 0dd3426. Good catch. Tightened the Added a regression test that asserts a workflow declaring Test results: 136 passed in |
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/init.py:1901
_read_integration_json()still re-implements JSON parsing directly. Since this PR introducestry_read_integration_json()as the shared low-level loader (including handlingUnicodeDecodeErrorfor non-UTF8 files), consider using that helper here (and layering the CLI’s user-facing diagnostics on top) to avoid drift and to ensure non-UTF8integration.jsondoesn’t crash the CLI with an uncaught decode error.
from ._paths import INTEGRATION_JSON # noqa: E402 (re-export for backward compat)
def _read_integration_json(project_root: Path) -> dict[str, Any]:
"""Load ``.specify/integration.json``. Returns ``{}`` when missing."""
- Files reviewed: 5/5 changed files
- Comments generated: 1
…thub#2421 - workflows/speckit/workflow.yml: bump ``requires.speckit_version`` from ``>=0.7.2`` to ``>=0.8.3`` so older spec-kit versions, which lack the engine-side resolution of the ``integration: "auto"`` sentinel, do not pull this workflow from the catalog and then fail by treating "auto" as a literal integration key. Adds an inline comment explaining the reason for the floor. - src/specify_cli/__init__.py: ``_read_integration_json`` now also catches ``UnicodeDecodeError`` (non-UTF8 file) so the CLI fails with a targeted, actionable diagnostic instead of an uncaught traceback, and notes in the docstring that the function shares its low-level parsing surface with ``_paths.try_read_integration_json`` while keeping loud per-cause messaging on top.
|
Round 5 — addressed in ae4c94d. 1. 2. I deliberately kept Test results: 136 passed in |
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/5 changed files
- Comments generated: 0 new
…zed AI Closes github#2406 (squashed)
ae4c94d to
2886441
Compare
|
happy to address any further feedback whenever you have a chance to review. CI is green and all review threads are resolved. |
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: 1
| except UnicodeDecodeError as exc: | ||
| console.print(f"[red]Error:[/red] {path} is not valid UTF-8.") | ||
| console.print(f"Please fix or delete {INTEGRATION_JSON} and retry.") | ||
| console.print(f"[dim]Details:[/dim] {exc}") | ||
| raise typer.Exit(1) |
|
Please addres Copilot feedback. If not applicable, please explain why |
Address Copilot feedback: UnicodeDecodeError can be raised by both read_text() and json.loads(), so combining the handlers ensures both cases produce a consistent, clear error message.
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: 2
| data = json.loads(path.read_text(encoding="utf-8")) | ||
| except (json.JSONDecodeError, OSError, UnicodeDecodeError): | ||
| return None |
| """`integration: auto` should resolve to .specify/integration.json's integration.""" | ||
| from specify_cli.workflows.engine import WorkflowEngine, WorkflowDefinition | ||
|
|
||
| specify_dir = project_dir / ".specify" | ||
| specify_dir.mkdir(parents=True, exist_ok=True) | ||
| (specify_dir / "integration.json").write_text( | ||
| json.dumps({"integration": "opencode", "version": "0.7.4"}), |
|
Please address Copilot feedback |
Closes #2406
Problem
The
speckitworkflow'sintegrationinput haddefault: "copilot"hardcoded inworkflows/speckit/workflow.yml. This meant projects initialized withclaudeorgeminiwould silently fall back to copilot when running the workflow without an explicit--input integration=...flag, breaking the principle of least surprise.Fix
workflows/speckit/workflow.yml: changedefault: "copilot"→default: "auto", update the prompt to describe the new behavior, and broadenrequires.integrations.anyto reflect that the workflow's four core commands (specify/plan/tasks/implement) are provided by every integration.src/specify_cli/_paths.py(new): single-source-of-truth forINTEGRATION_JSONand a shared low-leveltry_read_integration_jsonhelper used by both the CLI and the workflow engine.src/specify_cli/workflows/engine.py:_resolve_default()so when an input default is the sentinel string"auto"and the input name is"integration", the engine reads.specify/integration.json(via the shared helper) and uses the project's initialized integration. Falls back to the literal"auto"when the file is missing or malformed._coerce_input()so workflow defaults are validated against declaredtype/enumconstraints, just like user-provided inputs.validate_workflow()to validate input defaults at authoring time so enum/type mismatches surface during install/validation rather than at execution. Theintegration: autosentinel is exempt because it is runtime-resolved against project state.src/specify_cli/__init__.py: re-exportINTEGRATION_JSONfrom_pathsfor backward compatibility.tests/test_workflows.py: 8 new regression tests covering the auto-resolve happy path, the missing-file fallback, the explicit-input override, the malformed JSON path, the non-UTF8 path, default coercion, default enum validation, and thevalidate_workflowexemption forauto.The change is opt-in via the YAML default, so existing workflows that hardcode
default: "copilot"(or any other literal) keep working unchanged.Test plan
1732 passed, 125 skipped)speckitworkflow respects.specify/integration.jsonwhen no--inputis given--input integration=copilotstill overrides the project default.specify/integration.jsondoes not crash the enginevalidate_workflow()rejects defaults that violate declared enum/type