Skip to content

Port basecamp-cli's onboarding flow: setup wizard, agent setup, login/logout - #228

Open
jeremy wants to merge 13 commits into
mainfrom
onboarding
Open

Port basecamp-cli's onboarding flow: setup wizard, agent setup, login/logout#228
jeremy wants to merge 13 commits into
mainfrom
onboarding

Conversation

@jeremy

@jeremy jeremy commented Aug 21, 2026

Copy link
Copy Markdown
Member

Ports basecamp-cli's onboarding flow so a first run of hey is seamless instead of Error: not logged in.

Behavior

  • Bare hey at an interactive terminal, logged out → the setup wizard (welcome → OAuth sign-in → linked-account greeting → coding-agent setup → summary). Once onboarded, a later logged-out run gets the lite wizard (sign-in only). Every other bare hey prints help, preserving main's hey tui contract (#8d3e675).
  • hey setup always runs the full wizard. --json never prompts; logged out without a terminal reports status: incomplete with a hey auth login breadcrumb, so the piped installer can never hang on a browser.
  • hey setup claude|codex|agents backed by a new internal/harness agent registry. Claude gets the hey@37signals plugin from basecamp/claude-plugins plus a skill link; Codex gets the skill only until a .codex-plugin ships. setup agents is the installer's non-interactive path: HEY_SETUP_AGENT=claude|codex|all|none, ambiguity connects nobody.
  • hey login / hey logout shortcuts; OAuth login greets by identity and nudges toward hey setup <agent> when a detected agent is unhealthy.
  • requireAuth() at an interactive terminal asks "Not logged in. Sign in now?" and continues after OAuth; piped, declined or machine-output runs get Error: Not logged in / Run: hey auth login, exit 3.
  • hey doctor gains baseline-skill and per-agent diagnostics; a PersistentPostRunE hook re-syncs installed skill copies once per release version.
  • Installer hands off to hey setup on a TTY and to hey setup agents otherwise (HEY_SKIP_SETUP, HEY_SETUP_AGENT, per-command HEY_NO_KEYRING=1); install.ps1 mirrors it.
  • HEY_NONINTERACTIVE=1 disables every prompt, including on a PTY.

Safety properties worth reviewing

  • hey-cli never fabricates ~/.claude (hey setup claude on a machine without Claude refuses).
  • Skill directories hey-cli writes carry a .managed-by-hey-cli marker. Replacement requires the marker plus a known-files allowlist; the automatic refresh skips anything unmarked and never touches symlinks. A user-authored ~/.claude/skills/hey — even one that is a single SKILL.md — is preserved byte-for-byte.
  • setup agents|claude|codex skip the local-config trust gate (they never touch the server); the wizard itself stays gated.

Verification

make check, full go test ./..., all 40 installer bats (incl. the pwsh AST-extracted ones), .surface additions only. Manual matrix and PTY probes against the built binary are in the commit messages. Not yet verified: live OAuth and make test-smoke — the dev server at app.hey.localhost:3003 was unreachable throughout; that is the final merge gate.

Follow-ups deliberately out of scope: wordmark animation, .codex-plugin + native Codex plugin install, a quick-start JSON envelope for non-TTY bare hey.


Summary by cubic

