/** * Conversation-first spawn gate, the decision half. * * Owner ruling: an inbound message, from ANY channel, gets a conversational * response. If it looks like it warrants a workstream, the agent PROPOSES one * and waits for agreement, it does not start one. Work that was already * agreed to (a schedule, a trigger, an on-exit chain, or a proposal the owner * just said yes to) was authorized when it was created and runs without * re-asking. * * goodvibes-tui is exempt: the operator is sitting in front of it and typed * the thing, so work starting is the expected outcome. TUI spawns never reach * this module, the gate is installed on the channel-surface adapter context * (see daemon/surface-actions.ts), which the TUI does not go through. * * This file is pure: no I/O, no clock beyond what callers inject. The pending * proposal state lives in work-proposal-store.ts; the per-surface confirmation * routing lives in daemon/work-proposal-reply.ts. */ /** * How the gate treats inbound channel messages. * * - 'propose' (default) Conversation is free; work is proposed and waits * for agreement over the channel it arrived on. * - 'confirm-all' Every inbound message that would start ANY agent run is * confirmed first, including ones that read as pure chat. * Maximum caution for a noisy or shared channel. * - 'off' Legacy behavior: an inbound message starts work immediately. */ export type ConversationGateMode = 'propose' | 'confirm-all' | 'off'; export declare function isConversationGateMode(value: unknown): value is ConversationGateMode; /** * Channel surfaces the gate applies to by default, every conversational * ingress surface the platform ships. 'webhook' is intentionally absent: a * generic webhook is machine-to-machine automation that was authorized when * the webhook was registered, so it is pre-authorized work by construction. */ export declare const CONVERSATION_GATE_DEFAULT_SURFACES: readonly string[]; export interface ConversationGateConfig { readonly mode: ConversationGateMode; /** How long an unanswered proposal stays answerable. */ readonly proposalTtlMs: number; /** Hard cap on simultaneously pending proposals across all surfaces. */ readonly maxPendingProposals: number; /** Surfaces the gate applies to. Anything not listed spawns as before. */ readonly gatedSurfaces: readonly string[]; } export declare const CONVERSATION_GATE_DEFAULTS: ConversationGateConfig; export interface ConversationGateConfigReader { get(key: string): unknown; /** * `gatedSurfaces` is an array, so it is not a scalar ConfigKey, it is read * through the category, mirroring how wrfc.gates is read. * * `string`, NOT the literal `'conversationGate'`. `ConfigManager.getCategory` * is generic over `keyof GoodVibesConfig`, and `conversationGate` joins that * union through a module augmentation declared in * config/schema-domain-conversation-gate.ts. Inside this package that * augmentation is always loaded, so the literal appeared to work, but a * CONSUMER's program only loads the declaration files its own imports reach, * and a consumer importing `ConfigManager` and `SharedSessionBroker` does not * necessarily pull that schema domain in. There, `keyof GoodVibesConfig` has * no `conversationGate` member and a plain ConfigManager is rejected by the * very interface that exists to accept it: * * Type '"conversationGate"' is not assignable to type 'keyof GoodVibesConfig' * * which is what stopped both consumers from passing `conversationGateConfig` * at all. Typing the parameter as `string` makes the contract depend on no * augmentation. test/types/conversation-gate-config-reader.ts pins it from a * consumer's vantage point, resolving through the package name. */ getCategory?(name: string): unknown; } /** * Read the gate's configuration. Every value is bounded here rather than at * the use site, so a hand-edited config cannot produce a gate that never * expires a proposal or accepts an unbounded number of them. */ export declare function readConversationGateConfig(reader: ConversationGateConfigReader): ConversationGateConfig; export declare function isGatedSurface(config: ConversationGateConfig, surfaceKind: string | undefined): boolean; export type InboundIntent = { readonly kind: 'conversation'; readonly reason: string; } | { readonly kind: 'work'; readonly reason: string; readonly summary: string; }; /** One short line naming what the proposed work is, for the lock screen. */ export declare function summarizeWorkRequest(text: string, maxLength?: number): string; /** * Decide whether an inbound message is conversation or a work request. * * Conversation is the default and the burden of proof is on "work": a message * only classifies as work when a base-form work verb sits in an imperative or * request position, or when it both references real code and uses a work verb * somewhere. Everything else, greetings, single words, questions, status * checks, opinions, is conversation and gets a conversational reply. */ export declare function classifyInboundIntent(rawText: string | undefined): InboundIntent; export type WorkProposalReply = { readonly decision: 'affirmative'; readonly note?: string | undefined; } | { readonly decision: 'negative'; readonly note?: string | undefined; }; /** * Parse a channel reply as agreement or refusal for a pending proposal. * * Forgiving of natural phrasing, the owner should be able to type "yeah go * for it" from a phone, not a magic token, but the reply must be ONLY an * answer. A message that carries its own request is that request, never a * "yes" to something proposed earlier, no matter how politely it opens. * Anything unrecognized returns null and flows through as a normal message, * so a pending proposal cannot swallow unrelated conversation. */ export declare function parseWorkProposalReply(rawText: string | undefined): WorkProposalReply | null; /** * The proposal message. One short line per real thing, readable on a phone * lock screen without expanding the notification. */ export declare function renderWorkProposalMessage(input: { readonly summary: string; readonly expiresInMs: number; }): string; export declare function renderProposalDeclinedMessage(summary: string): string; export declare function renderProposalExpiredMessage(summary: string): string; //# sourceMappingURL=conversation-gate.d.ts.map