@@ -940,3 +940,171 @@ pub trait CommandSkillGroupContext {
940940 /// `/restore` trust gate posture (yolo / trust_mode).
941941 fn approval_state ( & self ) -> CommandApprovalState ;
942942}
943+
944+ // ---------------------------------------------------------------------------
945+ // Session lifecycle capability (FEAT-023).
946+ //
947+ // One contract-owned facet for the seven host-dependent lifecycle commands;
948+ // `/compact` and `/purge` stay pure. The shared `CommandSessionContext` above
949+ // stays unchanged: it
950+ // serves commands outside this slice and must not gain persistence,
951+ // navigation, picker, or lifecycle mutation authority (D2). No concrete App,
952+ // SessionManager, session-journal, picker, configuration, or view-stack type
953+ // crosses this boundary; successful results are structured portable fields so
954+ // the handlers retain exact message composition (D2/D5).
955+ // ---------------------------------------------------------------------------
956+
957+ /// Portable synchronization fields a lifecycle handler maps into the
958+ /// temporary `SyncSession` action payload. The conversation and prompt types
959+ /// are `codewhale-core` request types shared by the contract and the TUI
960+ /// (FEAT-037 will move shared outcome ownership; FEAT-023 keeps the bounded
961+ /// reference only for `/fork` and `/new` transitions, D6).
962+ #[ derive( Clone , Debug , PartialEq ) ]
963+ pub struct SessionSyncPayload {
964+ pub session_id : Option < String > ,
965+ pub messages : Vec < Message > ,
966+ pub system_prompt : Option < SystemPrompt > ,
967+ pub model : String ,
968+ pub workspace : PathBuf ,
969+ pub mode : CommandMode ,
970+ }
971+
972+ /// `/branch` success projection (`session/branch.rs`). The handler composes
973+ /// the exact success line from these deterministic fields.
974+ #[ derive( Clone , Debug , PartialEq ) ]
975+ pub struct SessionBranchOutcome {
976+ pub leaf_display : String ,
977+ pub journal_entries_before : usize ,
978+ }
979+
980+ /// `/fork` success projection for an active-conversation fork. The handler
981+ /// composes `Forked session {parent} -> {fork}` from these required fields.
982+ #[ derive( Clone , Debug , PartialEq ) ]
983+ pub struct SessionForkReceipt {
984+ pub parent_label : String ,
985+ pub fork_label : String ,
986+ pub sync : SessionSyncPayload ,
987+ }
988+
989+ /// `/fork <session_id|prefix>` success projection. Explicit-source forks
990+ /// always report their spawn depth, so the contract makes that field required
991+ /// rather than permitting an invalid missing-depth state.
992+ #[ derive( Clone , Debug , PartialEq ) ]
993+ pub struct SessionForkFromReceipt {
994+ pub parent_label : String ,
995+ pub fork_label : String ,
996+ pub spawn_depth : u64 ,
997+ pub sync : SessionSyncPayload ,
998+ }
999+
1000+ /// `/save` success projection. The host performs the full baseline sequence
1001+ /// (snapshot, serialization, atomic write, metadata application, work-state
1002+ /// publication); the handler renders `Session saved to {display_path} (ID:
1003+ /// {truncated_id})`.
1004+ #[ derive( Clone , Debug , PartialEq ) ]
1005+ pub struct SessionSaveReceipt {
1006+ pub display_path : String ,
1007+ pub truncated_id : String ,
1008+ }
1009+
1010+ /// `/new` success projection. The handler renders
1011+ /// `Started new session {truncated_id} (New Session). Previous sessions
1012+ /// remain available via /resume.`
1013+ #[ derive( Clone , Debug , PartialEq ) ]
1014+ pub struct SessionNewReceipt {
1015+ pub truncated_id : String ,
1016+ pub sync : SessionSyncPayload ,
1017+ }
1018+
1019+ /// `/sessions archive|unarchive|restore` success projection. The handler
1020+ /// renders `Archived session {id} ({title})` or `Restored session ...` from
1021+ /// the verb it dispatched.
1022+ #[ derive( Clone , Debug , PartialEq ) ]
1023+ pub struct SessionArchiveReceipt {
1024+ pub truncated_id : String ,
1025+ pub title : String ,
1026+ }
1027+
1028+ /// `/tree` body projection. The body rendering source (journal tree and
1029+ /// linear transcript) stays TUI-owned; the handler appends the exact
1030+ /// guidance lines (D5).
1031+ #[ derive( Clone , Debug , PartialEq ) ]
1032+ pub enum TreeBodyProjection {
1033+ /// Journal render already includes the trailing newline before guidance.
1034+ Journal {
1035+ rendered : String ,
1036+ } ,
1037+ /// Linear pre-journal render (the marker lines).
1038+ Linear {
1039+ rendered : String ,
1040+ } ,
1041+ EmptySession ,
1042+ NoSession ,
1043+ }
1044+
1045+ /// Lifecycle authority for the session command slice (FEAT-023 D2).
1046+ ///
1047+ /// Operation-granular synchronous delegates over the exact minimum host work
1048+ /// the nine commands consume. Delegates may return the explicit host-error
1049+ /// text the baseline surfaces for a failing stage; successful results are
1050+ /// structured portable fields so handlers retain byte-identical composition.
1051+ pub trait CommandSessionLifecycleContext {
1052+ /// Live transition gate. Handlers return their own blocked-error text
1053+ /// before invoking any mutating delegate, matching the baseline ordering
1054+ /// (`/branch`, `/fork`, `/load`, `/new`). `/fork picker` and `/tree`
1055+ /// never consult it in the baseline, so their paths must not either.
1056+ fn transition_blocked ( & self ) -> bool ;
1057+
1058+ /// `/branch` with no argument: the current leaf when an active journaled
1059+ /// session resolves, otherwise `None` (the baseline silently falls back
1060+ /// to the usage message on this path).
1061+ fn branch_current_leaf_hint ( & self ) -> Option < String > ;
1062+
1063+ /// `/branch <entry_id>`: persist the leaf move and apply the branched
1064+ /// transcript. Errors are the exact baseline message for the failing
1065+ /// stage (no active session, directory open, load, persist, or branch
1066+ /// failure).
1067+ fn branch_to ( & mut self , entry_id : & str ) -> Result < SessionBranchOutcome , String > ;
1068+
1069+ /// `/tree`: produce the journal/linear/empty/no-session projection.
1070+ /// Errors are the exact baseline directory-open message.
1071+ fn tree_body ( & self ) -> Result < TreeBodyProjection , String > ;
1072+
1073+ /// `/save [path]`: the full baseline persistence sequence.
1074+ fn save_session ( & mut self , explicit_path : Option < String > )
1075+ -> Result < SessionSaveReceipt , String > ;
1076+
1077+ /// `/fork` (active conversation): the full baseline parent/child save and
1078+ /// switch sequence.
1079+ fn fork_active ( & mut self ) -> Result < SessionForkReceipt , String > ;
1080+
1081+ /// `/fork <session_id|prefix>`: explicit-source fork.
1082+ fn fork_from ( & mut self , session_id_or_prefix : & str ) -> Result < SessionForkFromReceipt , String > ;
1083+
1084+ /// `/new [--force]`: fresh-session transition. The caller has already
1085+ /// parsed the argument and applied the transition-blocked gate; blocker,
1086+ /// busy-work-state, and success handling match the baseline.
1087+ fn fresh_session ( & mut self , force : bool ) -> Result < SessionNewReceipt , String > ;
1088+
1089+ /// `/load <path>`: resolve the path (separator-bearing direct vs
1090+ /// workspace-relative) and validate the saved-session shape without
1091+ /// applying state or emitting a premature success receipt.
1092+ fn load_session ( & mut self , path : & str ) -> Result < PathBuf , String > ;
1093+
1094+ /// `/sessions` picker open with optional preselection (bare, `show`,
1095+ /// `list`, `picker`, and `open <id>` forms). Picker construction and
1096+ /// locale selection stay host-side.
1097+ fn open_picker ( & mut self , preselected : Option < String > ) ;
1098+
1099+ /// `/sessions archive|unarchive|restore <id>`: durable lifecycle state
1100+ /// update that also syncs the live cached metadata atomically.
1101+ fn set_archived (
1102+ & mut self ,
1103+ session_id : & str ,
1104+ archived : bool ,
1105+ ) -> Result < SessionArchiveReceipt , String > ;
1106+
1107+ /// `/sessions prune <days>`: prune persisted sessions older than `days`
1108+ /// days while protecting the active session; returns the number pruned.
1109+ fn prune_sessions ( & mut self , days : u64 ) -> Result < usize , String > ;
1110+ }
0 commit comments