import type { UserProfileSignals } from "./user-profile-harvest"; /** Product bar: entry never requires power-user literacy (Ingo — Thermomix mom to hard math). */ export const SKILL_FLOOR_LABEL = "grandma-simple entry"; /** Product bar: depth is expert-grade per specialty, not one mediocre generalist. */ export const SKILL_CEILING_LABEL = "expert-grade per specialty"; /** How the user wants to relate to Oos — never a lifestyle label. */ export type InteractionStyle = "unknown" | "geek-peer" | "quiet-doer"; /** Onboarding focus ids from `apps/api/src/onboarding.ts` — maps to crew fit, not persona for everyone. */ export type OnboardingFocusId = "day" | "inbox" | "research" | "crew"; export interface UserHelpVibeHints { interactionStyle: InteractionStyle; /** Specialist lanes to consider when spawning — suggestions only, not assignments. */ suggestedLanes: string[]; /** Honest observed signals — no invented hobbies or scene/club/office guesses. */ observedSignals: string[]; focusChoiceId?: OnboardingFocusId; inferredAt: string; } /** Focus choice → help shape. Lifestyle-agnostic: same lanes can serve any user. */ export const FOCUS_HELP_VIBE_MAP: Record< OnboardingFocusId, { interactionStyle: InteractionStyle; suggestedLanes: string[] } > = { day: { interactionStyle: "quiet-doer", suggestedLanes: ["inbox-work", "chief-operator", "curator"], }, inbox: { interactionStyle: "quiet-doer", suggestedLanes: ["inbox-work", "inbox-life", "chief-operator"], }, research: { interactionStyle: "geek-peer", suggestedLanes: ["curator", "builder", "engineer"], }, crew: { interactionStyle: "unknown", suggestedLanes: ["chief-operator", "specialist"], }, }; export function mergeHelpVibeHints( base: UserHelpVibeHints, focusId: OnboardingFocusId, ): UserHelpVibeHints { const mapped = FOCUS_HELP_VIBE_MAP[focusId]; const lanes = [...new Set([...base.suggestedLanes, ...mapped.suggestedLanes])]; return { ...base, focusChoiceId: focusId, interactionStyle: base.interactionStyle === "unknown" ? mapped.interactionStyle : base.interactionStyle, suggestedLanes: lanes, observedSignals: [...base.observedSignals, `focus:${focusId}`], inferredAt: new Date().toISOString(), }; } /** * Infer help & vibe from signals we actually have — never fake lifestyle or scene/office type. * Interaction style stays `unknown` until focus choice or explicit prefs land. */ export function inferHelpVibeHints( signals: UserProfileSignals, extras?: { focusChoiceId?: OnboardingFocusId }, ): UserHelpVibeHints { const observedSignals: string[] = []; const suggestedLanes = new Set(); if (signals.computerHost) { observedSignals.push("computer-attached"); suggestedLanes.add("builder"); suggestedLanes.add("engineer"); } if (signals.defaultProvider === "openai-compatible") { observedSignals.push("self-hosted-model"); suggestedLanes.add("engineer"); } if (signals.uiLocale) { observedSignals.push(`ui-locale:${signals.uiLocale}`); } let hints: UserHelpVibeHints = { interactionStyle: "unknown", suggestedLanes: [...suggestedLanes], observedSignals, inferredAt: new Date().toISOString(), }; if (extras?.focusChoiceId) { hints = mergeHelpVibeHints(hints, extras.focusChoiceId); } return hints; } export function summarizeHelpVibe(hints: UserHelpVibeHints): string { const parts: string[] = []; if (hints.interactionStyle !== "unknown") { parts.push(hints.interactionStyle === "geek-peer" ? "geek-peer OK" : "quiet-doer"); } if (hints.suggestedLanes.length) { parts.push(hints.suggestedLanes.slice(0, 3).join(", ")); } if (hints.focusChoiceId) { parts.push(`focus:${hints.focusChoiceId}`); } return parts.length ? parts.join(" · ") : "learning from you"; } const HELP_VIBE_SECTION = "## Help & vibe (inferred)"; export function mergeHelpVibeIntoMemory(existing: string, hints: UserHelpVibeHints): string { const block = formatHelpVibeBlock(hints); const marker = HELP_VIBE_SECTION; const start = existing.indexOf(marker); if (start === -1) { const trimmed = existing.trimEnd(); const prefix = trimmed.length ? `${trimmed}\n\n` : ""; return `${prefix}${marker}\n\n${block}\n`; } const rest = existing.slice(start + marker.length); const nextSection = rest.search(/\n## /); const tail = nextSection === -1 ? "" : rest.slice(nextSection); return `${existing.slice(0, start).trimEnd()}\n\n${marker}\n\n${block}${tail}`; } function formatHelpVibeBlock(hints: UserHelpVibeHints): string { const styleLine = hints.interactionStyle === "unknown" ? "unknown — learn from conversations (geek-peer vs quiet-doer)" : hints.interactionStyle === "geek-peer" ? "geek-peer — fachsimpeln welcome when they want depth" : "quiet-doer — execute without acting like we know better"; const lines = [ `- **Updated:** ${hints.inferredAt}`, `- **Interaction style:** ${styleLine}`, hints.focusChoiceId ? `- **Focus choice:** ${hints.focusChoiceId}` : null, hints.observedSignals.length ? `- **Observed signals:** ${hints.observedSignals.join(", ")}` : null, hints.suggestedLanes.length ? `- **Suggested specialist lanes:** ${hints.suggestedLanes.join(", ")} (_suggestions — spawn/select accordingly_)` : null, `- **Skill floor:** ${SKILL_FLOOR_LABEL}`, `- **Skill ceiling:** ${SKILL_CEILING_LABEL}`, "", "_Simple entry, deep ceiling — domain excellence without power-user literacy. Not one mediocre Wunderwuzzi._", ]; return lines.filter((line) => line !== null).join("\n"); }