@@ -14,15 +14,21 @@ import (
1414 "github.com/LAA-Software-Engineering/agentic-control-plane/internal/render"
1515 "github.com/LAA-Software-Engineering/agentic-control-plane/internal/runtime"
1616 "github.com/LAA-Software-Engineering/agentic-control-plane/internal/runtime/local"
17+ "github.com/LAA-Software-Engineering/agentic-control-plane/internal/spec"
1718 "github.com/LAA-Software-Engineering/agentic-control-plane/internal/state"
1819 "github.com/LAA-Software-Engineering/agentic-control-plane/internal/state/sqlite"
20+ "github.com/mattn/go-isatty"
1921 "github.com/spf13/cobra"
2022)
2123
2224func newRunCmd () * cobra.Command {
2325 var inputFile string
2426 var inputPairs []string
2527 var approves []string
28+ var autoApprove bool
29+ var decision string
30+ var decisionEditJSON string
31+ var decisionSwitchTarget string
2632 var resumeRunID string
2733
2834 cmd := & cobra.Command {
@@ -37,6 +43,8 @@ Workflow input is built from optional --input-file (JSON object) plus repeated -
3743--approve using the full uses string (e.g. tool.helper.echo).
3844
3945Resume an interrupted or incomplete run with --resume <run-id> (no workflow argument).
46+ When a run pauses for human approval, resume with --decision and related flags, or use
47+ --auto-approve / AGENTCTL_AUTO_APPROVE=1 for non-interactive approval.
4048
4149Examples:
4250 agentctl run workflow/demo --input topic=hello
@@ -71,12 +79,16 @@ Exit codes (section 11.2):
7179 return NewExitError (ExitValidationError , err )
7280 }
7381 }
74- return runRun (cmd , wfName , resumeRunID , inputFile , inputPairs , approves )
82+ return runRun (cmd , wfName , resumeRunID , inputFile , inputPairs , approves , autoApprove , decision , decisionEditJSON , decisionSwitchTarget )
7583 },
7684 }
7785 cmd .Flags ().StringVar (& inputFile , "input-file" , "" , "path to JSON file with workflow input object" )
7886 cmd .Flags ().StringArrayVar (& inputPairs , "input" , nil , "workflow input as key=value (repeatable; values are strings)" )
7987 cmd .Flags ().StringArrayVar (& approves , "approve" , nil , "approve a policy-gated tool uses string (repeatable)" )
88+ cmd .Flags ().BoolVar (& autoApprove , "auto-approve" , false , "auto-approve human-in-the-loop gates (or set AGENTCTL_AUTO_APPROVE=1)" )
89+ cmd .Flags ().StringVar (& decision , "decision" , "" , "HITL decision when resuming: approve, reject, edit, or switch" )
90+ cmd .Flags ().StringVar (& decisionEditJSON , "decision-edit-json" , "" , "JSON object of edited tool args when --decision edit" )
91+ cmd .Flags ().StringVar (& decisionSwitchTarget , "decision-switch-target" , "" , "target operation when --decision switch" )
8092 cmd .Flags ().StringVar (& resumeRunID , "resume" , "" , "resume an interrupted or incomplete run by id" )
8193 return cmd
8294}
@@ -165,7 +177,7 @@ func classifyRunError(err error) int {
165177 }
166178}
167179
168- func runRun (cmd * cobra.Command , wfName , resumeRunID , inputFile string , inputPairs , approves []string ) error {
180+ func runRun (cmd * cobra.Command , wfName , resumeRunID , inputFile string , inputPairs , approves []string , autoApprove bool , decision , decisionEditJSON , decisionSwitchTarget string ) error {
169181 ctx := context .Background ()
170182 g := Globals ()
171183
@@ -203,33 +215,68 @@ func runRun(cmd *cobra.Command, wfName, resumeRunID, inputFile string, inputPair
203215 defer func () { _ = st .Close () }()
204216
205217 rt := local .NewRuntime (root , st )
206- opts := runtime.WorkflowRunOptions {
207- EnvironmentName : strings .TrimSpace (g .Env ),
208- Env : env ,
209- InputJSON : inputJSON ,
210- ApprovedActions : approves ,
211- Resume : resumeID != "" ,
212- RunID : resumeID ,
213- }
214- if ! opts .Resume {
215- opts .WorkflowName = wfName
216- }
217- runID , runErr := rt .ExecuteWorkflow (ctx , opts )
218218
219- outWfName := wfName
220- if opts .Resume && runID != "" {
221- if r , gerr := st .GetRun (ctx , runID ); gerr == nil && r != nil {
222- outWfName = r .WorkflowName
219+ for {
220+ opts := runtime.WorkflowRunOptions {
221+ EnvironmentName : strings .TrimSpace (g .Env ),
222+ Env : env ,
223+ InputJSON : inputJSON ,
224+ ApprovedActions : approves ,
225+ Resume : resumeID != "" ,
226+ RunID : resumeID ,
223227 }
224- }
228+ if err := applyHitlRunOptions (& opts , autoApprove , decision , decisionEditJSON , decisionSwitchTarget ); err != nil {
229+ return NewExitError (ExitValidationError , err )
230+ }
231+ if ! opts .Resume {
232+ opts .WorkflowName = wfName
233+ }
234+ runID , runErr := rt .ExecuteWorkflow (ctx , opts )
225235
226- if werr := writeRunOutput (cmd , ctx , st , env , dsn , outWfName , runID , runErr , g ); werr != nil {
227- return werr
228- }
229- if runErr != nil {
230- return NewExitError (classifyRunError (runErr ), fmt .Errorf ("run: %w" , runErr ))
236+ outWfName := wfName
237+ if opts .Resume && runID != "" {
238+ if r , gerr := st .GetRun (ctx , runID ); gerr == nil && r != nil {
239+ outWfName = r .WorkflowName
240+ }
241+ }
242+
243+ if runErr == nil && runID != "" {
244+ if r , gerr := st .GetRun (ctx , runID ); gerr == nil && r != nil && r .Status == state .RunStatusInterrupted {
245+ if opts .AutoApprove || strings .TrimSpace (decision ) != "" {
246+ resumeID = runID
247+ continue
248+ }
249+ gate , gerr := loadPendingHitlGate (ctx , st , runID )
250+ if gerr != nil {
251+ return fmt .Errorf ("run: load hitl gate: %w" , gerr )
252+ }
253+ if gate != nil && isatty .IsTerminal (os .Stdin .Fd ()) {
254+ dec , perr := maybePromptHitlDecision (cmd .InOrStdin (), cmd .OutOrStdout (), * gate )
255+ if perr != nil {
256+ return perr
257+ }
258+ if dec != nil {
259+ resumeID = runID
260+ decision = string (dec .Kind )
261+ if dec .Kind == spec .HitlDecisionEdit {
262+ b , _ := json .Marshal (dec .EditedWith )
263+ decisionEditJSON = string (b )
264+ }
265+ decisionSwitchTarget = dec .SwitchTarget
266+ continue
267+ }
268+ }
269+ }
270+ }
271+
272+ if werr := writeRunOutput (cmd , ctx , st , env , dsn , outWfName , runID , runErr , g ); werr != nil {
273+ return werr
274+ }
275+ if runErr != nil {
276+ return NewExitError (classifyRunError (runErr ), fmt .Errorf ("run: %w" , runErr ))
277+ }
278+ return nil
231279 }
232- return nil
233280}
234281
235282func writeRunOutput (cmd * cobra.Command , ctx context.Context , st * sqlite.Store , env , dsn , wfName , runID string , runErr error , g * Global ) error {
0 commit comments