/** * Turn a question an agent asked the owner in its reply into a decision card * (specs/2026-10-01-reply-open-items.md §4). Pure. * * - A stated fallback decides the default: "不同意的话我就删掉,只留事实" → * options Yes / No (No = "删掉,只留事实"), default No — what the agent said * it would do without an answer. "没意见的话我就发" → default Yes. * - "A 还是 B?" / "Should I A or B?" → options A and B, default wait. * - Anything else → Yes / No / Reply in thread, default wait. * * Option labels the harness writes are English; the agent's own words stay * as they are (in an option's label or detail). * * @module services/open-items/open-item-card */ import type { DecisionOption, DecisionSensitiveKind } from '../../types/decision.types.js'; import type { ExtractedQuestion } from './open-item-extractor.js'; /** The card to post for a question. */ export interface DerivedQuestionCard { /** One line */ question: string; options: DecisionOption[]; /** Option key or `wait` */ defaultKey: string; /** Option a plain "yes" / ✅ picks */ yesKey?: string; sensitive?: DecisionSensitiveKind; /** How the options were found */ derivedFrom: 'fallback_no' | 'fallback_yes' | 'choice' | 'generic'; /** * What the question is about, shown under it: the owner's ask, the agent's * words around it, the draft it points at, a link to the full message — * mrkdwn sections ({@link questionContextBlocks}) */ context?: string[]; } /** * The two alternatives of a choice question, when each fits on a button. * * @param q - Question sentence * @returns [A, B] or null */ export declare function choiceAlternatives(q: string): [string, string] | null; /** * The short button label of one question of an either/or: * "还是先按现在的版本试一周再看?" → "先按现在的版本试一周再看". * * @param question - The question sentence * @returns Label (≤ 40 characters), or '' when nothing is left */ export declare function alternativeLabel(question: string): string; /** * Group consecutive questions of one reply that are alternatives of each * other: the later one starts with 还是 / 或者 / 或是 / "or", or the earlier * ends in an either/or clause (and is not already a complete "A 还是 B" * question). Groups hold at most 4 alternatives with distinct labels. * * @param questions - Questions in reply order * @returns Groups (a lone question is a group of one) */ export declare function groupEitherOr(questions: ExtractedQuestion[]): ExtractedQuestion[][]; /** * One card for questions that are alternatives: one button per alternative * (single choice) plus "Reply in thread". Default `wait`. * * @param qs - Two or more questions of a group from {@link groupEitherOr} * @returns Card fields; each option carries its full question */ export declare function deriveEitherOrCard(qs: ExtractedQuestion[]): DerivedQuestionCard; /** * Which approval boundary a question touches (never auto-applied), if any. * * @param text - Question and fallback * @returns Sensitive kind, or undefined */ export declare function sensitiveKindOf(text: string): DecisionSensitiveKind | undefined; /** * Build the card for a question. * * @param q - The question (and its fallback sentence) * @returns Card fields * * @example * deriveQuestionCard({ type: 'question', text: '这个读法,你同意吗?', fallback: '不同意的话我就删掉,只留事实。' }) * // → options Yes / No ("删掉,只留事实"), default b (No), yesKey a */ export declare function deriveQuestionCard(q: ExtractedQuestion, ctx?: QuestionCardContext): DerivedQuestionCard; /** Message and ticket words that make a derived card readable on its own (#1242). */ export interface QuestionCardContext { /** The agent message the question came from */ content?: string; /** What the work is about (the owner's ask / ticket title), used when the message has no subject sentence */ title?: string; } /** * The sentence right before the question in the message: what "这样可以吗?" * is about. Skips questions, headings, quotes and sentences with nothing in them. * * @param content - The agent message * @param question - The question sentence * @returns The subject sentence, or '' */ export declare function questionSubject(content: string, question: string): string; /** * Whether a derived card's question is still too thin to post after * enrichment (nothing in it says what it is about). * * @param card - The enriched card * @returns True when no card should be posted */ export declare function isTooVague(card: DerivedQuestionCard): boolean; /** * Character-bigram overlap of two questions (Dice coefficient), ignoring * spaces and punctuation. Used to tell "the agent asked this through * ask-owner already". * * @param a - Question * @param b - Question * @returns 0..1 */ export declare function questionSimilarity(a: string, b: string): number; /** * Whether a question points back at earlier text ("这样安排行不行?", "Does * this plan work?") and so needs context to stand on its own. * * @param question - The question sentence * @returns True when it refers back */ export declare function refersBack(question: string): boolean; /** Input to {@link questionContextBlocks}. */ export interface QuestionContextInput { /** The agent message */ content: string; /** The question sentence lifted out of it */ question: string; /** The owner's original ask (the request's description, else its title) */ ownerAsk?: string; /** Display name of the asking agent */ agentName?: string; /** Link to the agent's full message in Slack, when it was posted there */ messageLink?: string; /** Whether {@link messageLink} points at the thread rather than the message itself */ linkIsThread?: boolean; /** Link to the ticket's old thread, when the card is posted somewhere else */ oldThreadLink?: string; } /** * The context every auto-extracted question card carries, as mrkdwn sections * (specs/2026-10-02-decision-card-thread-answers.md §5). A question lifted * out of a message must stand on its own, wherever the card lands: * * - `*About:*` — the owner's original ask, clipped; * - the agent's words before the question (its last paragraph(s), up to * ~500 characters), quoted; * - the fenced / quoted block (a draft) the question points at — "按这个草稿回?" * — up to ~1200 characters; * - a link to the agent's full message (or "in Crewly chat"), and to the old * ticket thread when the card is posted elsewhere. * * With nothing quotable, the card still says what it is about and how to * find out more: "Context: — reply in thread to ask for details". * * @param input - The agent message, the question, and where things are * @returns Sections (never empty) * * @example * questionContextBlocks({ content: '方案:Nova 负责 CE 的 codex 任务。\n\n这样安排行不行?', question: '这样安排行不行?', ownerAsk: '加一个 codex agent', agentName: 'Orc' }) * // → ['*About:* 加一个 codex agent', '_Orc wrote:_\n> 方案:Nova 负责 CE 的 codex 任务。', '_Full message in Crewly chat._'] */ export declare function questionContextBlocks(input: QuestionContextInput): string[]; //# sourceMappingURL=open-item-card.d.ts.map