@@ -541,6 +541,152 @@ def version(
541541app .add_typer (_self_app , name = "self" )
542542
543543
544+ # ===== Spec / Plan Commands (direct CLI access with dry-run) =====
545+
546+ specify_app = typer .Typer (
547+ name = "specify" ,
548+ help = "Create a feature specification (direct CLI alternative to /speckit.specify in coding agents)" ,
549+ add_completion = False ,
550+ )
551+ app .add_typer (specify_app , name = "specify" )
552+
553+
554+ @specify_app .command ("specify" )
555+ def specify_specify (
556+ spec : str = typer .Option (
557+ ..., "--spec" , "-s" , help = "Feature description (what to build and why)"
558+ ),
559+ dry_run : bool = typer .Option (
560+ False , "--dry-run" , help = "Show rendered prompt/inputs without invoking the AI"
561+ ),
562+ ):
563+ """Create a feature specification from a description.
564+
565+ This is a direct CLI alternative to the /speckit.specify agent command.
566+ Runs the spec workflow and generates spec.md in the feature directory.
567+
568+ Examples:
569+ specify specify --spec "Build a kanban board with drag-and-drop"
570+ specify specify --spec "Photo album app" --dry-run
571+ """
572+ from .workflows .engine import WorkflowEngine
573+
574+ project_root = _require_specify_project ()
575+ engine = WorkflowEngine (project_root )
576+ engine .on_step_start = lambda sid , label : console .print (f" \u25b8 [{ sid } ] { label } \u2026 " )
577+
578+ try :
579+ definition = engine .load_workflow ("speckit" )
580+ except FileNotFoundError :
581+ console .print ("[red]Error:[/red] speckit workflow not installed. Run 'specify init' first." )
582+ raise typer .Exit (1 )
583+ except ValueError as exc :
584+ console .print (f"[red]Error:[/red] Invalid workflow: { exc } " )
585+ raise typer .Exit (1 )
586+
587+ errors = engine .validate (definition )
588+ if errors :
589+ console .print ("[red]Workflow validation failed:[/red]" )
590+ for err in errors :
591+ console .print (f" \u2022 { err } " )
592+ raise typer .Exit (1 )
593+
594+ inputs = {"spec" : spec , "integration" : "auto" , "scope" : "full" }
595+
596+ console .print (f"\n [bold cyan]Running:[/bold cyan] specify specify" )
597+ console .print (f"[dim]Spec: { spec [:60 ]} { '...' if len (spec ) > 60 else '' } [/dim]\n " )
598+
599+ if dry_run :
600+ console .print ("[bold yellow]DRY RUN — no AI invocation will occur[/bold yellow]\n " )
601+
602+ try :
603+ state = engine .execute (definition , inputs , dry_run = dry_run )
604+ except ValueError as exc :
605+ console .print (f"[red]Error:[/red] { exc } " )
606+ raise typer .Exit (1 )
607+ except Exception as exc :
608+ console .print (f"[red]Workflow failed:[/red] { exc } " )
609+ raise typer .Exit (1 )
610+
611+ if dry_run :
612+ status_color = "yellow"
613+ else :
614+ status_color = {"completed" : "green" , "paused" : "yellow" , "failed" : "red" , "aborted" : "red" }.get (
615+ state .status .value , "white"
616+ )
617+ console .print (f"\n [{ status_color } ]Status: { state .status .value } [/{ status_color } ]" )
618+ if dry_run :
619+ console .print ("[dim]Run with --dry-run to see step details. Run without --dry-run to execute.[/dim]" )
620+
621+
622+ @specify_app .command ("plan" )
623+ def specify_plan (
624+ spec : str = typer .Option (
625+ ..., "--spec" , "-s" , help = "Feature description (what to build and why)"
626+ ),
627+ dry_run : bool = typer .Option (
628+ False , "--dry-run" , help = "Show rendered prompt/inputs without invoking the AI"
629+ ),
630+ ):
631+ """Create an implementation plan from a feature description.
632+
633+ This is a direct CLI alternative to the /speckit.plan agent command.
634+ Runs the plan step of the speckit workflow.
635+
636+ Examples:
637+ specify plan --spec "Build a kanban board with drag-and-drop"
638+ specify plan --spec "Photo album app" --dry-run
639+ """
640+ from .workflows .engine import WorkflowEngine
641+
642+ project_root = _require_specify_project ()
643+ engine = WorkflowEngine (project_root )
644+ engine .on_step_start = lambda sid , label : console .print (f" \u25b8 [{ sid } ] { label } \u2026 " )
645+
646+ try :
647+ definition = engine .load_workflow ("speckit" )
648+ except FileNotFoundError :
649+ console .print ("[red]Error:[/red] speckit workflow not installed. Run 'specify init' first." )
650+ raise typer .Exit (1 )
651+ except ValueError as exc :
652+ console .print (f"[red]Error:[/red] Invalid workflow: { exc } " )
653+ raise typer .Exit (1 )
654+
655+ errors = engine .validate (definition )
656+ if errors :
657+ console .print ("[red]Workflow validation failed:[/red]" )
658+ for err in errors :
659+ console .print (f" \u2022 { err } " )
660+ raise typer .Exit (1 )
661+
662+ inputs = {"spec" : spec , "integration" : "auto" , "scope" : "full" }
663+
664+ console .print (f"\n [bold cyan]Running:[/bold cyan] specify plan" )
665+ console .print (f"[dim]Spec: { spec [:60 ]} { '...' if len (spec ) > 60 else '' } [/dim]\n " )
666+
667+ if dry_run :
668+ console .print ("[bold yellow]DRY RUN — no AI invocation will occur[/bold yellow]\n " )
669+
670+ try :
671+ state = engine .execute (definition , inputs , dry_run = dry_run )
672+ except ValueError as exc :
673+ console .print (f"[red]Error:[/red] { exc } " )
674+ raise typer .Exit (1 )
675+ except Exception as exc :
676+ console .print (f"[red]Workflow failed:[/red] { exc } " )
677+ raise typer .Exit (1 )
678+
679+ if dry_run :
680+ status_color = "yellow"
681+ else :
682+ status_color = {"completed" : "green" , "paused" : "yellow" , "failed" : "red" , "aborted" : "red" }.get (
683+ state .status .value , "white"
684+ )
685+ console .print (f"\n [{ status_color } ]Status: { state .status .value } [/{ status_color } ]" )
686+ if dry_run :
687+ console .print ("[dim]Run with --dry-run to see step details. Run without --dry-run to execute.[/dim]" )
688+
689+
544690# ===== Extension Commands =====
545691
546692extension_app = typer .Typer (
@@ -4012,6 +4158,9 @@ def workflow_run(
40124158 input_values : list [str ] | None = typer .Option (
40134159 None , "--input" , "-i" , help = "Input values as key=value pairs"
40144160 ),
4161+ dry_run : bool = typer .Option (
4162+ False , "--dry-run" , help = "Show the rendered prompt/inputs for each step without invoking the AI"
4163+ ),
40154164):
40164165 """Run a workflow from an installed ID or local YAML path."""
40174166 from .workflows .engine import WorkflowEngine
@@ -4050,8 +4199,11 @@ def workflow_run(
40504199 console .print (f"\n [bold cyan]Running workflow:[/bold cyan] { definition .name } ({ definition .id } )" )
40514200 console .print (f"[dim]Version: { definition .version } [/dim]\n " )
40524201
4202+ if dry_run :
4203+ console .print ("[bold yellow]DRY RUN — no AI invocation will occur[/bold yellow]\n " )
4204+
40534205 try :
4054- state = engine .execute (definition , inputs )
4206+ state = engine .execute (definition , inputs , dry_run = dry_run )
40554207 except ValueError as exc :
40564208 console .print (f"[red]Error:[/red] { exc } " )
40574209 raise typer .Exit (1 )
0 commit comments