feat: enhance lesson management and review system
- Added LessonAccept and LessonReject actions to manage lesson outcomes. - Implemented probe for build/test verification before triggering reviews. - Introduced a mechanism for recording shadow hits and evaluating trials in the ReviewSystem. - Enhanced lesson structure with additional fields for scope, snippets, and provenances. - Updated command parsing to include lesson acceptance and rejection commands. - Improved session state management with staleness sweeps and retrospective creation. - Enhanced UI to display detailed usage statistics and quality trends.
This commit is contained in:
@@ -52,6 +52,12 @@ pub enum Action {
|
||||
LessonImport {
|
||||
path: String,
|
||||
},
|
||||
LessonAccept {
|
||||
name: String,
|
||||
},
|
||||
LessonReject {
|
||||
name: String,
|
||||
},
|
||||
#[expect(dead_code)]
|
||||
RecordUsage {
|
||||
tokens_in: u64,
|
||||
@@ -71,6 +77,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
match action {
|
||||
Action::Quit => {
|
||||
save_current_session(state);
|
||||
auto_create_retrospective(state);
|
||||
state.quit = true;
|
||||
}
|
||||
Action::ForceQuit => {
|
||||
@@ -326,7 +333,13 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
Action::Tick => {
|
||||
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||
state.misc.drain_expired_toasts(now_ms);
|
||||
let events: Vec<TurnEvent> = {
|
||||
// Idle-time housekeeping.
|
||||
crate::app::review::maybe_run_staleness_sweep(state);
|
||||
if let Some(ref rt) = state.session_runtime {
|
||||
let _ = crate::app::review::process_pending_lessons(&rt.session_dir, &state.memory_dir);
|
||||
}
|
||||
|
||||
let events: Vec<TurnEvent> = {
|
||||
if let Ok(mut q) = state.turn_events.lock() {
|
||||
q.drain(..).collect()
|
||||
} else {
|
||||
@@ -371,8 +384,31 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
if should_trigger_review(state, Origin::Main) {
|
||||
let _ = trigger_review(state);
|
||||
}
|
||||
} else if kind == "review" {
|
||||
// Track review outcome: check if lessons were found.
|
||||
// Format: "Quality review: <verdict> [N lesson(s)]"
|
||||
let lessons_found = if message.contains("lesson") || message.contains("Lesson") {
|
||||
// Check for "N lesson(s)" pattern at end
|
||||
message.rsplit(' ').next().and_then(|w| {
|
||||
w.trim_end_matches(')').trim_end_matches('s')
|
||||
.split('(').next_back()
|
||||
.and_then(|n| n.parse::<u32>().ok())
|
||||
}).unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
if lessons_found > 0 {
|
||||
rt.consecutive_empty_reviews = 0;
|
||||
rt.lesson_count += lessons_found;
|
||||
} else {
|
||||
rt.consecutive_empty_reviews += 1;
|
||||
}
|
||||
}
|
||||
state.push_toast(Toast::new(ToastKind::Info, message));
|
||||
} else {
|
||||
state.push_toast(Toast::new(ToastKind::Info, message));
|
||||
}
|
||||
state.push_toast(Toast::new(ToastKind::Info, message));
|
||||
}
|
||||
TurnEvent::Error(msg) => {
|
||||
state.push_toast(Toast::new(ToastKind::Error, msg));
|
||||
@@ -402,6 +438,26 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::LessonAccept { name } => {
|
||||
if let Some(ref rt) = state.session_runtime {
|
||||
let _ = crate::app::review::resolve_pending_lesson(
|
||||
&rt.session_dir, &state.memory_dir, &name, true,
|
||||
);
|
||||
}
|
||||
state.push_toast(Toast::new(ToastKind::Success,
|
||||
format!("accepted lesson: {}", name)));
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::LessonReject { name } => {
|
||||
if let Some(ref rt) = state.session_runtime {
|
||||
let _ = crate::app::review::resolve_pending_lesson(
|
||||
&rt.session_dir, &state.memory_dir, &name, false,
|
||||
);
|
||||
}
|
||||
state.push_toast(Toast::new(ToastKind::Info,
|
||||
format!("rejected lesson: {}", name)));
|
||||
state.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -655,3 +711,39 @@ fn save_current_session(state: &AppStateRest) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn auto_create_retrospective(state: &mut AppStateRest) {
|
||||
if state.session_runtime.is_none() {
|
||||
return;
|
||||
}
|
||||
let session = crate::model::session::Session::new(
|
||||
state.session_id.clone(),
|
||||
"session".to_string(),
|
||||
);
|
||||
match crate::model::memory::auto_create_retrospective(&state.session_dir, &session) {
|
||||
Ok(Some(retro)) => {
|
||||
state.push_toast(crate::app::state::types::Toast::new(
|
||||
crate::app::state::types::ToastKind::Info,
|
||||
format!("Retrospective created: {}", retro.name),
|
||||
));
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
// Silently handle — retrospective is best-effort.
|
||||
let _ = e;
|
||||
}
|
||||
}
|
||||
// Attempt consensus promotion for global-scope lessons.
|
||||
let lessons: Vec<crate::model::memory::Memory> = crate::model::memory::Memory::list(&state.memory_dir)
|
||||
.iter()
|
||||
.filter_map(|n| crate::model::memory::Memory::read(&state.memory_dir, n).ok())
|
||||
.filter(|m| m.kind == "lesson")
|
||||
.collect();
|
||||
if let Some(global_dir) = dirs::data_dir().map(|d| d.join("zesdex")) {
|
||||
for lesson in &lessons {
|
||||
if lesson.scope.as_deref() != Some("global") {
|
||||
let _ = crate::model::memory::promote_with_consensus(&global_dir, lesson);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user