/** * Decision cards (specs/2026-10-01-decision-cards.md). * * The responsible agent asks the owner ONE structured question; its own * Slack bot posts a Block Kit card in the team channel (the ticket's thread, * or a new thread), and the owner answers with a button, a reaction, a * thread reply or the dashboard. The answer updates the card in place, goes * into the ticket log, is delivered to the asking agent and clears the * owner-message watchdog for that thread. At the deadline the default is * applied — except for sensitive asks, which are re-asked once and parked. * * Every collaborator is injected ({@link DecisionServiceDeps}); the wiring * with the real Slack / tickets / agents lives in `decision.wiring.ts`. * * @module services/decisions/decision.service */ import { type ComponentLogger } from '../core/logger.service.js'; import type { SlackBlock, SlackIncomingMessage, SlackOutgoingMessage } from '../../types/slack.types.js'; import type { AskOwnerInput, DecisionKind, DecisionLanguage, DecisionOption, DecisionSensitiveKind, DecisionSource, DecisionSystemRef, OwnerDecision } from '../../types/decision.types.js'; import { formatWhen } from './decision-card.js'; import { DecisionStore } from './decision-store.js'; import type { TicketThreadStore } from './ticket-thread-store.js'; import type { ActivityEntryInput } from '../activity/activity-log.service.js'; /** Thrown for requests the caller must fix (HTTP 4xx). */ export declare class DecisionError extends Error { readonly status: number; constructor(status: number, message: string); } /** The slice of SlackService used here. */ export interface DecisionSlackApi { isConnected(): boolean; sendMessage(message: SlackOutgoingMessage): Promise; updateMessage(channelId: string, messageTs: string, text: string, blocks?: SlackBlock[], botToken?: string): Promise; } /** How an agent posts: its own bot token, else the shared bot with its name/icon. */ export interface DecisionPostIdentity { botToken?: string; username?: string; iconEmoji?: string; iconUrl?: string; } /** A ticket the question is about, resolved by the wiring. */ export interface DecisionTicketContext { projectId: string; projectPath: string; projectName?: string; id: string; title: string; /** Session that owns the question: the assignee, else the team's lead */ asker: string; /** Team whose channel carries the card */ teamId?: string; } /** A Slack place to post into. */ export interface DecisionSlackPlace { slackChannelId: string; threadTs?: string; teamId?: string; } /** Collaborators. */ export interface DecisionServiceDeps { store: DecisionStore; threads: TicketThreadStore; slack: () => DecisionSlackApi | null; /** This instance (button values carry it; Cloud routes clicks by it) */ instanceId: () => string; /** Whether a Slack user may answer (the owner) */ isOwner: (slackUserId: string) => boolean; /** The owner's Slack user id, for mentions in reminders */ ownerUserId?: () => string | null; /** Display name of a Slack user ("Steve") */ userName?: (slackUserId: string) => Promise; /** How an agent posts */ identityOf: (session: string) => Promise; /** Slack channel of a team */ teamChannelOf: (teamId: string) => Promise; /** The team an agent belongs to */ teamOf: (session: string) => Promise; /** Resolve a ticket ask (access-checked); throws DecisionError */ resolveTicket: (project: string, ticketId: string, callerSession: string | undefined) => Promise; /** Mark the ticket as waiting on the owner (label + log line) */ markTicketAsked: (ctx: DecisionTicketContext, question: string, decisionId: string) => Promise; /** Append a ticket log line; `clearNeedsOwner` removes the needs-owner label */ logTicket: (ticket: NonNullable, line: string, clearNeedsOwner: boolean) => Promise; /** The Slack destination of the agent's current work, when it is a Slack place */ workDestination?: (session: string) => Promise; /** The work item the agent is on */ currentWorkItemId?: (session: string) => Promise; /** Deliver a message to an agent (wakes a stopped one); false when it could not */ deliverToAgent: (session: string, text: string) => Promise; /** Append to the activity log (crewly#881): who asked / answered what, and when. Absent = not logged. */ recordActivity?: (entry: ActivityEntryInput) => void; /** * Hold a message the agent could not take now on its persistent queue * (delivered when it is next idle or registers; a drop is reported). * False when it could not be queued. crewly#1015 §9. */ queueForAgent?: (session: string, text: string) => boolean; /** Close the owner-message watchdog entries this agent owes in the thread */ closeWatchdog?: (session: string, slackChannelId: string, threadTs: string) => void; /** The owner's DM with the bot of `identity` (system decisions); null when there is none */ ownerDmOf?: (identity: DecisionPostIdentity) => Promise; /** * When the agent asked the question a `reply_question` card tracks (its * open item's `createdAt`). Tells a backfilled legacy card (no `source`) * from a live one in {@link DecisionService.skipAll}. */ openItemAskedAt?: (ref: NonNullable) => Promise; /** * Why what the card tracks is already closed (its open item / Request / * ticket), or null while it still needs an answer. Checked right before * anything is posted to the owner: a moot card is withdrawn silently * (specs/2026-10-02-decision-card-thread-answers.md §3). */ trackedClosed?: (d: OwnerDecision) => Promise; /** * Why an ask-owner card is about work a ticket put on hold (the same PR / * branch / worktree), or null when it may go up. A refusal quotes the hold; * a card that only asks whether to close the work passes (crewly#1369). */ holdRefusal?: (input: { text: string; context?: string; ticketId?: string; }) => Promise; /** The asking agent's display name ("Owen"), for "so Owen will go with …" */ displayName?: (session: string) => Promise; /** * The owner Slack thread the asker owes right now (it promised a follow-up * there, or the owner's message there is unanswered). A card whose work * destination is only the team channel goes into that thread instead of * top-level (2026-10-08: D-476 / D-477 landed top-level in * #pro-think-tank while the owner waited in a thread). */ ownerThreadOf?: (session: string) => Promise; /** * A card went up (`posted`) or its decision reached an end state * (`settled`: resolved, defaulted, parked, expired, cancelled). The * owner-thread sentinel says so in the thread that waits on the asker. * Must not throw. */ onCardEvent?: (decision: OwnerDecision, what: 'posted' | 'settled') => void; /** * An answered decision: hand an approved change to a recurring job run by * someone else to that agent (crewly#1137). Returns a line for the asker, or * null when nothing was handed off. Must not throw. */ handOffChange?: (decision: OwnerDecision) => Promise; /** * The language the owner writes in at a Slack place (their own messages * there, else their recent messages anywhere); null when unknown. Cards * from agents follow it (crewly#1367). */ ownerLanguage?: (place: { slackChannelId?: string; threadTs?: string; } | null) => Promise; now?: () => Date; logger?: ComponentLogger; } /** A decision the harness asks itself ({@link DecisionService.askSystem}). */ export interface SystemAskInput { /** Kind whose registered handler acts on the answer */ kind: DecisionKind; system: DecisionSystemRef; /** Card header */ title: string; /** One line */ question: string; /** Extra mrkdwn sections under the question */ body?: string[]; /** `["Label", "Label — detail"]` or `[{label, detail?}]` */ options: unknown[]; /** Option label */ default: string; deadline: Date; sensitive?: DecisionSensitiveKind; } /** What a Slack interaction did. */ export interface InteractionOutcome { handled: boolean; reason: string; decision?: OwnerDecision; } /** A Slack `block_actions` payload (the fields used). */ export interface BlockActionsPayload { type?: string; user?: { id?: string; name?: string; username?: string; }; actions?: Array<{ action_id?: string; value?: string; action_ts?: string; }>; container?: { channel_id?: string; message_ts?: string; thread_ts?: string; }; channel?: { id?: string; }; message?: { ts?: string; thread_ts?: string; }; } /** A `reaction_added` event (the fields used). */ export interface ReactionEvent { user?: string; reaction?: string; item?: { type?: string; channel?: string; ts?: string; }; } /** * Acts on the outcome of a decision Crewly asked itself ({@link DecisionKind}). */ export interface DecisionKindHandler { /** * Called once when such a decision settles (resolved, defaulted at the * deadline, parked, cancelled or expired). Does the kind's work (e.g. lets a held * browser click through) and returns the note for the asking agent, which * replaces the generic `[DECISION]` note; null sends nothing. * * @param decision - The settled decision * @param fallback - The generic note the asker would get without a handler (null for none) * @returns Note for the asker, or null */ onSettled(decision: OwnerDecision, fallback?: string | null): Promise; } /** A question Crewly builds itself (no ask-owner contract parsing). */ export interface PrebuiltAsk { kind: DecisionKind; /** Agent the question is about — its bot posts the card and it gets the answer */ asker: string; question: string; options: DecisionOption[]; defaultKey: string; yesKey?: string; deadline: Date; sensitive?: DecisionSensitiveKind; browser?: OwnerDecision['browser']; gmail?: OwnerDecision['gmail']; /** What the owner would grant (kind `app_collaborator`) */ appCollaborator?: OwnerDecision['appCollaborator']; /** The template the owner would list (kind `app_template`) */ appTemplate?: OwnerDecision['appTemplate']; /** The tier changes the owner would apply (kind `model_tier_change`) */ tierChange?: OwnerDecision['tierChange']; /** Card header (default "Decision D-n") */ title?: string; /** Extra mrkdwn sections under the question (e.g. the quoted context a question points back at) */ body?: string[]; /** Post the card here (a thread) instead of the agent's work destination */ place?: OwnerDecision['place']; /** The Request open item it tracks */ requestRef?: OwnerDecision['requestRef']; /** `backfill` when the open-items backfill carded an old reply */ source?: DecisionSource; /** When the agent asked it (ISO) */ askedAt?: string; } /** Where a skipped question must not be asked again ({@link DecisionService.findSkipped}). */ export interface SkipScope { /** The Request the question was about */ requestId?: string; /** The project ticket the question was about */ ticket?: { projectPath: string; id: string; }; /** The asking agent (used only when there is no request / ticket) */ asker?: string; } /** Filters of {@link DecisionService.skipAll}. */ export interface SkipAllInput { /** Only decisions created before this moment */ olderThan?: Date; /** `backfill` = only cards the open-items backfill created; `all` (default) = every open card */ source?: 'backfill' | 'all'; /** Report what would be skipped without changing anything */ dryRun?: boolean; } /** One row of {@link SkipAllResult}. */ export interface SkipAllRow { id: string; question: string; asker: string; createdAt: string; ticket?: string; source: DecisionSource; /** What skipping does to it: a real skip, or (sensitive / system / browser) its safe "no" option */ outcome: 'skipped' | 'declined'; } /** Result of {@link DecisionService.skipAll}. */ export interface SkipAllResult { dryRun: boolean; /** How many open decisions match */ matched: number; /** Ids actually settled (empty on a dry run) */ settled: string[]; rows: SkipAllRow[]; } /** * The scope keys of a decision for the skipped-question dedupe: its Request * and its ticket; the asking agent only when it has neither. * * @param s - Scope * @returns Keys */ export declare function skipScopeKeys(s: SkipScope): string[]; /** * The Slack thread a decision's card is in (or will go to): `thread::`. * * @param d - Decision * @returns Key, or null when it has no thread yet */ export declare function threadKeyOf(d: Pick): string | null; /** * The topic keys of a decision: its Request, its ticket and its thread. Two * decisions from the same asker that share one are about the same thing. * * @param d - Decision (or an ask about to be stored) * @returns Keys */ export declare function topicKeys(d: Pick): string[]; /** The fields of an inbound Slack message a thread reply needs. */ export type ThreadReplyMessage = Pick & Partial>; /** * Decision cards service. */ export declare class DecisionService { private static instance; private readonly deps; private readonly logger; /** * Thread-reply runs by Slack message (`channel:ts`), so the decision * listener and the room router share one run per message * (specs/2026-10-03-one-responder-per-message.md §3). */ private readonly threadReplyRuns; private readonly now; private timer; private ticking; /** * When this process started ticking (epoch ms; set by the first tick). A * `wait` card whose asker was told about the deadline before that is never * reminded: after an upgrade, old cards must not all ping the owner at once. */ private startedAt; /** Last time open cards were checked for what they track having closed (epoch ms) */ private lastMootCheck; /** * @param deps - Collaborators */ constructor(deps: DecisionServiceDeps); /** @returns The process-wide instance, or null before wiring */ static getInstance(): DecisionService | null; /** @param service - Instance to install (null clears) */ static setInstance(service: DecisionService | null): void; /** * Register the handler of a decision kind. * * @param kind - Decision kind * @param handler - Handler (null removes it) */ static registerKindHandler(kind: DecisionKind, handler: DecisionKindHandler | null): void; /** Start the deadline / reminder tick. */ start(): void; /** * Redraw open cards drawn with an older layout revision, so cards posted * before a layout change (e.g. the Skip button) get the new controls. * Paced to stay under Slack's chat.update rate limit; never throws. * * @param gapMs - Pause between two redraws (tests pass 0) * @returns How many cards were redrawn */ refreshStaleCards(gapMs?: number): Promise; /** Stop the tick. */ stop(): void; /** * Ask the owner. Validates the contract, stores the decision and posts the * card as the responsible agent's own bot. * * @param callerSession - Agent calling ask-owner (undefined = owner / dashboard) * @param input - The ask * @returns The stored decision (with `card`, or `postError` when Slack refused; retried on the tick) * @throws DecisionError(400) for a contract violation, other 4xx from ticket resolution */ ask(callerSession: string | undefined, input: AskOwnerInput): Promise; /** * Ask the owner on the harness's own behalf (no agent): the card goes to * the owner's DM with this machine's orc bot, and the answer goes to the * kind's handler only — no agent is told or woken. * * @param input - The ask * @returns The stored decision (with `card`, or `postError`; retried on the tick) * @throws DecisionError(400) for bad options / default / deadline */ askSystem(input: SystemAskInput): Promise; /** * Ask a question Crewly built itself (e.g. a held browser action). The card * goes where an ask-owner card without a ticket goes: the thread of the * agent's current work item, else its team channel — from its own bot. * * @param ask - The prebuilt question * @returns The stored decision (with `card`, or `postError`; retried on the tick) */ askPrebuilt(ask: PrebuiltAsk): Promise; /** * Post a line in a decision's card thread (as the asker's bot). * * @param id - Decision id * @param text - mrkdwn text * @returns True when there was a card to reply to */ replyInThread(id: string, text: string): Promise; /** * Mark an open decision as expired: what it asked about is gone. The card * says so, and the kind's handler tells the agent. * * @param id - Decision id * @returns The expired decision, or null when it was not pending */ /** * Append a line to the activity log (crewly#881). Never throws: the log * never breaks the decision it describes. * * @param d - The decision as stored after the step * @param action - `decision.asked` / `.resolved` / `.skipped` / `.defaulted` / `.expired` * @param actor - Who did it * @param note - The answer in words, when there is one */ private audit; expire(id: string): Promise; /** * Post (or re-try posting) the card of an open decision. * * @param decision - Decision without a card * @returns The decision with `card` or `postError` */ postCard(decision: OwnerDecision): Promise; /** * The deadline a card carries once it is actually posted. A card whose * earlier posts failed gets its original window again from now, so the * default is never applied to a question the owner has not seen in time * (D-665 could only be posted a day after its deadline). * * @param decision - Decision about to be posted * @returns ISO deadline to store and render */ private deadlineWhenPosted; /** * Where the card goes — always the conversation the question belongs to: * * 1. the thread the question was asked in (`place`: reply questions); * 2. the owner thread the asker owes right now (the owner is waiting there); * 3. the ticket's thread, when it has one; * 4. the thread of the asker's current work; * 5. a new ticket thread (ticket asks); * 6. top-level in the team channel, only when there is no thread at all; * 7. the owner's DM, when the asker has no team channel (the orc). * * The owner's conversation used to lose to the ticket thread: a ticket ask * landed in an old ticket thread while the owner waited elsewhere * (2026-10-08 card audit). */ private placeFor; /** * The existing thread a card of `asker` goes to (steps 2–4 of * {@link placeFor}), without creating anything. * * @param asker - Asking agent * @param ticket - The ticket asked about, if any * @returns The thread, or null when the card would start one / go top-level */ private threadPlace; /** * The owner's language where a new card of `asker` would go (its thread, * else the owner's recent messages). Null when unknown or not wired. * * @param asker - Asking agent * @param ticket - The ticket asked about, if any * @returns `zh` / `en`, or null */ private ownerLanguageFor; /** * The thread key a new card of `asker` would land in, for the duplicate check. * * @param asker - Asking agent * @param ticket - The ticket asked about, if any * @returns Key, or null */ private previewThreadKey; /** * An open card of `asker` that shares a topic key and asks a similar * question ({@link DECISION_CONSTANTS.SAME_OPEN_QUESTION_SIMILARITY}). * * @param asker - Asking agent * @param question - The new question * @param keys - Topic keys of the new ask ({@link topicKeys}) * @param eligible - Which open cards may stand in for it * @returns The open card, or null */ private findOpenDuplicate; /** * The owner settled `d`: close every other open card of the same asker * that the same answer settles — * * - a similar question in the same thread / ticket / request * ("✓ Closed — answered in D-7"); * - when the owner answered in words in the thread: the asker's older * question cards in that thread, which his message moved past * ("✓ Closed — answered by your reply to D-7"). * * Sensitive, system, browser and app cards are never closed this way. * * @param d - The settled decision * @returns Ids closed */ private closeSiblings; /** * Send as the agent's own bot; when Slack refuses that bot (not in the * channel, revoked), fall back to the shared bot with the agent's name. */ private send; /** * A Slack `block_actions` payload (Cloud relay, Socket Mode or the HTTP * endpoint). Ignores anything that is not one of this instance's cards. * * @param payload - Slack payload * @returns What happened */ handleInteraction(payload: BlockActionsPayload): Promise; /** * Redraw the card of a click that was not applied (its current state, by * channel + ts), then pass the outcome on. * * @param out - The rejected outcome * @returns The same outcome */ private redrawRejected; /** * A reaction on a card: ✅ default/first, ❌ the "no" option, ⏰ remind, * 🚫 / ⏭️ skip. * * @param event - `reaction_added` event * @returns What happened */ handleReaction(event: ReactionEvent): Promise; /** * An owner reply in a card's thread (channel or DM thread), posted after * the card. Text: the newest open card takes it; words that match no * option are passed to the asker verbatim. A voice note, audio or other * file with no text answers every open card in the thread "in thread" * (specs/2026-10-02-decision-card-thread-answers.md §1). Every such owner * message stamps `ownerRepliedAt` on the open cards there. * * One run per Slack message: a second call with the same message returns * the first run's outcome (the room router asks too — §3 of * specs/2026-10-03-one-responder-per-message.md). * * @param message - Inbound Slack message * @returns What happened */ handleThreadReply(message: ThreadReplyMessage): Promise; /** * The outcome of a thread reply for the room router: the run the decision * listener already started for this Slack message (the router's copy may * carry file references the bridge appended to its text), else a run of * its own. Shared, so a card is never answered twice * (specs/2026-10-03-one-responder-per-message.md §3). * * @param message - Inbound Slack message * @returns What happened */ threadReplyOutcome(message: ThreadReplyMessage): Promise; /** {@link handleThreadReply} without the shared-run memo. */ private answerThreadReply; /** * A voice note / file with no text in a thread with open cards * (specs/2026-10-02-decision-card-thread-answers.md §1): * * - sensitive, system and browser cards are never settled by a file; their * askers get the file as information; * - a voice note / audio / video answers the newest other card; any file * answers the only other card when there is exactly one; * - an image or other file with several open cards settles nothing. * * Every asker whose card stays open is told what the owner posted, once. */ private applyThreadFiles; /** * The owner answered from the dashboard. * * @param id - Decision id * @param optionKey - Option key (or label / number) * @returns The resolved decision * @throws DecisionError(404/409/400) */ chooseFromDashboard(id: string, optionKey: string, via?: 'dashboard' | 'voice'): Promise; /** * The owner answered a card in their own words away from Slack (Drive mode, * specs/2026-10-08-drive-mode.md). Read exactly like a reply in the card's * thread: an option it names, a yes / no / skip / remind word, or else the * words themselves as the answer — the asker gets the full text either way. * * @param id - Decision id * @param text - What the owner said * @param via - How it arrived * @returns The settled (or snoozed) decision * @throws DecisionError(404/409/400) — 400 when the words settle nothing (e.g. a held browser action needs a yes or no) */ answerInWords(id: string, text: string, via?: 'voice' | 'dashboard'): Promise; /** * "Remind me tomorrow" from the dashboard. * * @param id - Decision id * @returns The snoozed decision */ remindFromDashboard(id: string): Promise; /** * "Skip" from the dashboard (sensitive / system cards: their safe "no"). * * @param id - Decision id * @returns The settled decision */ skipFromDashboard(id: string): Promise; /** * Skip every matching open decision at once ("clear the stale cards"): * each card is updated, each linked open item closed, and each asking * agent gets ONE note listing what it should drop. Sensitive / system / * browser cards get their safe "no" instead ({@link skipChoice}). * * @param input - Filters and dry-run * @returns What matched and what was settled */ skipAll(input?: SkipAllInput): Promise; /** * A question the owner skipped in the same scope within * {@link DECISION_CONSTANTS.SKIP_DEDUPE_MS}: the agent must not ask it again. * * @param scope - Request / ticket (or the asker when it has neither) * @param question - The question about to be asked * @returns The skipped decision, or null */ findSkipped(scope: SkipScope, question: string): Promise; /** The refusal for a re-ask of a skipped question. */ private alreadySkippedError; /** * Whether the open-items backfill created this card: `source` when set; * for older cards, a reply-question card made long after the agent asked. * * @param d - Decision * @returns True for a backfilled card */ isBackfilled(d: OwnerDecision): Promise; /** * Whether a card was made long enough before another to be a different ask * (cards of one reply are made together and stay side by side). * * @param older - The earlier card * @param newer - The card just made * @returns True when the earlier one is older than the same-reply window */ private olderThanSameReply; /** * Withdraw open decisions (the asker no longer needs an answer, or the * ticket's mark was cleared). * * @param filter - Which decisions * @param note - Why (ticket log) * @returns How many were withdrawn */ cancelWhere(filter: (d: OwnerDecision) => boolean, note?: string): Promise; /** * Withdraw the decisions of a team that moved to another machine, without * touching their Slack cards (the target keeps the live copy; team move, * crewly#1189). * * @param ids - Decision ids * @param reopen - True to bring them back (rollback) * @returns How many changed */ setMovedStatus(ids: readonly string[], reopen?: boolean): Promise; /** * Take over open decisions from a team that moved here (crewly#1189). Each * gets a new id; the Slack card reference is kept, so a tap on the existing * card resolves the copy. A decision whose card is already known is skipped. * * @param incoming - Decisions as the source had them * @returns How many were added */ importMoved(incoming: ReadonlyArray>): Promise; /** * List decisions. * * @param which - `open` (open + parked) or `all` * @returns Decisions, newest first */ list(which?: 'open' | 'all'): Promise; /** * Every decision created at or after `sinceMs`, plus every one still * waiting (any age). Not capped like {@link list}: for counting * (Settings › Security). Settled decisions are kept 30 days. * * @param sinceMs - Lower bound (epoch ms) * @returns Decisions, newest first */ listSince(sinceMs: number): Promise; /** * One decision. * * @param id - Decision id * @returns Decision or null */ get(id: string): Promise; private requirePending; /** * Apply a choice: resolve (or snooze), update the card, log the ticket, * tell the asker, close the watchdog entry. */ private apply; /** * The owner skipped a card: settle it as `skipped`, show who skipped it, * close what it tracks, and tell the asker once to drop it. */ private applySkip; /** The note the asker receives when the owner skipped its question. */ private skipNote; /** The note the asker receives when the owner answered. */ private answerNote; /** * Where the asker's follow-up belongs: a command naming the decision — the * harness finds the card's thread (specs/2026-10-02-harness-owned-routing.md §4). */ private whereLine; /** * One pass: retry unposted cards, post due reminders, apply defaults at * the deadline, re-ask / park sensitive asks, prune old decisions. * * @returns Ids acted on */ tick(): Promise; /** * Withdraw a card whose tracked item is already closed, silently: the * card says why, nothing is posted in the thread and the asker is not woken * (specs/2026-10-02-decision-card-thread-answers.md §3). * * @param d - Open decision about to post something to the owner * @returns True when it was withdrawn */ private withdrawIfMoot; /** * Redraw cards Slack refused to update (a closed decision must not keep * live buttons), at most every {@link DECISION_CONSTANTS.CARD_SYNC_RETRY_MS}. * * @param now - Clock * @returns Ids redrawn */ private retryCardSync; /** * Withdraw open cards whose tracked item / ticket has closed meanwhile (the * owner answered in words, the ticket finished, the item expired) — the * card closes in Slack instead of dangling. Silent: nothing is posted to the * owner and the asker is not woken. At most every {@link DECISION_CONSTANTS.MOOT_CHECK_MS}. * * @param now - Clock * @returns Ids withdrawn */ private withdrawMootCards; /** Post the "Remind me tomorrow" reminder in the card's thread. */ private remindNow; /** * A `wait` card past its deadline (specs/2026-10-02-decision-card-thread-answers.md §2): * first only the asker is told — nothing goes to the owner; later, once, a * reminder that says what to do, when the owner has not touched the thread. */ private waitStep; /** * Post the due `wait` reminders: ONE per thread, listing that thread's open * questions; every listed card is marked as reminded. * * @param due - Cards due for their reminder (already checked) * @param now - Clock * @returns Ids reminded */ private postWaitReminders; /** Non-sensitive deadline with a real default: apply it and say who does what. */ private applyDefault; /** Sensitive deadline: re-ask once after 24 h, then park. Never auto-applied. */ private sensitiveStep; /** Re-render the card for the decision's current state (with the posting bot's token). */ private refreshCard; /** {@link refreshCard} without the retry bookkeeping. */ private drawCard; /** The asking agent's display name ("Owen"), when known. */ private askerName; /** Post a line in the card's thread, as the asker. */ private postInThread; private logTicket; /** * Tell the asker how a decision settled: through its kind's handler when * it has one (which also acts on the answer), else with `fallback`. */ private notifyAsker; /** * Tell the asker. `owner`: the note carries the owner's answer — if the * asker is busy it waits at the front of its queue, not behind reminders * (2026-10-05, D-270 sat 7th for ~18 min); it is dropped there only as an * exact duplicate (same decision id). */ private tellAsker; /** * An answer the asking agent could not take now is kept on its queue, not * only logged: the owner answered, and the agent must hear it when it is * back (2026-10-01, D-43: "Could not deliver the decision to the asking * agent", never retried — crewly#1015 §9). * * @param session - The asking agent * @param text - What it must be told * @param ctx - For the log */ private holdForAgent; /** Tell the owner-thread sentinel about a card change (best-effort). */ private emitCardEvent; private closeWatchdog; } /** Re-exported for the deadline line in the dashboard/API. */ export { formatWhen }; //# sourceMappingURL=decision.service.d.ts.map