Ports basecamp-cli’s onboarding so first run is seamless instead of “Not logged in,” and hardens skill installs and refreshes to never overwrite user content.

  • Bare hey at an interactive terminal routes on auth: logged out → setup wizard (welcome → OAuth → linked accounts → agent setup → summary); logged in → TUI. Non-interactive runs never prompt; set HEY_NONINTERACTIVE=1 to disable prompts everywhere.

  • hey setup always runs the full wizard; --json never prompts. When stored credentials are rejected by HEY, the wizard reports “Stored sign-in rejected” with a hey auth login hint.

  • hey setup agents|claude|codex runs non-interactively. It installs the baseline HEY agent skill and connects selected or detected agents. An agent counts as connected only if its handler succeeded and health checks pass. It never fabricates ~/.claude or ~/.codex.

  • hey login / hey logout are top‑level shortcuts for hey auth login/logout. OAuth login greets by identity and nudges toward hey setup <agent> when a detected agent is unhealthy.

  • requireAuth() at an interactive terminal offers “Not logged in. Sign in now?” and continues after OAuth; piped, declined, or machine‑output runs fail with “Not logged in” (exit 3) and a hey auth login hint.

  • hey doctor checks the baseline skill and per‑agent health via internal/harness. It warns when the baseline skill directory is not managed by the CLI and suggests adoption. A persistent post‑run hook refreshes installed skill copies once per CLI version and Codex home.

  • Installers hand off to hey setup on a TTY and to hey setup agents otherwise. They respect HEY_SKIP_SETUP, HEY_SETUP_AGENT, and set HEY_NO_KEYRING=1 on invoked commands. install.ps1 mirrors this.

  • Config adds a global onboarded flag (hey config set onboarded=true|false). .surface gains login, logout, and setup agents|claude|codex.

  • Safety and rollout

    • Enforce ownership at write: the CLI claims a skill directory with a .managed-by-hey-cli marker before writing. It refuses to write into unmarked or symlinked directories.
    • Replacement requires provenance: only our canonical Claude symlink or a marked, allow‑listed copy directory is removed; foreign links, regular files, or unmarked directories are preserved.
    • Automatic refresh updates only marked, non‑symlinked files, tracks the active Codex home, and skips entirely when no config directory is available (no sentinels in the cwd). It never rewrites symlinks.
    • Setup subcommands (setup agents|claude|codex) skip the local‑config trust gate; the wizard stays gated.
    • setup <agent> exits non‑zero when the agent remains unhealthy after setup.
    • Legacy‑credential migration is narrowly exempted: only setup subcommands and hey skill skip it; hey config set now migrates legacy credentials before rewriting the config.

Written for commit 8a18480. Summary will update on new commits.

Review in cubic

jeremy added 11 commits August 20, 2026 17:55
- config: persisted onboarded flag (global file only — a repository's
  .hey/config.json can neither suppress nor force the wizard),
  config set/show support, and the HEY_NONINTERACTIVE escape hatch
- auth: LoginOptions.Logger so callers can route OAuth progress output;
  nil keeps today's os.Stderr prints byte for byte
- cmd: stdin/stdout terminal checks become seam variables behind
  interactiveStdio(), which also honors HEY_NONINTERACTIVE; success,
  warning and muted styles for wizard output
- tui: Confirm prompt on bubbletea v2 (renders to stderr so stdout stays
  data) and the wordmark hoisted from root.go for reuse
- root: runTUI seam so routing is testable without a terminal
- internal/harness: agent registry with Claude Code and Codex detection,
  plugin/skill health checks (hey@37signals in installed_plugins.json,
  ~/.claude/skills/hey link, Codex skill copy under $CODEX_HOME|~/.codex).
  Codex is skill-presence only until a native .codex-plugin ships.
- skill install: extracted installSkillFiles/linkSkillToClaude helpers
  (with a copy fallback where symlinks are unavailable — but never over a
  populated real directory), a Codex skill copy when Codex is detected,
  and an .installed-version stamp so staleness is observable
- doctor: replaces the cwd-relative .claude-plugin check with the
  baseline-skill check plus per-detected-agent diagnostics
- skill refresh: a PersistentPostRunE hook that re-syncs installed skill
  copies once per release-version change (never on dev builds, never
  installing anything new), repairing a broken Claude symlink on the way
Bare `hey` at an interactive terminal now routes on auth state: logged
out runs the setup wizard (welcome, OAuth sign-in, a look at the linked
accounts, coding-agent setup, summary) and stops there; logged in opens
the TUI as before. Once onboarded, a later logged-out run gets the lite
wizard (sign-in only). Pipes, cron and machine flags keep getting help.

- hey setup: the full wizard, always. With --json it never prompts; not
  signed in without a terminal reports status incomplete and points at
  hey auth login, so the piped installer can never hang on a browser.
- hey setup claude|codex: install the skill and connect one agent;
  structured {plugin_installed, agent_detected, errors, manual_commands}
  when piped. Claude refuses to fabricate ~/.claude on a machine that
  has no Claude Code — detection must stay honest.
- hey setup agents: the installer's non-interactive path, HEY_SETUP_AGENT
  (claude|codex|all|none, unset = auto-detect a single agent, ambiguity
  connects nobody), flat envelope.
- hey login / hey logout: top-level shortcuts for auth login/logout;
  OAuth login greets by identity and nudges toward hey setup <agent>
  when a detected agent is unhealthy (never setup agents).
