feat: agent and scripting friendly non-TTY prompt errors#587
Conversation
Re-render the prompt question and annotate each option with the equivalent --flag=value invocation when a required prompt is reached without a TTY. Lets agents and devops scripts read the error and re-run with the right flag. Adds an optional Options []PromptOption field on SelectPromptConfig and MultiSelectPromptConfig; configs that don't set it keep the prior "Try --flag" remediation. Migrates the team_select and app_select reference sites; other prompt sites are unchanged.
Move the prompt-mirror (question + per-option `--flag=value`) out of
the Remediation block and into the error body via Details, so the
Suggestion header stops framing the visualization as advice. The
Suggestion now carries a short, single-line directive.
UX tweaks: replace the leading `?` with `›`, prepend a context line
("The prompt that would have been shown is below:"), and align the
`--flag=value` column using lipgloss.Width so labels with embedded
ANSI escapes line up.
Non-migrated prompts still render the question (and any options),
just without per-option flag values, preserving the existing
`Try --flag` Suggestion.
Drop the hand-typed flag-name string in favor of the registered flag pointer. Call sites now mention the flag name once (via Flags.Lookup) instead of duplicating it across the prompt config and each PromptOption, so a flag rename is a one-line change.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #587 +/- ##
==========================================
+ Coverage 71.66% 71.71% +0.04%
==========================================
Files 226 226
Lines 19176 19234 +58
==========================================
+ Hits 13743 13794 +51
- Misses 4222 4229 +7
Partials 1211 1211 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
mwbrooks
left a comment
There was a problem hiding this comment.
Comments for the wise minds 🧠 ✨
| // PromptOption pairs an interactive option label with the flag invocation | ||
| // that picks the same option non-interactively. When a prompt is reached in | ||
| // a non-TTY context, the resulting error renders one of these per option so | ||
| // agents and scripts can re-run with the right --flag=value. | ||
| type PromptOption struct { | ||
| Label string // The option as rendered in the interactive list | ||
| Flag *pflag.Flag // The flag substitute for this option | ||
| Value string // The value to pass, e.g. "T0123" or "A0ABCD" | ||
| } | ||
|
|
||
| // PromptOptionsConfig is optionally implemented by prompt configs that can | ||
| // enumerate options as flag invocations. Configs that do not implement it | ||
| // (or return an empty slice) keep the simpler "Try --flag" remediation. | ||
| type PromptOptionsConfig interface { | ||
| GetPromptOptions() []PromptOption | ||
| } | ||
|
|
There was a problem hiding this comment.
note: New structs to allow us to pass flag names and values that correspond to each option. For now, I've kept it simple with just 1 flag. If we discover prompts that require multiple flags (e.g. --template <url> --subdir <path> then we can refactor later. For now, I'm leaning toward setting up the essentials to keep it readable.
| // errInteractivityFlags formats an error for when flag substitutes are needed. | ||
| // It re-renders the prompt question and any enumerable options (with their | ||
| // equivalent --flag=value invocations) as part of the error body so agents | ||
| // and devops scripts can read the error and re-run with the right flags. | ||
| // The Suggestion remains a short, single-line directive. | ||
| func errInteractivityFlags(cfg PromptConfig, message string, options []string) error { |
There was a problem hiding this comment.
note: This is the meat and potatoes of the PR 🥔 🍠 It handles rendering the error message, which now support displaying the original prompt that should have been shown.
| appFlag := clients.Config.Flags.Lookup("app") | ||
| labels := []string{} | ||
| for _, label := range options { | ||
| labels = append(labels, label.label) | ||
| appOptions := []iostreams.PromptOption{} | ||
| for _, opt := range options { | ||
| labels = append(labels, opt.label) | ||
| // Synthetic entries ("Create a new app", "No app") have no AppID and | ||
| // are emitted as label-only so the option list stays 1:1 with labels. | ||
| promptOpt := iostreams.PromptOption{Label: opt.label} | ||
| if opt.app.App.AppID != "" { | ||
| promptOpt.Flag = appFlag | ||
| promptOpt.Value = opt.app.App.AppID | ||
| } | ||
| appOptions = append(appOptions, promptOpt) |
There was a problem hiding this comment.
note: This is the migration of the App Select prompt. We have 36 total call sites, so this gives you an idea of what a straight-forward migration will look like. I've scoped this PR to just migrate 2 call sites to allow us to focus on the design and infrastructure changes.
| teamFlag := clients.Config.Flags.Lookup("team") | ||
| var teamLabels []string | ||
| var teamOptions []iostreams.PromptOption | ||
| for _, auth := range allAuths { | ||
| teamLabels = append( | ||
| teamLabels, | ||
| style.TeamSelectLabel(auth.TeamDomain, auth.TeamID), | ||
| ) | ||
| label := style.TeamSelectLabel(auth.TeamDomain, auth.TeamID) | ||
| teamLabels = append(teamLabels, label) | ||
| teamOptions = append(teamOptions, iostreams.PromptOption{ | ||
| Label: label, | ||
| Flag: teamFlag, | ||
| Value: auth.TeamID, | ||
| }) |
There was a problem hiding this comment.
note: This is the migration of the Team Select prompt. We have 36 total call sites, so this gives you an idea of what a straight-forward migration will look like. I've scoped this PR to just migrate 2 call sites to allow us to focus on the design and infrastructure changes.
Changelog
Summary
This pull request adds a non-TTY error renderer that re-creates the interactive prompt and annotates each option with the
--flag=valueinvocation that would pick it.The motivation is non-human callers - AI agents and devops scripts. Today, when one of these callers triggers a required prompt without a TTY, the CLI returns a generic "input device is not a TTY" error and a flag name. The caller has no way to know what the prompt was about to ask, what options it would have shown, or which concrete value to pass. The new error puts all of that in the body, so the caller can read it and re-run with the right flag.
This PR lands the infrastructure plus two reference call-site migrations:
PromptOptionstruct (Label,Flag *pflag.Flag,Value string) andPromptOptionsConfiginterface.Options []PromptOptionfield onSelectPromptConfigandMultiSelectPromptConfig.errInteractivityFlagsnow renders the prompt question, an option list and a short single-lineSuggestion.internal/prompts/team_select.goandinternal/prompts/app_select.go. Every other prompt site continues to work, with a graceful degraded render (see below).Follow-up PRs
See Coverage Strategy section below for a suggested strategy to migrate the remaining prompts. This pull request is focused on the infrastructure changes and migrating App Select and Team Select as examples.
Preview
Video
2026-06-11-agent-friendly-prompts.mov
Migrated
team_selectandapp_select(e.g.slack sandbox list)Before:
After:
Non-migrated prompt sites (e.g.
slack auth logout)auth logoutis one of the 36 SelectPrompt sites this PR does not migrate. It still renders the prompt question and the visible options, but without per-option flag mappings. The original "Try--team" suggestion is preserved.Why surface the question + options for non-migrated sites instead of skipping them?
InputPromptwith no options (e.g.slack login)Free-text prompts have no enumerable options, so the visualization is just the question. Suggestion remains the existing per-config text.
The CLI supports
slack login --challenge <code>, so savvy AI agents could start a login, ask the human to paste the slash command and provide the LLM with the 2FA code.Testing
All commands below should be run with stdout piped (e.g.
| cat) soIsTTY()returnsfalse.Migrated SelectPrompt:
Expect prompt rendering with
--team flags:Migrated SelectPrompt for apps:
Expect prompt rendering with
--appflags:Non-migrated SelectPrompt - degraded path still works:
Expect prompt rendering and suggestion to use
--team flagbut no flag values:InputPrompt - no flag, just the question:
Expect
› Enter challenge code:TTY path is unchanged - interactive prompt should still appear:
$ ./bin/slack sandbox list # no pipeNotes
Coverage Strategy
The CLI has 38
SelectPromptcall sites. This PR migrates 2 (the high-traffic team and app selects) and ships infrastructure that the remaining 36 can adopt incrementally. Rough breakdown of the 36:Flag(e.g.--datastore,--trigger-id,--template,--provider,--name).auth logout's--teamexists but isn't on the prompt config) or the option set is conceptual rather than flag-mapped (e.g.triggers access's permission levels). Need a small flag wiring decision before migration.ConfirmPrompt,InputPrompt,PasswordPrompt, andMultiSelectPromptsites are not migrated in this PR.What a future migration looks like
Migrating
cmd/triggers/triggers.go:388(the simplest single-flag shape):Before:
After:
Design choices worth flagging
PromptOption.Flagis*pflag.Flag, not a string. Using the registered flag pointer means the flag name appears once per call site (inLookup) instead of being duplicated in every option literal - a flag rename is then a one-line change.Suggestion.Suggestionis reserved for short, actionable directives ("Re-run with one of the--teamvalues shown above"). The prompt mirror is context, not advice.Requirements