Skip to content

Fix #528: non-TTY installer hardcodes the Claude agent - #558

Merged
jeremy merged 5 commits into
mainfrom
fix/installer-codex-non-tty
Jul 23, 2026
Merged

Fix #528: non-TTY installer hardcodes the Claude agent#558
jeremy merged 5 commits into
mainfrom
fix/installer-codex-non-tty

Conversation

@jeremy

@jeremy jeremy commented Jul 22, 2026

Copy link
Copy Markdown
Member

Fixes #528.

Both symptoms

  1. Installer hardcoded setup claude — the piped install (curl -fsSL https://basecamp.com/install-cli | bash, a 302 redirect to this repo's scripts/install.sh) took the non-TTY branch, which unconditionally ran basecamp setup claude. The BASECAMP_SKIP_SETUP=1 branch did the same.
  2. Claude-first login nudgeprintAgentNudge pointed at the first detected-unhealthy agent in registry order, so it always recommended Claude when both agents were present.

Approach — detection proves presence, not intent

New basecamp setup agents command (always non-interactive): installs the baseline skill and connects coding agents by intent.

  • BASECAMP_SETUP_AGENT wins: claude | codex | all | none.
  • Auto (unset): 0 detected → skill only · 1 → connect it · ≥2 → skill only + surface setup <id> choices (never guess).
  • all forces every registered handler and synthesizes symmetric missing-binary remediation — the Claude handler treats an absent binary as no-op success while Codex errors, so the aggregate runner fills the gap for either.
  • Invalid value → structured warning + baseline only. BASECAMP_SKIP_SETUP=1 skips only the interactive wizard; setup agents still runs.

Styled-safe result envelope

curl | bash renders styled output (piped stdin, TTY stdout), and the styled renderer skips nested map/[]map. So every safety-critical outcome — skill_installed, selector, ambiguous, detected_before, attempted_agents, errors (union of baseline + id-prefixed per-agent), warnings, manual_commands — lives in a top-level flat field; the agents array is JSON-only detail. Ordering is deterministic: set-like fields sorted by id; manual_commands/errors preserve each handler's own sequence with stable first-seen dedup (Codex's remediation is an ordered marketplace-add → upgrade → plugin-add sequence, never lexicographically sorted).

Changes

  • internal/commands/wizard_agents.gonewSetupAgentsCmd + runNonInteractiveAgentSetup and the typed envelope. Existing setup claude/setup codex unchanged.
  • internal/commands/auth.go — rewrite printAgentNudge: one detected-unhealthy agent → its setup <id>; several → every choice printed directly.
  • scripts/install.sh / scripts/install.ps1 — both non-TTY and skip branches run setup agents; install.sh gains an if-form source guard and documents BASECAMP_SETUP_AGENT.
  • .surface regenerated (only setup agents additions); smoke declaration + new e2e/installer.bats; install.md updated.

Tests

  • internal/commands/setup_agents_test.go — full-CLI (cobra Execute) coverage: styled-output regression (failure survives via top-level errors), 0/1/≥2 detection, baseline-skill failure, =all zero/one/two, =codex missing-binary contract, Codex manual-order preservation (guards against string-sorting), =claude no-binary (symlink + detection flip + synthesized remediation), =none, invalid selector.
  • internal/commands/auth_test.go — nudge: none / single / multiple detected-unhealthy.
  • e2e/installer.bats — sourcing guard, post_install_setup dispatch (non-TTY + skip → setup agents, never setup claude), static branch assertions for both scripts.

Verified: make build, targeted go test, make check-surface/check-skill-drift/check-bare-groups/check-naming/check-smoke-coverage/fmt-check/vet/lint, and the smoke + installer bats suites all green. (The full unit suite has a pre-existing network-dependent todos-reposition test that hangs offline; unrelated to this change.)


Summary by cubic

