Refine planner mode UI, logging, and history tracking

- Display coach feedback content (up to 25 lines) instead of just length
- Write GIT COMMIT entry to history before actual commit for better a...
- Implement single-line status updates during LLM processing with too...
- Display non-tool LLM text responses in planner UI
- Redirect all logs to <workspace>/logs directory instead of codepath
- Preserve TODO file in planner mode for history (prevent deletion)

Completed files:
- completed_requirements_2025-12-09_16-16-51.md
- completed_todo_2025-12-09_16-16-51.md
This commit is contained in:
Jochen
2025-12-09 16:17:53 +11:00
parent ff8b3e7c7b
commit 633da0d8a6
10 changed files with 310 additions and 71 deletions

View File

@@ -129,26 +129,27 @@ impl ErrorContext {
return;
}
let logs_dir = std::path::Path::new("logs/errors");
let base_logs_dir = crate::logs_dir();
let logs_dir = base_logs_dir.join("errors");
if !logs_dir.exists() {
if let Err(e) = std::fs::create_dir_all(logs_dir) {
if let Err(e) = std::fs::create_dir_all(&logs_dir) {
error!("Failed to create error logs directory: {}", e);
return;
}
}
let filename = format!(
"logs/errors/error_{}_{}.json",
let filename = logs_dir.join(format!(
"error_{}_{}.json",
self.timestamp,
self.session_id.as_deref().unwrap_or("unknown")
);
));
match serde_json::to_string_pretty(self) {
Ok(json_content) => {
if let Err(e) = std::fs::write(&filename, json_content) {
error!("Failed to save error context to {}: {}", filename, e);
error!("Failed to save error context to {:?}: {}", &filename, e);
} else {
info!("Error details saved to: {}", filename);
info!("Error details saved to: {:?}", &filename);
}
}
Err(e) => {

View File

@@ -52,6 +52,27 @@ fn get_todo_path() -> std::path::PathBuf {
}
}
/// Get the path to the logs directory.
///
/// Checks for G3_WORKSPACE_PATH environment variable first (used by planning mode),
/// then falls back to "logs" in the current directory.
fn get_logs_dir() -> std::path::PathBuf {
if let Ok(workspace_path) = std::env::var("G3_WORKSPACE_PATH") {
std::path::PathBuf::from(workspace_path).join("logs")
} else {
std::env::current_dir().unwrap_or_default().join("logs")
}
}
/// Public accessor for the logs directory path (for use by submodules)
pub fn logs_dir() -> std::path::PathBuf {
get_logs_dir()
}
/// Environment variable name for workspace path
/// Used to direct all logs to the workspace directory
pub const G3_WORKSPACE_PATH_ENV: &str = "G3_WORKSPACE_PATH";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub tool: String,
@@ -1832,18 +1853,19 @@ impl<W: UiWriter> Agent<W> {
TOOL_LOG
.get_or_init(|| {
if let Err(e) = std::fs::create_dir_all("logs") {
let logs_dir = get_logs_dir();
if let Err(e) = std::fs::create_dir_all(&logs_dir) {
error!("Failed to create logs directory for tool log: {}", e);
return None;
}
let ts = Local::now().format("%Y%m%d_%H%M%S").to_string();
let path = format!("logs/tool_calls_{}.log", ts);
let path = logs_dir.join(format!("tool_calls_{}.log", ts));
match OpenOptions::new().create(true).append(true).open(&path) {
Ok(file) => Some(Mutex::new(file)),
Err(e) => {
error!("Failed to open tool log file {}: {}", path, e);
error!("Failed to open tool log file {:?}: {}", path, e);
None
}
}
@@ -2202,9 +2224,9 @@ impl<W: UiWriter> Agent<W> {
.as_secs();
// Create logs directory if it doesn't exist
let logs_dir = std::path::Path::new("logs");
let logs_dir = get_logs_dir();
if !logs_dir.exists() {
if let Err(e) = std::fs::create_dir_all(logs_dir) {
if let Err(e) = std::fs::create_dir_all(&logs_dir) {
error!("Failed to create logs directory: {}", e);
return;
}
@@ -2212,9 +2234,9 @@ impl<W: UiWriter> Agent<W> {
// Use session-based filename if we have a session ID, otherwise fall back to timestamp
let filename = if let Some(ref session_id) = self.session_id {
format!("logs/g3_session_{}.json", session_id)
logs_dir.join(format!("g3_session_{}.json", session_id))
} else {
format!("logs/g3_context_{}.json", timestamp)
logs_dir.join(format!("g3_context_{}.json", timestamp))
};
let context_data = serde_json::json!({
@@ -2231,8 +2253,8 @@ impl<W: UiWriter> Agent<W> {
match serde_json::to_string_pretty(&context_data) {
Ok(json_content) => {
if let Err(e) = std::fs::write(&filename, json_content) {
error!("Failed to save context window to {}: {}", filename, e);
if let Err(e) = std::fs::write(&filename, &json_content) {
error!("Failed to save context window to {:?}: {}", &filename, e);
}
}
Err(e) => {
@@ -2290,17 +2312,17 @@ impl<W: UiWriter> Agent<W> {
};
// Create logs directory if it doesn't exist
let logs_dir = std::path::Path::new("logs");
let logs_dir = get_logs_dir();
if !logs_dir.exists() {
if let Err(e) = std::fs::create_dir_all(logs_dir) {
if let Err(e) = std::fs::create_dir_all(&logs_dir) {
error!("Failed to create logs directory: {}", e);
return;
}
}
// Generate filename using same pattern as save_context_window
let filename = format!("logs/context_window_{}.txt", session_id);
let symlink_path = "logs/current_context_window";
let filename = logs_dir.join(format!("context_window_{}.txt", session_id));
let symlink_path = logs_dir.join("current_context_window");
// Build the summary content
let mut summary_lines = Vec::new();
@@ -2354,23 +2376,23 @@ impl<W: UiWriter> Agent<W> {
let summary_content = summary_lines.join("");
if let Err(e) = std::fs::write(&filename, summary_content) {
error!(
"Failed to write context window summary to {}: {}",
filename, e
"Failed to write context window summary to {:?}: {}",
&filename, e
);
return;
}
// Update symlink
// Remove old symlink if it exists
let _ = std::fs::remove_file(symlink_path);
let _ = std::fs::remove_file(&symlink_path);
// Create new symlink
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let target = format!("context_window_{}.txt", session_id);
if let Err(e) = symlink(&target, symlink_path) {
error!("Failed to create symlink {}: {}", symlink_path, e);
if let Err(e) = symlink(&target, &symlink_path) {
error!("Failed to create symlink {:?}: {}", &symlink_path, e);
}
}
@@ -2378,13 +2400,13 @@ impl<W: UiWriter> Agent<W> {
{
use std::os::windows::fs::symlink_file;
let target = format!("context_window_{}.txt", session_id);
if let Err(e) = symlink_file(&target, symlink_path) {
error!("Failed to create symlink {}: {}", symlink_path, e);
if let Err(e) = symlink_file(&target, &symlink_path) {
error!("Failed to create symlink {:?}: {}", &symlink_path, e);
}
}
debug!(
"Context window summary written to {} ({} messages)",
"Context window summary written to {:?} ({} messages)",
filename,
self.context_window.conversation_history.len()
);
@@ -2443,7 +2465,8 @@ impl<W: UiWriter> Agent<W> {
.unwrap_or_default()
.as_secs();
let filename = format!("logs/g3_session_{}.json", session_id);
let logs_dir = get_logs_dir();
let filename = logs_dir.join(format!("g3_session_{}.json", session_id));
// Read existing session log
let mut session_data: serde_json::Value = if std::path::Path::new(&filename).exists() {
@@ -5293,8 +5316,11 @@ impl<W: UiWriter> Agent<W> {
});
// If all todos are complete, delete the file instead of writing
if !has_incomplete && (content_str.contains("- [x]") || content_str.contains("- [X]")) {
let todo_path = get_todo_path();
// EXCEPT in planner mode (G3_TODO_PATH is set) - preserve for rename to completed_todo_*.md
let in_planner_mode = std::env::var("G3_TODO_PATH").is_ok();
let todo_path = get_todo_path();
if !in_planner_mode && !has_incomplete && (content_str.contains("- [x]") || content_str.contains("- [X]")) {
if todo_path.exists() {
match std::fs::remove_file(&todo_path) {
Ok(_) => {
@@ -5315,7 +5341,6 @@ impl<W: UiWriter> Agent<W> {
}
// Write to todo.g3.md file (uses G3_TODO_PATH env var if set, else current dir)
let todo_path = get_todo_path();
match std::fs::write(&todo_path, content_str) {
Ok(_) => {

View File

@@ -11,8 +11,6 @@ use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::Path;
use crate::prompts;
/// Format a timestamp for planner_history.txt entries
/// Format: YYYY-MM-DD HH:MM:SS (ISO 8601 for readability)
pub fn format_timestamp() -> String {

View File

@@ -196,12 +196,19 @@ pub fn extract_summary(response: &str) -> Option<String> {
/// Write the codebase report to logs directory
fn write_code_report(report: &str) -> Result<()> {
// Ensure logs directory exists
fs::create_dir_all("logs")?;
// Get logs directory from workspace path or current dir
let logs_dir = if let Ok(workspace_path) = std::env::var("G3_WORKSPACE_PATH") {
std::path::PathBuf::from(workspace_path).join("logs")
} else {
std::env::current_dir().unwrap_or_default().join("logs")
};
// Ensure logs directory exists
fs::create_dir_all(&logs_dir)?;
// Generate timestamp in same format as tool_calls log
let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
let filename = format!("logs/code_report_{}.log", timestamp);
let filename = logs_dir.join(format!("code_report_{}.log", timestamp));
// Write the report to file
let mut file = OpenOptions::new()
@@ -218,12 +225,19 @@ fn write_code_report(report: &str) -> Result<()> {
/// Write the discovery commands to logs directory
fn write_discovery_commands(commands: &[String]) -> Result<()> {
// Get logs directory from workspace path or current dir
let logs_dir = if let Ok(workspace_path) = std::env::var("G3_WORKSPACE_PATH") {
std::path::PathBuf::from(workspace_path).join("logs")
} else {
std::env::current_dir().unwrap_or_default().join("logs")
};
// Ensure logs directory exists
fs::create_dir_all("logs")?;
fs::create_dir_all(&logs_dir)?;
// Generate timestamp in same format as tool_calls log
let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
let filename = format!("logs/discovery_commands_{}.log", timestamp);
let filename = logs_dir.join(format!("discovery_commands_{}.log", timestamp));
// Write the commands to file
let mut file = OpenOptions::new()

View File

@@ -182,8 +182,33 @@ pub async fn generate_commit_message(
}
/// A simple UiWriter implementation for planner output
/// Uses single-line status updates during LLM processing
#[derive(Clone)]
pub struct PlannerUiWriter;
pub struct PlannerUiWriter {
tool_count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl Default for PlannerUiWriter {
fn default() -> Self {
Self::new()
}
}
impl PlannerUiWriter {
pub fn new() -> Self {
Self {
tool_count: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
}
}
/// Clear the current line and print a status message
fn print_status_line(&self, message: &str) {
use std::io::Write;
// Use carriage return to overwrite previous line, pad to 80 chars to clear old content
print!("\r{:<80}", message);
std::io::stdout().flush().ok();
}
}
impl g3_core::ui_writer::UiWriter for PlannerUiWriter {
fn print(&self, message: &str) {
@@ -209,7 +234,11 @@ impl g3_core::ui_writer::UiWriter for PlannerUiWriter {
}
fn print_tool_header(&self, tool_name: &str) {
println!("🔧 {}", tool_name);
// Increment tool count and show on single line
let count = self.tool_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
// Clear the "Thinking..." line and print tool header on new line
print!("\r{:<80}\n", ""); // Clear status line
println!("🔧 [{}] {}", count, tool_name);
}
fn print_tool_arg(&self, _key: &str, _value: &str) {}
@@ -218,9 +247,25 @@ impl g3_core::ui_writer::UiWriter for PlannerUiWriter {
fn print_tool_output_line(&self, _line: &str) {}
fn print_tool_output_summary(&self, _hidden_count: usize) {}
fn print_tool_timing(&self, _duration_str: &str) {}
fn print_agent_prompt(&self) {}
fn print_agent_response(&self, _content: &str) {}
fn notify_sse_received(&self) {}
fn print_agent_prompt(&self) {
// Clear any status line before agent response
print!("\r{:<80}\n", "");
}
fn print_agent_response(&self, content: &str) {
// Display non-tool text messages from LLM
if !content.trim().is_empty() {
print!("{}", content);
use std::io::Write;
std::io::stdout().flush().ok();
}
}
fn notify_sse_received(&self) {
// Show "Thinking..." status on single line
self.print_status_line("💭 Thinking...");
}
fn flush(&self) {
use std::io::Write;
@@ -254,7 +299,7 @@ pub async fn call_refinement_llm_with_tools(
// Create agent with planner config
let planner_config = config.for_planner()?;
let ui_writer = PlannerUiWriter;
let ui_writer = PlannerUiWriter::new();
// Create project pointing to codepath as workspace
let workspace = std::path::PathBuf::from(codepath);

View File

@@ -11,7 +11,6 @@ use std::path::{Path, PathBuf};
use crate::git;
use crate::history;
use crate::llm;
use crate::prompts;
use crate::state::{
ApprovalChoice, BranchConfirmChoice, CompletionChoice, DirtyFilesChoice,
PlannerState, RecoveryChoice, RecoveryInfo,
@@ -482,14 +481,14 @@ pub fn stage_and_commit(
return Ok(());
}
// Log commit to history BEFORE making the commit (provides audit trail even if commit fails)
history::write_git_commit(&config.plan_dir(), summary)?;
// Make commit
print_msg("📝 Making git commit...");
let _commit_sha = git::commit(&config.codepath, summary, description)?;
print_msg("✅ Commit successful");
// Log commit to history
history::write_git_commit(&config.plan_dir(), summary)?;
Ok(())
}
@@ -588,6 +587,9 @@ pub async fn run_coach_player_loop(
// Set environment variable for custom todo path
std::env::set_var("G3_TODO_PATH", planner_config.todo_path().display().to_string());
// Set environment variable for workspace path (used for logs)
std::env::set_var("G3_WORKSPACE_PATH", planner_config.codepath.display().to_string());
let mut turn = 1;
let mut coach_feedback = String::new();
@@ -598,7 +600,7 @@ pub async fn run_coach_player_loop(
print_msg("🎯 Player: Implementing requirements...");
let player_config = g3_config.for_player()?;
let ui_writer = llm::PlannerUiWriter;
let ui_writer = llm::PlannerUiWriter::new();
let mut player_agent = Agent::new_autonomous_with_readme_and_quiet(
player_config,
ui_writer,
@@ -633,7 +635,7 @@ pub async fn run_coach_player_loop(
print_msg("🎓 Coach: Reviewing implementation...");
let coach_config = g3_config.for_coach()?;
let coach_ui_writer = llm::PlannerUiWriter;
let coach_ui_writer = llm::PlannerUiWriter::new();
let mut coach_agent = Agent::new_autonomous_with_readme_and_quiet(
coach_config,
coach_ui_writer,
@@ -657,7 +659,19 @@ pub async fn run_coach_player_loop(
return Ok(());
}
coach_feedback = result.response;
print_msg(&format!("📝 Coach feedback: {} chars", coach_feedback.len()));
// Display first 25 lines of coach feedback
let lines: Vec<&str> = coach_feedback.lines().collect();
let display_lines = if lines.len() > 25 {
let mut truncated: Vec<&str> = lines[..25].to_vec();
truncated.push("...");
truncated
} else {
lines
};
print_msg(&format!("📝 Coach feedback ({} chars):", coach_feedback.len()));
for line in display_lines {
print_msg(&format!(" {}", line));
}
}
Err(e) => {
print_msg(&format!("⚠️ Coach error: {}", e));

View File

@@ -0,0 +1,124 @@
{{CURRENT REQUIREMENTS}}
These requirements refine the planner mode implementation in `g3-planner` crate.
## 1. Display Coach Feedback Content (Not Just Length)
**Location**: `crates/g3-planner/src/planner.rs`, `run_coach_player_loop()` function around line 610
**Current behavior**:
```rust
coach_feedback = result.response;
print_msg(&format!("📝 Coach feedback: {} chars", coach_feedback.len()));
```
**Required change**:
- Display the first 25 lines of coach feedback content (not just the character count)
- Truncate with "..." indicator if feedback exceeds 25 lines
- Keep showing the char count as secondary info
**Example output**:
```
📝 Coach feedback (1234 chars):
The implementation looks good but needs:
1. Error handling for edge cases
2. Unit tests for the new function
...
```
## 2. TODO File Location and Preservation in Planning Mode
**Issue**: The TODO file must be:
1. Ensure Written to `<codepath>/g3-plan/todo.g3.md` during implementation (this appears to work via `G3_TODO_PATH` env var)
2. If anything in the system prompt or elsewhere instructs deletion, do NOT delete when in planner mode, since it needs to be renamed to `completed_todo_<timestamp>.md`
**Current behavior to verify**:
- `G3_TODO_PATH` is set in `run_coach_player_loop()` at line ~596
- The `todo_read` and `todo_write` tools in g3-core should respect this env var
**Required changes**:
- In `prompt_for_new_requirements()` function (around line 255), the code deletes `todo.g3.md` when starting fresh refinement. This is correct behavior.
- Verify that during the coach/player loop, the TODO file is NOT deleted by the final_output tool or any cleanup logic
- If there is cleanup logic or other code other than the rename in at completion in planning, add a mechanism to prevent TODO deletion in planner mode (e.g., check for `G3_TODO_PATH` env var or add a planner mode flag)
**Files to check**:
- `crates/g3-core/src/lib.rs` - `todo_write` tool implementation, ensure it respects `G3_TODO_PATH`
- Check if `final_output` tool deletes the TODO file
## 3. Write GIT COMMIT Entry BEFORE Actual Commit
**Location**: `crates/g3-planner/src/planner.rs`, `stage_and_commit()` function around line 568
**Current behavior**:
```rust
// Make commit
print_msg("📝 Making git commit...");
let _commit_sha = git::commit(&config.codepath, summary, description)?;
print_msg("✅ Commit successful");
// Log commit to history (AFTER commit - wrong order)
history::write_git_commit(&config.plan_dir(), summary)?;
```
**Required change**:
After getting user go-ahead to commit, then do:
```rust
// Log commit to history BEFORE making the commit
history::write_git_commit(&config.plan_dir(), summary)?;
// Make commit
print_msg("📝 Making git commit...");
let _commit_sha = git::commit(&config.codepath, summary, description)?;
print_msg("✅ Commit successful");
```
**Rationale**: If the commit fails, the history will still record the attempt. This provides better audit trail and allows recovery.
## 4. Single-Line UI Updates During LLM Processing
**Location**: `crates/g3-planner/src/llm.rs`, `PlannerUiWriter` implementation
**Current behavior**:
- `print_tool_header` prints each tool on a new line
- Agent text responses are not displayed during refinement
**Required changes**:
a) **Single-line status updates**: Instead of printing a new line for each tool call, use carriage return (`\r`) to update a single status line:
- Show "Thinking..." while waiting
- Show context window size (if available)
- Show tool count: "Executing tool 3..."
- Use `print!("\r{:<80}", status_line)` pattern to overwrite previous line
b) **Display non-tool text messages**: When the LLM sends text content (not tool calls), print it to the UI:
- Implement `print_agent_response(&self, content: &str)` to actually print content
- This allows the planner to communicate its reasoning to the user
## 5. Write Logs to Workspace Path (Not Relative)
Logs are written to the current/or codepath directory. Instead write them to the workspace path.
This applies to logs such as conversation history, tools calls, context window, errors etc...
*ALL logs throughout the g3 codebase* should be exclusively written to <workspace>/logs.
{{ORIGINAL USER REQUIREMENTS -- THIS SECTION WILL BE IGNORED BY THE IMPLEMENTATION}}
1.
In planner.rs Show coach feedback: up to 25 lines
coach_feedback = result.response;
print_msg(&format!("📝 Coach feedback: {} chars", coach_feedback.len()));
2.
I can't find where the TODO file is written during implementation in planning mode. Please check that it's written to the g3-plan directory.
It looks like there are explicit instructions to delete the TODO file when complete, potentially in player mode. DO NOT ALLOW it to be deleted when in planner mode since we want to copy it for history.
3.
Make sure to write the "GIT COMMIT (<message>)" to the planner_history.txt file *immediately before* doing the actual commit (not after, like the current implementation does).
4. In planner mode, do not write a new line in UI writer for each tool call. Instead keep a single line that says "thinking...." While the llm is working. Keep each update on a single line (use backspace or something to erase the last update?) and show the context window size and that we're waiting for the llm to finish tool calls. HOWEVER, DO PRINT to the UI all non-tool comments (text messages) that the llm sends (that's currently not happening).
5. Logs are written to the <codepath> directory. Instead write them to the workspace path.

View File

@@ -0,0 +1,26 @@
# G3 Planner Requirements Review
## 1. Display Coach Feedback Content (Not Just Length)
- [x] Display first 25 lines of coach feedback content
- [x] Truncate with "..." indicator if feedback exceeds 25 lines
- [x] Keep showing char count as secondary info
## 2. TODO File Location and Preservation in Planning Mode
- [x] G3_TODO_PATH is set in run_coach_player_loop()
- [x] todo_write checks for planner mode before deletion
- [x] TODO file preserved for rename to completed_todo_*.md
## 3. Write GIT COMMIT Entry BEFORE Actual Commit
- [x] history::write_git_commit() called at line 485
- [x] git::commit() called at line 489 (AFTER history write)
## 4. Single-Line UI Updates During LLM Processing
- [x] print_status_line uses \r to overwrite previous line
- [x] notify_sse_received shows "Thinking..." status
- [x] print_tool_header clears status line and prints tool on new line
- [x] print_agent_response displays non-tool text messages
## 5. Write Logs to Workspace Path (Not Relative)
- [x] G3_WORKSPACE_PATH set in run_coach_player_loop()
- [x] get_logs_dir() checks G3_WORKSPACE_PATH first
- [x] All logging uses get_logs_dir()

View File

@@ -1,19 +0,0 @@
1.
In planner.rs Show coach feedback: up to 25 lines
coach_feedback = result.response;
print_msg(&format!("📝 Coach feedback: {} chars", coach_feedback.len()));
2.
I cant find where the TODO file is written during implementation in planning mode. Please check that its written to the g3-plan directory.
It looks like there are explicit instructions to delete the TODO file when complete, potentially in player mode. DO NOT ALLOW it to be deleted when in planner mode since we want to copy it for history.
3.
Make sure to write the “GIT COMMIT (<message>)” to the planner_history.txt file *immediately before* doing the actual commit (not after, like the current implementation does).
4. In planner mode, do not write a new line in UI writer for each tool call. Instead keep a single line that says “thinking....” While the llm is working. Keep each update on a single line (use backspace or something to erase the last update?) and show the context window size and that were waiting for the llm to finish tool calls. HOWEVER, DO PRINT to the UI all non-tool comments (text messages) that the llm sends (thats currently not happening).
5.Logs are written to the <codepath> directory. Instead write them to the workspace path.

View File

@@ -6,3 +6,14 @@
>>
2025-12-08 18:30:00 - COMPLETED REQUIREMENTS (completed_requirements_2025-12-08_18-30-00.md)
2025-12-08 18:30:01 - GIT COMMIT (Implement planning mode)
2025-12-09 14:47:50 - REFINING REQUIREMENTS (new_requirements.md)
2025-12-09 15:23:04 - GIT HEAD (9a3688fd05f099225652f705bc7b0715b6abbe44)
2025-12-09 15:23:10 - START IMPLEMENTING (current_requirements.md)
<<
Planner mode refinements for g3-planner: display first 25 lines of coach feedback (not just char count), ensure TODO
file writes to g3-plan dir and prevent deletion during planning (needed for history rename), write GIT COMMIT history
entry before actual commit for better audit trail, use single-line UI updates with carriage return during LLM processing
(show thinking/tool count/context size) while still printing agent text responses, and redirect all logs to workspace...
>>
2025-12-09 16:16:51 - COMPLETED REQUIREMENTS (completed_requirements_2025-12-09_16-16-51.md, completed_todo_2025-12-09_16-16-51.md)
2025-12-09 16:17:54 - GIT COMMIT (Refine planner mode UI, logging, and history tracking)