diff --git a/use-cases/hermes-everos-memory/OWNER_PACKET.md b/use-cases/hermes-everos-memory/OWNER_PACKET.md index a361c7de8..17ee1f660 100644 --- a/use-cases/hermes-everos-memory/OWNER_PACKET.md +++ b/use-cases/hermes-everos-memory/OWNER_PACKET.md @@ -21,6 +21,8 @@ but EverCore is not yet proven active on the remote loopback service. - Raven Hermes chat bridge: `raven chat send`, bare-text/`/chat` REPL turns, and the TUI Hermes panel share one sanitized adapter; TUI execution runs in the background so redraw and key handling remain live. +- Raven single-agent loop surface: `raven loop`, `/loop`, and TUI `l` expose + capture, plan, act, observe, verify, and receipt phases above raw chat. - Raven v2 research harness: `raven research lanes`, `raven research packet `, and `raven research synthesize` keep v2 work as live-gate-calibrated decision packets instead of freeform research prose. @@ -51,6 +53,8 @@ cd use-cases/hermes-everos-memory && just raven-verify cd use-cases/hermes-everos-memory && just raven-console-check cd use-cases/hermes-everos-memory && just raven-status cd use-cases/hermes-everos-memory && bin/raven status --json +cd use-cases/hermes-everos-memory && just raven-loop +cd use-cases/hermes-everos-memory && bin/raven loop --json cd use-cases/hermes-everos-memory && just raven-research-lanes cd use-cases/hermes-everos-memory && just raven-research-packet-smoke cd use-cases/hermes-everos-memory && just raven-research-synthesis diff --git a/use-cases/hermes-everos-memory/justfile b/use-cases/hermes-everos-memory/justfile index ef1051a8e..f1e4e06f7 100644 --- a/use-cases/hermes-everos-memory/justfile +++ b/use-cases/hermes-everos-memory/justfile @@ -63,6 +63,9 @@ raven-status: raven-packet: bin/raven packet show +raven-loop: + bin/raven loop + raven-gates: bin/raven gates @@ -112,7 +115,7 @@ raven-chat-receipt-smoke: RAVEN_HERMES_BIN=/bin/echo bin/raven chat send --receipt - raven chat smoke raven-repl-smoke: - printf '/status\n/gates\n/research native-feel\n/chat raven chat smoke\n/memory raven\n/agents\n/runs\n/audit\n/quit\n' | RAVEN_HERMES_BIN=/bin/echo bin/raven repl + printf '/status\n/loop\n/gates\n/research native-feel\n/chat raven chat smoke\n/memory raven\n/agents\n/runs\n/audit\n/quit\n' | RAVEN_HERMES_BIN=/bin/echo bin/raven repl raven-tui-smoke: RAVEN_TUI_ONCE=1 bin/raven tui diff --git a/use-cases/hermes-everos-memory/raven-console/src/audit.rs b/use-cases/hermes-everos-memory/raven-console/src/audit.rs index 16770eb1c..5d173465a 100644 --- a/use-cases/hermes-everos-memory/raven-console/src/audit.rs +++ b/use-cases/hermes-everos-memory/raven-console/src/audit.rs @@ -22,6 +22,7 @@ pub fn run(ctx: &Context) -> NativeAuditReport { "keybindings", if source.contains("KeyCode::Char('q')") && source.contains("KeyCode::Char('?')") + && source.contains("KeyCode::Char('l')") && source.contains("KeyCode::Char('h')") && source.contains("KeyCode::Char('i')") && source.contains("KeyCode::Char('o')") @@ -30,7 +31,7 @@ pub fn run(ctx: &Context) -> NativeAuditReport { } else { Verdict::Block }, - "TUI exposes h/c chat, i prompt input, q, ?, :, /, s, p, m, a, g, r, o, d, n, Esc, and Ctrl-C paths.", + "TUI exposes l loop, h/c chat, i prompt input, q, ?, :, /, s, p, m, a, g, r, o, d, n, Esc, and Ctrl-C paths.", true, ), item( @@ -88,13 +89,13 @@ pub fn run(ctx: &Context) -> NativeAuditReport { item( "typed IPC", Verdict::Pass, - "RavenSnapshot, RavenReceipt, HermesChatTurn, and ScReport are serde-typed JSON contracts.", + "RavenSnapshot, AgenticLoopState, RavenReceipt, HermesChatTurn, and ScReport are serde-typed JSON contracts.", false, ), item( "evidence visibility", Verdict::Pass, - "remote hard gates, local gates, runs, docs, and watchlist evidence are visible.", + "remote hard gates, loop phases, local gates, runs, docs, and watchlist evidence are visible.", false, ), item( diff --git a/use-cases/hermes-everos-memory/raven-console/src/commands.rs b/use-cases/hermes-everos-memory/raven-console/src/commands.rs index a4e53b69a..52e7c33ab 100644 --- a/use-cases/hermes-everos-memory/raven-console/src/commands.rs +++ b/use-cases/hermes-everos-memory/raven-console/src/commands.rs @@ -41,6 +41,11 @@ pub enum Commands { #[command(subcommand)] command: MemoryCommand, }, + /// Show the single-agent goal/act/observe/verify loop. + Loop { + #[command(subcommand)] + command: Option, + }, /// Show agent lane status. Agents { #[command(subcommand)] @@ -116,6 +121,12 @@ pub enum AgentsCommand { List, } +#[derive(Subcommand)] +pub enum LoopCommand { + /// Show loop state. + Status, +} + #[derive(Subcommand)] pub enum RunsCommand { /// List saved receipts or configured verification commands. @@ -227,6 +238,15 @@ pub fn execute(cli: Cli, ctx: &Context) -> RavenResult<()> { } } }, + Commands::Loop { command: _ } => { + let snapshot = snapshot::build(ctx); + if cli.json { + output::json(&snapshot.loop_state) + } else { + output::agentic_loop(&snapshot.loop_state); + Ok(()) + } + } Commands::Agents { command: _ } => { let snapshot = snapshot::build(ctx); if cli.json { @@ -408,6 +428,7 @@ pub fn dispatch_repl(ctx: &Context, input: &str) -> RavenResult { println!("/help"); println!("/status"); println!("/packet"); + println!("/loop"); println!("/chat "); println!("/memory "); println!("/agents"); @@ -421,6 +442,7 @@ pub fn dispatch_repl(ctx: &Context, input: &str) -> RavenResult { } "/status" => output::status(&snapshot::build(ctx)), "/packet" => output::packet(&snapshot::build(ctx)), + "/loop" => output::agentic_loop(&snapshot::build(ctx).loop_state), "/agents" => output::agents(&snapshot::build(ctx)), "/gates" => output::gates(&snapshot::build(ctx)), "/research" => output::research_lanes(&research::list_lanes()), diff --git a/use-cases/hermes-everos-memory/raven-console/src/model.rs b/use-cases/hermes-everos-memory/raven-console/src/model.rs index 68399a443..a64991bbf 100644 --- a/use-cases/hermes-everos-memory/raven-console/src/model.rs +++ b/use-cases/hermes-everos-memory/raven-console/src/model.rs @@ -41,6 +41,36 @@ impl fmt::Display for Verdict { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum AgenticLoopPhase { + Capture, + Plan, + Act, + Observe, + Verify, + Receipt, +} + +impl AgenticLoopPhase { + pub fn as_str(self) -> &'static str { + match self { + Self::Capture => "CAPTURE", + Self::Plan => "PLAN", + Self::Act => "ACT", + Self::Observe => "OBSERVE", + Self::Verify => "VERIFY", + Self::Receipt => "RECEIPT", + } + } +} + +impl fmt::Display for AgenticLoopPhase { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + #[derive(Clone, Debug, Deserialize, Serialize)] pub struct RunPacket { pub id: String, @@ -190,6 +220,28 @@ pub struct RunView { pub receipt_path: Option, } +#[derive(Clone, Debug, Serialize)] +pub struct AgenticLoopStep { + pub phase: AgenticLoopPhase, + pub label: String, + pub verdict: Verdict, + pub evidence: String, +} + +#[derive(Clone, Debug, Serialize)] +pub struct AgenticLoopState { + pub verdict: Verdict, + pub objective: String, + pub active_phase: AgenticLoopPhase, + pub mode: String, + pub mutation_policy: String, + pub allowed_actions: Vec, + pub stop_conditions: Vec, + pub evidence_required: Vec, + pub output_contract: String, + pub steps: Vec, +} + #[derive(Clone, Debug, Serialize)] pub struct ScStatusView { pub verdict: Verdict, @@ -256,6 +308,7 @@ pub struct RavenSnapshot { pub memory: MemoryHealth, pub runs: Vec, pub sc: ScReport, + pub loop_state: AgenticLoopState, pub risks: Vec, pub next_actions: Vec, pub public_safety: PublicSafetyResult, diff --git a/use-cases/hermes-everos-memory/raven-console/src/output.rs b/use-cases/hermes-everos-memory/raven-console/src/output.rs index bcfd6f071..8d1b343aa 100644 --- a/use-cases/hermes-everos-memory/raven-console/src/output.rs +++ b/use-cases/hermes-everos-memory/raven-console/src/output.rs @@ -1,7 +1,7 @@ use crate::model::{ - DoctorReport, HermesChatTurn, MemorySearchResult, NativeAuditReport, RavenReceipt, - RavenSnapshot, ResearchLane, ResearchPacket, ResearchSynthesis, ScProviderView, ScReport, - ScSessionView, ScStatusView, ScWorktreeView, Verdict, + AgenticLoopState, DoctorReport, HermesChatTurn, MemorySearchResult, NativeAuditReport, + RavenReceipt, RavenSnapshot, ResearchLane, ResearchPacket, ResearchSynthesis, ScProviderView, + ScReport, ScSessionView, ScStatusView, ScWorktreeView, Verdict, }; use crate::sanitizer::{sanitize_json, sanitize_text}; use crate::util::one_line; @@ -29,6 +29,10 @@ pub fn status(snapshot: &RavenSnapshot) { "MEMORY: {} ({})", snapshot.memory.verdict, snapshot.memory.status )); + line(&format!( + "LOOP: {} (phase {})", + snapshot.loop_state.verdict, snapshot.loop_state.active_phase + )); line(""); line("WATCHLIST:"); for issue in &snapshot.watchlist_issues { @@ -70,6 +74,35 @@ pub fn packet(snapshot: &RavenSnapshot) { } } +pub fn agentic_loop(state: &AgenticLoopState) { + line("RAVEN_AGENTIC_LOOP"); + line(&format!("VERDICT: {}", state.verdict)); + line(&format!("OBJECTIVE: {}", state.objective)); + line(&format!("ACTIVE_PHASE: {}", state.active_phase)); + line(&format!("MODE: {}", state.mode)); + line(&format!("MUTATION_POLICY: {}", state.mutation_policy)); + line("ALLOWED_ACTIONS:"); + for action in &state.allowed_actions { + line(&format!("- {action}")); + } + line("STOP_CONDITIONS:"); + for condition in &state.stop_conditions { + line(&format!("- {condition}")); + } + line("EVIDENCE_REQUIRED:"); + for item in &state.evidence_required { + line(&format!("- {item}")); + } + line("STEPS:"); + for step in &state.steps { + line(&format!( + "- {}: {} {} evidence={}", + step.phase, step.verdict, step.label, step.evidence + )); + } + line(&format!("OUTPUT_CONTRACT: {}", state.output_contract)); +} + pub fn packet_export_markdown(snapshot: &RavenSnapshot) -> String { let mut output = Vec::new(); output.push(format!("# {}", snapshot.packet.title)); diff --git a/use-cases/hermes-everos-memory/raven-console/src/snapshot.rs b/use-cases/hermes-everos-memory/raven-console/src/snapshot.rs index 2c4667693..a0ceabf87 100644 --- a/use-cases/hermes-everos-memory/raven-console/src/snapshot.rs +++ b/use-cases/hermes-everos-memory/raven-console/src/snapshot.rs @@ -5,8 +5,9 @@ use crate::constants::{ }; use crate::context::Context; use crate::model::{ - AgentView, IssueView, LocalGateView, MemoryHealth, PacketSummary, PublicSafetyResult, - RavenSnapshot, RemoteGate, RunView, ScReport, Verdict, + AgentView, AgenticLoopPhase, AgenticLoopState, AgenticLoopStep, IssueView, LocalGateView, + MemoryHealth, PacketSummary, PublicSafetyResult, RavenSnapshot, RemoteGate, RunView, ScReport, + Verdict, }; struct SnapshotParts { @@ -86,6 +87,7 @@ fn assemble(ctx: &Context, parts: SnapshotParts) -> RavenSnapshot { sc, } = parts; let verdict = overall_verdict(packet_verdict, &remote_gates); + let loop_state = agentic_loop_state(ctx, packet_verdict, &remote_gates, &memory, &runs); let mut next_actions = ctx.packet.next_actions.clone(); if remote_gates @@ -125,6 +127,7 @@ fn assemble(ctx: &Context, parts: SnapshotParts) -> RavenSnapshot { memory, runs, sc, + loop_state, risks: vec![ "Remote deploy remains separate from local packet PASS.".to_string(), "DAS-2675 adapter repair cannot change DAS-2666 verdict.".to_string(), @@ -138,6 +141,131 @@ fn assemble(ctx: &Context, parts: SnapshotParts) -> RavenSnapshot { } } +fn agentic_loop_state( + ctx: &Context, + packet_verdict: Verdict, + remote_gates: &[RemoteGate], + memory: &MemoryHealth, + runs: &[RunView], +) -> AgenticLoopState { + let remote_blocked = remote_gates + .iter() + .any(|gate| gate.hard_gate && gate.verdict == Verdict::Block); + let auth_repaired = remote_gates + .iter() + .any(|gate| gate.id == ISSUE_AUTH_BLOCKER && gate.verdict == Verdict::Pass); + let receipt_ready = runs.iter().any(|run| run.receipt_path.is_some()); + let verify_verdict = if remote_blocked { + Verdict::Flag + } else if packet_verdict == Verdict::Pass { + Verdict::Pass + } else { + packet_verdict + }; + + let steps = vec![ + AgenticLoopStep { + phase: AgenticLoopPhase::Capture, + label: "Goal captured".to_string(), + verdict: Verdict::Pass, + evidence: "Run packet, watchlist gates, and prompt surfaces are typed.".to_string(), + }, + AgenticLoopStep { + phase: AgenticLoopPhase::Plan, + label: "One bounded objective".to_string(), + verdict: Verdict::Pass, + evidence: "Plan stays inside Raven packet scope and visible stop conditions." + .to_string(), + }, + AgenticLoopStep { + phase: AgenticLoopPhase::Act, + label: "Hermes turn boundary".to_string(), + verdict: if memory.verdict == Verdict::Block { + Verdict::Block + } else { + Verdict::Flag + }, + evidence: "CLI, REPL, and TUI execute one sanitized Hermes turn at a time.".to_string(), + }, + AgenticLoopStep { + phase: AgenticLoopPhase::Observe, + label: "Evidence stays attached".to_string(), + verdict: Verdict::Pass, + evidence: "TUI evidence drawer, chat transcript, gates, and runs are stable panes." + .to_string(), + }, + AgenticLoopStep { + phase: AgenticLoopPhase::Verify, + label: "Gates decide closure".to_string(), + verdict: verify_verdict, + evidence: if remote_blocked { + "Local packet may pass; remote deploy remains blocked by hard gate evidence." + .to_string() + } else if auth_repaired { + "Auth repair is present; remaining verifier gates decide loop closure.".to_string() + } else { + "Verifier has no remote hard block in the current snapshot.".to_string() + }, + }, + AgenticLoopStep { + phase: AgenticLoopPhase::Receipt, + label: "Receipt is explicit".to_string(), + verdict: if receipt_ready { + Verdict::Pass + } else { + Verdict::Flag + }, + evidence: "Use --receipt or --save to materialize sanitized run evidence.".to_string(), + }, + ]; + + let verdict = if steps.iter().any(|step| step.verdict == Verdict::Block) { + Verdict::Block + } else if steps.iter().any(|step| step.verdict == Verdict::Flag) { + Verdict::Flag + } else { + Verdict::Pass + }; + + AgenticLoopState { + verdict, + objective: ctx.packet.goal.clone(), + active_phase: if remote_blocked { + AgenticLoopPhase::Verify + } else { + AgenticLoopPhase::Act + }, + mode: "single-agent / human-in-the-loop".to_string(), + mutation_policy: "read-only by default; writes require explicit command scope or receipt target" + .to_string(), + allowed_actions: vec![ + "status/gates refresh".to_string(), + "memory search".to_string(), + "Hermes chat turn".to_string(), + "run verify".to_string(), + "receipt save/export".to_string(), + "Superconductor inspect".to_string(), + ], + stop_conditions: vec![ + "DAS-2666 BLOCK keeps remote deploy red".to_string(), + "Hermes failure or timeout returns FLAG, not console crash".to_string(), + "public-safety sanitizer failure blocks receipt publication".to_string(), + "operator approval required before remote deploy or external mutation".to_string(), + ], + evidence_required: vec![ + "operator prompt".to_string(), + "Hermes response or stderr excerpt".to_string(), + "gate effects".to_string(), + "public-safety verdict".to_string(), + "receipt path or explicit no-save state".to_string(), + ], + output_contract: + "RavenReceipt plus visible loop transcript; local loop evidence never greens remote deploy" + .to_string(), + steps, + } +} + fn fallback_watchlist() -> Vec { WATCHLIST_ISSUES .iter() diff --git a/use-cases/hermes-everos-memory/raven-console/src/tui.rs b/use-cases/hermes-everos-memory/raven-console/src/tui.rs index 77d1b1d6b..c2bbc8a76 100644 --- a/use-cases/hermes-everos-memory/raven-console/src/tui.rs +++ b/use-cases/hermes-everos-memory/raven-console/src/tui.rs @@ -1,6 +1,8 @@ use crate::adapters::hermes; use crate::context::Context; -use crate::model::{HermesChatTranscriptLine, HermesChatTurn, RavenSnapshot, Verdict}; +use crate::model::{ + AgenticLoopPhase, HermesChatTranscriptLine, HermesChatTurn, RavenSnapshot, Verdict, +}; use crate::sanitizer::sanitize_text; use crate::snapshot; use crate::RavenResult; @@ -26,6 +28,7 @@ use std::time::Duration; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum Panel { Status, + Loop, Packet, Chat, Memory, @@ -60,6 +63,7 @@ struct TuiState { input: String, evidence: String, chat: VecDeque, + loop_activity: VecDeque, chat_inflight: bool, } @@ -69,6 +73,12 @@ struct ChatLine { verdict: Option, } +struct LoopActivityLine { + phase: AgenticLoopPhase, + verdict: Verdict, + text: String, +} + enum BackgroundEvent { Snapshot(Box), Chat(HermesChatTurn), @@ -76,6 +86,7 @@ enum BackgroundEvent { const SURFACE_TITLE: &str = "RAVEN // DOOMSDAY-MAXXED-MOGGED"; const CHAT_HISTORY_LIMIT: usize = 24; +const LOOP_ACTIVITY_LIMIT: usize = 16; impl Default for TuiState { fn default() -> Self { @@ -86,6 +97,7 @@ impl Default for TuiState { evidence: "Remote gates stay red until live evidence proves every hard gate." .to_string(), chat: VecDeque::new(), + loop_activity: VecDeque::new(), chat_inflight: false, } } @@ -136,6 +148,26 @@ fn run_loop( BackgroundEvent::Chat(turn) => { state.chat_inflight = false; state.evidence = turn.evidence.clone(); + push_loop_activity( + &mut state, + LoopActivityLine { + phase: AgenticLoopPhase::Observe, + verdict: turn.verdict, + text: format!( + "Hermes observed in {}ms via {}.", + turn.duration_ms, turn.runtime + ), + }, + ); + push_loop_activity( + &mut state, + LoopActivityLine { + phase: AgenticLoopPhase::Verify, + verdict: Verdict::Flag, + text: "Gate state unchanged; remote hard gates still decide closure." + .to_string(), + }, + ); push_chat_line( &mut state, ChatLine { @@ -221,6 +253,13 @@ fn push_chat_line(state: &mut TuiState, line: ChatLine) { state.chat.push_back(line); } +fn push_loop_activity(state: &mut TuiState, line: LoopActivityLine) { + if state.loop_activity.len() == LOOP_ACTIVITY_LIMIT { + state.loop_activity.pop_front(); + } + state.loop_activity.push_back(line); +} + fn receive_background_event(rx: &Receiver) -> Option { match rx.try_recv() { Ok(event) => Some(event), @@ -244,8 +283,14 @@ fn handle_normal_key(key: KeyEvent, state: &mut TuiState) -> TuiAction { KeyCode::Char('q') => return TuiAction::Quit, KeyCode::Char('u') => return TuiAction::Refresh, KeyCode::Char('?') => state.panel = Panel::Help, + KeyCode::Char('l') => state.panel = Panel::Loop, KeyCode::Char('h') | KeyCode::Char('c') => state.panel = Panel::Chat, - KeyCode::Char('i') | KeyCode::Enter if state.panel == Panel::Chat => { + KeyCode::Char('i') if matches!(state.panel, Panel::Chat | Panel::Loop) => { + state.mode = InputMode::Chat; + state.input.clear(); + state.evidence = "Hermes input mode. Enter sends; Esc cancels.".to_string(); + } + KeyCode::Enter if matches!(state.panel, Panel::Chat | Panel::Loop) => { state.mode = InputMode::Chat; state.input.clear(); state.evidence = "Hermes input mode. Enter sends; Esc cancels.".to_string(); @@ -301,7 +346,9 @@ fn handle_input_key(key: KeyEvent, state: &mut TuiState) -> TuiAction { } else if state.chat_inflight { state.evidence = "Hermes turn already running.".to_string(); } else { - state.panel = Panel::Chat; + if state.panel != Panel::Loop { + state.panel = Panel::Chat; + } push_chat_line( state, ChatLine { @@ -318,6 +365,22 @@ fn handle_input_key(key: KeyEvent, state: &mut TuiState) -> TuiAction { verdict: Some(Verdict::Flag), }, ); + push_loop_activity( + state, + LoopActivityLine { + phase: AgenticLoopPhase::Capture, + verdict: Verdict::Pass, + text: "Operator prompt captured and sanitized.".to_string(), + }, + ); + push_loop_activity( + state, + LoopActivityLine { + phase: AgenticLoopPhase::Act, + verdict: Verdict::Flag, + text: "Hermes turn queued; UI remains live.".to_string(), + }, + ); state.mode = InputMode::Normal; state.input.clear(); return TuiAction::SendChat(prompt); @@ -338,6 +401,7 @@ fn handle_input_key(key: KeyEvent, state: &mut TuiState) -> TuiAction { fn apply_palette(input: &str, state: &mut TuiState) { match input.trim().to_ascii_lowercase().as_str() { "status" | "s" => state.panel = Panel::Status, + "loop" | "agentic" | "single" | "l" => state.panel = Panel::Loop, "packet" | "p" => state.panel = Panel::Packet, "chat" | "hermes" | "h" | "c" => state.panel = Panel::Chat, "memory" | "m" => state.panel = Panel::Memory, @@ -434,6 +498,7 @@ fn render_body(frame: &mut Frame<'_>, area: Rect, snapshot: &RavenSnapshot, stat fn render_rail(frame: &mut Frame<'_>, area: Rect, active: Panel) { let items = [ ("s", "Status", "truth stack", Panel::Status), + ("l", "Loop", "single agent", Panel::Loop), ("p", "Packet", "owner view", Panel::Packet), ("h", "Hermes Chat", "dialogue", Panel::Chat), ("m", "Memory", "bridge health", Panel::Memory), @@ -475,6 +540,7 @@ fn render_rail(frame: &mut Frame<'_>, area: Rect, active: Panel) { fn render_panel(frame: &mut Frame<'_>, area: Rect, snapshot: &RavenSnapshot, state: &TuiState) { let (title, lines) = match state.panel { Panel::Status => ("Status", status_lines(snapshot)), + Panel::Loop => ("Agentic Loop", loop_lines(snapshot, state)), Panel::Packet => ("Packet", packet_lines(snapshot)), Panel::Chat => ("Hermes Chat", chat_lines(state)), Panel::Memory => ("Memory", memory_lines(snapshot)), @@ -514,6 +580,17 @@ fn render_evidence(frame: &mut Frame<'_>, area: Rect, snapshot: &RavenSnapshot, ])); } lines.push(Line::from("")); + lines.push(section("AGENTIC LOOP")); + lines.push(Line::from(vec![ + chip("loop", snapshot.loop_state.verdict.to_string()), + Span::raw(" "), + chip("phase", snapshot.loop_state.active_phase.to_string()), + ])); + lines.push(Line::from(vec![Span::styled( + snapshot.loop_state.output_contract.clone(), + Style::default().fg(Color::Gray), + )])); + lines.push(Line::from("")); lines.push(section("RISK REGISTER")); for risk in &snapshot.risks { lines.push(Line::from(vec![ @@ -534,7 +611,7 @@ fn render_input(frame: &mut Frame<'_>, area: Rect, state: &TuiState) { let (title, prompt, color) = match state.mode { InputMode::Normal => ( "INPUT // NORMAL", - "keys: h chat | i input | u refresh | ? help | : palette | / memory | s/p/m/a/g/r/o/d/n panels | q quit" + "keys: l loop | h chat | i input | u refresh | ? help | : palette | / memory | s/p/m/a/g/r/o/d/n panels | q quit" .to_string(), Color::DarkGray, ), @@ -629,6 +706,73 @@ fn packet_lines(snapshot: &RavenSnapshot) -> Vec> { lines } +fn loop_lines(snapshot: &RavenSnapshot, state: &TuiState) -> Vec> { + let loop_state = &snapshot.loop_state; + let mut lines = vec![ + section("SINGLE AGENTIC LOOP"), + Line::from(vec![ + chip("verdict", loop_state.verdict.to_string()), + Span::raw(" "), + chip("active", loop_state.active_phase.to_string()), + ]), + kv("mode", &loop_state.mode), + kv("objective", &loop_state.objective), + kv("mutation", &loop_state.mutation_policy), + Line::from(""), + section("ALLOWED ACTIONS"), + ]; + for action in &loop_state.allowed_actions { + lines.push(bullet(action)); + } + lines.push(Line::from("")); + lines.push(section("STOP CONDITIONS")); + for condition in &loop_state.stop_conditions { + lines.push(bullet(condition)); + } + lines.push(Line::from("")); + lines.push(section("PHASES")); + for step in &loop_state.steps { + lines.push(Line::from(vec![ + phase_span(step.phase), + Span::raw(" "), + verdict_span(step.verdict.to_string()), + Span::raw(" "), + Span::styled( + format!("{:<24}", step.label), + Style::default().fg(Color::White), + ), + Span::styled(step.evidence.clone(), Style::default().fg(Color::Gray)), + ])); + } + + lines.push(Line::from("")); + lines.push(section("LIVE TURN")); + if state.loop_activity.is_empty() { + lines.push(Line::from(vec![ + Span::styled("idle", Style::default().fg(Color::DarkGray)), + Span::raw(" "), + Span::styled( + "press i here or in Hermes Chat to run one bounded turn.", + Style::default().fg(Color::Gray), + ), + ])); + } else { + for item in &state.loop_activity { + lines.push(Line::from(vec![ + phase_span(item.phase), + Span::raw(" "), + verdict_span(item.verdict.to_string()), + Span::raw(" "), + Span::styled(item.text.clone(), Style::default().fg(Color::Gray)), + ])); + } + } + + lines.push(Line::from("")); + lines.push(kv("contract", &loop_state.output_contract)); + lines +} + fn memory_lines(snapshot: &RavenSnapshot) -> Vec> { vec![ section("MEMORY BRIDGE"), @@ -891,12 +1035,13 @@ fn help_lines() -> Vec> { kv("?", "help"), kv(":", "palette"), kv("/", "memory/search"), + kv("l", "single-agent loop panel"), kv("h/c", "Hermes chat panel"), - kv("i", "prompt input when Hermes panel is active"), + kv("i", "prompt input when Loop or Hermes panel is active"), kv("u", "refresh live Multica + memory data"), kv( "panels", - "s status | p packet | h chat | m memory | a agents", + "s status | l loop | p packet | h chat | m memory | a agents", ), kv("panels", "g gates | r runs | d doctor | n native audit"), kv("panels", "o superconductor"), @@ -937,6 +1082,7 @@ fn shell_block(title: &'static str, accent: Color) -> Block<'static> { fn panel_color(panel: Panel) -> Color { match panel { Panel::Status => Color::Cyan, + Panel::Loop => Color::LightGreen, Panel::Packet => Color::Magenta, Panel::Chat => Color::Magenta, Panel::Memory => Color::Green, @@ -995,6 +1141,22 @@ fn section(label: &'static str) -> Line<'static> { )]) } +fn phase_span(phase: AgenticLoopPhase) -> Span<'static> { + Span::styled( + format!("{:<8}", phase), + Style::default() + .fg(Color::LightGreen) + .add_modifier(Modifier::BOLD), + ) +} + +fn bullet(value: &str) -> Line<'static> { + Line::from(vec![ + Span::styled("- ", Style::default().fg(Color::Yellow)), + Span::styled(value.to_string(), Style::default().fg(Color::Gray)), + ]) +} + fn kv(label: &'static str, value: &str) -> Line<'static> { Line::from(vec![ Span::styled(format!("{label:<10}"), Style::default().fg(Color::DarkGray)), @@ -1053,4 +1215,49 @@ mod tests { Some("turn-29") ); } + + #[test] + fn loop_activity_is_bounded_fifo() { + let mut state = TuiState::default(); + + for index in 0..20 { + push_loop_activity( + &mut state, + LoopActivityLine { + phase: AgenticLoopPhase::Act, + verdict: Verdict::Flag, + text: format!("loop-{index}"), + }, + ); + } + + assert_eq!(state.loop_activity.len(), LOOP_ACTIVITY_LIMIT); + assert_eq!( + state.loop_activity.front().map(|line| line.text.as_str()), + Some("loop-4") + ); + assert_eq!( + state.loop_activity.back().map(|line| line.text.as_str()), + Some("loop-19") + ); + } + + #[test] + fn loop_prompt_stays_on_loop_panel() { + let mut state = TuiState { + panel: Panel::Loop, + mode: InputMode::Chat, + input: "one bounded turn".to_string(), + ..TuiState::default() + }; + + let action = handle_input_key( + KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()), + &mut state, + ); + + assert!(matches!(action, TuiAction::SendChat(prompt) if prompt == "one bounded turn")); + assert_eq!(state.panel, Panel::Loop); + assert_eq!(state.loop_activity.len(), 2); + } } diff --git a/use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md b/use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md index 9525f3a96..6ff2de90d 100644 --- a/use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md +++ b/use-cases/hermes-everos-memory/raven/COMMAND_CONTRACT.md @@ -20,6 +20,7 @@ It owns: - run packet validation; - memory health/search before work starts; - lane and mutation-policy visibility; +- single-agent loop visibility; - gate visibility and conservative verdict calculation; - sanitized JSON snapshot and receipt output; - owner packet export; @@ -41,6 +42,7 @@ It does not own: | `raven tui` | terminal | ratatui console with status, rail, active panel, evidence drawer, input line | `RAVEN_TUI_ONCE=1` must render deterministic smoke output | | `raven repl` | slash commands | same handlers as CLI | piped smoke stays deterministic | | `raven chat send [--cwd ] [--json] [--receipt ] [--save] ` | bounded prompt text | sanitized `HermesChatTurn` or `RavenReceipt` | Hermes failure is `FLAG`, not UI crash; chat receipts cannot green remote deploy | +| `raven loop [status] [--json]` | packet + gate + run state | `AgenticLoopState` or compact loop contract | loop closure is `FLAG` while remote hard gates or missing receipts remain | | `raven packet show [--json]` | local packet/docs | packet summary | source docs resolve | | `raven packet export [--output ]` | snapshot | sanitized owner packet markdown | public-safety sanitizer clean | | `raven memory health [--json]` | EverOS bridge | health verdict | provider failure is `FLAG`, not crash | @@ -79,6 +81,30 @@ State transitions: 5. `done` only when every blocking gate is `pass`. 6. `blocked` when a blocking gate needs human or external action. +## Single Agentic Loop Behavior + +Raven keeps a visible single-agent loop above the raw chat transcript: + +```text +CAPTURE -> PLAN -> ACT -> OBSERVE -> VERIFY -> RECEIPT +``` + +The loop is intentionally human-in-the-loop. It captures one bounded objective, +routes one Hermes turn, keeps observations attached to the evidence drawer, then +lets gates and receipts decide closure. This is the missing bridge between +`chat` and `runs`: a prompt can produce useful local evidence, but it cannot +silently mutate gate state. + +Loop invariants: + +- `raven loop` prints the typed `AgenticLoopState`; +- `/loop` in the REPL maps to the same handler; +- `l` in the TUI opens the loop panel, and `i` from that panel starts one + Hermes prompt; +- chat turns add live loop breadcrumbs for capture, act, observe, and verify; +- receipts are explicit through `--receipt` or `--save`; +- remote deploy remains red until `DAS-2666` hard evidence passes. + ## Memory Behavior Before execution Raven asks memory for: diff --git a/use-cases/hermes-everos-memory/raven/NATIVE_FEEL_AUDIT.md b/use-cases/hermes-everos-memory/raven/NATIVE_FEEL_AUDIT.md index 7e8b8d9d0..3749b9ca7 100644 --- a/use-cases/hermes-everos-memory/raven/NATIVE_FEEL_AUDIT.md +++ b/use-cases/hermes-everos-memory/raven/NATIVE_FEEL_AUDIT.md @@ -12,22 +12,22 @@ tools without copying any external reference implementation. | Category | Gate | Current Evidence | Verdict | | --- | --- | --- | --- | | Latency | Commands must return usable `PASS/FLAG/BLOCK` state without crashing when bridges are absent. | Memory and Multica adapters degrade to `FLAG` or fallback watch state. | PASS | -| Keybindings | A TUI operator can move without memorizing long commands. | `h`/`c` chat, `i` prompt input, `?`, `:`, `/`, `s`, `p`, `m`, `a`, `g`, `r`, `o` Superconductor, `d`, `n`, `q`, `Esc`, and `Ctrl-C` are handled. | PASS | -| Focus | The active panel is explicit state. | Panels are `Status`, `Packet`, `Chat`, `Memory`, `Agents`, `Gates`, `Runs`, `Doctor`, `NativeAudit`, and `Help`. | PASS | +| Keybindings | A TUI operator can move without memorizing long commands. | `l` loop, `h`/`c` chat, `i` prompt input, `?`, `:`, `/`, `s`, `p`, `m`, `a`, `g`, `r`, `o` Superconductor, `d`, `n`, `q`, `Esc`, and `Ctrl-C` are handled. | PASS | +| Focus | The active panel is explicit state. | Panels are `Status`, `Loop`, `Packet`, `Chat`, `Memory`, `Agents`, `Gates`, `Runs`, `Doctor`, `NativeAudit`, and `Help`. | PASS | | Scrollback | Evidence remains visible without layout churn. | The evidence drawer stays fixed; deep historical receipts live in `raven/.local-runs/`. | PASS | | Interrupt behavior | Interrupts must exit or cancel cleanly. | `Esc` cancels prompt modes; `Ctrl-C` exits the TUI loop. | PASS | | REPL history | Interactive command recall should feel local-native. | `rustyline` backs the interactive REPL; piped input stays deterministic for smoke tests. | PASS | | Pane stability | Dynamic data cannot resize the command surface unpredictably. | `ratatui` uses fixed status, rail, evidence, and input regions around a flexible active panel. | PASS | -| Command grammar | CLI and REPL commands share the same operator vocabulary. | Slash commands map to status, packet, chat, memory, agents, gates, runs, doctor, audit, and quit handlers. | PASS | -| Typed IPC | Machine output is typed and redacted. | `RavenSnapshot`, `RavenReceipt`, `HermesChatTurn`, and `ScReport` are serialized through the sanitizer before JSON printing. | PASS | -| Evidence visibility | Hard gates and receipts are first-class. | DAS-2666, DAS-2669, local packet gates, saved receipts, and configured verification commands render directly. | PASS | +| Command grammar | CLI and REPL commands share the same operator vocabulary. | Slash commands map to status, packet, loop, chat, memory, agents, gates, runs, doctor, audit, and quit handlers. | PASS | +| Typed IPC | Machine output is typed and redacted. | `RavenSnapshot`, `AgenticLoopState`, `RavenReceipt`, `HermesChatTurn`, and `ScReport` are serialized through the sanitizer before JSON printing. | PASS | +| Evidence visibility | Hard gates, loop phases, and receipts are first-class. | DAS-2666, DAS-2669, capture/plan/act/observe/verify/receipt state, local packet gates, saved receipts, and configured verification commands render directly. | PASS | | Public-safety redaction | Public output must not expose private paths, hosts/IPs, tokens, credential paths, or signed URLs. | Human and JSON output pass through the sanitizer; receipts store sanitized excerpts. | PASS | ## Hard PASS Blockers `raven native-audit` must refuse `PASS` when any hard category fails: -- missing keybindings for chat/input/quit/help/palette/search/status/gates/runs/audit; +- missing keybindings for loop/chat/input/quit/help/palette/search/status/gates/runs/audit; - missing stable TUI panes; - unsafe interrupt behavior; - missing typed JSON snapshot or receipt contracts; diff --git a/use-cases/hermes-everos-memory/raven/README.md b/use-cases/hermes-everos-memory/raven/README.md index 45bcdeba7..ff5767a75 100644 --- a/use-cases/hermes-everos-memory/raven/README.md +++ b/use-cases/hermes-everos-memory/raven/README.md @@ -37,6 +37,8 @@ bin/raven status bin/raven status --json bin/raven packet show bin/raven packet export --output - +bin/raven loop +bin/raven loop --json bin/raven chat send "summarize current hard gates" bin/raven chat send --receipt - "summarize current hard gates" bin/raven chat send --save "summarize current hard gates" @@ -63,6 +65,7 @@ Just targets: ```bash just raven-status just raven-packet +just raven-loop just raven-gates just raven-agents just raven-research-lanes @@ -122,6 +125,11 @@ label, detected Hermes runtime, command shape, and sanitized transcript. Chat receipts can be printed with `--receipt -` or saved with `--save`; they never change remote deploy gate state. +The single-agent loop is also explicit: `raven loop`, `/loop`, and the TUI `l` +panel expose capture, plan, act, observe, verify, and receipt phases. Prompt +turns add live loop breadcrumbs, but gate closure still requires verifier and +remote hard-gate evidence. + Superconductor state is visible through `raven sc`. The adapter is read-only, times out quickly, and turns socket or merge-base failures into `FLAG` evidence instead of blocking the Raven console.