You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
teamai init <repo> --role <id> --force is documented as "a fully non-interactive init (suitable for CI/CD or AI agents)" (docs/usage-guide.md). It is not. When no credential is present and stdin is not a terminal, init spawns an interactive provider login (gh auth login --web with stdio: 'inherit') and waits for the device flow to hit its deadline, about five minutes. It then exits 1 with the provider's error. The output never says which credential was missing.
The prompt layer already handles this case. askQuestion throws without a TTY and askConfirmation returns its default (src/utils/prompt.ts). The gap is the second way init talks to the user, a child process that takes over the terminal:
init
provider.authenticate()
github ghAuthLogin() spawns `gh auth login --web`, stdio inherit blocks until the device-flow deadline
tgit gfAuthLogin() spawns `gf auth login`, stdio inherit blocks (documented in e2e.test.ts:704)
cnb cnbLogin() spawns `cnb login`, stdio inherit blocks
gitcode askQuestion(...) throws without a TTY and names GITCODE_TOKEN correct, the pattern to copy
gitlab ensureGitLabAvailable() throws and names GITLAB_TOKEN correct
provider.cloneRepo()
git clone GIT_TERMINAL_PROMPT is not set, so a credential helper can prompt (GUI keychain dialog on macOS)
Who this affects:
The e2e suite. src/__tests__/e2e/e2e.test.ts:704-708 describes gf auth login as "an interactive (inheritStdio) login that no amount of stdin piping can satisfy" and skips the init test for every provider except GitHub. Each PR verification script rebuilds the same shims by hand (</dev/null, GIT_TERMINAL_PROMPT=0, GIT_CONFIG_NOSYSTEM=1, a token in the environment).
Agents in a sandbox without a terminal (CI jobs, Claude Code or Codex cloud runs). teamai init sits for five minutes and dies with failed to authenticate via web browser: context deadline exceeded. Nothing in the log points at GITHUB_TOKEN.
Agents that allocate a pseudo-TTY. process.stdin.isTTY is true, nobody types, and askQuestion waits forever. The CLI has no explicit way to declare an unattended run. CI, TEAMAI_NONINTERACTIVE, --non-interactive and --yes are all unrecognised today.
#591 fixed the same class of bug for teamai remove.
Reproduction
Deterministic, about eight seconds, no network. A fake gh on PATH reports no session, and its auth login behaves like the real device flow when nobody opens the browser.
Expected. With no terminal and no credential, init exits 1 at once and names the credential. A throwaway patch that checks process.stdin.isTTY before ghAuthLogin() gives exactly that against the same reproduction:
ℹ Not logged in — starting authentication
✖ Authentication failed: GitHub authentication unavailable without a terminal. Export GITHUB_TOKEN (or GH_TOKEN) with repo scope, or run `gh auth login` in an interactive shell first.
exit=1 after 0s
Provider: GitHub (the tgit and cnb login paths have the same shape)
AI tool(s): Claude Code
Logs
Output of the reproduction on main, killed after eight seconds
ℹ Initializing teamai...
ℹ Scope: user
ℹ config → <HOME>/.teamai/config.yaml
ℹ resources → <HOME>/.claude/skills, ...
- Checking authentication...
ℹ Not logged in — starting authentication
ℹ Starting GitHub authentication via gh CLI...
! First copy your one-time code: XXXX-XXXX
Open this URL to continue in your web browser: https://github.com/login/device
<still running after 8s, killed>
Proposed fix
Three steps. The first one alone turns the reproduction green. The other two close the gaps it cannot see.
ensureGhAuthenticated / gfAuthLogin / cnbLogin
whoami succeeded? return the login
+ not interactive? throw "…unavailable without a terminal. Export <TOKEN_VAR>…"
spawn the login with stdio inherit
utils/prompt.ts
- process.stdin.isTTY (repeated at 12 call sites)+ isInteractive() = stdin.isTTY && !CI && !TEAMAI_NONINTERACTIVE && !--non-interactive
clone.ts / source.ts, every git spawn
+ env.GIT_TERMINAL_PROMPT = '0' when not interactive
src/__tests__/e2e/e2e.test.ts
- PROVIDER_IS_GITHUB skip and the blank-line stdin "defense in depth"+ run init for every provider with a token in the environment only
Guard every subprocess login with the rule prompt.ts already applies. In ensureGhAuthenticated, gfAuthLogin and cnbLogin, throw when the run is not interactive and name the variable (GITHUB_TOKEN or GH_TOKEN, TGIT_TOKEN, the CNB token), the way providers/gitcode/index.ts:57-62 does. Check: the reproduction exits 1 in under a second and the message contains the variable name, once per provider with a fake gh, gf and cnb binary.
Replace the scattered process.stdin.isTTY checks with one isInteractive() in utils/prompt.ts that also honours CI, TEAMAI_NONINTERACTIVE and a flag on init. When it is false, every prompt takes its default or fails naming the flag to pass (--role, --agent, the token variable). Check: run the reproduction inside script -q /dev/null with CI=1 and it still exits fast.
Spawn git with GIT_TERMINAL_PROMPT=0 when not interactive, so a missing credential surfaces as fatal: could not read Username instead of a keychain dialog or a hang on a pseudo-TTY. Check: a clone of a private URL with no credential fails in under two seconds.
Then remove the workaround in e2e.test.ts:704-733 and update docs/usage-guide.md:173 with the token variable each provider needs for an unattended init.
Open question for the maintainer. --force means "overwrite existing config" today, and promptForSelfModeAgents already treats it as "do not prompt" (init.ts:670). Reusing it avoids a new flag. The cost is that an unattended run cannot both refuse to overwrite and refuse to prompt. A separate --non-interactive keeps the two meanings apart.
Description
teamai init <repo> --role <id> --forceis documented as "a fully non-interactive init (suitable for CI/CD or AI agents)" (docs/usage-guide.md). It is not. When no credential is present and stdin is not a terminal,initspawns an interactive provider login (gh auth login --webwithstdio: 'inherit') and waits for the device flow to hit its deadline, about five minutes. It then exits 1 with the provider's error. The output never says which credential was missing.The prompt layer already handles this case.
askQuestionthrows without a TTY andaskConfirmationreturns its default (src/utils/prompt.ts). The gap is the second wayinittalks to the user, a child process that takes over the terminal:Who this affects:
src/__tests__/e2e/e2e.test.ts:704-708describesgf auth loginas "an interactive (inheritStdio) login that no amount of stdin piping can satisfy" and skips the init test for every provider except GitHub. Each PR verification script rebuilds the same shims by hand (</dev/null,GIT_TERMINAL_PROMPT=0,GIT_CONFIG_NOSYSTEM=1, a token in the environment).teamai initsits for five minutes and dies withfailed to authenticate via web browser: context deadline exceeded. Nothing in the log points atGITHUB_TOKEN.process.stdin.isTTYis true, nobody types, andaskQuestionwaits forever. The CLI has no explicit way to declare an unattended run.CI,TEAMAI_NONINTERACTIVE,--non-interactiveand--yesare all unrecognised today.#591 fixed the same class of bug for
teamai remove.Reproduction
Deterministic, about eight seconds, no network. A fake
ghon PATH reports no session, and itsauth loginbehaves like the real device flow when nobody opens the browser.Create
bin/ghand make it executable:Run
initwith an isolated HOME, no token, and stdin closed:The process is still running after eight seconds. With the real
ghit exits 1 after about five minutes with the same lines, which is what happened twice while verifying feat(skill): serve builtin skill content from the CLI, deploy a discovery stub #699 on 2026-09-21.Expected. With no terminal and no credential,
initexits 1 at once and names the credential. A throwaway patch that checksprocess.stdin.isTTYbeforeghAuthLogin()gives exactly that against the same reproduction:Environment
mainat 2c0ae96Logs
Output of the reproduction on
main, killed after eight secondsProposed fix
Three steps. The first one alone turns the reproduction green. The other two close the gaps it cannot see.
prompt.tsalready applies. InensureGhAuthenticated,gfAuthLoginandcnbLogin, throw when the run is not interactive and name the variable (GITHUB_TOKENorGH_TOKEN,TGIT_TOKEN, the CNB token), the wayproviders/gitcode/index.ts:57-62does. Check: the reproduction exits 1 in under a second and the message contains the variable name, once per provider with a fakegh,gfandcnbbinary.process.stdin.isTTYchecks with oneisInteractive()inutils/prompt.tsthat also honoursCI,TEAMAI_NONINTERACTIVEand a flag oninit. When it is false, every prompt takes its default or fails naming the flag to pass (--role,--agent, the token variable). Check: run the reproduction insidescript -q /dev/nullwithCI=1and it still exits fast.gitwithGIT_TERMINAL_PROMPT=0when not interactive, so a missing credential surfaces asfatal: could not read Usernameinstead of a keychain dialog or a hang on a pseudo-TTY. Check: a clone of a private URL with no credential fails in under two seconds.Then remove the workaround in
e2e.test.ts:704-733and updatedocs/usage-guide.md:173with the token variable each provider needs for an unattendedinit.Open question for the maintainer.
--forcemeans "overwrite existing config" today, andpromptForSelfModeAgentsalready treats it as "do not prompt" (init.ts:670). Reusing it avoids a new flag. The cost is that an unattended run cannot both refuse to overwrite and refuse to prompt. A separate--non-interactivekeeps the two meanings apart.