Replaces the non-TTY installer’s hardcoded Claude path with an intent-based, non-interactive agent setup. Adds basecamp setup agents, makes installers cross-version safe, and prevents old releases from launching the interactive wizard.

  • New Features

    • Added basecamp setup agents (non-interactive): installs the baseline skill and connects coding agents by intent; honors BASECAMP_SETUP_AGENT = claude | codex | all | none (unset auto-detects: 0→skill only, 1→connect it, ≥2→skill only + surface setup <id> choices).
    • Installers (scripts/install.sh, scripts/install.ps1) run setup agents for non-TTY and BASECAMP_SKIP_SETUP=1. Docs updated with piped-install auto-connect and how to set BASECAMP_SETUP_AGENT in Bash/PowerShell.
    • Styled-safe result: all critical outcomes are top-level flat fields so they render in styled output; the agents array is JSON-only detail.
  • Bug Fixes

    • Cross-version installers: probe for setup agents. On older binaries, fall back without reintroducing Claude-first behavior—all runs each supported per-agent setup; explicit claude/codex is capability-checked and degrades to skill install if unsupported; all calls are guarded so failures never abort install.
    • PowerShell Invoke-PostInstallSetup: marks attempted agents on dispatch (not success) to match Bash, so =all only falls back to skill install when no supported per-agent subcommands exist.
    • setup agents rejects positional args to prevent accidental setup <id> typos.
    • Login nudge points to the detected-unhealthy agent; with multiple, it lists each basecamp setup <id> option (no bias).

Written for commit 5cdc4ec. Summary will update on new commits.

Review in cubic

The piped install (`curl … | bash`) takes the non-TTY branch of
scripts/install.sh, which unconditionally ran `basecamp setup claude`.
The post-login nudge likewise pointed at the first detected-unhealthy
agent in registry order (Claude-first when both were present).

Add `basecamp setup agents`: a non-interactive command that installs the
baseline skill and connects coding agents by intent, not registry order.
Selection is driven by BASECAMP_SETUP_AGENT (claude|codex|all|none); unset
auto-detects — 0 detected installs the skill only, 1 connects it, ≥2 stays
neutral and surfaces the per-agent commands. Every safety-critical outcome
lives in a top-level flat field of the result envelope so it survives the
styled renderer (which skips nested map/[]map); the `agents` array is
JSON-only detail. `all` forces every registered handler and synthesizes
symmetric missing-binary remediation (the Claude handler treats an absent
binary as no-op success while Codex errors).

- install.sh / install.ps1: both non-TTY and skip branches now run
  `setup agents`; install.sh gains an if-form source guard and documents
  BASECAMP_SETUP_AGENT.
- auth.go: rewrite printAgentNudge — one detected-unhealthy agent prints
  its `setup <id>`; several print every choice directly, never guessing.
- Regenerate .surface; add smoke declaration and e2e/installer.bats.
Copilot AI review requested due to automatic review settings July 22, 2026 21:48
@github-actions github-actions Bot added commands CLI command implementations tests Tests (unit and e2e) docs labels Jul 22, 2026
@github-actions github-actions Bot added the bug Something isn't working label Jul 22, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 76dc027cdb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/install.sh Outdated
Comment thread internal/commands/wizard.go

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

This PR fixes the non-TTY installer and post-login “agent nudge” bias by introducing an intent-based, non-interactive basecamp setup agents command and routing non-interactive install paths through it, avoiding Claude-first guessing when multiple agents are present.

Changes:

  • Added basecamp setup agents (non-interactive) to install the baseline skill and connect agents based on BASECAMP_SETUP_AGENT or auto-detection.
  • Updated Bash/PowerShell installers to run setup agents in non-interactive / skip-wizard paths (instead of hardcoding setup claude) and added a sourcing guard to install.sh.
  • Updated the login nudge logic to avoid Claude-first ordering and to surface explicit per-agent basecamp setup <id> choices.

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 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
scripts/install.sh Routes non-interactive install flows through setup agents; adds sourcing guard and documents BASECAMP_SETUP_AGENT.
scripts/install.ps1 Runs setup agents for skip/non-interactive install flows and documents env selectors.
internal/commands/wizard.go Registers the new setup agents subcommand under setup.
internal/commands/wizard_agents.go Implements setup agents selection logic and the flat, styled-safe result envelope.
internal/commands/setup_agents_test.go Adds JSON + styled-output regression coverage for setup agents behaviors and ordering contracts.
internal/commands/auth.go Updates printAgentNudge to avoid “Claude-first” nudging and list explicit choices when needed.
internal/commands/auth_test.go Adds test coverage for nudge behavior across none/single/multiple unhealthy detections.
install.md Documents piped-installer behavior and how to force agent selection via BASECAMP_SETUP_AGENT.
e2e/smoke/smoke_lifecycle.bats Marks setup agents as out-of-scope for smoke lifecycle.
e2e/installer.bats Adds e2e assertions that installers dispatch to setup agents and that install.sh is safe to source.
.surface Regenerates CLI surface snapshot to include basecamp setup agents.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/install.ps1 Outdated
Comment thread scripts/install.ps1 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.

