Skip to content

Commit c32a4ce

Browse files
committed
Address fourteenth review: prompt fails without CLI, resolved step data, fan-out normalization
- PromptStep returns FAILED when CLI not installed (was silent COMPLETED) - Engine step_data prefers resolved values from step output - Fan-out normalizes output.results=[] for empty item lists - subprocess.run inherits stdout/stderr (no explicit sys.stdout) - Registry tests use issubset for extensibility
1 parent 3094425 commit c32a4ce

5 files changed

Lines changed: 35 additions & 25 deletions

File tree

src/specify_cli/integrations/base.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ def dispatch_command(
156156
support CLI dispatch.
157157
"""
158158
import subprocess
159-
import sys
160159

161160
prompt = self.build_command_invocation(command_name, args)
162161
# When streaming to the terminal, request text output so the
@@ -181,8 +180,6 @@ def dispatch_command(
181180
try:
182181
result = subprocess.run(
183182
exec_args,
184-
stdout=sys.stdout,
185-
stderr=sys.stderr,
186183
text=True,
187184
cwd=cwd,
188185
)

src/specify_cli/integrations/copilot/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ def dispatch_command(
7575
the user's arguments.
7676
"""
7777
import subprocess
78-
import sys
7978

8079
stem = command_name
8180
if "." in stem:
@@ -104,8 +103,6 @@ def dispatch_command(
104103
try:
105104
result = subprocess.run(
106105
cli_args,
107-
stdout=sys.stdout,
108-
stderr=sys.stderr,
109106
text=True,
110107
cwd=cwd,
111108
)

src/specify_cli/workflows/engine.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -530,13 +530,18 @@ def _execute_steps(
530530

531531
result: StepResult = step_impl.execute(step_config, context)
532532

533-
# Record step results in context
533+
# Record step results — prefer resolved values from step output
534534
step_data = {
535-
"integration": step_config.get("integration")
535+
"integration": result.output.get("integration")
536+
or step_config.get("integration")
536537
or context.default_integration,
537-
"model": step_config.get("model") or context.default_model,
538-
"options": step_config.get("options", {}),
539-
"input": step_config.get("input", {}),
538+
"model": result.output.get("model")
539+
or step_config.get("model")
540+
or context.default_model,
541+
"options": result.output.get("options")
542+
or step_config.get("options", {}),
543+
"input": result.output.get("input")
544+
or step_config.get("input", {}),
540545
"output": result.output,
541546
"status": result.status.value,
542547
}
@@ -609,9 +614,10 @@ def _execute_steps(
609614
return
610615

611616
# Fan-out: execute nested step template per item with unique IDs
612-
if step_type == "fan-out" and result.output.get("items"):
617+
if step_type == "fan-out":
618+
items = result.output.get("items", [])
613619
template = result.output.get("step_template", {})
614-
if template:
620+
if template and items:
615621
fan_out_results = []
616622
for item_idx, item_val in enumerate(result.output["items"]):
617623
context.item = item_val
@@ -646,6 +652,11 @@ def _execute_steps(
646652
RunStatus.ABORTED,
647653
):
648654
return
655+
else:
656+
# Empty items or no template — normalize output
657+
result.output["results"] = []
658+
context.steps[step_id]["output"] = result.output
659+
state.step_results[step_id]["output"] = result.output
649660

650661
def _resolve_inputs(
651662
self,

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,22 @@ def execute(self, config: dict[str, Any], context: StepContext) -> StepResult:
7777
or f"Prompt exited with code {dispatch_result['exit_code']}"
7878
),
7979
)
80+
return StepResult(
81+
status=StepStatus.COMPLETED,
82+
output=output,
83+
)
8084
else:
81-
output["exit_code"] = 0
85+
output["exit_code"] = 1
8286
output["dispatched"] = False
83-
84-
return StepResult(
85-
status=StepStatus.COMPLETED,
86-
output=output,
87-
)
87+
return StepResult(
88+
status=StepStatus.FAILED,
89+
output=output,
90+
error=(
91+
f"Cannot dispatch prompt: "
92+
f"integration {config.get('integration') or context.default_integration!r} "
93+
f"CLI not found or not installed."
94+
),
95+
)
8896

8997
@staticmethod
9098
def _try_dispatch(
@@ -114,7 +122,6 @@ def _try_dispatch(
114122
return None
115123

116124
import subprocess
117-
import sys
118125

119126
project_root = (
120127
Path(context.project_root) if context.project_root else Path.cwd()
@@ -123,8 +130,6 @@ def _try_dispatch(
123130
try:
124131
result = subprocess.run(
125132
exec_args,
126-
stdout=sys.stdout,
127-
stderr=sys.stderr,
128133
text=True,
129134
cwd=str(project_root),
130135
)

tests/test_workflows.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class TestStepRegistry:
9292
def test_registry_populated(self):
9393
from specify_cli.workflows import STEP_REGISTRY
9494

95-
assert len(STEP_REGISTRY) == 10
95+
assert len(STEP_REGISTRY) >= 10
9696

9797
def test_all_step_types_registered(self):
9898
from specify_cli.workflows import STEP_REGISTRY
@@ -101,7 +101,7 @@ def test_all_step_types_registered(self):
101101
"command", "shell", "prompt", "gate", "if", "switch",
102102
"while", "do-while", "fan-out", "fan-in",
103103
}
104-
assert set(STEP_REGISTRY.keys()) == expected
104+
assert expected.issubset(set(STEP_REGISTRY.keys()))
105105

106106
def test_get_step_type(self):
107107
from specify_cli.workflows import get_step_type
@@ -580,7 +580,7 @@ def test_execute_basic(self):
580580
"prompt": "Review {{ inputs.file }} for security issues",
581581
}
582582
result = step.execute(config, ctx)
583-
assert result.status == StepStatus.COMPLETED
583+
assert result.status == StepStatus.FAILED
584584
assert result.output["prompt"] == "Review auth.py for security issues"
585585
assert result.output["integration"] == "claude"
586586
assert result.output["dispatched"] is False

0 commit comments

Comments
 (0)