Skip to content

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

Merged
monorkin merged 25 commits into
mainfrom
onboarding
Aug 21, 2026
Merged

Port basecamp-cli's onboarding flow: setup wizard, agent setup, login/logout#228
monorkin merged 25 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 guides setup instead of “Not logged in,” adds a coding‑agent harness and hey login/hey logout, and hardens agent skill install/refresh and credential migration for safety and idempotence.

  • Bare hey: interactive and logged out → setup wizard; logged in → TUI. Non‑interactive or machine output never prompts. HEY_NONINTERACTIVE=1 suppresses prompts and OAuth but still connects detected agents and sets onboarded. Prompts require stdin, stdout, and stderr TTYs.
  • hey setup: always runs the full wizard. --json keeps structured output and may start browser sign‑in when stdin is a TTY; set HEY_NONINTERACTIVE=1 to avoid OAuth.
  • hey setup agents|claude|codex: non‑interactive via internal/harness; an agent connects only if its handler succeeded and health checks pass. They never fabricate ~/.claude or ~/.codex, load only global config (skip the local‑config trust gate), and reject --ids-only/--count. In machine mode setup claude|codex exits non‑zero when the integration did not connect; setup agents stays envelope‑only and synthesizes a missing‑binary hint only when absence blocked connection. Linking Claude now requires a healthy, managed baseline.
  • Auth and routing: hey login / hey logout are shortcuts for hey auth login/logout. At an interactive terminal, requireAuth() offers to sign in and continues after OAuth; piped, declined, or machine‑output runs fail with “Not logged in” (exit 3) and a targeted hint (“hey auth login” or “Update or unset HEY_TOKEN”). internal/auth.LoginOptions.Logger can route OAuth progress output.
  • Doctor: reports baseline skill presence and per‑agent health via internal/harness. Unmanaged skills at canonical paths and symlinked SKILL.md files fail health with clear hints; baseline health requires the ownership marker. Claude plugin detection reads installed_plugins.json only — a malformed registry reports “not installed.”
  • Safety and refresh: every write claims a skill directory with a regular‑file .managed-by-hey-cli marker; the CLI refuses to write into unmarked or symlinked directories, never writes through symlinked files, and only writes regular files. Replacement requires provenance (only our canonical Claude symlink or a marked, allow‑listed copy directory is removed). Automatic refresh updates only marked, non‑symlinked regular files, tracks the active Codex home, skips entirely when no config dir is available, prints its notice only when the refresh completed, and suppresses that notice for automatic JSON output. Reads may traverse user‑symlinked directories; writes never follow symlinks.
  • Installer: after a verified install, a TTY runs hey setup; otherwise it runs hey setup agents. It respects HEY_SKIP_SETUP and HEY_SETUP_AGENT, and sets HEY_NO_KEYRING=1 on invoked commands. A canceled or non‑zero wizard prints next steps and does not fail. It never invokes legacy setup commands on old binaries; those get printed next steps. Windows scripts/install.ps1 mirrors this behavior.
  • Config and migration: adds a global onboarded flag and hey config set onboarded=true|false. Only setup subcommands and hey skill skip legacy‑credential migration; hey config set migrates before rewriting and preserves unknown keys (including cover). Migration runs only when the legacy credential’s origin matches the effective credential key; successful migration scrubs legacy secrets (retried when a usable stored credential already loads). A literal null config.json no longer crashes writes.
  • Merge with main: passes tui.Watchers through the runTUI seam and fixes tests; config rewrites now round‑trip cover.

Written for commit 13343ff. Summary will update on new commits.

Review in cubic

@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 Outdated
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
jeremy added a commit that referenced this pull request Aug 21, 2026
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
jeremy added a commit that referenced this pull request Aug 21, 2026
- 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.

@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: 8a184800c4

ℹ️ 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.go Outdated
Comment thread internal/cmd/setup.go Outdated
Comment thread internal/cmd/formatting.go
Comment thread internal/cmd/skill_install.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: d8b72235d8

