Skip to content

Add BASECAMP_NONINTERACTIVE escape hatch for prompts#520

Merged
robzolkos merged 6 commits into
mainfrom
noninteractive-env-escape-hatch
Jul 3, 2026
Merged

Add BASECAMP_NONINTERACTIVE escape hatch for prompts#520
robzolkos merged 6 commits into
mainfrom
noninteractive-env-escape-hatch

Conversation

@robzolkos

@robzolkos robzolkos commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Problem

An agent that runs basecamp todos list --in <id> --md can 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 --md is deliberately not a machine-output mode — only --agent/--json/--quiet/--ids-only/--count suppress prompts (IsInteractive() in both internal/appctx/context.go and internal/tui/resolve/resolve.go). When the CLI runs under an allocated PTY, stdout looks 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 --agent per invocation is exactly what the issue asks us to avoid.

Change

Add BASECAMP_NONINTERACTIVE as an explicit escape hatch. When set to a truthy value (1/true), it forces IsInteractive() to false in 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 --force cannot 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 --md Markdown 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 strict parseEnvBool (accepts 1/true, ignores unrecognized values).
  • Both IsInteractive() chokepoints consult it first; resolver prompts and app.IsInteractive() command sites flow through those gates.
  • Command-level prompt guards also consult it, covering direct confirmations that do not use the resolver.
  • Explicit setup flows (basecamp setup and interactive skill/setup wizards) remain intentionally interactive; use direct flags/subcommands for non-interactive setup.
  • skills/basecamp/SKILL.md now documents that --md does not suppress prompts and points to the env var / disambiguating flags.

Notes for reviewers

  • The env var's effect on IsInteractive() is inherently TTY-dependent, so the unit tests assert the parser (config) and the invariant that the escape hatch does not flip IsMachineOutput() — matching the limitation of the existing IsInteractive tests.
  • This does not touch the pre-existing --agent behavior.

Refs #395


Summary by cubic

Add a BASECAMP_NONINTERACTIVE env escape hatch to disable prompts without changing output format, so --md runs under a PTY don’t get stuck. Also treats --jq as machine-output for the profile picker and clarifies character-device detection in docs.

  • New Features
    • Set BASECAMP_NONINTERACTIVE to 1 or true to force non-interactive mode across the app, resolver, profile picker, and command prompts; ambiguous targets return clear errors (e.g., use --todoset <id>).
    • Output format is unchanged; you can keep --md while suppressing prompts. --jq implies JSON and now suppresses the profile picker.
    • Docs updated to note stdout “character device (e.g., a terminal)” detection and fix disambiguation examples in 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.

Review in cubic

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
Copilot AI review requested due to automatic review settings July 3, 2026 02:15
@github-actions github-actions Bot added tui Terminal UI tests Tests (unit and e2e) skills Agent skills enhancement New feature or request labels Jul 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 parse BASECAMP_NONINTERACTIVE (1/true) as a non-interactive override.
  • Apply the override to both interactive “chokepoints”: internal/appctx.App.IsInteractive() and internal/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.

Comment thread internal/config/config_test.go Outdated
Comment thread internal/appctx/context_test.go
Comment thread skills/basecamp/SKILL.md Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 6 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread internal/appctx/context_test.go
Comment thread skills/basecamp/SKILL.md Outdated
Comment thread internal/config/config_test.go Outdated
- 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.
@robzolkos

Copy link
Copy Markdown
Collaborator Author

Addressed all three review findings in 986e96c:

  1. config_test env leak — the unset case now uses t.Setenv("") (which NonInteractiveEnv treats as unset) instead of os.Unsetenv, so testing.T restores the prior value.
  2. context_test not exercising the short-circuit — the test now swaps os.Stdout to /dev/null (a char device), asserts the interactive baseline with require.True, then sets BASECAMP_NONINTERACTIVE=1 and asserts IsInteractive() flips to false. It now fails if the short-circuit is removed.
  3. SKILL.md contradiction — the sentence now lists the flags and BASECAMP_NONINTERACTIVE=1 together as prompt suppressors.

Full test suite green against the pinned SDK (GOWORK=off go test ./...).

Copilot AI review requested due to automatic review settings July 3, 2026 02:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comment thread internal/cli/root.go
Comment thread internal/cli/root_test.go Outdated
Comment thread internal/appctx/context_test.go Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread internal/cli/root.go Outdated
Comment thread internal/cli/root_test.go Outdated
robzolkos added 2 commits July 2, 2026 23:09
- 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.
Copilot AI review requested due to automatic review settings July 3, 2026 03:38
@github-actions github-actions Bot added the commands CLI command implementations label Jul 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Comment thread internal/tui/resolve/resolve.go Outdated
Comment thread internal/cli/root.go Outdated
Comment thread skills/basecamp/SKILL.md Outdated
- 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.
@robzolkos robzolkos merged commit f9041ac into main Jul 3, 2026
26 checks passed
@robzolkos robzolkos deleted the noninteractive-env-escape-hatch branch July 3, 2026 03:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commands CLI command implementations enhancement New feature or request skills Agent skills tests Tests (unit and e2e) tui Terminal UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants