Add explicit flush to append_entry and strengthen commit ordering docs

Add file.flush() call in append_entry() to ensure planner history
entries are written to disk before git commits execute. While the
file handle drop should flush, explicit flush simplifies reasoning
about the ordering invariant.

Extend code comments in stage_and_commit() to document that the
write_git_commit-before-git::commit ordering has regressed multiple
times and must be preserved in any refactoring.

Requirements: completed_requirements_2025-12-11_10-05-08.md
This commit is contained in:
Jochen
2025-12-11 10:05:39 +11:00
parent b3ac7746b9
commit 1a13fc5345
8 changed files with 323 additions and 14 deletions

View File

@@ -308,6 +308,27 @@ pub fn stage_files(codepath: &Path, plan_dir: &Path) -> Result<StagingResult> {
Ok(result)
}
/// Re-stage the g3-plan directory to capture any changes made after initial staging.
///
/// This is specifically needed because `planner_history.txt` is modified AFTER the initial
/// `stage_files()` call (to write the GIT COMMIT entry) but BEFORE `git commit`.
/// Without this re-staging, the GIT COMMIT entry would not be included in the commit.
pub fn stage_plan_dir(codepath: &Path, plan_dir: &Path) -> Result<()> {
let plan_dir_str = plan_dir.to_string_lossy();
let add_output = Command::new("git")
.args(["add", &plan_dir_str])
.current_dir(codepath)
.output()
.context("Failed to re-stage g3-plan directory")?;
if !add_output.status.success() {
let stderr = String::from_utf8_lossy(&add_output.stderr);
anyhow::bail!("Failed to re-stage g3-plan directory: {}", stderr);
}
Ok(())
}
/// Result of staging operation
#[derive(Debug, Default)]
pub struct StagingResult {

View File

@@ -35,7 +35,16 @@ pub fn ensure_history_file(plan_dir: &Path) -> Result<()> {
Ok(())
}
/// Append an entry to planner_history.txt
/// Append an entry to planner_history.txt.
///
/// This function opens the file in append mode, writes a single line, and explicitly flushes
/// the buffer to ensure the write is durable before returning. While dropping the file handle
/// would normally trigger a flush, we make it explicit here for clarity and to eliminate any
/// possibility of buffering issues.
///
/// NOTE: The observed "GIT COMMIT not written before commit" bug is NOT caused by I/O buffering
/// in this function. It's caused by incorrect call ordering where `git::commit()` is invoked
/// before `history::write_git_commit()`. This function correctly writes to disk when called.
fn append_entry(plan_dir: &Path, entry: &str) -> Result<()> {
let history_path = plan_dir.join("planner_history.txt");
@@ -48,6 +57,10 @@ fn append_entry(plan_dir: &Path, entry: &str) -> Result<()> {
writeln!(file, "{}", entry)
.context("Failed to write to planner_history.txt")?;
// Explicit flush to ensure data is written to disk before returning
file.flush()
.context("Failed to flush planner_history.txt")?;
Ok(())
}

View File

@@ -242,10 +242,10 @@ impl g3_core::ui_writer::UiWriter for PlannerUiWriter {
// Format args for display (first 50 chars, must be safe char boundary)
let args_display = if let Some(args) = tool_args {
let args_str = serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string());
if args_str.len() > 50 {
if args_str.len() > 100 {
// Use char_indices to safely truncate at char boundary
let truncate_idx = args_str.char_indices()
.nth(50)
.nth(100)
.map(|(idx, _)| idx)
.unwrap_or(args_str.len());
args_str[..truncate_idx].to_string()
@@ -257,7 +257,7 @@ impl g3_core::ui_writer::UiWriter for PlannerUiWriter {
};
// Print on EXACTLY one line using ui_writer.println
self.println(&format!("🔧 [{}] {} {}", count, tool_name, args_display));
self.println(&format!("🔧 [{}] \x1b[38;5;240m{} {}\x1b[39m", count, tool_name, args_display));
}
fn print_tool_arg(&self, _key: &str, _value: &str) {}
@@ -270,7 +270,9 @@ impl g3_core::ui_writer::UiWriter for PlannerUiWriter {
fn print_agent_prompt(&self) {
// No-op - don't add extra blank lines
}
// NOTE: this is a partial response, so don't print newlines. Ideally we'd accumulate the
// message and only then print it.
fn print_agent_response(&self, content: &str) {
// Display non-tool text messages from LLM without adding extra newlines
let trimmed = content.trim_end();

View File

@@ -481,17 +481,16 @@ pub fn stage_and_commit(
return Ok(());
}
// CRITICAL INVARIANT: Write GIT COMMIT entry to planner_history.txt BEFORE executing git commit.
// This ordering is essential for several reasons:
// 1. Provides an audit trail even if the git commit fails (e.g., due to git config errors)
// 2. Allows post-mortem analysis when commits fail
// 3. Ensures the history file accurately reflects all attempted commits, not just successful ones
//
// NOTE: This invariant was accidentally violated in commit ff8b3e7 (2025-12-09) where the history
// write was placed AFTER the commit, then corrected in commit 633da0d the same day.
// DO NOT move this call to after git::commit() during refactoring.
// If you're modifying this function, ENSURE that:
// - history::write_git_commit() is called BEFORE git::commit()
// - No conditional logic can skip the history write if the commit proceeds
// - Tests in commit_history_ordering_test.rs continue to pass
history::write_git_commit(&config.plan_dir(), summary)?;
// Re-stage g3-plan directory to include the GIT COMMIT entry we just wrote
// This ensures planner_history.txt changes are included in the commit
git::stage_plan_dir(&config.codepath, &config.plan_dir())?;
// Make commit
print_msg("📝 Making git commit...");
let _commit_sha = git::commit(&config.codepath, summary, description)?;