- requireAuth: at an interactive terminal a logged-out data command asks
  "Not logged in. Sign in now?" and continues after OAuth; declined,
  piped or machine-output runs get Error: Not logged in / Run: hey auth
  login, exit 3.
- .surface gains login, logout and setup agents|claude|codex (additions
  only)
…smoke

README gains a Getting started section (bare hey signs you in, shows your
linked accounts, offers agent setup; hey setup reruns it; login/logout
shortcuts; piped runs never prompt) and the Agent Skill section becomes
AI agent integration (setup claude|codex|agents, skill install, doctor,
HEY_SETUP_AGENT, HEY_NONINTERACTIVE). AGENTS.md describes the routing,
the requireAuth prompt, internal/harness, the onboarded flag, and the
.surface/help-literal reminders for new commands. The embedded skill
mentions hey setup and the shortcuts.
After a verified install, install.sh runs `hey setup` when stdin and
stdout are a terminal; otherwise (CI, piped input, coding-agent shells,
HEY_SKIP_SETUP=1) it runs the non-interactive `hey setup agents` and
prints next steps. post_install_setup capability-probes `setup --help`
so the hosted script stays correct against older release binaries:
without `setup agents` it connects only an explicitly selected agent
(HEY_SETUP_AGENT), capability-checked per agent so an unadvertised id is
never handed to an old parent as a stray arg, and otherwise installs the
shared skill. Every real call carries HEY_NO_KEYRING=1 per command so a
locked headless keychain cannot wedge the installer; the help probe
stays bare. install.ps1 mirrors this with Invoke-PostInstallSetup and a
Test-InteractiveSession gate, restoring HEY_NO_KEYRING on the way out.

installer.bats replaces the old "no interactive setup" pin with the
ported contract: stub binaries logging argv, new/old binary dispatch,
selector fallbacks, the keyring belt, and the ps1 helper evaluated from
its AST.
interactiveStdio() honored the escape hatch, but the wizard branched on
styled output alone: on a real PTY with HEY_NONINTERACTIVE=1 it still
ran tui.Confirm (eating a keystroke and emitting Bubble Tea control
traffic) and, logged out, parked in the six-minute OAuth wait — exactly
what the README promised it would not do.

Sign-in now runs only when it can be seen through: stdin is a terminal
and HEY_NONINTERACTIVE is off (deliberately looser than
interactiveStdio() so `hey setup --json` from a terminal still signs
in with progress on stderr). The agent-setup prompt runs only under
interactiveStdio(); otherwise the wizard proceeds with the prompt's
default answer, matching the machine-mode contract. The prompt moves
behind a confirmAgentSetup seam so the boundary is testable, and
AGENTS.md's routing paragraph catches up with the hey tui contract.
The installer's non-TTY handoff runs `hey setup agents` from whatever
directory curl was piped in — possibly a repository with an untrusted
.hey/config.json — and the trust gate was failing it before the skill
ever installed, a failure install.sh's best-effort `|| true` then
swallowed without a trace. setup agents|claude|codex only touch local
agent files, never the effective server or account, so they now skip the
gate the same way skill/upgrade/version do. The wizard itself signs in
against the effective server and stays gated.
After a symlink failure the copy fallback leaves a real, populated
directory at ~/.claude/skills/hey, and the next install died trying to
os.Remove it before ever reaching the fallback again. Removal now
recognizes exactly the shape our fallback writes — a plain directory
holding only SKILL.md and its version stamp — and replaces it; any
other populated directory is user content and still refused, as the
existing regression pins. The symlink call moves behind a seam so two
consecutive fallback installs run as a real test.
Shape was standing in for ownership: any directory holding only a
SKILL.md matched the copy-fallback profile and could be RemoveAll'd,
so a user's hand-authored ~/.claude/skills/hey was deleted and replaced
by a symlink. The release refresh was broader still — it rewrote every
existing Claude/Codex SKILL.md at the known paths without proving
hey-cli wrote them, so merely running any command on a new release
clobbered a custom skill.

Every hey-cli install now stamps its directory with a .managed-by-hey-cli
marker (the copy fallback claims its freshly created directory up front,
so a partial copy stays recognizable and replaceable). Replacement
requires the marker AND the known-files allowlist; automatic refresh
skips any location whose directory lacks the marker, treating foreign
skills as finished state — the sentinel still advances, no retry loop —
and stamps the baseline version only when that directory is ours.
Pre-marker installs are adopted the next time hey skill install or the
setup wizard runs.

