/** * Owner-facing wording of decision cards (crewly#1367, crewly#1369 §3). Pure. * * A card is read by the owner on a phone: it leads with the question in * plain words, in the owner's language. Ticket ids, PR numbers, branch and * worktree names and `GOAL:` prefixes are internal mechanics; they are * taken out of the question when the card is created (a PR link may stay in * a short details line). The harness's own text on the card — buttons, the * context line, reminders — follows the owner's language. * * Card D-822 (2026-10-16) is the incident: its title was * "CREW-646 · GOAL: Decide the fate of OPEN PR #1138 (…, CREW-257), worktree * crewly-projec…", in English, to an owner who writes Chinese; the question * really was "should agent-sent Gmail go out only after you approve a card?". * * The Chinese strings below are owner-facing text in the owner's language * (an allowed exception to the English-only rule). * * @module services/decisions/decision-wording */ import type { DecisionContext, DecisionLanguage, DecisionOption } from '../../types/decision.types.js'; /** Result of {@link stripInternalRefs}. */ export interface StrippedText { /** The text without internal references (one line, tidied) */ text: string; /** PR references and links that were taken out, for a details line */ refs: string[]; } /** * Take ticket ids, PR numbers, links, branch / worktree names, paths and * `GOAL:` prefixes out of a question or title (crewly#1367). * * @param raw - Question or title * @returns The plain text, and the PR references / links removed * * @example * ```typescript * stripInternalRefs('CREW-646 · GOAL: Merge PR #1138 (Gmail approve card)?'); * // { text: 'Merge (Gmail approve card)?', refs: ['PR #1138'] } * ``` */ export declare function stripInternalRefs(raw: string | undefined | null): StrippedText; /** How many letters (any script) a text has. */ export declare function letterCount(s: string): number; /** * Whether an ask is about merging a PR: the question (before stripping) or * one of its options talks about merging. A card that only names a PR (to * close it, to review it) is not a merge card. * * @param rawQuestion - The question as the agent wrote it * @param options - Its options * @returns True for a merge card */ export declare function isMergeAsk(rawQuestion: string, options: ReadonlyArray>): boolean; /** * Parse the optional card context. Each field is one short line. * * @param raw - `{requestedBy?, ownerLastSaid?, runtimeChanges?, link?}` * @returns The context, or undefined when empty * @throws Error (message for the agent) when a field is too long or not text */ export declare function parseDecisionContext(raw: unknown): DecisionContext | undefined; /** * Whether a question is written in the owner's language. Only a Chinese * owner is checked (an English owner reading a bilingual question is fine). * * @param question - The plain question * @param ownerLanguage - The owner's language, when known * @returns True when the question can go out as is */ export declare function questionFitsOwnerLanguage(question: string, ownerLanguage: DecisionLanguage | null | undefined): boolean; /** * The option label in the owner's language: generic English labels ("Yes", * "Not now", "Yes, merge") become Chinese for a Chinese owner; a merge card * names the outcome (合并 / 先不合). Any other label is left as written. * * @param label - Label as asked * @param lang - Owner language * @param merge - The card is a merge question * @returns Label to show (and to match replies against) */ export declare function localizeOptionLabel(label: string, lang: DecisionLanguage | undefined, merge?: boolean): string; /** * Localize every option label; keeps labels distinct (a collision keeps the * original label). * * @param options - Options * @param lang - Owner language * @param merge - Merge card * @returns Options with localized labels */ export declare function localizeOptions(options: DecisionOption[], lang: DecisionLanguage | undefined, merge?: boolean): DecisionOption[]; /** * A question clipped for a one-line reminder. * * @param q - Question * @param max - Max characters * @returns Clipped text */ export declare function clipQuestion(q: string, max?: number): string; /** The harness's own card text, per owner language. */ export interface CardText { remindButton: string; skipButton: string; details: string; requestedBy: string; ownerLastSaid: string; runtimeChanges: string; mergeRuntimeChanges: string; remindingAt: (when: string) => string; declineDefault: (when: string, label: string) => string; heldDefault: (when: string, label: string) => string; sensitive: (kind: string) => string; waitHold: string; goWithDefault: (when: string, label: string) => string; waitReminder: (at: string, q: string) => string; waitReminderMany: (at: string, n: number) => string; remindNow: (at: string, q: string) => string; sensitiveReask: (at: string, q: string, kind: string) => string; chose: (who: string, label: string, at: string) => string; answered: (who: string, words: string, at: string) => string; answeredInThread: (at: string) => string; parked: (id: string) => string; closed: (reason: string, at: string) => string; skipped: (who: string, at: string) => string; theOwner: string; defaultNoAnswerOpen: (when: string) => string; defaultNoAnswer: (when: string, verb: string, label: string) => string; defaultVerb: (name: string | null, settled: boolean) => string; } /** * The harness's card text in a language. * * @param lang - Owner language (absent = English) * @returns Strings */ export declare function cardText(lang: DecisionLanguage | undefined): CardText; /** * Short local time in Chinese: "14:05" today, "明天 12:00", else "10月2日 周四 12:00". * * @param at - Moment * @param now - Clock * @returns Text */ export declare function formatWhenZh(at: Date, now?: Date): string; /** * The context lines of a card (who asked, what the owner said last, what * changes), as mrkdwn — null when there is no context. * * @param context - Card context * @param lang - Owner language * @param merge - Merge card ("What merging changes") * @returns mrkdwn, or null */ export declare function contextSection(context: DecisionContext | undefined, lang: DecisionLanguage | undefined, merge?: boolean): string | null; /** * The short details line (PR link / references), or null. * * @param d - Decision context + stripped refs * @param lang - Owner language * @returns mrkdwn, or null */ export declare function detailsLine(d: { context?: DecisionContext; details?: string[]; }, lang: DecisionLanguage | undefined): string | null; //# sourceMappingURL=decision-wording.d.ts.map