Skip to content

Commit adff325

Browse files
mnriemCopilot
andcommitted
fix: address review — integration defaults, integration_options, engine-owned dirs
- Apply DEFAULT_INIT_INTEGRATION fallback when neither step config nor workflow context provides an integration, so output.integration always reflects the actual integration used. - Add integration_options config field to support --integration-options passthrough (required for generic integration and --skills mode). - Exclude .specify/ from the non-empty directory fast-fail check so that here: true works when the engine has already created its run-state directory before steps execute. - Note: mix_stderr=False is not needed — Click 8.2+ captures stderr separately by default and the existing try/except handles access. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5540606 commit adff325

3 files changed

Lines changed: 83 additions & 2 deletions

File tree

src/specify_cli/workflows/steps/init/__init__.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,19 @@
1111
import os
1212
from typing import Any
1313

14+
from specify_cli._agent_config import DEFAULT_INIT_INTEGRATION
1415
from specify_cli.workflows.base import StepBase, StepContext, StepResult, StepStatus
1516
from specify_cli.workflows.expressions import evaluate_expression
1617

1718
#: Valid ``script`` values, mirroring ``specify init --script``.
1819
VALID_SCRIPT_TYPES = ("sh", "ps")
1920

21+
#: Directories the workflow engine may create before steps run.
22+
#: These are excluded from the "non-empty directory" fast-fail check so
23+
#: that ``here: true`` works without requiring ``force: true`` when the
24+
#: only pre-existing content is engine run-state.
25+
_ENGINE_OWNED_DIRS = {".specify"}
26+
2027

2128
class InitStep(StepBase):
2229
"""Bootstrap a project, equivalent to running ``specify init``.
@@ -47,7 +54,10 @@ class InitStep(StepBase):
4754
Initialize in the target directory instead of creating a new one.
4855
``integration``
4956
Integration key (e.g. ``copilot``). Defaults to the workflow's
50-
default integration.
57+
default integration, then to ``DEFAULT_INIT_INTEGRATION``.
58+
``integration_options``
59+
Extra options for the integration (e.g. ``"--skills"`` or
60+
``"--commands-dir .myagent/cmds"``).
5161
``script``
5262
Script type, ``sh`` or ``ps``.
5363
``force``
@@ -67,7 +77,14 @@ def execute(self, config: dict[str, Any], context: StepContext) -> StepResult:
6777

6878
integration = config.get("integration") or context.default_integration
6979
integration = self._resolve(integration, context)
80+
# Apply the same default that specify init uses in non-interactive mode
81+
# so that output.integration reflects the actual integration used.
82+
if not integration:
83+
integration = DEFAULT_INIT_INTEGRATION
7084

85+
integration_options = self._resolve(
86+
config.get("integration_options"), context
87+
)
7188
script = self._resolve(config.get("script"), context)
7289
preset = self._resolve(config.get("preset"), context)
7390

@@ -95,7 +112,10 @@ def execute(self, config: dict[str, Any], context: StepContext) -> StepResult:
95112
base = context.project_root or os.getcwd()
96113
try:
97114
with os.scandir(base) as it:
98-
not_empty = any(it)
115+
not_empty = any(
116+
entry for entry in it
117+
if entry.name not in _ENGINE_OWNED_DIRS
118+
)
99119
except OSError:
100120
not_empty = False
101121
if not_empty:
@@ -110,6 +130,7 @@ def execute(self, config: dict[str, Any], context: StepContext) -> StepResult:
110130
"project": project,
111131
"here": here,
112132
"integration": integration,
133+
"integration_options": integration_options,
113134
"script": script,
114135
"exit_code": 1,
115136
"stdout": "",
@@ -120,6 +141,8 @@ def execute(self, config: dict[str, Any], context: StepContext) -> StepResult:
120141

121142
if integration:
122143
argv.extend(["--integration", str(integration)])
144+
if integration_options:
145+
argv.extend(["--integration-options", str(integration_options)])
123146
if script:
124147
argv.extend(["--script", str(script)])
125148
if preset:
@@ -136,6 +159,7 @@ def execute(self, config: dict[str, Any], context: StepContext) -> StepResult:
136159
"project": project,
137160
"here": here,
138161
"integration": integration,
162+
"integration_options": integration_options,
139163
"script": script,
140164
"exit_code": exit_code,
141165
"stdout": stdout,

tests/test_workflows.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,62 @@ def test_non_empty_current_dir_without_force_fails_fast(self, tmp_path):
10721072
assert "force: true" in (result.error or "")
10731073
assert not (tmp_path / ".specify").exists()
10741074

1075+
def test_engine_owned_dirs_do_not_trigger_non_empty_check(self, tmp_path):
1076+
from specify_cli.workflows.steps.init import InitStep
1077+
from specify_cli.workflows.base import StepContext, StepStatus
1078+
1079+
# Simulate the engine creating its run-state directory before steps run
1080+
(tmp_path / ".specify" / "workflows" / "runs" / "abc123").mkdir(
1081+
parents=True
1082+
)
1083+
1084+
step = InitStep()
1085+
ctx = StepContext(
1086+
project_root=str(tmp_path), default_integration="copilot"
1087+
)
1088+
result = step.execute(
1089+
{"id": "bootstrap", "here": True, "script": "sh", "force": True},
1090+
ctx,
1091+
)
1092+
assert result.status == StepStatus.COMPLETED
1093+
1094+
def test_default_integration_when_none_provided(self, tmp_path):
1095+
from specify_cli.workflows.steps.init import InitStep
1096+
from specify_cli.workflows.base import StepContext, StepStatus
1097+
1098+
step = InitStep()
1099+
# No default_integration on context either
1100+
ctx = StepContext(project_root=str(tmp_path))
1101+
result = step.execute(
1102+
{"id": "bootstrap", "here": True, "script": "sh"},
1103+
ctx,
1104+
)
1105+
assert result.status == StepStatus.COMPLETED
1106+
assert result.output["integration"] == "copilot"
1107+
1108+
def test_integration_options_passed_through(self, tmp_path):
1109+
from specify_cli.workflows.steps.init import InitStep
1110+
from specify_cli.workflows.base import StepContext, StepStatus
1111+
1112+
step = InitStep()
1113+
ctx = StepContext(
1114+
project_root=str(tmp_path), default_integration="copilot"
1115+
)
1116+
result = step.execute(
1117+
{
1118+
"id": "bootstrap",
1119+
"here": True,
1120+
"script": "sh",
1121+
"integration": "copilot",
1122+
"integration_options": "--skills",
1123+
},
1124+
ctx,
1125+
)
1126+
assert result.status == StepStatus.COMPLETED
1127+
assert "--integration-options" in result.output["argv"]
1128+
assert "--skills" in result.output["argv"]
1129+
assert result.output["integration_options"] == "--skills"
1130+
10751131
def test_validate_rejects_bad_script(self):
10761132
from specify_cli.workflows.steps.init import InitStep
10771133

workflows/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ and resolves the integration from the step config or the workflow default:
126126
type: init
127127
here: true # or: project: my-project
128128
integration: copilot # Optional: defaults to workflow integration
129+
integration_options: "--skills" # Optional: extra options for the integration
129130
script: sh # Optional: sh or ps
130131
force: true # Optional: required to merge into a non-empty directory
131132
preset: healthcare-compliance # Optional preset ID

0 commit comments

Comments
 (0)