All reported issues were addressed across 11 files

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

Re-trigger cubic

Comment thread scripts/install.sh Outdated
Comment thread scripts/install.ps1
…kill docs

Three follow-up fixes from review of #558:

1. Cross-version installer strategy (install.sh + install.ps1). The hosted
   install script from main can outrun the latest release, whose binary lacks
   `setup agents` (v0.7.2 has only `setup`/`setup claude`/`skill install`). Both
   scripts now probe the installed binary for `setup agents` support and, on an
   older binary, fall back WITHOUT reintroducing the Claude-first bug: only an
   explicitly selected agent is connected; an unset/auto/ambiguous selector
   installs the shared skill via `skill install`. Added old-binary regression
   tests in e2e/installer.bats.

2. Guard PowerShell post-install setup. Both calls now route through a single
   best-effort Invoke-PostInstallSetup helper wrapped in try/catch, so a nonzero
   native exit (under $ErrorActionPreference='Stop' +
   $PSNativeCommandUseErrorActionPreference) can no longer terminate an
   otherwise-successful install.

3. Skill docs. Document `setup agents` + BASECAMP_SETUP_AGENT in
   skills/basecamp/SKILL.md and add it to the remediation list in
   skills/basecamp-doctor/SKILL.md.
Copilot AI review requested due to automatic review settings July 22, 2026 23:04
@github-actions github-actions Bot added the skills Agent skills label Jul 22, 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 13 out of 13 changed files in this pull request and generated 1 comment.

Comment thread internal/commands/wizard_agents.go

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 267c7efebf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/install.sh Outdated
… binaries

- setup agents: add cobra.NoArgs — selection is env-driven, so positional args
  (typos, or confusion with `setup <id>`) are now rejected, not silently ignored.
- install.sh / install.ps1: on old release binaries lacking `setup agents`,
  BASECAMP_SETUP_AGENT=all now dispatches every per-agent `setup <id>` the binary
  supports instead of collapsing to skill-only. Explicit "all" is honored intent,
  distinct from the intent-neutral unset/auto/none fallback. PowerShell calls are
  now individually guarded so one agent's failure can't skip the rest.
- Tests: NoArgs rejection (Go) + `all` old-binary dispatch (installer.bats).
Copilot AI review requested due to automatic review settings July 23, 2026 00:03

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 13 out of 13 changed files in this pull request and generated no new comments.

@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.

All reported issues were addressed across 5 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread scripts/install.ps1 Outdated
On a pre-`setup agents` release (v0.7.2 advertises only `setup claude`), an
explicit BASECAMP_SETUP_AGENT=codex ran `basecamp setup codex`. The old `setup`
parent accepts `codex` as a stray positional arg and launches the INTERACTIVE
setup/OAuth wizard — violating the non-interactive contract.

Both installers now capability-check explicit claude|codex selectors the same
way `all` already did: only dispatch `setup <id>` if the binary advertises it,
otherwise degrade to `skill install` and never invoke the unknown subcommand.

Tests: the old-binary stub now rejects every unadvertised `setup <sub>` (not
just `setup agents`), and new coverage asserts explicit `codex` degrades to
`skill install` (install.sh) plus a static guard on install.ps1's capability
check.
Copilot AI review requested due to automatic review settings July 23, 2026 00:12

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 13 out of 13 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings July 23, 2026 00:16

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 13 out of 13 changed files in this pull request and generated no new comments.