ℹ️ 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.go
Comment thread internal/cmd/setup.go Outdated
Comment thread internal/cmd/root.go
Comment thread scripts/install.sh Outdated
Comment thread internal/cmd/skill_refresh.go Outdated
jeremy added a commit that referenced this pull request Aug 21, 2026
The credential finding was the third variation of config rewrites
interacting with legacy embedded secrets, so the fix is at the
instrument: saveGlobalConfig now rewrites only the keys the schema owns
and carries every unknown key through verbatim — unmigrated legacy
credentials and any future version's settings survive every rewrite,
including a wizard that persists onboarded after a failed migration.
Successful migration scrubs the secret keys explicitly
(config.ScrubLegacyCredentials), so the cleanup that used to happen by
accident still happens, now on purpose.

The rest:
- wizardBreadcrumbs joins the checklist on a shared hasAuthIssue
  predicate, so rejected stored credentials route machine clients to
  hey auth login instead of hey tui.
- The bundled SKILL.md catches up with the hey tui entry point.
- The installer's TTY branch treats a wizard the user cancels as a
  warning plus next steps, not an install failure under set -e.
- The skill-refresh notice is suppressed for automatic JSON output
  (piped stdout), not just explicit machine flags.
jeremy added a commit that referenced this pull request Aug 21, 2026
The credential finding was the third variation of config rewrites
interacting with legacy embedded secrets, so the fix is at the
instrument: saveGlobalConfig now rewrites only the keys the schema owns
and carries every unknown key through verbatim — unmigrated legacy
credentials and any future version's settings survive every rewrite,
including a wizard that persists onboarded after a failed migration.
Successful migration scrubs the secret keys explicitly
(config.ScrubLegacyCredentials), so the cleanup that used to happen by
accident still happens, now on purpose.

The rest:
- wizardBreadcrumbs joins the checklist on a shared hasAuthIssue
  predicate, so rejected stored credentials route machine clients to
  hey auth login instead of hey tui.
- The bundled SKILL.md catches up with the hey tui entry point.
- The installer's TTY branch treats a wizard the user cancels as a
  warning plus next steps, not an install failure under set -e.
- The skill-refresh notice is suppressed for automatic JSON output
  (piped stdout), not just explicit machine flags.
Comment thread internal/cmd/skill_install.go Outdated
Comment thread internal/cmd/skill_install.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: 7b922a0f6a

ℹ️ 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/skill_install.go Outdated
Comment thread internal/cmd/setup_agent.go
Comment thread internal/cmd/setup_agents.go Outdated
jeremy added a commit that referenced this pull request Aug 21, 2026
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.
jeremy added a commit that referenced this pull request Aug 21, 2026
- 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.
jeremy added a commit that referenced this pull request Aug 21, 2026
The credential finding was the third variation of config rewrites
interacting with legacy embedded secrets, so the fix is at the
instrument: saveGlobalConfig now rewrites only the keys the schema owns
and carries every unknown key through verbatim — unmigrated legacy
credentials and any future version's settings survive every rewrite,
including a wizard that persists onboarded after a failed migration.
Successful migration scrubs the secret keys explicitly
(config.ScrubLegacyCredentials), so the cleanup that used to happen by
accident still happens, now on purpose.

The rest:
- wizardBreadcrumbs joins the checklist on a shared hasAuthIssue
  predicate, so rejected stored credentials route machine clients to
  hey auth login instead of hey tui.
- The bundled SKILL.md catches up with the hey tui entry point.
- The installer's TTY branch treats a wizard the user cancels as a
  warning plus next steps, not an install failure under set -e.
- The skill-refresh notice is suppressed for automatic JSON output
  (piped stdout), not just explicit machine flags.
jeremy added a commit that referenced this pull request Aug 21, 2026
- Read-side health now shares the write-side file-shape rule: a new
  harness.RegularSkillFile (Lstat, regular file) backs both agent skill
  checks and baselineSkillInstalled, so a symlinked SKILL.md — the state
  every write path refuses and refresh skips — is reported as a failing
  check instead of a healthy install the wizard shortcuts past.
