Story 50: Unified Current Work Directory
- Move current/ to .story_kit/current/ (out of stories/) - Type-aware routing for bugs, spikes, stories - close_bug_to_archive() for bug lifecycle - All path references updated across agents.rs, workflow.rs, mcp.rs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -177,7 +177,7 @@ pub fn load_upcoming_stories(ctx: &AppContext) -> Result<Vec<UpcomingStory>, Str
|
||||
|
||||
fn load_current_story_metadata(ctx: &AppContext) -> Result<Vec<(String, StoryMetadata)>, String> {
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let current_dir = root.join(".story_kit").join("stories").join("current");
|
||||
let current_dir = root.join(".story_kit").join("current");
|
||||
|
||||
if !current_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
@@ -513,7 +513,7 @@ impl WorkflowApi {
|
||||
#[oai(path = "/workflow/todos", method = "get")]
|
||||
async fn story_todos(&self) -> OpenApiResult<Json<TodoListResponse>> {
|
||||
let root = self.ctx.state.get_project_root().map_err(bad_request)?;
|
||||
let current_dir = root.join(".story_kit").join("stories").join("current");
|
||||
let current_dir = root.join(".story_kit").join("current");
|
||||
|
||||
if !current_dir.exists() {
|
||||
return Ok(Json(TodoListResponse {
|
||||
@@ -699,15 +699,18 @@ fn git_commit_story_file(root: &Path, filepath: &Path, story_id: &str) -> Result
|
||||
git_stage_and_commit(root, &[filepath], &msg)
|
||||
}
|
||||
|
||||
/// Locate a story file by searching current/ then upcoming/.
|
||||
/// Locate a story file by searching .story_kit/current/ then stories/upcoming/.
|
||||
fn find_story_file(project_root: &Path, story_id: &str) -> Result<PathBuf, String> {
|
||||
let base = project_root.join(".story_kit").join("stories");
|
||||
let filename = format!("{story_id}.md");
|
||||
for subdir in &["current", "upcoming"] {
|
||||
let path = base.join(subdir).join(&filename);
|
||||
if path.exists() {
|
||||
return Ok(path);
|
||||
}
|
||||
// Check unified current/ directory first
|
||||
let current_path = project_root.join(".story_kit").join("current").join(&filename);
|
||||
if current_path.exists() {
|
||||
return Ok(current_path);
|
||||
}
|
||||
// Fall back to stories/upcoming/
|
||||
let upcoming_path = project_root.join(".story_kit").join("stories").join("upcoming").join(&filename);
|
||||
if upcoming_path.exists() {
|
||||
return Ok(upcoming_path);
|
||||
}
|
||||
Err(format!(
|
||||
"Story '{story_id}' not found in current/ or upcoming/."
|
||||
@@ -852,11 +855,12 @@ fn slugify_name(name: &str) -> String {
|
||||
}
|
||||
|
||||
fn next_story_number(root: &std::path::Path) -> Result<u32, String> {
|
||||
let base = root.join(".story_kit").join("stories");
|
||||
let stories_base = root.join(".story_kit").join("stories");
|
||||
let mut max_num: u32 = 0;
|
||||
|
||||
for subdir in &["upcoming", "current", "archived"] {
|
||||
let dir = base.join(subdir);
|
||||
// Scan stories/upcoming/ and stories/archived/ for story numbers
|
||||
for subdir in &["upcoming", "archived"] {
|
||||
let dir = stories_base.join(subdir);
|
||||
if !dir.exists() {
|
||||
continue;
|
||||
}
|
||||
@@ -866,7 +870,24 @@ fn next_story_number(root: &std::path::Path) -> Result<u32, String> {
|
||||
let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
// Extract leading digits from filename
|
||||
let num_str: String = name_str.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||||
if let Ok(n) = num_str.parse::<u32>()
|
||||
&& n > max_num
|
||||
{
|
||||
max_num = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also scan unified .story_kit/current/ for story numbers
|
||||
let current_dir = root.join(".story_kit").join("current");
|
||||
if current_dir.exists() {
|
||||
for entry in
|
||||
fs::read_dir(¤t_dir).map_err(|e| format!("Failed to read current directory: {e}"))?
|
||||
{
|
||||
let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
let num_str: String = name_str.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||||
if let Ok(n) = num_str.parse::<u32>()
|
||||
&& n > max_num
|
||||
@@ -882,11 +903,16 @@ fn next_story_number(root: &std::path::Path) -> Result<u32, String> {
|
||||
pub fn validate_story_dirs(
|
||||
root: &std::path::Path,
|
||||
) -> Result<Vec<StoryValidationResult>, String> {
|
||||
let base = root.join(".story_kit").join("stories");
|
||||
let mut results = Vec::new();
|
||||
|
||||
for subdir in &["current", "upcoming"] {
|
||||
let dir = base.join(subdir);
|
||||
// Directories to validate: unified current/ + stories/upcoming/
|
||||
let dirs_to_validate: Vec<PathBuf> = vec![
|
||||
root.join(".story_kit").join("current"),
|
||||
root.join(".story_kit").join("stories").join("upcoming"),
|
||||
];
|
||||
|
||||
for dir in &dirs_to_validate {
|
||||
let subdir = dir.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
if !dir.exists() {
|
||||
continue;
|
||||
}
|
||||
@@ -1120,7 +1146,7 @@ mod tests {
|
||||
#[test]
|
||||
fn validate_story_dirs_valid_files() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".story_kit/stories/current");
|
||||
let current = tmp.path().join(".story_kit/current");
|
||||
let upcoming = tmp.path().join(".story_kit/stories/upcoming");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
@@ -1144,7 +1170,7 @@ mod tests {
|
||||
#[test]
|
||||
fn validate_story_dirs_missing_front_matter() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".story_kit/stories/current");
|
||||
let current = tmp.path().join(".story_kit/current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("28_todos.md"), "# No front matter\n").unwrap();
|
||||
|
||||
@@ -1157,7 +1183,7 @@ mod tests {
|
||||
#[test]
|
||||
fn validate_story_dirs_missing_required_fields() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".story_kit/stories/current");
|
||||
let current = tmp.path().join(".story_kit/current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(current.join("28_todos.md"), "---\n---\n# Story\n").unwrap();
|
||||
|
||||
@@ -1172,7 +1198,7 @@ mod tests {
|
||||
#[test]
|
||||
fn validate_story_dirs_missing_test_plan_only() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".story_kit/stories/current");
|
||||
let current = tmp.path().join(".story_kit/current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::write(
|
||||
current.join("28_todos.md"),
|
||||
@@ -1244,7 +1270,7 @@ mod tests {
|
||||
fn next_story_number_scans_all_dirs() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let upcoming = tmp.path().join(".story_kit/stories/upcoming");
|
||||
let current = tmp.path().join(".story_kit/stories/current");
|
||||
let current = tmp.path().join(".story_kit/current");
|
||||
let archived = tmp.path().join(".story_kit/stories/archived");
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
@@ -1354,7 +1380,7 @@ mod tests {
|
||||
fn check_criterion_marks_first_unchecked() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo(tmp.path());
|
||||
let current = tmp.path().join(".story_kit/stories/current");
|
||||
let current = tmp.path().join(".story_kit/current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("1_test.md");
|
||||
fs::write(&filepath, story_with_criteria(3)).unwrap();
|
||||
@@ -1381,7 +1407,7 @@ mod tests {
|
||||
fn check_criterion_marks_second_unchecked() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo(tmp.path());
|
||||
let current = tmp.path().join(".story_kit/stories/current");
|
||||
let current = tmp.path().join(".story_kit/current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("2_test.md");
|
||||
fs::write(&filepath, story_with_criteria(3)).unwrap();
|
||||
@@ -1408,7 +1434,7 @@ mod tests {
|
||||
fn check_criterion_out_of_range_returns_error() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo(tmp.path());
|
||||
let current = tmp.path().join(".story_kit/stories/current");
|
||||
let current = tmp.path().join(".story_kit/current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("3_test.md");
|
||||
fs::write(&filepath, story_with_criteria(2)).unwrap();
|
||||
@@ -1434,7 +1460,7 @@ mod tests {
|
||||
fn set_test_plan_updates_pending_to_approved() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo(tmp.path());
|
||||
let current = tmp.path().join(".story_kit/stories/current");
|
||||
let current = tmp.path().join(".story_kit/current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("4_test.md");
|
||||
fs::write(
|
||||
@@ -1464,7 +1490,7 @@ mod tests {
|
||||
fn set_test_plan_missing_field_returns_error() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo(tmp.path());
|
||||
let current = tmp.path().join(".story_kit/stories/current");
|
||||
let current = tmp.path().join(".story_kit/current");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
let filepath = current.join("5_test.md");
|
||||
fs::write(
|
||||
@@ -1491,7 +1517,7 @@ mod tests {
|
||||
#[test]
|
||||
fn find_story_file_searches_current_then_upcoming() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let current = tmp.path().join(".story_kit/stories/current");
|
||||
let current = tmp.path().join(".story_kit/current");
|
||||
let upcoming = tmp.path().join(".story_kit/stories/upcoming");
|
||||
fs::create_dir_all(¤t).unwrap();
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user