From 8d714c2d84aead894ff00ae305bce5ca6f752c38 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Tue, 25 Aug 2026 01:06:20 -0700 Subject: [PATCH] fix(cli): skip the foreground-start prompt after the service starts (#12153) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The CLI onboarding wizard can install Paperclip as a background service, and it offers a foreground start when nothing else will serve > - After #12148, an interactive onboard installs and starts the service, then still asks "Start Paperclip now?" > - Answering yes runs the foreground start into the already-running instance guard, so a fully successful onboard ends with an error message > - This pull request excludes the just-installed-service case from the foreground-start prompt > - The benefit is that an interactive onboard that installs the service ends cleanly instead of steering the user into a guard refusal ## Linked Issues or Issue Description Refs #12148 — found while verifying that fix interactively. The `shouldRunNow` flag already accounts for `serviceInstalled`, but the interactive TTY fallback prompt did not, so only real interactive runs hit it: `--yes` runs, CI, and container smokes all skip the prompt branch. **What happened?** Interactive `onboard`, accept the background-service prompt. Output ends with: service installed and started, then "Start Paperclip now?" → yes → "Paperclip instance 'default' is already running as ing.paperclip.paperclipai. Use 'paperclipai service status --instance default' or pass --force to bypass this safety check." **What did you expect to happen?** Onboarding ends cleanly after "Installed and started …" — there is nothing left to start, so no prompt. **Steps to reproduce** Run `npx paperclipai@2026.825.0-nightly.1 onboard --data-dir "$(mktemp -d)"` in a terminal, accept the service prompt, then accept "Start Paperclip now?". ## What Changed - New `shouldOfferForegroundStart` predicate in `cli/src/onboard-service.ts`: the foreground-start prompt is offered only when the start was not already decided by flags, the service was not just installed, onboarding was not invoked by `run`, and the terminal is interactive. - Both onboarding call sites in `cli/src/commands/onboard.ts` use the predicate instead of the inline condition that ignored `serviceInstalled`. - Unit tests cover the predicate matrix in `cli/src/__tests__/onboard-service.test.ts`. ## Verification - `npx vitest run src/__tests__/onboard-service.test.ts` in `cli/`: 12 passed (5 new). - `tsc --noEmit` reports no errors in the changed files (remaining errors are pre-existing in `server/`). - Manual reproduction of the defect on macOS with `2026.825.0-nightly.1` before the fix: service installed, started, and healthy, then the prompt steered into the guard refusal. ## Risks - Low risk. The prompt still appears in every case it did before except when the service was just installed and is already serving. - No behavior change for `--yes`, `--run`, `--install-service` in non-interactive runs: those paths never reached the prompt. ## Model Used - Claude Fable 5 (Anthropic, model ID `claude-fable-5`), extended thinking, agentic tool use via Claude Code. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --- cli/src/__tests__/onboard-service.test.ts | 26 ++++++++++++++++++++++- cli/src/commands/onboard.ts | 6 +++--- cli/src/onboard-service.ts | 17 +++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/cli/src/__tests__/onboard-service.test.ts b/cli/src/__tests__/onboard-service.test.ts index 5b3f406433..1b2b6baf4a 100644 --- a/cli/src/__tests__/onboard-service.test.ts +++ b/cli/src/__tests__/onboard-service.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { handleOnboardService, isInstallableReleaseVersion } from "../onboard-service.js"; +import { handleOnboardService, isInstallableReleaseVersion, shouldOfferForegroundStart } from "../onboard-service.js"; function supportedDetection() { return { @@ -138,3 +138,27 @@ describe("isInstallableReleaseVersion", () => { expect(isInstallableReleaseVersion("not-a-version")).toBe(false); }); }); + +describe("shouldOfferForegroundStart", () => { + const base = { serviceInstalled: false, startAlreadyDecided: false, invokedByRun: false, interactive: true }; + + it("offers a foreground start on a plain interactive onboard", () => { + expect(shouldOfferForegroundStart(base)).toBe(true); + }); + + it("never prompts after the service was installed and started", () => { + expect(shouldOfferForegroundStart({ ...base, serviceInstalled: true })).toBe(false); + }); + + it("never prompts when the start decision was already made by flags", () => { + expect(shouldOfferForegroundStart({ ...base, startAlreadyDecided: true })).toBe(false); + }); + + it("never prompts when run itself invoked onboarding", () => { + expect(shouldOfferForegroundStart({ ...base, invokedByRun: true })).toBe(false); + }); + + it("never prompts without an interactive terminal", () => { + expect(shouldOfferForegroundStart({ ...base, interactive: false })).toBe(false); + }); +}); diff --git a/cli/src/commands/onboard.ts b/cli/src/commands/onboard.ts index 31850714d7..db94cb24a7 100644 --- a/cli/src/commands/onboard.ts +++ b/cli/src/commands/onboard.ts @@ -52,7 +52,7 @@ import { trackInstallStarted, trackInstallCompleted, } from "../telemetry.js"; -import { handleOnboardService } from "../onboard-service.js"; +import { handleOnboardService, shouldOfferForegroundStart } from "../onboard-service.js"; import { readInstallManifest, isManagedExecutable } from "../install-store.js"; type SetupMode = "quickstart" | "advanced"; @@ -459,7 +459,7 @@ export async function onboard(opts: OnboardOptions): Promise { const serviceInstalled = await handleOnboardService(opts); let shouldRunNow = !serviceInstalled && (opts.run === true || opts.yes === true); - if (!shouldRunNow && !opts.invokedByRun && process.stdin.isTTY && process.stdout.isTTY) { + if (shouldOfferForegroundStart({ serviceInstalled, startAlreadyDecided: shouldRunNow, invokedByRun: opts.invokedByRun === true, interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY) })) { const answer = await p.confirm({ message: "Start Paperclip now?", initialValue: true, @@ -725,7 +725,7 @@ export async function onboard(opts: OnboardOptions): Promise { const serviceInstalled = await handleOnboardService(opts); let shouldRunNow = !serviceInstalled && (opts.run === true || opts.yes === true); - if (!shouldRunNow && !opts.invokedByRun && process.stdin.isTTY && process.stdout.isTTY) { + if (shouldOfferForegroundStart({ serviceInstalled, startAlreadyDecided: shouldRunNow, invokedByRun: opts.invokedByRun === true, interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY) })) { const answer = await p.confirm({ message: "Start Paperclip now?", initialValue: true, diff --git a/cli/src/onboard-service.ts b/cli/src/onboard-service.ts index 268f121e51..1f52e970aa 100644 --- a/cli/src/onboard-service.ts +++ b/cli/src/onboard-service.ts @@ -165,3 +165,20 @@ export async function handleOnboardService( deps.success(`Installed and started ${detection.manager.serviceName}.`); return true; } + +// Onboarding falls back to offering a foreground start when nothing else +// will serve. A just-installed service is already serving, so offering the +// start would only run the user into the already-running instance guard. +export function shouldOfferForegroundStart(options: { + serviceInstalled: boolean; + startAlreadyDecided: boolean; + invokedByRun: boolean; + interactive: boolean; +}): boolean { + return ( + !options.startAlreadyDecided && + !options.serviceInstalled && + !options.invokedByRun && + options.interactive + ); +}