Skip to content

Commit 2092400

Browse files
fix(prompt-gen): auto-update STATE.md and improve skip logic for completed features
- What: skip features with progress.last_done == P4, not just status == READY - What: auto-update STATE.md when step completes if status is UNKNOWN - What: add expected_status_for_step() and update_state_status() functions - What: add tests for new state management functions - Why: fix issue where completed features (P4) with missing STATE.md would re-run from start 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b383575 commit 2092400

File tree

2 files changed

+154
-7
lines changed

2 files changed

+154
-7
lines changed

src/features/prompt_gen/interactive.rs

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ use dialoguer::{theme::ColorfulTheme, Confirm, FuzzySelect, Input, MultiSelect,
1515
use std::path::{Path, PathBuf};
1616

1717
use super::executor::{CliType, Executor, ExecutorConfig};
18-
use super::progress::{read_state_status, FeatureInfo, FeatureStatus, Step};
18+
use super::progress::{
19+
expected_status_for_step, read_state_status, update_state_status, FeatureInfo, FeatureStatus,
20+
Step,
21+
};
1922

2023
// ============================================================================
2124
// 執行模式
@@ -652,8 +655,28 @@ impl InteractiveRunner {
652655
feature.progress.mark_done(step, result.session_id);
653656
feature.progress.save_to_file(feature.progress_file())?;
654657

655-
// 更新狀態
656-
feature.status = read_state_status(feature.state_file())?;
658+
// 自動更新 STATE.md(如果 LLM 沒有更新的話)
659+
let expected_status = expected_status_for_step(step);
660+
let current_status = read_state_status(feature.state_file())?;
661+
662+
// 只有當狀態落後或未知時才自動更新
663+
if current_status == FeatureStatus::Unknown
664+
|| (step == Step::P4 && !current_status.is_ready())
665+
{
666+
update_state_status(
667+
feature.state_file(),
668+
&feature.feature_key,
669+
expected_status.clone(),
670+
)?;
671+
feature.status = expected_status;
672+
println!(
673+
"{} STATE.md 已自動更新: {}",
674+
style("[狀態]").magenta(),
675+
feature.status
676+
);
677+
} else {
678+
feature.status = current_status;
679+
}
657680

658681
println!(
659682
"{} 進度已保存: {}",
@@ -717,12 +740,19 @@ impl InteractiveRunner {
717740
let feature_dir = self.features[idx].feature_dir.clone();
718741
self.features[idx] = FeatureInfo::load_from_dir(&feature_dir, &feature_key)?;
719742

720-
// 檢查是否已完成
721-
if self.features[idx].status.is_ready() {
743+
// 檢查是否已完成(P4 完成或 status 為 READY)
744+
let is_completed = self.features[idx].progress.last_done == Step::P4
745+
|| self.features[idx].status.is_ready();
746+
if is_completed {
722747
println!(
723-
"{} 功能 {} 已就緒,跳過",
748+
"{} 功能 {} 已完成 ({}), 跳過",
724749
style("[跳過]").dim(),
725-
style(&feature_key).cyan()
750+
style(&feature_key).cyan(),
751+
if self.features[idx].status.is_ready() {
752+
"READY"
753+
} else {
754+
"P4 done"
755+
}
726756
);
727757
continue;
728758
}

src/features/prompt_gen/progress.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,62 @@ pub fn read_state_status<P: AsRef<Path>>(state_file: P) -> Result<FeatureStatus>
287287
Ok(FeatureStatus::Unknown)
288288
}
289289

290+
/// 根據步驟取得預期狀態
291+
pub fn expected_status_for_step(step: Step) -> FeatureStatus {
292+
match step {
293+
Step::None => FeatureStatus::Unknown,
294+
Step::P1 => FeatureStatus::P1DoneIntDeployed,
295+
Step::P2 => FeatureStatus::P2E2EPassed,
296+
Step::P3 => FeatureStatus::P3RefactoredIntDeployed,
297+
Step::P4 => FeatureStatus::Ready,
298+
}
299+
}
300+
301+
/// 更新 STATE.md 檔案中的狀態
302+
pub fn update_state_status<P: AsRef<Path>>(
303+
state_file: P,
304+
feature_key: &str,
305+
new_status: FeatureStatus,
306+
) -> Result<()> {
307+
let path = state_file.as_ref();
308+
let status_str = new_status.to_string();
309+
310+
// 確保父目錄存在
311+
if let Some(parent) = path.parent() {
312+
std::fs::create_dir_all(parent)?;
313+
}
314+
315+
if path.exists() {
316+
// 更新現有檔案
317+
let content = std::fs::read_to_string(path)
318+
.with_context(|| format!("無法讀取狀態檔案:{}", path.display()))?;
319+
320+
let re = Regex::new(r"STATUS[::]\s*\S+")?;
321+
322+
let new_content = if re.is_match(&content) {
323+
// 替換現有 STATUS
324+
re.replace(&content, format!("STATUS: {}", status_str))
325+
.to_string()
326+
} else {
327+
// 在檔案開頭加入 STATUS
328+
format!("STATUS: {}\n\n{}", status_str, content)
329+
};
330+
331+
std::fs::write(path, new_content)
332+
.with_context(|| format!("無法寫入狀態檔案:{}", path.display()))?;
333+
} else {
334+
// 建立新檔案
335+
let content = format!(
336+
"# {} State\n\nFEATURE_KEY: {}\nSTATUS: {}\n\n## Notes\n\n(Auto-generated by runner)\n",
337+
feature_key, feature_key, status_str
338+
);
339+
std::fs::write(path, content)
340+
.with_context(|| format!("無法建立狀態檔案:{}", path.display()))?;
341+
}
342+
343+
Ok(())
344+
}
345+
290346
// ============================================================================
291347
// 功能資訊
292348
// ============================================================================
@@ -398,4 +454,65 @@ LAST_DONE="p2"
398454
FeatureStatus::P1DoneIntDeployed
399455
);
400456
}
457+
458+
#[test]
459+
fn test_expected_status_for_step() {
460+
assert_eq!(expected_status_for_step(Step::None), FeatureStatus::Unknown);
461+
assert_eq!(
462+
expected_status_for_step(Step::P1),
463+
FeatureStatus::P1DoneIntDeployed
464+
);
465+
assert_eq!(
466+
expected_status_for_step(Step::P2),
467+
FeatureStatus::P2E2EPassed
468+
);
469+
assert_eq!(
470+
expected_status_for_step(Step::P3),
471+
FeatureStatus::P3RefactoredIntDeployed
472+
);
473+
assert_eq!(expected_status_for_step(Step::P4), FeatureStatus::Ready);
474+
}
475+
476+
#[test]
477+
fn test_update_state_status_creates_file() {
478+
let temp_dir = std::env::temp_dir().join("test_state_md");
479+
std::fs::create_dir_all(&temp_dir).unwrap();
480+
let state_file = temp_dir.join("STATE.md");
481+
482+
// 確保檔案不存在
483+
let _ = std::fs::remove_file(&state_file);
484+
485+
// 建立新檔案
486+
update_state_status(&state_file, "test-feature", FeatureStatus::Ready).unwrap();
487+
488+
let content = std::fs::read_to_string(&state_file).unwrap();
489+
assert!(content.contains("STATUS: READY"));
490+
assert!(content.contains("FEATURE_KEY: test-feature"));
491+
492+
// 清理
493+
let _ = std::fs::remove_file(&state_file);
494+
let _ = std::fs::remove_dir(&temp_dir);
495+
}
496+
497+
#[test]
498+
fn test_update_state_status_updates_existing() {
499+
let temp_dir = std::env::temp_dir().join("test_state_md_update");
500+
std::fs::create_dir_all(&temp_dir).unwrap();
501+
let state_file = temp_dir.join("STATE.md");
502+
503+
// 建立初始檔案
504+
std::fs::write(&state_file, "# Test\n\nSTATUS: P1_DONE_DEPLOYED\n\nSome content").unwrap();
505+
506+
// 更新狀態
507+
update_state_status(&state_file, "test-feature", FeatureStatus::Ready).unwrap();
508+
509+
let content = std::fs::read_to_string(&state_file).unwrap();
510+
assert!(content.contains("STATUS: READY"));
511+
assert!(!content.contains("P1_DONE_DEPLOYED"));
512+
assert!(content.contains("Some content"));
513+
514+
// 清理
515+
let _ = std::fs::remove_file(&state_file);
516+
let _ = std::fs::remove_dir(&temp_dir);
517+
}
401518
}

0 commit comments

Comments
 (0)