@jeremy jeremy added this to the v0.8.0 milestone Jul 23, 2026
@jeremy
jeremy merged commit 2579885 into main Jul 23, 2026
27 checks passed
@jeremy
jeremy deleted the fix/installer-codex-non-tty branch July 23, 2026 00:20
jeremy added a commit that referenced this pull request Jul 27, 2026
* Add post-deploy installer smoke canary

The install endpoint serves main, so merging a change to
scripts/install.sh deploys it immediately. #558 shipped a sourcing guard
that broke the documented `curl | bash` path under `set -u`; the bats
suite now gates that regression pre-merge (#568), but nothing checked
the published installer post-merge.

Add .github/workflows/installer-smoke.yml: nightly + workflow_dispatch +
push-to-main (scripts/install.sh) runs of the canonical Quick Start
command against the published endpoint, cache-busted with the commit SHA
since raw.githubusercontent.com serves max-age=300. Two legs: Linux
(modern bash) and macOS via stock /bin/bash, which asserts exactly Bash
3.2 through the same interpreter so the coverage can't silently degrade.
Installs into a scratch HOME with explicit BASECAMP_BIN_DIR and asserts
the deterministic installed path.

Also two installer-scoped comment fixes from the incident review:
correct the "Keep PATH empty" description in e2e/installer.bats (PATH is
a binary-free tmpdir, not empty), and document why the guard's `:-$0`
fallback is required so a future cleanup doesn't re-break piping.

Refs #558, #568

* Set cancel-in-progress explicitly to match workflow convention
jeremy added a commit that referenced this pull request Jul 27, 2026
* ci: harden installer canary and add pre-merge Bash 3.2 gate

Canary (installer-smoke.yml):
- Trigger on scripts/install.ps1 pushes too — the raw URL serves main, so
  merge == deploy for the PowerShell installer as well.
- New Windows job running the documented irm | iex shape under both
  Windows PowerShell 5.1 and pwsh 7 (matrix), against scratch
  USERPROFILE/BASECAMP_BIN_DIR under RUNNER_TEMP.
- New notify job: on any leg's failure, file or update an
  "Installer canary failure" issue (exact-title dedupe, best-effort gh
  calls, ::error:: annotation). Skipped entirely on green runs.

Pre-merge (test.yml): new installer-bash32 job on macos-latest — plain
steps, not bats (bats 1.11 execs `env bash` at every layer, so the
interpreter is not pinnable through it). Asserts /bin/bash is exactly
3.2, syntax-checks install.sh, and runs it piped with curl off PATH,
asserting the guard's clean failure message and the absence of
unbound-variable errors (the #558 regression class). Path-filtered to
installer files plus test.yml itself so the job proves itself on the PR
that introduces or edits it; skipped filters still report success, so
the job is safe to mark required.

Refs #558 #568 #569

* TEMP: notify drill (revert me)

* Revert "TEMP: notify drill (revert me)"

This reverts commit 37cf5f7.

* ci: run a full offline install under Bash 3.2, not just the curl guard

The piped-execution step stops at the curl prerequisite, and `bash -n`
only checks grammar — neither catches Bash-4-only constructs in the
download, checksum, extract, or post-install code. Add a step that runs
one complete install path under real /bin/bash 3.2 using a fixture curl
serving a local release (stub binary, real sha256 checksums) with PATH
restricted to fixture + system dirs, then asserts the installed stub
runs. Review feedback on #575.

* ci: grant the 3.2 gate pull-requests read for paths-filter

dorny/paths-filter enumerates changed PR files via the REST API and
documents a pull-requests: read requirement; the job-level permissions
block zeroes everything unlisted. Review feedback on #575.
jeremy added a commit that referenced this pull request Jul 27, 2026
Every CLI command runs root PersistentPreRunE -> appctx.NewApp ->
auth.NewManager -> auth.NewStore, and credstore.NewStore eagerly probes
OS keyring availability with a write. On macOS that probe shells out to
an uncancellable `security` child; a locked keychain with no TTY or GUI
blocks it forever — on *every* command, including credential-free ones
like `setup agents` and `skill install`. That is how the installer hung
headless (#568, canary discovery #569/#571 -> #574 drill lineage).

Go fix (reaches users at the next release): Store now records the
fallback dir at construction and builds the credstore lazily behind
sync.Once on first credential operation. Credential-free commands never
touch the keyring; credential-touching commands probe on first use
exactly as before, with the documented BASECAMP_NO_KEYRING=1 escape
hatch. No timeout was added deliberately: an abandoned goroutine cannot
kill the hung `security` child, so a timeout would hide the hang while
keeping the leak. Bounded fallback needs cancellation support upstream
in credstore/go-keyring first.

Installer belt (deploys on merge, covers released binaries <= v0.7.2
which still probe eagerly): post_install_setup in install.sh prefixes
each best-effort child with per-command BASECAMP_NO_KEYRING=1;
Invoke-PostInstallSetup in install.ps1 sets it for the duration of setup
and restores the caller's value on exit. These children never touch
credentials — the var only skips the old binaries' startup probe.

Tests: keyring_test.go pins constructor laziness through an injectable
credstore seam. installer.bats gains belt tests that explicitly unset
the suite-wide BASECAMP_NO_KEYRING and assert both directions (real
calls carry it, the sh capability probe does not), plus a pwsh test that
extracts Invoke-PostInstallSetup from install.ps1's AST (no test hooks
in the production script), runs it against a logging stub, and asserts
set + restore; it fails closed when pwsh is missing in CI.

The canary's workflow-level BASECAMP_NO_KEYRING is removed with no
step-scoped replacement: the macOS leg now genuinely exercises the
deployed belt (a missing belt = hung security child = timeout = notify),
and the direct `basecamp version` asserts skip PersistentPreRunE
entirely.

Refs #558 #568 #569
jeremy added a commit that referenced this pull request Jul 27, 2026
* Defer keyring probe to first credential use; belt the installers

Every CLI command runs root PersistentPreRunE -> appctx.NewApp ->
auth.NewManager -> auth.NewStore, and credstore.NewStore eagerly probes
OS keyring availability with a write. On macOS that probe shells out to
an uncancellable `security` child; a locked keychain with no TTY or GUI
blocks it forever — on *every* command, including credential-free ones
like `setup agents` and `skill install`. That is how the installer hung
headless (#568, canary discovery #569/#571 -> #574 drill lineage).

Go fix (reaches users at the next release): Store now records the
fallback dir at construction and builds the credstore lazily behind
sync.Once on first credential operation. Credential-free commands never
touch the keyring; credential-touching commands probe on first use
exactly as before, with the documented BASECAMP_NO_KEYRING=1 escape
hatch. No timeout was added deliberately: an abandoned goroutine cannot
kill the hung `security` child, so a timeout would hide the hang while
keeping the leak. Bounded fallback needs cancellation support upstream
in credstore/go-keyring first.

Installer belt (deploys on merge, covers released binaries <= v0.7.2
which still probe eagerly): post_install_setup in install.sh prefixes
each best-effort child with per-command BASECAMP_NO_KEYRING=1;
Invoke-PostInstallSetup in install.ps1 sets it for the duration of setup
and restores the caller's value on exit. These children never touch
credentials — the var only skips the old binaries' startup probe.

Tests: keyring_test.go pins constructor laziness through an injectable
credstore seam. installer.bats gains belt tests that explicitly unset
the suite-wide BASECAMP_NO_KEYRING and assert both directions (real
calls carry it, the sh capability probe does not), plus a pwsh test that
extracts Invoke-PostInstallSetup from install.ps1's AST (no test hooks
in the production script), runs it against a logging stub, and asserts
set + restore; it fails closed when pwsh is missing in CI.

The canary's workflow-level BASECAMP_NO_KEYRING is removed with no
step-scoped replacement: the macOS leg now genuinely exercises the
deployed belt (a missing belt = hung security child = timeout = notify),
and the direct `basecamp version` asserts skip PersistentPreRunE
entirely.

Refs #558 #568 #569

* test(installer): pin restore of a caller-set BASECAMP_NO_KEYRING

The pwsh belt test proved restore-to-unset only; a regression that
overwrites an existing caller value would have passed. Second driver
pass sets the var to 0 before Invoke-PostInstallSetup and asserts it
survives. Review feedback on #578.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working commands CLI command implementations docs skills Agent skills tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Non-TTY installer hardcodes Claude setup instead of supporting Codex cleanly

2 participants