Boundary tests pin both adversarial cases: an unmarked directory holding
only SKILL.md is refused and preserved by install, and a synthetic
release refresh leaves unmarked baseline/Claude/Codex skills
byte-for-byte alone.
The post-upgrade refresh treated every dangling ~/.claude/skills/hey
symlink as ours and relinked it to the baseline, so a user's link to a
temporarily unmounted volume was silently destroyed by the first command
run on a new release — even with a genuinely marked baseline beside it.

Repair now requires provenance on both ends: os.Readlink must equal the
exact canonical relative target hey-cli writes, and the baseline it
points at must carry the ownership marker. Every other broken link is
preserved. The old test blessed an arbitrary dangling target; it is
replaced by the provenance table (canonical over marked, canonical over
unmarked, foreign target, absolute spelling of the same place) plus
end-to-end pins that a foreign broken link survives the refresh and our
canonical link passes through it intact.
With provenance required on both ends the repair could never fire: a
canonical link over a marked baseline is by definition healthy, and a
missing baseline has no marker. Safety was correct, but a function named
repair that cannot repair misstates what the refresh does. Refresh now
touches files it owns and nothing else; the end-to-end pins stay —
a foreign dangling link survives, and our canonical link passes through
intact.
@jeremy
jeremy requested a review from a team as a code owner August 21, 2026 01:07
Copilot AI balanced review requested due to automatic review settings August 21, 2026 01:07
@github-actions

Copy link
Copy Markdown

Sensitive Change Detection (shadow mode)

This PR modifies control-plane files:

  • scripts/install.ps1
  • scripts/install.sh

Shadow mode — this check is informational only. When activated, changes to these paths will require approval from a maintainer.

Copilot AI 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.

Pull request overview

Ports the onboarding flow, agent integration setup, authentication shortcuts, and installer handoff into hey-cli.

Changes:

  • Adds first-run setup and interactive authentication flows.
  • Adds Claude Code/Codex setup, diagnostics, and skill refresh.
  • Updates installers, documentation, and test coverage.

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

Show a summary per file
File Description
.surface Adds new command surfaces.
AGENTS.md Documents onboarding architecture.
README.md Documents setup and agent integration.
skills/hey/SKILL.md Adds setup command guidance.
scripts/install.sh Adds setup handoff on Unix.
scripts/install.ps1 Adds setup handoff on Windows.
tests/e2e/installer.bats Tests installer setup behavior.
tests/smoke/auth_test.go Tests login/logout shortcuts.
internal/auth/auth.go Adds configurable OAuth progress logging.
internal/auth/auth_test.go Tests OAuth logging.
internal/config/config.go Adds onboarding and noninteractive state.
internal/config/config_test.go Tests new configuration behavior.
internal/harness/harness.go Defines integration health results.
internal/harness/agent.go Adds the agent registry.
internal/harness/agent_test.go Tests registry behavior.
internal/harness/claude.go Adds Claude detection and checks.
internal/harness/claude_test.go Tests Claude integration checks.
internal/harness/codex.go Adds Codex detection and checks.
internal/harness/codex_test.go Tests Codex integration checks.
internal/tui/brand.go Extracts the HEY wordmark.
internal/tui/prompt.go Adds confirmation prompts.
internal/tui/prompt_test.go Tests prompt behavior.
internal/cmd/auth.go Adds shortcuts, greetings, and agent nudges.
internal/cmd/auth_commands_test.go Tests authentication changes.
internal/cmd/config.go Exposes onboarding configuration.
internal/cmd/doctor.go Adds agent diagnostics.
internal/cmd/formatting.go Adds prompt-aware terminal detection.
internal/cmd/help_test.go Updates setup help expectations.
internal/cmd/local_config_trust.go Exempts local-only setup commands.
internal/cmd/local_config_trust_test.go Tests trust-gate exemptions.
internal/cmd/login.go Adds the login shortcut.
internal/cmd/logout.go Adds the logout shortcut.
internal/cmd/root.go Routes onboarding and auth prompts.
internal/cmd/root_test.go Tests root routing and authentication.
internal/cmd/setup.go Implements the setup wizard.
internal/cmd/setup_test.go Tests wizard behavior.
internal/cmd/setup_agent.go Implements per-agent setup.
internal/cmd/setup_agents.go Implements noninteractive agent setup.
internal/cmd/setup_agents_test.go Tests agent selection and setup.
internal/cmd/skill_install.go Extends managed skill installation.
internal/cmd/skill_install_test.go Tests skill replacement safety.
internal/cmd/skill_refresh.go Adds release-based skill refresh.
internal/cmd/skill_refresh_test.go Tests safe skill refresh.
internal/cmd/tui.go Routes TUI through a test seam.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/cmd/skill_install.go
Comment thread internal/cmd/skill_install.go Outdated
Comment thread internal/cmd/skill_install.go Outdated
Comment thread internal/cmd/skill_refresh.go Outdated
Comment thread internal/cmd/setup_agent.go Outdated
Comment thread internal/cmd/skill_refresh.go Outdated

