/** * Session handoff computation and persistence. * * Creates structured handoff data when a session ends, containing: * - Last focused task * - Tasks completed and created * - Decisions recorded * - Next suggested tasks * - Open blockers and bugs * - Human override notes * * @task T4915 * @epic T4914 */ import type { KnowledgeCoverage, SessionHandoffShowParams } from '@cleocode/contracts'; /** * Handoff data schema - structured state for session transition. */ export interface HandoffData { /** Assessment captured with this handoff; later corrections are presented separately. */ knowledgeCoverage?: KnowledgeCoverage; /** Last task being worked on */ lastTask: string | null; /** Tasks completed in session */ tasksCompleted: string[]; /** Tasks created in session */ tasksCreated: string[]; /** Count of decisions recorded */ decisionsRecorded: number; /** Top-3 from tasks.next */ nextSuggested: string[]; /** Tasks with blockers */ openBlockers: string[]; /** Open bugs */ openBugs: string[]; /** * Human override note. **Always present** — `null` when none was recorded. * * GH #1277: this was `note?: string`, assigned only when a note existed, so * a handoff that recorded no note omitted the key and read as a complete * handoff. Absence and emptiness must not be the same byte sequence. */ note: string | null; /** * Length of {@link note} in characters, `0` when none was recorded. * * Lets a consumer distinguish "no note" from "note withheld by a projection" * without carrying the note itself. */ noteChars: number; /** * Human override next action. **Always present** — `null` when none was set. * * GH #1277: carried the identical defect to {@link note} — assigned only * under `if (options.nextAction)`, so both "none set" and "set to an empty * string" collapsed into an absent key. Fixed in the same change, because a * `requireKeys` rule that guards `note` while its twin sits beside it * knowingly under-covers the object it names. */ nextAction: string | null; /** * Length of {@link nextAction} in characters, `0` when none was set. * * Same purpose as {@link noteChars}: separates "not set" from "withheld by a * projection" without carrying the value. */ nextActionChars: number; } /** * Options for computing handoff data. */ export interface ComputeHandoffOptions { sessionId: string; /** Optional human note override */ note?: string; /** Optional human next action override */ nextAction?: string; } /** * Compute handoff data for a session. * Gathers all session statistics and auto-computes structured state. */ export declare function computeHandoff(projectRoot: string, options: ComputeHandoffOptions): Promise; /** * Persist handoff data to a session (write-once, append-only). * * Writes the handoff JSON into `session_handoff_entries` via an INSERT. * The table enforces write-once semantics at the SQL level: * - `insertHandoffEntry` rejects a second row for a session (T13777). * - A BEFORE UPDATE trigger makes rows physically immutable. * - An AFTER INSERT trigger mirrors the value to `sessions.handoff_json` * so all existing read paths continue to work without modification. * * Throws `CleoError(SESSION_NOT_FOUND)` when the session does not exist. * Throws `CleoError(HANDOFF_ALREADY_PERSISTED)` when a handoff was already * written for this session. * * @task T1609 */ export declare function persistHandoff(projectRoot: string, sessionId: string, handoff: HandoffData): Promise; /** * Get handoff data for a session. */ export declare function getHandoff(projectRoot: string, sessionId: string): Promise; /** * Get handoff data for the most recent ended session. * Filters by scope if provided. */ export declare function getLastHandoff(projectRoot: string, scope?: { type: string; epicId?: string; rootTaskId?: string; }): Promise<{ sessionId: string; handoff: HandoffData; } | null>; /** * Git state snapshot captured at session end. */ export interface GitState { branch: string; commitCount: number; lastCommitHash: string | null; uncommittedChanges: boolean; } /** * Decision summary for debrief output. */ export interface DebriefDecision { id: string; decision: string; rationale: string; taskId: string; } /** * Rich debrief data — superset of HandoffData. * Captures comprehensive session state for cross-conversation continuity. * * @epic T4959 */ export interface DebriefData { /** Standard handoff data (backward compat). */ handoff: HandoffData; /** Session that produced this debrief. */ sessionId: string; /** Agent/conversation identifier (if known). */ agentIdentifier: string | null; /** Session start time. */ startedAt: string; /** Session end time. */ endedAt: string; /** Duration in minutes. */ durationMinutes: number; /** Decisions made during the session. */ decisions: DebriefDecision[]; /** Git state at session end (best-effort). */ gitState: GitState | null; /** Position in the session chain (1-based). */ chainPosition: number; /** Total length of the session chain. */ chainLength: number; } /** * Options for computing debrief data. */ export interface ComputeDebriefOptions extends ComputeHandoffOptions { /** Agent/conversation identifier. */ agentIdentifier?: string | null; /** Session start time. */ startedAt?: string; /** Session end time. */ endedAt?: string; } /** * Compute rich debrief data for a session. * Builds on computeHandoff() and adds decisions, git state, chain position. * * @epic T4959 */ export declare function computeDebrief(projectRoot: string, options: ComputeDebriefOptions): Promise; /** * Normalized Core entry point for session.handoff.show dispatch op. * Converts the wire-format scope string to the internal scope object, * then delegates to getLastHandoff. * @task T1450 */ export declare function sessionHandoffShow(projectRoot: string, params: SessionHandoffShowParams): Promise<{ sessionId: string; handoff: HandoffData; } | null>; //# sourceMappingURL=handoff.d.ts.map