From 759c00556e0d6fa3f323fae21a141e2d9352af20 Mon Sep 17 00:00:00 2001 From: dave Date: Fri, 3 Apr 2026 11:50:30 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20timer=20supports=20backlog=20stories=20?= =?UTF-8?q?=E2=80=94=20moves=20to=20current=20before=20starting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- server/src/chat/timer.rs | 65 +++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/server/src/chat/timer.rs b/server/src/chat/timer.rs index 87614662..a40ef1fd 100644 --- a/server/src/chat/timer.rs +++ b/server/src/chat/timer.rs @@ -151,9 +151,23 @@ pub fn spawn_timer_tick_loop( let due = store.take_due(now); for entry in due { crate::slog!( - "[timer] Timer fired for story {}; calling start_agent", + "[timer] Timer fired for story {}", 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 .start_agent(&project_root, &entry.story_id, None, None) .await @@ -167,7 +181,8 @@ pub fn spawn_timer_tick_loop( } Err(e) => { 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 ); } @@ -325,13 +340,14 @@ pub async fn handle_timer_command( } }; - // The story must already be in 2_current/ — the timer does not move stories. - let current_dir = project_root.join(".storkit").join("work").join("2_current"); - let story_file = current_dir.join(format!("{story_id}.md")); - if !story_file.exists() { + // The story must be in backlog or current. When the timer fires, + // backlog stories are moved to current automatically. + let work_dir = project_root.join(".storkit").join("work"); + 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!( - "Story **{story_id}** is not in `work/2_current/`. \ - Move it to current before scheduling a timer." + "Story **{story_id}** is not in backlog or current." ); } @@ -802,9 +818,10 @@ mod tests { } #[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(); - // 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(); let store = TimerStore::load(dir.path().join("timers.json")); let result = handle_timer_command( @@ -817,11 +834,37 @@ mod tests { ) .await; assert!( - result.contains("not in `work/2_current/`"), + result.contains("not in backlog or current"), "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] async fn handle_schedule_success() { let dir = TempDir::new().unwrap();