@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: e89a59b472

ℹ️ 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 internal/cmd/setup_agents.go
Comment thread internal/cmd/skill_install.go Outdated
Comment thread internal/cmd/setup_agent.go
Comment thread internal/cmd/setup_agent.go
Comment thread scripts/install.sh
Review of #228 surfaced that ownership was enforced at removal and
refresh but not at write: installSkillFiles and installSkillToCodex
overwrote an unmarked user-authored skill and then claimed it — reachable
automatically through the installer's setup agents handoff — and
removeExistingSkillLink's bare os.Remove deleted any user file or
symlink before the directory check ever ran. Rather than patch each
site, ownership now lives at the single layer that writes:

- claimSkillDir gates every skill write (baseline, Codex, copy
  fallback): create-and-mark a missing or empty directory, accept a
  marked one, refuse anything else — including symlinks, which would
  land the write somewhere never inspected.
- removeExistingSkillLink starts from Lstat: only our canonical symlink
  or a marked, allowlisted copy directory is removed; a foreign link,
  regular file, or unmarked directory errors.
- refresh writes only marked, non-symlinked regular files in
  non-symlinked directories, and skips entirely when ConfigDir() is
  empty rather than dropping its sentinel in the cwd.
- setup codex mirrors the Claude guard: it never fabricates ~/.codex and
  then counts its own creation as detection.
- a styled hey setup <agent> verdicts from a fresh health snapshot and
  exits nonzero (setup_incomplete) instead of telling the user to start
  a session against an unconnected integration.
- commands that never touch the server (setup agents|claude|codex,
  skill, …) skip legacy-credential migration, so the installer's
  HEY_NO_KEYRING=1 handoff can no longer move old config.json tokens
  into plaintext credentials.json.
- doctor warns on an unmanaged baseline skill with the adoption path;
  pre-marker hey-cli installs adopt via hey skill install.

Regression tests pin each refusal: unmarked baseline/Codex skills
survive explicit install and setup agents byte-for-byte and unclaimed,
foreign links and regular files are refused, refresh never writes
through symlinks, the sentinel never lands in the cwd, and legacy
credentials migrate for auth status but not for setup agents.

@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: dfcf241e36

ℹ️ 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 internal/cmd/root.go Outdated
Comment thread internal/cmd/setup.go
Comment thread internal/cmd/setup_agents.go
Comment thread internal/cmd/skill_refresh.go
- Narrow the credential-migration exemption to commands that neither use
  credentials nor rewrite the global config (setup subcommands, skill).
  The previous predicate exempted all of config, whose Save* helpers
  decode config.json into a struct with no credential fields — so
  `hey config set` on a legacy install deleted embedded tokens instead
  of migrating them. Regression test pins that config set migrates
  before it rewrites.
- The wizard no longer reports a complete, signed-in setup over stored
  credentials HEY rejects: an auth-coded identity failure becomes a
  "Stored sign-in rejected" issue with a hey auth login hint (other
  failures keep the best-effort greeting, so offline setup still works).
- setup agents counts an agent connected only when its handler succeeded
  AND health checks pass. Presence checks alone flipped a refused
  install (unmanaged skill at the canonical path) back to
  plugin_installed: true under a "connected" summary.
- The refresh sentinel records the active Codex home alongside the
  version, so switching CODEX_HOME mid-release triggers one rescan
  instead of leaving the other home's marked skill stale until the next
  release.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants