@@ -68,16 +68,21 @@ func (o AIProviderOptions) ToConfig() ai.Config {
6868 return cfg
6969}
7070
71- type AIPromptOptions struct {
71+ // AIRuntimeOptions binds the per-invocation knobs every AI command shares —
72+ // model selection (via embedded AIProviderOptions), generation parameters
73+ // (max tokens, temperature, timeout, reasoning), permission/sandbox toggles
74+ // (edit, allowed/disallowed tools, permission mode), and ambient-context
75+ // toggles (mcp/hooks/skills/user/project/memory/bare). It deliberately
76+ // omits the user-prompt fields so non-prompt commands (e.g. gavel's lint
77+ // --ai-fix loop) can embed it without inheriting a required --prompt flag.
78+ //
79+ // AIPromptOptions embeds this struct and adds Prompt/System/AppendSystem/
80+ // NoStream on top.
81+ type AIRuntimeOptions struct {
7282 AIProviderOptions
73- Prompt string `flag:"prompt" help:"Prompt text" short:"p" required:"true" stdin:"true"`
74- System string `flag:"system" help:"System prompt" short:"s"`
75- AppendSystem string `flag:"append-system" help:"Append text to the default system prompt"`
76- MaxTokens int `flag:"max-tokens" help:"Maximum output tokens" default:"4096"`
77- Temperature string `flag:"temperature" help:"Sampling temperature" default:"0"`
78- Timeout string `flag:"timeout" help:"Request timeout" default:"120s"`
7983
80- NoStream bool `flag:"no-stream" help:"Disable streaming; print only the final text to stdout"`
84+ MaxTokens int `flag:"max-tokens" help:"Maximum output tokens" default:"4096"`
85+ Temperature string `flag:"temperature" help:"Sampling temperature" default:"0"`
8186
8287 Edit bool `flag:"edit" help:"Safe defaults: acceptEdits + Read/Edit/Write/Glob/Grep allowlist"`
8388 AllowedTools []string `flag:"allowed-tools" help:"Override --edit's built-in allowlist (claude only)"`
@@ -94,6 +99,16 @@ type AIPromptOptions struct {
9499 Bare bool `flag:"bare" help:"Skip hooks, skills, memory, and ambient settings"`
95100}
96101
102+ type AIPromptOptions struct {
103+ AIRuntimeOptions
104+
105+ Prompt string `flag:"prompt" help:"Prompt text" short:"p" required:"true" stdin:"true"`
106+ System string `flag:"system" help:"System prompt" short:"s"`
107+ AppendSystem string `flag:"append-system" help:"Append text to the default system prompt"`
108+ Timeout string `flag:"timeout" help:"Request timeout" default:"120s"`
109+ NoStream bool `flag:"no-stream" help:"Disable streaming; print only the final text to stdout"`
110+ }
111+
97112type AIPromptResult struct {
98113 Text string `json:"text" pretty:"label=Response"`
99114 Model string `json:"model" pretty:"label=Model"`
@@ -104,25 +119,29 @@ type AIPromptResult struct {
104119 Duration string `json:"duration" pretty:"label=Duration"`
105120}
106121
107- // ToRequest translates the user-facing AIPromptOptions into the typed
108- // ai.Request. Truthy flags like --mcp/--hooks/--memory invert into the
109- // No*-style fields the providers consume. When --bare is set, it implicitly
110- // strips memory/hooks/skills/user/project regardless of those flags' values
111- // (claude --bare composes them) so we let the provider decide the final argv.
122+ // ToRequest translates the runtime knobs into the typed ai.Request. Truthy
123+ // flags like --mcp/--hooks/--memory invert into the No*-style fields the
124+ // providers consume. When --bare is set, it implicitly strips memory/hooks/
125+ // skills/user/project regardless of those flags' values (claude --bare
126+ // composes them) so we let the provider decide the final argv.
127+ //
128+ // systemPrompt / appendSystemPrompt / userPrompt are passed explicitly so
129+ // non-prompt callers (gavel's ai-fix loop) can build them per-iteration
130+ // without leaking those fields into the shared runtime struct.
112131//
113132// Saved defaults from ~/.captain.yaml overlay onto unset fields. For the
114- // boolean toggles, "saved off" wins over "flag default on" because clicky does
115- // not yet expose a Changed() bit.
133+ // boolean toggles, "saved off" wins over "flag default on" because clicky
134+ // does not yet expose a Changed() bit.
116135//
117- // WORKAROUND(no-flag-changed-bit): The boolean toggles default to true, so we
118- // cannot tell whether --mcp=true was passed explicitly or inherited from the
119- // default. As a result, when the saved config has NoMCP=true the user cannot
120- // force MCP back on from the command line by passing --mcp=true alone — they
121- // must edit ~/.captain.yaml or rerun `captain configure`.
136+ // WORKAROUND(no-flag-changed-bit): The boolean toggles default to true, so
137+ // we cannot tell whether --mcp=true was passed explicitly or inherited from
138+ // the default. As a result, when the saved config has NoMCP=true the user
139+ // cannot force MCP back on from the command line by passing --mcp=true alone
140+ // — they must edit ~/.captain.yaml or rerun `captain configure`.
122141// Correct fix: thread clicky's per-flag Changed() bit (or a tri-state flag
123- // type) through AIPromptOptions so we can distinguish "explicitly true" from
124- // "default true". Ref: discussed with user 2026-05-07.
125- func (o AIPromptOptions ) ToRequest () ai.Request {
142+ // type) through AIRuntimeOptions so we can distinguish "explicitly true"
143+ // from "default true". Ref: discussed with user 2026-05-07.
144+ func (o AIRuntimeOptions ) ToRequest (systemPrompt , appendSystemPrompt , userPrompt string ) ai.Request {
126145 saved := loadSavedAI ()
127146 temperature , _ := strconv .ParseFloat (o .Temperature , 64 )
128147
@@ -134,9 +153,9 @@ func (o AIPromptOptions) ToRequest() ai.Request {
134153 effort := saved .ReasoningEffort
135154
136155 return ai.Request {
137- SystemPrompt : o . System ,
138- AppendSystemPrompt : o . AppendSystem ,
139- Prompt : o . Prompt ,
156+ SystemPrompt : systemPrompt ,
157+ AppendSystemPrompt : appendSystemPrompt ,
158+ Prompt : userPrompt ,
140159 MaxTokens : maxTokens ,
141160 Temperature : temperature ,
142161 ReasoningEffort : effort ,
@@ -155,6 +174,12 @@ func (o AIPromptOptions) ToRequest() ai.Request {
155174 }
156175}
157176
177+ // ToRequest delegates to AIRuntimeOptions.ToRequest, lifting the prompt
178+ // fields the prompt-shaped command owns onto the typed request.
179+ func (o AIPromptOptions ) ToRequest () ai.Request {
180+ return o .AIRuntimeOptions .ToRequest (o .System , o .AppendSystem , o .Prompt )
181+ }
182+
158183func RunAIPrompt (opts AIPromptOptions ) (any , error ) {
159184 if opts .Prompt == "" {
160185 return nil , fmt .Errorf ("prompt text required (use --prompt or pipe via stdin)" )
0 commit comments