fix: timer supports backlog stories — moves to current before starting

The timer tick loop now calls move_story_to_current() before
start_agent(), so stories scheduled from the backlog are moved into the
pipeline automatically when the timer fires. The timer bot command also
accepts backlog stories (previously required current).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dave
2026-04-03 11:50:30 +00:00
parent 4808279873
commit 759c00556e
+54 -11
View File
@@ -151,9 +151,23 @@ pub fn spawn_timer_tick_loop(
let due = store.take_due(now); let due = store.take_due(now);
for entry in due { for entry in due {
crate::slog!( crate::slog!(
"[timer] Timer fired for story {}; calling start_agent", "[timer] Timer fired for story {}",
entry.story_id entry.story_id
); );
// Move from backlog to current if needed — the auto-assign
// watcher will then start an agent automatically.
if let Err(e) = crate::agents::lifecycle::move_story_to_current(
&project_root,
&entry.story_id,
) {
crate::slog!(
"[timer] Failed to move story {} to current: {e}",
entry.story_id
);
continue;
}
match agents match agents
.start_agent(&project_root, &entry.story_id, None, None) .start_agent(&project_root, &entry.story_id, None, None)
.await .await
@@ -167,7 +181,8 @@ pub fn spawn_timer_tick_loop(
} }
Err(e) => { Err(e) => {
crate::slog!( crate::slog!(
"[timer] Failed to start agent for story {}: {e}", "[timer] Failed to start agent for story {}: {e} \
(auto-assign may pick it up)",
entry.story_id entry.story_id
); );
} }
@@ -325,13 +340,14 @@ pub async fn handle_timer_command(
} }
}; };
// The story must already be in 2_current/ — the timer does not move stories. // The story must be in backlog or current. When the timer fires,
let current_dir = project_root.join(".storkit").join("work").join("2_current"); // backlog stories are moved to current automatically.
let story_file = current_dir.join(format!("{story_id}.md")); let work_dir = project_root.join(".storkit").join("work");
if !story_file.exists() { let in_backlog = work_dir.join("1_backlog").join(format!("{story_id}.md")).exists();
let in_current = work_dir.join("2_current").join(format!("{story_id}.md")).exists();
if !in_backlog && !in_current {
return format!( return format!(
"Story **{story_id}** is not in `work/2_current/`. \ "Story **{story_id}** is not in backlog or current."
Move it to current before scheduling a timer."
); );
} }
@@ -802,9 +818,10 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn handle_schedule_story_not_in_current() { async fn handle_schedule_story_not_in_backlog_or_current() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
// Set up directory structure with no story in 2_current // Set up directory structure with no story in backlog or current
std::fs::create_dir_all(dir.path().join(".storkit/work/1_backlog")).unwrap();
std::fs::create_dir_all(dir.path().join(".storkit/work/2_current")).unwrap(); std::fs::create_dir_all(dir.path().join(".storkit/work/2_current")).unwrap();
let store = TimerStore::load(dir.path().join("timers.json")); let store = TimerStore::load(dir.path().join("timers.json"));
let result = handle_timer_command( let result = handle_timer_command(
@@ -817,11 +834,37 @@ mod tests {
) )
.await; .await;
assert!( assert!(
result.contains("not in `work/2_current/`"), result.contains("not in backlog or current"),
"unexpected: {result}" "unexpected: {result}"
); );
} }
#[tokio::test]
async fn handle_schedule_accepts_backlog_story() {
let dir = TempDir::new().unwrap();
let backlog_dir = dir.path().join(".storkit/work/1_backlog");
std::fs::create_dir_all(&backlog_dir).unwrap();
std::fs::write(
backlog_dir.join("421_story_foo.md"),
"---\nname: Foo\n---\n",
)
.unwrap();
let store = TimerStore::load(dir.path().join("timers.json"));
let result = handle_timer_command(
TimerCommand::Schedule {
story_number_or_id: "421_story_foo".to_string(),
hhmm: "14:30".to_string(),
},
&store,
dir.path(),
)
.await;
assert!(
result.contains("Timer set"),
"backlog story should be accepted: {result}"
);
}
#[tokio::test] #[tokio::test]
async fn handle_schedule_success() { async fn handle_schedule_success() {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();