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:
+54
-11
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user