fix(cli): skip the foreground-start prompt after the service starts (#12153)

## 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
This commit is contained in:
Devin Foley 2026-08-25 01:06:20 -07:00 committed by GitHub
parent 0a01444514
commit 8d714c2d84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 45 additions and 4 deletions

View File

@ -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);
});
});

View File

@ -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<void> {
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<void> {
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,

View File

@ -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
);
}