story-kit: merge 151_story_split_archived_into_done_and_archived_with_time_based_promotion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-24 17:01:57 +00:00
parent 95ed60401f
commit aef022c74c
8 changed files with 212 additions and 49 deletions

View File

@@ -2031,9 +2031,9 @@ fn item_source_dir(project_root: &Path, _item_id: &str) -> PathBuf {
project_root.join(".story_kit").join("work").join("1_upcoming")
}
/// Return the archive directory path for a work item (always work/5_archived/).
/// Return the done directory path for a work item (always work/5_done/).
fn item_archive_dir(project_root: &Path, _item_id: &str) -> PathBuf {
project_root.join(".story_kit").join("work").join("5_archived")
project_root.join(".story_kit").join("work").join("5_done")
}
/// Move a work item (story, bug, or spike) from `work/1_upcoming/` to `work/2_current/`.
@@ -2075,21 +2075,22 @@ pub fn move_story_to_current(project_root: &Path, story_id: &str) -> Result<(),
Ok(())
}
/// Move a story from `work/2_current/` to `work/5_archived/` and auto-commit.
/// Move a story from `work/2_current/` to `work/5_done/` and auto-commit.
///
/// * If the story is in `2_current/`, it is moved to `5_archived/` and committed.
/// * If the story is in `4_merge/`, it is moved to `5_archived/` and committed.
/// * If the story is already in `5_archived/`, this is a no-op (idempotent).
/// * If the story is not found in `2_current/`, `4_merge/`, or `5_archived/`, an error is returned.
/// * If the story is in `2_current/`, it is moved to `5_done/` and committed.
/// * If the story is in `4_merge/`, it is moved to `5_done/` and committed.
/// * If the story is already in `5_done/` or `6_archived/`, this is a no-op (idempotent).
/// * If the story is not found in `2_current/`, `4_merge/`, `5_done/`, or `6_archived/`, an error is returned.
pub fn move_story_to_archived(project_root: &Path, story_id: &str) -> Result<(), String> {
let sk = project_root.join(".story_kit").join("work");
let current_path = sk.join("2_current").join(format!("{story_id}.md"));
let merge_path = sk.join("4_merge").join(format!("{story_id}.md"));
let archived_dir = sk.join("5_archived");
let archived_path = archived_dir.join(format!("{story_id}.md"));
let done_dir = sk.join("5_done");
let done_path = done_dir.join(format!("{story_id}.md"));
let archived_path = sk.join("6_archived").join(format!("{story_id}.md"));
if archived_path.exists() {
// Already archived — idempotent, nothing to do.
if done_path.exists() || archived_path.exists() {
// Already in done or archived — idempotent, nothing to do.
return Ok(());
}
@@ -2104,17 +2105,17 @@ pub fn move_story_to_archived(project_root: &Path, story_id: &str) -> Result<(),
));
};
std::fs::create_dir_all(&archived_dir)
.map_err(|e| format!("Failed to create work/5_archived/ directory: {e}"))?;
std::fs::rename(&source_path, &archived_path)
.map_err(|e| format!("Failed to move story '{story_id}' to 5_archived/: {e}"))?;
std::fs::create_dir_all(&done_dir)
.map_err(|e| format!("Failed to create work/5_done/ directory: {e}"))?;
std::fs::rename(&source_path, &done_path)
.map_err(|e| format!("Failed to move story '{story_id}' to 5_done/: {e}"))?;
let from_dir = if source_path == current_path {
"work/2_current/"
} else {
"work/4_merge/"
};
slog!("[lifecycle] Moved story '{story_id}' from {from_dir} to work/5_archived/");
slog!("[lifecycle] Moved story '{story_id}' from {from_dir} to work/5_done/");
Ok(())
}
@@ -2192,11 +2193,11 @@ pub fn move_story_to_qa(project_root: &Path, story_id: &str) -> Result<(), Strin
Ok(())
}
/// Move a bug from `work/2_current/` or `work/1_upcoming/` to `work/5_archived/` and auto-commit.
/// Move a bug from `work/2_current/` or `work/1_upcoming/` to `work/5_done/` and auto-commit.
///
/// * If the bug is in `2_current/`, it is moved to `5_archived/` and committed.
/// * If the bug is still in `1_upcoming/` (never started), it is moved directly to `5_archived/`.
/// * If the bug is already in `5_archived/`, this is a no-op (idempotent).
/// * If the bug is in `2_current/`, it is moved to `5_done/` and committed.
/// * If the bug is still in `1_upcoming/` (never started), it is moved directly to `5_done/`.
/// * If the bug is already in `5_done/`, this is a no-op (idempotent).
/// * If the bug is not found anywhere, an error is returned.
pub fn close_bug_to_archive(project_root: &Path, bug_id: &str) -> Result<(), String> {
let sk = project_root.join(".story_kit").join("work");
@@ -2220,12 +2221,12 @@ pub fn close_bug_to_archive(project_root: &Path, bug_id: &str) -> Result<(), Str
};
std::fs::create_dir_all(&archive_dir)
.map_err(|e| format!("Failed to create work/5_archived/ directory: {e}"))?;
.map_err(|e| format!("Failed to create work/5_done/ directory: {e}"))?;
std::fs::rename(&source_path, &archive_path)
.map_err(|e| format!("Failed to move bug '{bug_id}' to 5_archived/: {e}"))?;
.map_err(|e| format!("Failed to move bug '{bug_id}' to 5_done/: {e}"))?;
slog!(
"[lifecycle] Closed bug '{bug_id}' → work/5_archived/"
"[lifecycle] Closed bug '{bug_id}' → work/5_done/"
);
Ok(())
@@ -3676,7 +3677,7 @@ mod tests {
close_bug_to_archive(root, "2_bug_test").unwrap();
assert!(!current.join("2_bug_test.md").exists());
assert!(root.join(".story_kit/work/5_archived/2_bug_test.md").exists());
assert!(root.join(".story_kit/work/5_done/2_bug_test.md").exists());
}
#[test]
@@ -3691,7 +3692,7 @@ mod tests {
close_bug_to_archive(root, "3_bug_test").unwrap();
assert!(!upcoming.join("3_bug_test.md").exists());
assert!(root.join(".story_kit/work/5_archived/3_bug_test.md").exists());
assert!(root.join(".story_kit/work/5_done/3_bug_test.md").exists());
}
#[test]
@@ -3944,7 +3945,7 @@ mod tests {
move_story_to_archived(root, "22_story_test").unwrap();
assert!(!merge_dir.join("22_story_test.md").exists());
assert!(root.join(".story_kit/work/5_archived/22_story_test.md").exists());
assert!(root.join(".story_kit/work/5_done/22_story_test.md").exists());
}
#[test]
@@ -4036,10 +4037,10 @@ mod tests {
report.success || report.gate_output.contains("Failed to run") || !report.gates_passed,
"report should be coherent: {report:?}"
);
// Story should be archived if gates passed
// Story should be in done if gates passed
if report.story_archived {
let archived = repo.join(".story_kit/work/5_archived/23_test.md");
assert!(archived.exists(), "archived file should exist");
let done = repo.join(".story_kit/work/5_done/23_test.md");
assert!(done.exists(), "done file should exist");
}
}
@@ -5737,8 +5738,8 @@ theirs
assert_eq!(remaining.len(), 1, "only the other story's agent should remain");
assert_eq!(remaining[0].story_id, "61_story_other");
// Story file should be in 5_archived/
assert!(root.join(".story_kit/work/5_archived/60_story_cleanup.md").exists());
// Story file should be in 5_done/
assert!(root.join(".story_kit/work/5_done/60_story_cleanup.md").exists());
}
// ── bug 154: merge worktree installs frontend deps ────────────────────