Add BASECAMP_NONINTERACTIVE escape hatch for prompts#520
Conversation
Only the machine-output flags (--agent/--json/--quiet/--ids-only/--count) suppress interactive selection prompts. --md does not, so an agent that requests Markdown output and runs under an allocated PTY (where stdout looks like a terminal) can be wedged by a blocking picker when a target is ambiguous — e.g. a project with multiple todosets and no --todoset. Add BASECAMP_NONINTERACTIVE as an explicit escape hatch. When set to a truthy value it forces IsInteractive() to false in both gates (appctx and resolve), turning ambiguous resolutions into actionable errors instead of prompts — without changing the output format the way --agent does. Setting it is the environment/harness's responsibility, not the model's, so it does not rely on the LLM remembering a flag per invocation. Refs #395
There was a problem hiding this comment.
Pull request overview
Adds a BASECAMP_NONINTERACTIVE environment-variable escape hatch so agents/harnesses can reliably disable interactive selection prompts (even under a PTY) without switching output formats (e.g., keep --md Markdown output).
Changes:
- Introduce
config.NonInteractiveEnv()to strictly parseBASECAMP_NONINTERACTIVE(1/true) as a non-interactive override. - Apply the override to both interactive “chokepoints”:
internal/appctx.App.IsInteractive()andinternal/tui/resolve.Resolver.IsInteractive(). - Document prompt-suppression behavior and the new env var in
skills/basecamp/SKILL.md, and add unit tests for the env parsing / invariants.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| skills/basecamp/SKILL.md | Documents that --md doesn’t suppress prompts and adds BASECAMP_NONINTERACTIVE guidance. |
| internal/tui/resolve/resolve.go | Disables resolver prompts early when BASECAMP_NONINTERACTIVE is truthy. |
| internal/config/config.go | Adds NonInteractiveEnv() using existing strict parseEnvBool. |
| internal/config/config_test.go | Tests env parsing behavior for BASECAMP_NONINTERACTIVE. |
| internal/appctx/context.go | Disables app-level interactivity early when BASECAMP_NONINTERACTIVE is truthy. |
| internal/appctx/context_test.go | Adds an app-level test asserting the escape hatch doesn’t change machine-output mode. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
3 issues found across 6 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
- context_test: swap stdout to /dev/null (a char device) so the test
actually exercises the BASECAMP_NONINTERACTIVE short-circuit instead of
passing because go test's stdout is a pipe. Baseline require.True asserts
the interactive path, then the env var flips it to false.
- config_test: use t.Setenv("") for the unset case instead of os.Unsetenv,
so testing.T restores the prior value (no env leak into other tests).
- SKILL.md: include BASECAMP_NONINTERACTIVE in the prompt-suppression list
to remove the "Only <flags> suppress prompts" contradiction.
|
Addressed all three review findings in 986e96c:
Full test suite green against the pinned SDK ( |
There was a problem hiding this comment.
2 issues found across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
- root.go: isInteractiveTTY now treats --jq (JQFilter) as machine output, matching App.IsInteractive. Without it, --jq + multiple profiles could still hit the interactive profile picker despite --jq implying --json. - root_test.go, context_test.go: use os.DevNull instead of a hard-coded "/dev/null" so the tests stay meaningful on non-Unix platforms.
- resolve.go, root.go: doc/inline comments said "terminal", but the guard checks os.ModeCharDevice, which also matches non-terminal char devices like /dev/null (the tests rely on this). Reword to say "character device (e.g. a terminal)". - SKILL.md: the disambiguation list implied --in <project> resolves the "multiple todosets" ambiguity. Reword so each flag maps to the ambiguity it actually resolves.
Problem
An agent that runs
basecamp todos list --in <id> --mdcan be blocked by an interactive selection prompt (e.g. picking a todoset when a project has more than one), wedging the session. Reported in #395.The root cause is that
--mdis deliberately not a machine-output mode — only--agent/--json/--quiet/--ids-only/--countsuppress prompts (IsInteractive()in bothinternal/appctx/context.goandinternal/tui/resolve/resolve.go). When the CLI runs under an allocated PTY,stdoutlooks like a terminal, so ambiguous resolutions fall through to a blocking picker even though a human isn't there to answer it.Relying on the model to remember
--agentper invocation is exactly what the issue asks us to avoid.Change
Add
BASECAMP_NONINTERACTIVEas an explicit escape hatch. When set to a truthy value (1/true), it forcesIsInteractive()tofalsein both gates, so ambiguous resolutions become actionable errors (Multiple todosets found — specify one with --todoset <id>) instead of prompts.Command-level prompt guards also consult the escape hatch, so direct confirmations such as
basecamp chat delete ...without--forcecannot wedge an agent session under a PTY.Key property: it only disables prompts — it does not change the output format. That is the distinction from
--agent(which also forces quiet JSON): you keep--mdMarkdown output and just lose the wedge.Setting it is the environment/harness's responsibility (shell profile, MCP server env, wrapper), not the model's — so it doesn't depend on the LLM appending a flag every call. Mirrors the pattern the issue references from other agent CLIs.
Details
config.NonInteractiveEnv()reuses the existing strictparseEnvBool(accepts1/true, ignores unrecognized values).IsInteractive()chokepoints consult it first; resolver prompts andapp.IsInteractive()command sites flow through those gates.basecamp setupand interactive skill/setup wizards) remain intentionally interactive; use direct flags/subcommands for non-interactive setup.skills/basecamp/SKILL.mdnow documents that--mddoes not suppress prompts and points to the env var / disambiguating flags.Notes for reviewers
IsInteractive()is inherently TTY-dependent, so the unit tests assert the parser (config) and the invariant that the escape hatch does not flipIsMachineOutput()— matching the limitation of the existingIsInteractivetests.--agentbehavior.Refs #395
Summary by cubic
Add a
BASECAMP_NONINTERACTIVEenv escape hatch to disable prompts without changing output format, so--mdruns under a PTY don’t get stuck. Also treats--jqas machine-output for the profile picker and clarifies character-device detection in docs.BASECAMP_NONINTERACTIVEto1ortrueto force non-interactive mode across the app, resolver, profile picker, and command prompts; ambiguous targets return clear errors (e.g., use--todoset <id>).--mdwhile suppressing prompts.--jqimplies JSON and now suppresses the profile picker.skills/basecamp/SKILL.md; tests hardened to cover the env short-circuit, char-device stdout, and command prompt guards.Written for commit 58180e5. Summary will update on new commits.