- Machine-mode hey setup claude|codex returns an error envelope
  (setup_incomplete, manual steps in the hint) with a nonzero exit when
  the integration did not connect. ok:true with plugin_installed:false
  was in-band honesty that automation keying off the exit status never
  saw. The aggregate setup agents keeps its in-band contract: its core
  outcome is the skill, and per-agent detail rides in the envelope.
- setup agents synthesizes missing-binary remediation only when the
  absence actually prevented the connection — Codex is routinely
  detected by its home directory alone, and its skill-only setup was
  drawing a warning that told the user to rerun what had just worked.
- The two new gosec findings get the house #nosec annotations with
  justifications: fixed skill locations under the user's own home,
  gated by claimSkillDir and Lstat.

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

ℹ️ 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/skill_install.go Outdated
Comment thread internal/cmd/local_config_trust.go
Comment thread scripts/install.ps1 Outdated
jeremy added a commit that referenced this pull request Aug 21, 2026
- baselineSkillInstalled requires the ownership marker as well as a
  regular file — the final presence predicate aligned with the rules
  every write path enforces, so an unmarked hand-authored baseline is
  refused by install AND never reported as a healthy installation.
- The agent-local setup subcommands (and skill) enroll in #225's
  commandIgnoresLocalConfig path: they load the global config only,
  degrading to defaults, so a malformed repository .hey/config.json
  cannot stop the installer's setup agents handoff before the trust
  exemption is even consulted. Runtime-config commands still surface
  the parse error.
- install.ps1 gets the same optional-setup guard install.sh got: a
  wizard that exits nonzero (declined OAuth, timeout, busy callback
  port) prints next steps instead of failing or silently swallowing a
  completed install, with $LASTEXITCODE checked because Windows
  PowerShell 5.1 does not throw on native nonzero exits.
jeremy added 21 commits August 21, 2026 15:19
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.
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.
- 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.
Codex's third review round showed two classes still leaking at
individual sites, so the fixes move to the layer each class lives at
rather than patching the sites:

- Skill health checks now require the ownership marker. Presence was
  standing in for health, which is why every consumer — the wizard, the
  setup agents record, the per-agent verdict — kept needing its own
  handler-success patch: an unmanaged skill at the canonical path passed
  the check that all of them share. With the check itself reporting
  "occupied by a skill hey-cli did not write", every current and future
  consumer verdicts correctly. The wizard additionally records a handler
  refusal as its own issue for the rare failure that leaves no failing
  check.
- Every skill file write goes through writeSkillFile, which Lstat-refuses
  non-regular files: a symlinked SKILL.md planted inside a marked
  directory can no longer be followed and truncated by install, wizard,
  handoff, or the version stamp — matching the guard refresh already had.
- interactiveStdio requires stderr to be a terminal too: prompts render
  on stderr, and hey boxes 2>log at a TTY was waiting on a prompt the
  user could not see. Redirected stderr now takes the documented
  non-interactive auth-error path.
- The styled summary derives "Signed in" from the auth issues, so it can
  no longer show a green check beside "Stored sign-in rejected".
The credential finding was the third variation of config rewrites
interacting with legacy embedded secrets, so the fix is at the
instrument: saveGlobalConfig now rewrites only the keys the schema owns
and carries every unknown key through verbatim — unmigrated legacy
credentials and any future version's settings survive every rewrite,
including a wizard that persists onboarded after a failed migration.
Successful migration scrubs the secret keys explicitly
(config.ScrubLegacyCredentials), so the cleanup that used to happen by
accident still happens, now on purpose.

The rest:
- wizardBreadcrumbs joins the checklist on a shared hasAuthIssue
  predicate, so rejected stored credentials route machine clients to
  hey auth login instead of hey tui.
