/** * Onboarding checklist — the first-run steps after the harness is ready * (specs/onboarding-harness-login.md, Phase 3): * * harness → first team (starter template or Blank) → first task → Cloud → Slack * * Every step's `done` is read from the real system, not from UI flags: * * - `harness`: the orchestrator's harness is recorded, installed and not * known to be logged out (the same rule as the web setup redirect); * - `team`: at least one team exists, or the owner chose Blank; * - `first_task`: the owner has sent at least one message on any surface * (chat-v2 `user` rows an agent did not write), or a first task was handed * to the orchestrator from setup; * - `cloud`: this instance is connected to Crewly Cloud; * - `slack`: Slack is connected (Cloud-installed app or self-hosted tokens). * * The only stored flags are the dashboard card's `dismissed`, the Blank * choice and the first-task record (`onboarding-state.store.ts`). * * Shared by the REST routes (`/api/onboarding/*`, also reachable from the * phone / portal over the relay) and `crewly onboard`. * * @module services/onboarding/onboarding-checklist.service */ import type { Team } from '../../types/index.js'; import type { TeamTemplate } from '../../types/team-template.types.js'; import type { CreateFromTemplateResult } from '../template/template.service.js'; import type { BundleTemplate } from '../../types/solution-bundle.types.js'; import { OnboardingStateStore } from './onboarding-state.store.js'; /** Checklist step ids, in display order. */ export type ChecklistStepId = 'harness' | 'team' | 'first_task' | 'cloud' | 'slack'; /** Harness step detail. */ export interface HarnessStepDetail { /** The orchestrator's harness (null until chosen) */ orcHarness: string | null; installed: boolean; loginState: 'logged_in' | 'logged_out' | 'unknown' | null; error?: string; } /** Team step detail. */ export interface TeamStepDetail { teams: Array<{ id: string; name: string; templateId: string | null; }>; /** The owner chose Blank (the orchestrator only) */ blank: boolean; error?: string; } /** First-task step detail. */ export interface FirstTaskStepDetail { /** When setup handed a first task to the orchestrator */ sentAt: string | null; /** The owner has written to Crewly on some surface */ ownerMessageSeen: boolean; /** A first task from `crewly onboard` waits for the backend */ pending: boolean; error?: string; } /** Cloud step detail. */ export interface CloudStepDetail { connected: boolean; tier: string | null; /** * Phone-friendly sign-in: Google sign-in on Crewly Cloud that ends on the * portal's token page, whose token + refresh token are pasted back into * setup (`POST /api/cloud/connect`). */ tokenPageSignInUrl: string; error?: string; } /** Slack step detail. */ export interface SlackStepDetail { connected: boolean; /** Slack is installed through Crewly Cloud, so Cloud comes first */ cloudConnected: boolean; error?: string; } /** One checklist step. */ export type ChecklistStep = { id: 'harness'; done: boolean; detail: HarnessStepDetail; } | { id: 'team'; done: boolean; detail: TeamStepDetail; } | { id: 'first_task'; done: boolean; detail: FirstTaskStepDetail; } | { id: 'cloud'; done: boolean; detail: CloudStepDetail; } | { id: 'slack'; done: boolean; detail: SlackStepDetail; }; /** `GET /api/onboarding/checklist` payload. */ export interface OnboardingChecklist { steps: ChecklistStep[]; doneCount: number; total: number; allDone: boolean; /** The owner hid the dashboard card */ dismissed: boolean; dismissedAt: string | null; } /** * What picking a starter does: `template` creates the team * (`POST /starter-team`), `bundle` deploys a solution bundle after its * questions (`POST /api/bundles/apply`), `blank` records the Blank choice. */ export type OnboardingStarterKind = 'template' | 'bundle' | 'blank'; /** A starter team the owner can pick (a template, a solution bundle, or Blank). */ export interface OnboardingStarter { /** Template id, or `blank` */ id: string; /** How it is created */ kind: OnboardingStarterKind; name: string; /** Short Chinese display name */ label: string; tagline: string; description: string; recommended: boolean; members: Array<{ name: string; role: string; }>; /** Example first tasks */ suggestions: string[]; } /** Result of creating the starter team. */ export interface StarterTeamResult { starterId: string; /** The team (null for Blank) */ team: Team | null; /** False when a team from this starter already existed */ created: boolean; } /** Result of handing over the first task. */ export interface FirstTaskResult { /** True when the orchestrator received it or it is queued for it */ forwarded: boolean; /** Queued until the orchestrator is online */ queued: boolean; conversationId: string | null; teamId: string | null; sentAt: string | null; /** Why it was not forwarded, or a note (e.g. orchestrator offline) */ message: string | null; } /** Error codes of this service. */ export type OnboardingErrorCode = 'unknown_starter' | 'invalid_task' | 'unknown_team'; /** A request the checklist cannot fulfil. */ export declare class OnboardingError extends Error { readonly code: OnboardingErrorCode; /** * @param code - Machine-readable code (mapped to an HTTP status by the controller) * @param message - Human-readable message */ constructor(code: OnboardingErrorCode, message: string); } /** The template operations the checklist needs. */ export interface StarterTemplateSource { listOnboardingStarters(): TeamTemplate[]; getTemplate(id: string): TeamTemplate | null; createTeamFromTemplate(templateId: string, teamName: string): CreateFromTemplateResult | null; } /** What handing a message to the orchestrator returns. */ export interface OrchestratorSendResult { conversationId: string | null; forwarded: boolean; queued: boolean; error: string | null; } /** Dependencies (all injectable for tests). */ export interface OnboardingChecklistDeps { store: OnboardingStateStore; /** Ready solution bundles, offered after the free starters (optional) */ listBundles?(): BundleTemplate[]; /** Harness step */ getHarnessState(): Promise; listTeams(): Promise; saveTeam(team: Team): Promise; templates(): StarterTemplateSource; /** The orchestrator's harness, used as the runtime of new starter members */ getOrcHarness(): Promise; /** Whether the owner has written on any surface */ hasOwnerMessage(): boolean; getCloudState(): { connected: boolean; tier: string | null; }; isSlackConnected(): boolean; /** Record the owner's message and hand it to the orchestrator */ sendToOrchestrator(content: string, metadata: Record): Promise; now(): Date; } /** * Lower-case slug of ASCII letters, digits and dashes. * * @param value - Any text * @returns Slug (may be empty for non-ASCII text) */ export declare function toSessionSlug(value: string): string; /** * The Crewly Cloud sign-in that works from any device: Google sign-in on * Cloud, ending on the portal's token page (token + refresh token to paste). * * @returns Absolute URL */ export declare function buildTokenPageSignInUrl(): string; /** * The message the orchestrator receives for a first task. * * @param text - The owner's words * @param team - Team it is meant for, or null for the orchestrator itself * @returns Message content */ export declare function buildFirstTaskMessage(text: string, team: Pick | null): string; /** * First-run checklist: step states, starter teams, the first task, dismissal. */ export declare class OnboardingChecklistService { private readonly deps; /** * @param deps - Dependencies */ constructor(deps: OnboardingChecklistDeps); /** * Read every step from the live system. * * A step whose source fails reads as not done, with `detail.error` set; * the rest of the checklist is still returned. * * @returns The checklist */ getChecklist(): Promise; /** * The starter teams: templates marked `onboarding` (by `order`), then the * ready solution bundles, then Blank. * * @returns Starters, recommended first */ listStarters(): OnboardingStarter[]; /** * Create the first team from a starter, or record the Blank choice. * * Idempotent: a team already created from the same template is returned * (a double tap on a phone must not make two teams). Members run on the * orchestrator's harness, the only one first-time setup installs. * * @param starterId - Template id of a starter, or `blank` * @returns The team (null for Blank) and whether it was created now * @throws OnboardingError `unknown_starter` */ createStarterTeam(starterId: string): Promise; /** * Hand the owner's first task to the orchestrator (through the chat path, * so it is a normal owner message: ticket intake, queue while offline). * * @param text - The owner's words * @param teamId - Team it is meant for (omit for the orchestrator itself) * @returns Delivery result * @throws OnboardingError `invalid_task` | `unknown_team` */ sendFirstTask(text: unknown, teamId?: unknown): Promise; /** * Keep a first task typed while the backend was down (`crewly onboard`). * * @param text - The owner's words * @param teamId - Team it is meant for, or null * @throws OnboardingError `invalid_task` */ queuePendingFirstTask(text: string, teamId: string | null): Promise; /** * Deliver a pending first task (called when the backend starts). A team * that no longer exists sends it to the orchestrator itself. * * @returns The delivery result, or null when nothing was pending */ deliverPendingFirstTask(): Promise; /** * Hide or show the dashboard checklist card. * * @param dismissed - True to hide * @returns The checklist after the change */ setDismissed(dismissed: boolean): Promise; /** @returns Harness step */ private harnessStep; /** * @param state - Stored onboarding state * @returns Team step */ private teamStep; /** * @param state - Stored onboarding state * @returns First-task step */ private firstTaskStep; /** @returns Cloud step */ private cloudStep; /** @returns Slack step */ private slackStep; } //# sourceMappingURL=onboarding-checklist.service.d.ts.map