- The bundled SKILL.md catches up with the hey tui entry point.
- The installer's TTY branch treats a wizard the user cancels as a
  warning plus next steps, not an install failure under set -e.
- The skill-refresh notice is suppressed for automatic JSON output
  (piped stdout), not just explicit machine flags.
- Read-side health now shares the write-side file-shape rule: a new
  harness.RegularSkillFile (Lstat, regular file) backs both agent skill
  checks and baselineSkillInstalled, so a symlinked SKILL.md — the state
  every write path refuses and refresh skips — is reported as a failing
  check instead of a healthy install the wizard shortcuts past.
- Machine-mode hey setup claude|codex returns an error envelope
  (setup_incomplete, manual steps in the hint) with a nonzero exit when
  the integration did not connect. ok:true with plugin_installed:false
  was in-band honesty that automation keying off the exit status never
  saw. The aggregate setup agents keeps its in-band contract: its core
  outcome is the skill, and per-agent detail rides in the envelope.
- setup agents synthesizes missing-binary remediation only when the
  absence actually prevented the connection — Codex is routinely
  detected by its home directory alone, and its skill-only setup was
  drawing a warning that told the user to rerun what had just worked.
- The two new gosec findings get the house #nosec annotations with
  justifications: fixed skill locations under the user's own home,
  gated by claimSkillDir and Lstat.
- baselineSkillInstalled requires the ownership marker as well as a
  regular file — the final presence predicate aligned with the rules
  every write path enforces, so an unmarked hand-authored baseline is
  refused by install AND never reported as a healthy installation.
- The agent-local setup subcommands (and skill) enroll in #225's
  commandIgnoresLocalConfig path: they load the global config only,
  degrading to defaults, so a malformed repository .hey/config.json
  cannot stop the installer's setup agents handoff before the trust
  exemption is even consulted. Runtime-config commands still surface
  the parse error.
- install.ps1 gets the same optional-setup guard install.sh got: a
  wizard that exits nonzero (declined OAuth, timeout, busy callback
  port) prints next steps instead of failing or silently swallowing a
  completed install, with $LASTEXITCODE checked because Windows
  PowerShell 5.1 does not throw on native nonzero exits.
- The installer never invokes an old binary's legacy setup commands.
  hey's history differs from basecamp-cli's here: every release that
  predates setup agents also predates the ownership gate on skill
  install, so the ported cross-version fallback always dispatched into
  an implementation that overwrites whatever occupies the skill paths.
  The scaffolding guarded releases that never existed — removed rather
  than armored. Old binaries get the printed next steps; only the
  ownership-aware setup agents is ever run automatically. install.ps1
  matches, and the bats contract now pins never-invoke-legacy across
  every selector.
- The ownership marker itself must be a regular file: SkillDirOwned
  goes through RegularSkillFile (and cmd's duplicate predicate collapses
  onto it), so a symlink or directory planted in the marker's name
  confers no ownership and the unmanaged-content refusal still protects
  the SKILL.md next to it.
- doctor reports presence and health separately again: an occupied but
  unmanaged path gets the move-aside remediation instead of "Not
  installed" with a hint that would refuse.
- setup, setup agents and setup <agent> reject --ids-only/--count up
  front: a wizard result is not list data, and the writer's late refusal
  used to land after OAuth, agent installs and the onboarded flag.
…back

Two remaining name-based reads in the copy-fallback path, both closed
with the rules the class already established:

- isManagedSkillCopy requires every entry to be a regular file, so a
  symlink planted in the marker's name (or any other allowlisted name)
  authorizes no deletion and the user's SKILL.md beside it survives
  Claude setup.
- copySkillFiles copies only the files hey-cli owns (SKILL.md, version
  stamp, marker). A user file added to the managed baseline no longer
  rides into the Claude copy, where it would fail the copy's own
  allowlist on the next run and strand the fallback.
…rite

- A rejected HEY_TOKEN now points at the environment ("Update or unset
  HEY_TOKEN") instead of hey auth login, which the env token outranks —
  following the old hint repeated the same failure forever.
- The refresh sentinel is written through writeSkillFile, the same
  no-follow rule as every other file this feature writes, so a symlink
  planted at ~/.config/hey-cli/.last-run-version is never truncated.
- The wizard's structured breadcrumbs point a rejected HEY_TOKEN at the
  environment (unset HEY_TOKEN), completing the round-nine hint fix —
  the env token outranks anything hey auth login saves, so the old
  breadcrumb looped agents forever.
- The bundled skill stops promising that --json never prompts: with a
  terminal on stdin it deliberately starts browser sign-in, so agents
  on allocated PTYs are told to use HEY_NONINTERACTIVE=1 for
  status-only setup.
- pluginInstalled no longer falls back to raw text search when
  installed_plugins.json is unparseable: Claude itself cannot read a
  malformed registry, so its raw bytes prove nothing and the check
  reports not installed instead of shortcutting the wizard past a
  broken integration.
Same correction as the bundled skill: --json changes only the output
format, and a terminal on stdin (allocated PTYs included) still starts
browser OAuth. Agents are told to set HEY_NONINTERACTIVE=1 for a
status-only run.
Review arbitration on the three open judgment calls: the bare-hey
credential-validation and symlinked-directory declines stand, the
scrub-retry decline was overruled.

- migrateOldCredentials no longer returns blind when the store already
  loads: with a usable stored credential (access token or session
  cookie present) it retries the scrub, so a past save-success/
  scrub-failure cannot strand plaintext secrets in config.json forever.
  An empty stored record authorizes nothing — the legacy fields stay
  recoverable. Regression covers scrubbed secrets, a surviving
  unrelated future key, and byte-identical stored credentials.
- The RegularSkillFile and baselineSkillInstalled comments now state
  the deliberate bound instead of overclaiming symmetry: intermediate
  directory symlinks resolve, so a user-symlinked skill directory can
  pass reads that every write, removal and refresh still refuses.
Follow-ups to the last three changes, each a first-order consequence:

- The origin guard the scrub-retry needed applies to the whole
  migration: legacy config.json credentials carry their own server, and
  with HEY_BASE_URL pointed elsewhere the current store key proves
  nothing about them — migrating would misfile them under the wrong
  origin and scrubbing would delete the only copy for their real one.
  Migration now engages only when the legacy origin matches the
  effective credential key; foreign-origin fields ride along untouched
  (rewrites already preserve them).
- A refresh whose sentinel cannot be written is not complete: it now
  reports no completed refresh, so the update notice cannot repeat on
  every subsequent command from an unwritable config dir.
- The wizard docs stop calling HEY_NONINTERACTIVE=1 status-only: it
  suppresses OAuth and prompts but still connects detected agents and
  persists onboarded; hey doctor is the inspection path.
- A config.json containing literal JSON null decodes to a nil map
  without error, and the preserve-unknown-keys merge then panicked on
  assignment, crashing every config-writing path. The map is
  reinitialized after decoding.
- linkSkillToClaude requires a healthy managed baseline before creating
  the link: setup paths that recorded installSkillFiles' refusal used to
  continue into the link step and point ~/.claude/skills/hey at the very
  content the install declined to claim.
@monorkin
monorkin merged commit f95fa4a into main Aug 21, 2026
33 checks passed
monorkin pushed a commit that referenced this pull request Aug 21, 2026
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.
monorkin pushed a commit that referenced this pull request Aug 21, 2026
- 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.
monorkin pushed a commit that referenced this pull request Aug 21, 2026
The credential finding was the third variation of config rewrites
interacting with legacy embedded secrets, so the fix is at the
instrument: saveGlobalConfig now rewrites only the keys the schema owns
and carries every unknown key through verbatim — unmigrated legacy
credentials and any future version's settings survive every rewrite,
including a wizard that persists onboarded after a failed migration.
Successful migration scrubs the secret keys explicitly
(config.ScrubLegacyCredentials), so the cleanup that used to happen by
accident still happens, now on purpose.

The rest:
- wizardBreadcrumbs joins the checklist on a shared hasAuthIssue
  predicate, so rejected stored credentials route machine clients to
  hey auth login instead of hey tui.
- The bundled SKILL.md catches up with the hey tui entry point.
- The installer's TTY branch treats a wizard the user cancels as a
  warning plus next steps, not an install failure under set -e.
- The skill-refresh notice is suppressed for automatic JSON output
  (piped stdout), not just explicit machine flags.
monorkin pushed a commit that referenced this pull request Aug 21, 2026
- Read-side health now shares the write-side file-shape rule: a new
  harness.RegularSkillFile (Lstat, regular file) backs both agent skill
  checks and baselineSkillInstalled, so a symlinked SKILL.md — the state
  every write path refuses and refresh skips — is reported as a failing
  check instead of a healthy install the wizard shortcuts past.
- Machine-mode hey setup claude|codex returns an error envelope
  (setup_incomplete, manual steps in the hint) with a nonzero exit when
  the integration did not connect. ok:true with plugin_installed:false
  was in-band honesty that automation keying off the exit status never
  saw. The aggregate setup agents keeps its in-band contract: its core
  outcome is the skill, and per-agent detail rides in the envelope.
- setup agents synthesizes missing-binary remediation only when the
  absence actually prevented the connection — Codex is routinely
  detected by its home directory alone, and its skill-only setup was
  drawing a warning that told the user to rerun what had just worked.
- The two new gosec findings get the house #nosec annotations with
  justifications: fixed skill locations under the user's own home,
  gated by claimSkillDir and Lstat.
@monorkin
monorkin deleted the onboarding branch August 21, 2026 14:04
monorkin pushed a commit that referenced this pull request Aug 21, 2026
- baselineSkillInstalled requires the ownership marker as well as a
  regular file — the final presence predicate aligned with the rules
  every write path enforces, so an unmarked hand-authored baseline is
  refused by install AND never reported as a healthy installation.
- The agent-local setup subcommands (and skill) enroll in #225's
  commandIgnoresLocalConfig path: they load the global config only,
  degrading to defaults, so a malformed repository .hey/config.json
  cannot stop the installer's setup agents handoff before the trust
  exemption is even consulted. Runtime-config commands still surface
  the parse error.
- install.ps1 gets the same optional-setup guard install.sh got: a
  wizard that exits nonzero (declined OAuth, timeout, busy callback
  port) prints next steps instead of failing or silently swallowing a
  completed install, with $LASTEXITCODE checked because Windows
  PowerShell 5.1 does not throw on native nonzero exits.
monorkin pushed a commit that referenced this pull request Aug 21, 2026
- The installer never invokes an old binary's legacy setup commands.
  hey's history differs from basecamp-cli's here: every release that
  predates setup agents also predates the ownership gate on skill
  install, so the ported cross-version fallback always dispatched into
  an implementation that overwrites whatever occupies the skill paths.
  The scaffolding guarded releases that never existed — removed rather
  than armored. Old binaries get the printed next steps; only the
  ownership-aware setup agents is ever run automatically. install.ps1
  matches, and the bats contract now pins never-invoke-legacy across
  every selector.
- The ownership marker itself must be a regular file: SkillDirOwned
  goes through RegularSkillFile (and cmd's duplicate predicate collapses
  onto it), so a symlink or directory planted in the marker's name
  confers no ownership and the unmanaged-content refusal still protects
  the SKILL.md next to it.
- doctor reports presence and health separately again: an occupied but
  unmanaged path gets the move-aside remediation instead of "Not
  installed" with a hint that would refuse.
- setup, setup agents and setup <agent> reject --ids-only/--count up
  front: a wizard result is not list data, and the writer's late refusal
  used to land after OAuth, agent installs and the onboarded flag.
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.

4 participants