story-kit: merge 287_story_rename_upcoming_pipeline_stage_to_backlog

This commit is contained in:
Dave
2026-03-18 14:31:12 +00:00
parent 967ebd7a84
commit df6f792214
26 changed files with 250 additions and 228 deletions

View File

@@ -18,7 +18,7 @@ When you start a new session with this project:
This returns the full tool catalog (create stories, spawn agents, record tests, manage worktrees, etc.). Familiarize yourself with the available tools before proceeding. These tools allow you to directly manipulate the workflow and spawn subsidiary agents without manual file manipulation. This returns the full tool catalog (create stories, spawn agents, record tests, manage worktrees, etc.). Familiarize yourself with the available tools before proceeding. These tools allow you to directly manipulate the workflow and spawn subsidiary agents without manual file manipulation.
2. **Read Context:** Check `.story_kit/specs/00_CONTEXT.md` for high-level project goals. 2. **Read Context:** Check `.story_kit/specs/00_CONTEXT.md` for high-level project goals.
3. **Read Stack:** Check `.story_kit/specs/tech/STACK.md` for technical constraints and patterns. 3. **Read Stack:** Check `.story_kit/specs/tech/STACK.md` for technical constraints and patterns.
4. **Check Work Items:** Look at `.story_kit/work/1_upcoming/` and `.story_kit/work/2_current/` to see what work is pending. 4. **Check Work Items:** Look at `.story_kit/work/1_backlog/` and `.story_kit/work/2_current/` to see what work is pending.
--- ---
@@ -52,7 +52,7 @@ project_root/
├── README.md # This document ├── README.md # This document
├── project.toml # Agent configuration (roles, models, prompts) ├── project.toml # Agent configuration (roles, models, prompts)
├── work/ # Unified work item pipeline (stories, bugs, spikes) ├── work/ # Unified work item pipeline (stories, bugs, spikes)
│ ├── 1_upcoming/ # New work items awaiting implementation │ ├── 1_backlog/ # New work items awaiting implementation
│ ├── 2_current/ # Work in progress │ ├── 2_current/ # Work in progress
│ ├── 3_qa/ # QA review │ ├── 3_qa/ # QA review
│ ├── 4_merge/ # Ready to merge to master │ ├── 4_merge/ # Ready to merge to master
@@ -78,7 +78,7 @@ All work items (stories, bugs, spikes) live in the same `work/` pipeline. Items
Items move through stages by moving the file between directories: Items move through stages by moving the file between directories:
`1_upcoming` → `2_current` → `3_qa` → `4_merge` → `5_done` → `6_archived` `1_backlog` → `2_current` → `3_qa` → `4_merge` → `5_done` → `6_archived`
Items in `5_done` are auto-swept to `6_archived` after 4 hours by the server. Items in `5_done` are auto-swept to `6_archived` after 4 hours by the server.
@@ -87,7 +87,7 @@ Items in `5_done` are auto-swept to `6_archived` after 4 hours by the server.
The server watches `.story_kit/work/` for changes. When a file is created, moved, or modified, the watcher auto-commits with a deterministic message and broadcasts a WebSocket notification to the frontend. This means: The server watches `.story_kit/work/` for changes. When a file is created, moved, or modified, the watcher auto-commits with a deterministic message and broadcasts a WebSocket notification to the frontend. This means:
* MCP tools only need to write/move files — the watcher handles git commits * MCP tools only need to write/move files — the watcher handles git commits
* IDE drag-and-drop works (drag a story from `1_upcoming/` to `2_current/`) * IDE drag-and-drop works (drag a story from `1_backlog/` to `2_current/`)
* The frontend updates automatically without manual refresh * The frontend updates automatically without manual refresh
--- ---
@@ -156,7 +156,7 @@ Not everything needs to be a full story. Simple bugs can skip the story process:
* Performance issues with known fixes * Performance issues with known fixes
### Bug Process ### Bug Process
1. **Document Bug:** Create a bug file in `work/1_upcoming/` named `{id}_bug_{slug}.md` with: 1. **Document Bug:** Create a bug file in `work/1_backlog/` named `{id}_bug_{slug}.md` with:
* **Symptom:** What the user observes * **Symptom:** What the user observes
* **Root Cause:** Technical explanation (if known) * **Root Cause:** Technical explanation (if known)
* **Reproduction Steps:** How to trigger the bug * **Reproduction Steps:** How to trigger the bug
@@ -186,7 +186,7 @@ Not everything needs a story or bug fix. Spikes are time-boxed investigations to
* Need to validate performance constraints * Need to validate performance constraints
### Spike Process ### Spike Process
1. **Document Spike:** Create a spike file in `work/1_upcoming/` named `{id}_spike_{slug}.md` with: 1. **Document Spike:** Create a spike file in `work/1_backlog/` named `{id}_spike_{slug}.md` with:
* **Question:** What you need to answer * **Question:** What you need to answer
* **Hypothesis:** What you expect to be true * **Hypothesis:** What you expect to be true
* **Timebox:** Strict limit for the research * **Timebox:** Strict limit for the research
@@ -209,7 +209,7 @@ When the LLM context window fills up (or the chat gets slow/confused):
1. **Stop Coding.** 1. **Stop Coding.**
2. **Instruction:** Tell the user to open a new chat. 2. **Instruction:** Tell the user to open a new chat.
3. **Handoff:** The only context the new LLM needs is in the `specs/` folder and `.mcp.json`. 3. **Handoff:** The only context the new LLM needs is in the `specs/` folder and `.mcp.json`.
* *Prompt for New Session:* "I am working on Project X. Read `.mcp.json` to discover available tools, then read `specs/00_CONTEXT.md` and `specs/tech/STACK.md`. Then look at `work/1_upcoming/` and `work/2_current/` to see what is pending." * *Prompt for New Session:* "I am working on Project X. Read `.mcp.json` to discover available tools, then read `specs/00_CONTEXT.md` and `specs/tech/STACK.md`. Then look at `work/1_backlog/` and `work/2_current/` to see what is pending."
--- ---
@@ -221,7 +221,7 @@ If a user hands you this document and says "Apply this process to my project":
1. **Check for MCP Tools:** Look for `.mcp.json` in the project root. If it exists, you have programmatic access to workflow tools and agent spawning capabilities. 1. **Check for MCP Tools:** Look for `.mcp.json` in the project root. If it exists, you have programmatic access to workflow tools and agent spawning capabilities.
2. **Analyze the Request:** Ask for the high-level goal ("What are we building?") and the tech preferences ("Rust or Python?"). 2. **Analyze the Request:** Ask for the high-level goal ("What are we building?") and the tech preferences ("Rust or Python?").
3. **Git Check:** Check if the directory is a git repository (`git status`). If not, run `git init`. 3. **Git Check:** Check if the directory is a git repository (`git status`). If not, run `git init`.
4. **Scaffold:** Run commands to create the `work/` and `specs/` folders with the 6-stage pipeline (`work/1_upcoming/` through `work/6_archived/`). 4. **Scaffold:** Run commands to create the `work/` and `specs/` folders with the 6-stage pipeline (`work/1_backlog/` through `work/6_archived/`).
5. **Draft Context:** Write `specs/00_CONTEXT.md` based on the user's answer. 5. **Draft Context:** Write `specs/00_CONTEXT.md` based on the user's answer.
6. **Draft Stack:** Write `specs/tech/STACK.md` based on best practices for that language. 6. **Draft Stack:** Write `specs/tech/STACK.md` based on best practices for that language.
7. **Wait:** Ask the user for "Story #1". 7. **Wait:** Ask the user for "Story #1".

View File

@@ -34,7 +34,7 @@ You have these tools via the story-kit MCP server:
## Your Workflow ## Your Workflow
1. Read CLAUDE.md and .story_kit/README.md to understand the project and dev process 1. Read CLAUDE.md and .story_kit/README.md to understand the project and dev process
2. Read the story file from .story_kit/work/ to understand requirements 2. Read the story file from .story_kit/work/ to understand requirements
3. Move it to work/2_current/ if it is in work/1_upcoming/ 3. Move it to work/2_current/ if it is in work/1_backlog/
4. Start coder-1 on the story: call start_agent with story_id="{{story_id}}" and agent_name="coder-1" 4. Start coder-1 on the story: call start_agent with story_id="{{story_id}}" and agent_name="coder-1"
5. Wait for completion: call wait_for_agent with story_id="{{story_id}}" and agent_name="coder-1". The server automatically runs acceptance gates (cargo clippy + tests) when the coder process exits. wait_for_agent returns when the coder reaches a terminal state. 5. Wait for completion: call wait_for_agent with story_id="{{story_id}}" and agent_name="coder-1". The server automatically runs acceptance gates (cargo clippy + tests) when the coder process exits. wait_for_agent returns when the coder reaches a terminal state.
6. Check the result: inspect the "completion" field in the wait_for_agent response — if gates_passed is true, the work is done; if false, review the gate_output and decide whether to start a fresh coder. 6. Check the result: inspect the "completion" field in the wait_for_agent response — if gates_passed is true, the work is done; if false, review the gate_output and decide whether to start a fresh coder.

View File

@@ -0,0 +1,22 @@
---
name: "Rename upcoming pipeline stage to backlog"
---
# Story 287: Rename upcoming pipeline stage to backlog
## User Story
As a project owner, I want the "upcoming" pipeline stage renamed to "backlog" throughout the codebase, UI, and directory structure, so that the terminology better reflects that these items are not necessarily coming up next.
## Acceptance Criteria
- [ ] Directory renamed from 1_upcoming to 1_backlog
- [ ] All server code references updated (watcher, lifecycle, MCP tools, workflow, etc.)
- [ ] Frontend UI labels updated
- [ ] MCP tool descriptions and outputs use "backlog" instead of "upcoming"
- [ ] Existing story/bug files moved to the new directory
- [ ] Git commit messages use "backlog" for new items going forward
## Out of Scope
- TBD

View File

@@ -262,7 +262,7 @@ describe("ChatWebSocket", () => {
// Server pushes pipeline_state on fresh connection // Server pushes pipeline_state on fresh connection
const freshState = { const freshState = {
upcoming: [{ story_id: "1_story_test", name: "Test", error: null }], backlog: [{ story_id: "1_story_test", name: "Test", error: null }],
current: [], current: [],
qa: [], qa: [],
merge: [], merge: [],

View File

@@ -36,7 +36,7 @@ export interface PipelineStageItem {
} }
export interface PipelineState { export interface PipelineState {
upcoming: PipelineStageItem[]; backlog: PipelineStageItem[];
current: PipelineStageItem[]; current: PipelineStageItem[];
qa: PipelineStageItem[]; qa: PipelineStageItem[];
merge: PipelineStageItem[]; merge: PipelineStageItem[];
@@ -50,7 +50,7 @@ export type WsResponse =
| { type: "error"; message: string } | { type: "error"; message: string }
| { | {
type: "pipeline_state"; type: "pipeline_state";
upcoming: PipelineStageItem[]; backlog: PipelineStageItem[];
current: PipelineStageItem[]; current: PipelineStageItem[];
qa: PipelineStageItem[]; qa: PipelineStageItem[];
merge: PipelineStageItem[]; merge: PipelineStageItem[];
@@ -398,7 +398,7 @@ export class ChatWebSocket {
if (data.type === "error") this.onError?.(data.message); if (data.type === "error") this.onError?.(data.message);
if (data.type === "pipeline_state") if (data.type === "pipeline_state")
this.onPipelineState?.({ this.onPipelineState?.({
upcoming: data.upcoming, backlog: data.backlog,
current: data.current, current: data.current,
qa: data.qa, qa: data.qa,
merge: data.merge, merge: data.merge,

View File

@@ -165,7 +165,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
const [apiKeyInput, setApiKeyInput] = useState(""); const [apiKeyInput, setApiKeyInput] = useState("");
const [hasAnthropicKey, setHasAnthropicKey] = useState(false); const [hasAnthropicKey, setHasAnthropicKey] = useState(false);
const [pipeline, setPipeline] = useState<PipelineState>({ const [pipeline, setPipeline] = useState<PipelineState>({
upcoming: [], backlog: [],
current: [], current: [],
qa: [], qa: [],
merge: [], merge: [],
@@ -1017,8 +1017,8 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
onItemClick={(item) => setSelectedWorkItemId(item.story_id)} onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/> />
<StagePanel <StagePanel
title="Upcoming" title="Backlog"
items={pipeline.upcoming} items={pipeline.backlog}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)} onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/> />
</> </>

View File

@@ -9,7 +9,7 @@ import { StagePanel } from "./StagePanel";
function makePipeline(overrides: Partial<PipelineState> = {}): PipelineState { function makePipeline(overrides: Partial<PipelineState> = {}): PipelineState {
return { return {
upcoming: [], backlog: [],
current: [], current: [],
qa: [], qa: [],
merge: [], merge: [],

View File

@@ -115,7 +115,7 @@ export function LozengeFlyProvider({
const assignedAgentNames = useMemo(() => { const assignedAgentNames = useMemo(() => {
const names = new Set<string>(); const names = new Set<string>();
for (const item of [ for (const item of [
...pipeline.upcoming, ...pipeline.backlog,
...pipeline.current, ...pipeline.current,
...pipeline.qa, ...pipeline.qa,
...pipeline.merge, ...pipeline.merge,
@@ -165,13 +165,13 @@ export function LozengeFlyProvider({
const prev = prevPipelineRef.current; const prev = prevPipelineRef.current;
const allPrev = [ const allPrev = [
...prev.upcoming, ...prev.backlog,
...prev.current, ...prev.current,
...prev.qa, ...prev.qa,
...prev.merge, ...prev.merge,
]; ];
const allCurr = [ const allCurr = [
...pipeline.upcoming, ...pipeline.backlog,
...pipeline.current, ...pipeline.current,
...pipeline.qa, ...pipeline.qa,
...pipeline.merge, ...pipeline.merge,

View File

@@ -8,7 +8,7 @@ import { api } from "../api/client";
const { useEffect, useRef, useState } = React; const { useEffect, useRef, useState } = React;
const STAGE_LABELS: Record<string, string> = { const STAGE_LABELS: Record<string, string> = {
upcoming: "Upcoming", backlog: "Backlog",
current: "Current", current: "Current",
qa: "QA", qa: "QA",
merge: "To Merge", merge: "To Merge",

View File

@@ -16,9 +16,9 @@ pub(super) fn item_type_from_id(item_id: &str) -> &'static str {
} }
} }
/// Return the source directory path for a work item (always work/1_upcoming/). /// Return the source directory path for a work item (always work/1_backlog/).
fn item_source_dir(project_root: &Path, _item_id: &str) -> PathBuf { fn item_source_dir(project_root: &Path, _item_id: &str) -> PathBuf {
project_root.join(".story_kit").join("work").join("1_upcoming") project_root.join(".story_kit").join("work").join("1_backlog")
} }
/// Return the done directory path for a work item (always work/5_done/). /// Return the done directory path for a work item (always work/5_done/).
@@ -26,10 +26,10 @@ fn item_archive_dir(project_root: &Path, _item_id: &str) -> PathBuf {
project_root.join(".story_kit").join("work").join("5_done") 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/`. /// Move a work item (story, bug, or spike) from `work/1_backlog/` to `work/2_current/`.
/// ///
/// Idempotent: if the item is already in `2_current/`, returns Ok without committing. /// Idempotent: if the item is already in `2_current/`, returns Ok without committing.
/// If the item is not found in `1_upcoming/`, logs a warning and returns Ok. /// If the item is not found in `1_backlog/`, logs a warning and returns Ok.
pub fn move_story_to_current(project_root: &Path, story_id: &str) -> Result<(), String> { pub fn move_story_to_current(project_root: &Path, story_id: &str) -> Result<(), String> {
let sk = project_root.join(".story_kit").join("work"); let sk = project_root.join(".story_kit").join("work");
let current_dir = sk.join("2_current"); let current_dir = sk.join("2_current");
@@ -219,16 +219,16 @@ pub fn move_story_to_qa(project_root: &Path, story_id: &str) -> Result<(), Strin
Ok(()) Ok(())
} }
/// Move a bug from `work/2_current/` or `work/1_upcoming/` to `work/5_done/` and auto-commit. /// Move a bug from `work/2_current/` or `work/1_backlog/` to `work/5_done/` and auto-commit.
/// ///
/// * If the bug is in `2_current/`, it is moved to `5_done/` and committed. /// * 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 still in `1_backlog/` (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 already in `5_done/`, this is a no-op (idempotent).
/// * If the bug is not found anywhere, an error is returned. /// * 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> { pub fn close_bug_to_archive(project_root: &Path, bug_id: &str) -> Result<(), String> {
let sk = project_root.join(".story_kit").join("work"); let sk = project_root.join(".story_kit").join("work");
let current_path = sk.join("2_current").join(format!("{bug_id}.md")); let current_path = sk.join("2_current").join(format!("{bug_id}.md"));
let upcoming_path = sk.join("1_upcoming").join(format!("{bug_id}.md")); let backlog_path = sk.join("1_backlog").join(format!("{bug_id}.md"));
let archive_dir = item_archive_dir(project_root, bug_id); let archive_dir = item_archive_dir(project_root, bug_id);
let archive_path = archive_dir.join(format!("{bug_id}.md")); let archive_path = archive_dir.join(format!("{bug_id}.md"));
@@ -238,11 +238,11 @@ pub fn close_bug_to_archive(project_root: &Path, bug_id: &str) -> Result<(), Str
let source_path = if current_path.exists() { let source_path = if current_path.exists() {
current_path.clone() current_path.clone()
} else if upcoming_path.exists() { } else if backlog_path.exists() {
upcoming_path.clone() backlog_path.clone()
} else { } else {
return Err(format!( return Err(format!(
"Bug '{bug_id}' not found in work/2_current/ or work/1_upcoming/. Cannot close bug." "Bug '{bug_id}' not found in work/2_current/ or work/1_backlog/. Cannot close bug."
)); ));
}; };
@@ -269,15 +269,15 @@ mod tests {
use std::fs; use std::fs;
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let root = tmp.path(); let root = tmp.path();
let upcoming = root.join(".story_kit/work/1_upcoming"); let backlog = root.join(".story_kit/work/1_backlog");
let current = root.join(".story_kit/work/2_current"); let current = root.join(".story_kit/work/2_current");
fs::create_dir_all(&upcoming).unwrap(); fs::create_dir_all(&backlog).unwrap();
fs::create_dir_all(&current).unwrap(); fs::create_dir_all(&current).unwrap();
fs::write(upcoming.join("10_story_foo.md"), "test").unwrap(); fs::write(backlog.join("10_story_foo.md"), "test").unwrap();
move_story_to_current(root, "10_story_foo").unwrap(); move_story_to_current(root, "10_story_foo").unwrap();
assert!(!upcoming.join("10_story_foo.md").exists()); assert!(!backlog.join("10_story_foo.md").exists());
assert!(current.join("10_story_foo.md").exists()); assert!(current.join("10_story_foo.md").exists());
} }
@@ -295,25 +295,25 @@ mod tests {
} }
#[test] #[test]
fn move_story_to_current_noop_when_not_in_upcoming() { fn move_story_to_current_noop_when_not_in_backlog() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
assert!(move_story_to_current(tmp.path(), "99_missing").is_ok()); assert!(move_story_to_current(tmp.path(), "99_missing").is_ok());
} }
#[test] #[test]
fn move_bug_to_current_moves_from_upcoming() { fn move_bug_to_current_moves_from_backlog() {
use std::fs; use std::fs;
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let root = tmp.path(); let root = tmp.path();
let upcoming = root.join(".story_kit/work/1_upcoming"); let backlog = root.join(".story_kit/work/1_backlog");
let current = root.join(".story_kit/work/2_current"); let current = root.join(".story_kit/work/2_current");
fs::create_dir_all(&upcoming).unwrap(); fs::create_dir_all(&backlog).unwrap();
fs::create_dir_all(&current).unwrap(); fs::create_dir_all(&current).unwrap();
fs::write(upcoming.join("1_bug_test.md"), "# Bug 1\n").unwrap(); fs::write(backlog.join("1_bug_test.md"), "# Bug 1\n").unwrap();
move_story_to_current(root, "1_bug_test").unwrap(); move_story_to_current(root, "1_bug_test").unwrap();
assert!(!upcoming.join("1_bug_test.md").exists()); assert!(!backlog.join("1_bug_test.md").exists());
assert!(current.join("1_bug_test.md").exists()); assert!(current.join("1_bug_test.md").exists());
} }
@@ -335,17 +335,17 @@ mod tests {
} }
#[test] #[test]
fn close_bug_moves_from_upcoming_when_not_started() { fn close_bug_moves_from_backlog_when_not_started() {
use std::fs; use std::fs;
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let root = tmp.path(); let root = tmp.path();
let upcoming = root.join(".story_kit/work/1_upcoming"); let backlog = root.join(".story_kit/work/1_backlog");
fs::create_dir_all(&upcoming).unwrap(); fs::create_dir_all(&backlog).unwrap();
fs::write(upcoming.join("3_bug_test.md"), "# Bug 3\n").unwrap(); fs::write(backlog.join("3_bug_test.md"), "# Bug 3\n").unwrap();
close_bug_to_archive(root, "3_bug_test").unwrap(); close_bug_to_archive(root, "3_bug_test").unwrap();
assert!(!upcoming.join("3_bug_test.md").exists()); assert!(!backlog.join("3_bug_test.md").exists());
assert!(root.join(".story_kit/work/5_done/3_bug_test.md").exists()); assert!(root.join(".story_kit/work/5_done/3_bug_test.md").exists());
} }

View File

@@ -212,7 +212,7 @@ impl AgentPool {
let event_log: Arc<Mutex<Vec<AgentEvent>>> = Arc::new(Mutex::new(Vec::new())); let event_log: Arc<Mutex<Vec<AgentEvent>>> = Arc::new(Mutex::new(Vec::new()));
let log_session_id = uuid::Uuid::new_v4().to_string(); let log_session_id = uuid::Uuid::new_v4().to_string();
// Move story from upcoming/ to current/ before checking agent // Move story from backlog/ to current/ before checking agent
// availability so that auto_assign_available_work can pick it up even // availability so that auto_assign_available_work can pick it up even
// when all coders are currently busy (story 203). This is idempotent: // when all coders are currently busy (story 203). This is idempotent:
// if the story is already in 2_current/ or a later stage, the call is // if the story is already in 2_current/ or a later stage, the call is
@@ -1430,7 +1430,7 @@ impl AgentPool {
/// ///
/// Scans `work/2_current/`, `work/3_qa/`, and `work/4_merge/` for items that have no /// Scans `work/2_current/`, `work/3_qa/`, and `work/4_merge/` for items that have no
/// active agent and assigns the first free agent of the appropriate role. Items in /// active agent and assigns the first free agent of the appropriate role. Items in
/// `work/1_upcoming/` are never auto-started. /// `work/1_backlog/` are never auto-started.
/// ///
/// Respects the configured agent roster: the maximum number of concurrently active agents /// Respects the configured agent roster: the maximum number of concurrently active agents
/// per role is bounded by the count of agents of that role defined in `project.toml`. /// per role is bounded by the count of agents of that role defined in `project.toml`.
@@ -1603,7 +1603,7 @@ impl AgentPool {
// Determine which active stage the story is in. // Determine which active stage the story is in.
let stage_dir = match find_active_story_stage(project_root, story_id) { let stage_dir = match find_active_story_stage(project_root, story_id) {
Some(s) => s, Some(s) => s,
None => continue, // Not in any active stage (upcoming/archived or unknown). None => continue, // Not in any active stage (backlog/archived or unknown).
}; };
// 4_merge/ is left for auto_assign to handle with a fresh mergemaster. // 4_merge/ is left for auto_assign to handle with a fresh mergemaster.
@@ -2728,8 +2728,8 @@ mod tests {
fs::write(current.join("173_story_test.md"), "test").unwrap(); fs::write(current.join("173_story_test.md"), "test").unwrap();
// Ensure 3_qa/ exists for the move target // Ensure 3_qa/ exists for the move target
fs::create_dir_all(root.join(".story_kit/work/3_qa")).unwrap(); fs::create_dir_all(root.join(".story_kit/work/3_qa")).unwrap();
// Ensure 1_upcoming/ exists (start_agent calls move_story_to_current) // Ensure 1_backlog/ exists (start_agent calls move_story_to_current)
fs::create_dir_all(root.join(".story_kit/work/1_upcoming")).unwrap(); fs::create_dir_all(root.join(".story_kit/work/1_backlog")).unwrap();
// Write a project.toml with a qa agent so start_agent can resolve it. // Write a project.toml with a qa agent so start_agent can resolve it.
fs::create_dir_all(root.join(".story_kit")).unwrap(); fs::create_dir_all(root.join(".story_kit")).unwrap();
@@ -3498,14 +3498,14 @@ stage = "coder"
} }
/// Story 203: when all coders are busy the story file must be moved from /// Story 203: when all coders are busy the story file must be moved from
/// 1_upcoming/ to 2_current/ so that auto_assign_available_work can pick /// 1_backlog/ to 2_current/ so that auto_assign_available_work can pick
/// it up once a coder finishes. /// it up once a coder finishes.
#[tokio::test] #[tokio::test]
async fn start_agent_moves_story_to_current_when_coders_busy() { async fn start_agent_moves_story_to_current_when_coders_busy() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let sk = tmp.path().join(".story_kit"); let sk = tmp.path().join(".story_kit");
let upcoming = sk.join("work/1_upcoming"); let backlog = sk.join("work/1_backlog");
std::fs::create_dir_all(&upcoming).unwrap(); std::fs::create_dir_all(&backlog).unwrap();
std::fs::write( std::fs::write(
sk.join("project.toml"), sk.join("project.toml"),
r#" r#"
@@ -3515,9 +3515,9 @@ stage = "coder"
"#, "#,
) )
.unwrap(); .unwrap();
// Place the story in 1_upcoming/. // Place the story in 1_backlog/.
std::fs::write( std::fs::write(
upcoming.join("story-3.md"), backlog.join("story-3.md"),
"---\nname: Story 3\n---\n", "---\nname: Story 3\n---\n",
) )
.unwrap(); .unwrap();
@@ -3547,10 +3547,10 @@ stage = "coder"
current_path.exists(), current_path.exists(),
"story should be in 2_current/ after busy error, but was not" "story should be in 2_current/ after busy error, but was not"
); );
let upcoming_path = upcoming.join("story-3.md"); let backlog_path = backlog.join("story-3.md");
assert!( assert!(
!upcoming_path.exists(), !backlog_path.exists(),
"story should no longer be in 1_upcoming/" "story should no longer be in 1_backlog/"
); );
} }
@@ -3774,7 +3774,7 @@ stage = "coder"
// Create the story in upcoming so `move_story_to_current` succeeds, // Create the story in upcoming so `move_story_to_current` succeeds,
// but do NOT init a git repo — `create_worktree` will fail in the spawn. // but do NOT init a git repo — `create_worktree` will fail in the spawn.
let upcoming = root.join(".story_kit/work/1_upcoming"); let upcoming = root.join(".story_kit/work/1_backlog");
fs::create_dir_all(&upcoming).unwrap(); fs::create_dir_all(&upcoming).unwrap();
fs::write( fs::write(
upcoming.join("50_story_test.md"), upcoming.join("50_story_test.md"),
@@ -3924,7 +3924,7 @@ stage = "coder"
let root = tmp.path().to_path_buf(); let root = tmp.path().to_path_buf();
let sk_dir = root.join(".story_kit"); let sk_dir = root.join(".story_kit");
fs::create_dir_all(sk_dir.join("work/1_upcoming")).unwrap(); fs::create_dir_all(sk_dir.join("work/1_backlog")).unwrap();
fs::write( fs::write(
root.join(".story_kit/project.toml"), root.join(".story_kit/project.toml"),
"[[agent]]\nname = \"coder-1\"\n", "[[agent]]\nname = \"coder-1\"\n",
@@ -3933,12 +3933,12 @@ stage = "coder"
// Both stories must exist in upcoming so move_story_to_current can run // Both stories must exist in upcoming so move_story_to_current can run
// (only the winner reaches that point, but we set both up defensively). // (only the winner reaches that point, but we set both up defensively).
fs::write( fs::write(
root.join(".story_kit/work/1_upcoming/86_story_foo.md"), root.join(".story_kit/work/1_backlog/86_story_foo.md"),
"---\nname: Foo\n---\n", "---\nname: Foo\n---\n",
) )
.unwrap(); .unwrap();
fs::write( fs::write(
root.join(".story_kit/work/1_upcoming/130_story_bar.md"), root.join(".story_kit/work/1_backlog/130_story_bar.md"),
"---\nname: Bar\n---\n", "---\nname: Bar\n---\n",
) )
.unwrap(); .unwrap();
@@ -4138,14 +4138,14 @@ stage = "coder"
let root = tmp.path(); let root = tmp.path();
let sk_dir = root.join(".story_kit"); let sk_dir = root.join(".story_kit");
fs::create_dir_all(sk_dir.join("work/1_upcoming")).unwrap(); fs::create_dir_all(sk_dir.join("work/1_backlog")).unwrap();
fs::write( fs::write(
root.join(".story_kit/project.toml"), root.join(".story_kit/project.toml"),
"[[agent]]\nname = \"coder-1\"\n\n[[agent]]\nname = \"coder-2\"\n", "[[agent]]\nname = \"coder-1\"\n\n[[agent]]\nname = \"coder-2\"\n",
) )
.unwrap(); .unwrap();
fs::write( fs::write(
root.join(".story_kit/work/1_upcoming/99_story_baz.md"), root.join(".story_kit/work/1_backlog/99_story_baz.md"),
"---\nname: Baz\n---\n", "---\nname: Baz\n---\n",
) )
.unwrap(); .unwrap();

View File

@@ -339,7 +339,7 @@ impl AgentsApi {
.map_err(bad_request)?; .map_err(bad_request)?;
let stages = [ let stages = [
("1_upcoming", "upcoming"), ("1_backlog", "backlog"),
("2_current", "current"), ("2_current", "current"),
("3_qa", "qa"), ("3_qa", "qa"),
("4_merge", "merge"), ("4_merge", "merge"),
@@ -809,12 +809,12 @@ allowed_tools = ["Read", "Bash"]
} }
#[tokio::test] #[tokio::test]
async fn get_work_item_content_returns_content_from_upcoming() { async fn get_work_item_content_returns_content_from_backlog() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let root = tmp.path(); let root = tmp.path();
make_stage_dir(root, "1_upcoming"); make_stage_dir(root, "1_backlog");
std::fs::write( std::fs::write(
root.join(".story_kit/work/1_upcoming/42_story_foo.md"), root.join(".story_kit/work/1_backlog/42_story_foo.md"),
"---\nname: \"Foo Story\"\n---\n\n# Story 42: Foo Story\n\nSome content.", "---\nname: \"Foo Story\"\n---\n\n# Story 42: Foo Story\n\nSome content.",
) )
.unwrap(); .unwrap();
@@ -828,7 +828,7 @@ allowed_tools = ["Read", "Bash"]
.unwrap() .unwrap()
.0; .0;
assert!(result.content.contains("Some content.")); assert!(result.content.contains("Some content."));
assert_eq!(result.stage, "upcoming"); assert_eq!(result.stage, "backlog");
assert_eq!(result.name, Some("Foo Story".to_string())); assert_eq!(result.name, Some("Foo Story".to_string()));
} }
@@ -1113,7 +1113,7 @@ allowed_tools = ["Read", "Bash"]
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let root = tmp.path().to_path_buf(); let root = tmp.path().to_path_buf();
// Create work dirs including 2_current for the story file. // Create work dirs including 2_current for the story file.
for stage in &["1_upcoming", "2_current", "5_done", "6_archived"] { for stage in &["1_backlog", "2_current", "5_done", "6_archived"] {
std::fs::create_dir_all(root.join(".story_kit").join("work").join(stage)).unwrap(); std::fs::create_dir_all(root.join(".story_kit").join("work").join(stage)).unwrap();
} }

View File

@@ -672,7 +672,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
}, },
{ {
"name": "create_spike", "name": "create_spike",
"description": "Create a spike file in .story_kit/work/1_upcoming/ with a deterministic filename and YAML front matter. Returns the spike_id.", "description": "Create a spike file in .story_kit/work/1_backlog/ with a deterministic filename and YAML front matter. Returns the spike_id.",
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -690,7 +690,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
}, },
{ {
"name": "create_bug", "name": "create_bug",
"description": "Create a bug file in work/1_upcoming/ with a deterministic filename and auto-commit to master. Returns the bug_id.", "description": "Create a bug file in work/1_backlog/ with a deterministic filename and auto-commit to master. Returns the bug_id.",
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -725,7 +725,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
}, },
{ {
"name": "list_bugs", "name": "list_bugs",
"description": "List all open bugs in work/1_upcoming/ matching the _bug_ naming convention.", "description": "List all open bugs in work/1_backlog/ matching the _bug_ naming convention.",
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": {} "properties": {}
@@ -733,7 +733,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
}, },
{ {
"name": "create_refactor", "name": "create_refactor",
"description": "Create a refactor work item in work/1_upcoming/ with a deterministic filename and YAML front matter. Returns the refactor_id.", "description": "Create a refactor work item in work/1_backlog/ with a deterministic filename and YAML front matter. Returns the refactor_id.",
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -756,7 +756,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
}, },
{ {
"name": "list_refactors", "name": "list_refactors",
"description": "List all open refactors in work/1_upcoming/ matching the _refactor_ naming convention.", "description": "List all open refactors in work/1_backlog/ matching the _refactor_ naming convention.",
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": {} "properties": {}
@@ -764,7 +764,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
}, },
{ {
"name": "close_bug", "name": "close_bug",
"description": "Archive a bug from work/2_current/ or work/1_upcoming/ to work/5_done/ and auto-commit to master.", "description": "Archive a bug from work/2_current/ or work/1_backlog/ to work/5_done/ and auto-commit to master.",
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1022,7 +1022,7 @@ fn tool_create_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
.get("acceptance_criteria") .get("acceptance_criteria")
.and_then(|v| serde_json::from_value(v.clone()).ok()); .and_then(|v| serde_json::from_value(v.clone()).ok());
// Spike 61: write the file only — the filesystem watcher detects the new // Spike 61: write the file only — the filesystem watcher detects the new
// .md file in work/1_upcoming/ and auto-commits with a deterministic message. // .md file in work/1_backlog/ and auto-commits with a deterministic message.
let commit = false; let commit = false;
let root = ctx.state.get_project_root()?; let root = ctx.state.get_project_root()?;
@@ -1091,16 +1091,16 @@ fn tool_get_pipeline_status(ctx: &AppContext) -> Result<String, String> {
active.extend(map_items(&state.merge, "merge")); active.extend(map_items(&state.merge, "merge"));
active.extend(map_items(&state.done, "done")); active.extend(map_items(&state.done, "done"));
let upcoming: Vec<Value> = state let backlog: Vec<Value> = state
.upcoming .backlog
.iter() .iter()
.map(|s| json!({ "story_id": s.story_id, "name": s.name })) .map(|s| json!({ "story_id": s.story_id, "name": s.name }))
.collect(); .collect();
serde_json::to_string_pretty(&json!({ serde_json::to_string_pretty(&json!({
"active": active, "active": active,
"upcoming": upcoming, "backlog": backlog,
"upcoming_count": upcoming.len(), "backlog_count": backlog.len(),
})) }))
.map_err(|e| format!("Serialization error: {e}")) .map_err(|e| format!("Serialization error: {e}"))
} }
@@ -2452,7 +2452,7 @@ mod tests {
let root = tmp.path(); let root = tmp.path();
for (stage, id, name) in &[ for (stage, id, name) in &[
("1_upcoming", "10_story_upcoming", "Upcoming Story"), ("1_backlog", "10_story_upcoming", "Upcoming Story"),
("2_current", "20_story_current", "Current Story"), ("2_current", "20_story_current", "Current Story"),
("3_qa", "30_story_qa", "QA Story"), ("3_qa", "30_story_qa", "QA Story"),
("4_merge", "40_story_merge", "Merge Story"), ("4_merge", "40_story_merge", "Merge Story"),
@@ -2481,11 +2481,11 @@ mod tests {
assert!(stages.contains(&"merge")); assert!(stages.contains(&"merge"));
assert!(stages.contains(&"done")); assert!(stages.contains(&"done"));
// Upcoming backlog // Backlog
let upcoming = parsed["upcoming"].as_array().unwrap(); let backlog = parsed["backlog"].as_array().unwrap();
assert_eq!(upcoming.len(), 1); assert_eq!(backlog.len(), 1);
assert_eq!(upcoming[0]["story_id"], "10_story_upcoming"); assert_eq!(backlog[0]["story_id"], "10_story_upcoming");
assert_eq!(parsed["upcoming_count"], 1); assert_eq!(parsed["backlog_count"], 1);
} }
#[test] #[test]
@@ -2801,8 +2801,8 @@ mod tests {
let t = tool.unwrap(); let t = tool.unwrap();
let desc = t["description"].as_str().unwrap(); let desc = t["description"].as_str().unwrap();
assert!( assert!(
desc.contains("work/1_upcoming/"), desc.contains("work/1_backlog/"),
"create_bug description should reference work/1_upcoming/, got: {desc}" "create_bug description should reference work/1_backlog/, got: {desc}"
); );
assert!( assert!(
!desc.contains(".story_kit/bugs"), !desc.contains(".story_kit/bugs"),
@@ -2826,8 +2826,8 @@ mod tests {
let t = tool.unwrap(); let t = tool.unwrap();
let desc = t["description"].as_str().unwrap(); let desc = t["description"].as_str().unwrap();
assert!( assert!(
desc.contains("work/1_upcoming/"), desc.contains("work/1_backlog/"),
"list_bugs description should reference work/1_upcoming/, got: {desc}" "list_bugs description should reference work/1_backlog/, got: {desc}"
); );
assert!( assert!(
!desc.contains(".story_kit/bugs"), !desc.contains(".story_kit/bugs"),
@@ -2911,7 +2911,7 @@ mod tests {
assert!(result.contains("1_bug_login_crash")); assert!(result.contains("1_bug_login_crash"));
let bug_file = tmp let bug_file = tmp
.path() .path()
.join(".story_kit/work/1_upcoming/1_bug_login_crash.md"); .join(".story_kit/work/1_backlog/1_bug_login_crash.md");
assert!(bug_file.exists()); assert!(bug_file.exists());
} }
@@ -2927,15 +2927,15 @@ mod tests {
#[test] #[test]
fn tool_list_bugs_returns_open_bugs() { fn tool_list_bugs_returns_open_bugs() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let upcoming_dir = tmp.path().join(".story_kit/work/1_upcoming"); let backlog_dir = tmp.path().join(".story_kit/work/1_backlog");
std::fs::create_dir_all(&upcoming_dir).unwrap(); std::fs::create_dir_all(&backlog_dir).unwrap();
std::fs::write( std::fs::write(
upcoming_dir.join("1_bug_crash.md"), backlog_dir.join("1_bug_crash.md"),
"# Bug 1: App Crash\n", "# Bug 1: App Crash\n",
) )
.unwrap(); .unwrap();
std::fs::write( std::fs::write(
upcoming_dir.join("2_bug_typo.md"), backlog_dir.join("2_bug_typo.md"),
"# Bug 2: Typo in Header\n", "# Bug 2: Typo in Header\n",
) )
.unwrap(); .unwrap();
@@ -2963,9 +2963,9 @@ mod tests {
fn tool_close_bug_moves_to_archive() { fn tool_close_bug_moves_to_archive() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
setup_git_repo_in(tmp.path()); setup_git_repo_in(tmp.path());
let upcoming_dir = tmp.path().join(".story_kit/work/1_upcoming"); let backlog_dir = tmp.path().join(".story_kit/work/1_backlog");
std::fs::create_dir_all(&upcoming_dir).unwrap(); std::fs::create_dir_all(&backlog_dir).unwrap();
let bug_file = upcoming_dir.join("1_bug_crash.md"); let bug_file = backlog_dir.join("1_bug_crash.md");
std::fs::write(&bug_file, "# Bug 1: Crash\n").unwrap(); std::fs::write(&bug_file, "# Bug 1: Crash\n").unwrap();
// Stage the file so it's tracked // Stage the file so it's tracked
std::process::Command::new("git") std::process::Command::new("git")
@@ -3035,7 +3035,7 @@ mod tests {
assert!(result.contains("1_spike_compare_encoders")); assert!(result.contains("1_spike_compare_encoders"));
let spike_file = tmp let spike_file = tmp
.path() .path()
.join(".story_kit/work/1_upcoming/1_spike_compare_encoders.md"); .join(".story_kit/work/1_backlog/1_spike_compare_encoders.md");
assert!(spike_file.exists()); assert!(spike_file.exists());
let contents = std::fs::read_to_string(&spike_file).unwrap(); let contents = std::fs::read_to_string(&spike_file).unwrap();
assert!(contents.starts_with("---\nname: \"Compare Encoders\"\n---")); assert!(contents.starts_with("---\nname: \"Compare Encoders\"\n---"));
@@ -3050,7 +3050,7 @@ mod tests {
let result = tool_create_spike(&json!({"name": "My Spike"}), &ctx).unwrap(); let result = tool_create_spike(&json!({"name": "My Spike"}), &ctx).unwrap();
assert!(result.contains("1_spike_my_spike")); assert!(result.contains("1_spike_my_spike"));
let spike_file = tmp.path().join(".story_kit/work/1_upcoming/1_spike_my_spike.md"); let spike_file = tmp.path().join(".story_kit/work/1_backlog/1_spike_my_spike.md");
assert!(spike_file.exists()); assert!(spike_file.exists());
let contents = std::fs::read_to_string(&spike_file).unwrap(); let contents = std::fs::read_to_string(&spike_file).unwrap();
assert!(contents.starts_with("---\nname: \"My Spike\"\n---")); assert!(contents.starts_with("---\nname: \"My Spike\"\n---"));

View File

@@ -35,7 +35,7 @@ pub struct StoryValidationResult {
/// Full pipeline state across all stages. /// Full pipeline state across all stages.
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct PipelineState { pub struct PipelineState {
pub upcoming: Vec<UpcomingStory>, pub backlog: Vec<UpcomingStory>,
pub current: Vec<UpcomingStory>, pub current: Vec<UpcomingStory>,
pub qa: Vec<UpcomingStory>, pub qa: Vec<UpcomingStory>,
pub merge: Vec<UpcomingStory>, pub merge: Vec<UpcomingStory>,
@@ -46,7 +46,7 @@ pub struct PipelineState {
pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> { pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
let agent_map = build_active_agent_map(ctx); let agent_map = build_active_agent_map(ctx);
Ok(PipelineState { Ok(PipelineState {
upcoming: load_stage_items(ctx, "1_upcoming", &HashMap::new())?, backlog: load_stage_items(ctx, "1_backlog", &HashMap::new())?,
current: load_stage_items(ctx, "2_current", &agent_map)?, current: load_stage_items(ctx, "2_current", &agent_map)?,
qa: load_stage_items(ctx, "3_qa", &agent_map)?, qa: load_stage_items(ctx, "3_qa", &agent_map)?,
merge: load_stage_items(ctx, "4_merge", &agent_map)?, merge: load_stage_items(ctx, "4_merge", &agent_map)?,
@@ -130,7 +130,7 @@ fn load_stage_items(
} }
pub fn load_upcoming_stories(ctx: &AppContext) -> Result<Vec<UpcomingStory>, String> { pub fn load_upcoming_stories(ctx: &AppContext) -> Result<Vec<UpcomingStory>, String> {
load_stage_items(ctx, "1_upcoming", &HashMap::new()) load_stage_items(ctx, "1_backlog", &HashMap::new())
} }
/// Shared create-story logic used by both the OpenApi and MCP handlers. /// Shared create-story logic used by both the OpenApi and MCP handlers.
@@ -152,11 +152,11 @@ pub fn create_story_file(
} }
let filename = format!("{story_number}_story_{slug}.md"); let filename = format!("{story_number}_story_{slug}.md");
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming"); let backlog_dir = root.join(".story_kit").join("work").join("1_backlog");
fs::create_dir_all(&upcoming_dir) fs::create_dir_all(&backlog_dir)
.map_err(|e| format!("Failed to create upcoming directory: {e}"))?; .map_err(|e| format!("Failed to create backlog directory: {e}"))?;
let filepath = upcoming_dir.join(&filename); let filepath = backlog_dir.join(&filename);
if filepath.exists() { if filepath.exists() {
return Err(format!("Story file already exists: {filename}")); return Err(format!("Story file already exists: {filename}"));
} }
@@ -206,7 +206,7 @@ pub fn create_story_file(
// ── Bug file helpers ────────────────────────────────────────────── // ── Bug file helpers ──────────────────────────────────────────────
/// Create a bug file in `work/1_upcoming/` with a deterministic filename and auto-commit. /// Create a bug file in `work/1_backlog/` with a deterministic filename and auto-commit.
/// ///
/// Returns the bug_id (e.g. `"4_bug_login_crash"`). /// Returns the bug_id (e.g. `"4_bug_login_crash"`).
pub fn create_bug_file( pub fn create_bug_file(
@@ -226,9 +226,9 @@ pub fn create_bug_file(
} }
let filename = format!("{bug_number}_bug_{slug}.md"); let filename = format!("{bug_number}_bug_{slug}.md");
let bugs_dir = root.join(".story_kit").join("work").join("1_upcoming"); let bugs_dir = root.join(".story_kit").join("work").join("1_backlog");
fs::create_dir_all(&bugs_dir) fs::create_dir_all(&bugs_dir)
.map_err(|e| format!("Failed to create upcoming directory: {e}"))?; .map_err(|e| format!("Failed to create backlog directory: {e}"))?;
let filepath = bugs_dir.join(&filename); let filepath = bugs_dir.join(&filename);
if filepath.exists() { if filepath.exists() {
@@ -276,7 +276,7 @@ pub fn create_bug_file(
// ── Spike file helpers ──────────────────────────────────────────── // ── Spike file helpers ────────────────────────────────────────────
/// Create a spike file in `work/1_upcoming/` with a deterministic filename. /// Create a spike file in `work/1_backlog/` with a deterministic filename.
/// ///
/// Returns the spike_id (e.g. `"4_spike_filesystem_watcher_architecture"`). /// Returns the spike_id (e.g. `"4_spike_filesystem_watcher_architecture"`).
pub fn create_spike_file( pub fn create_spike_file(
@@ -292,11 +292,11 @@ pub fn create_spike_file(
} }
let filename = format!("{spike_number}_spike_{slug}.md"); let filename = format!("{spike_number}_spike_{slug}.md");
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming"); let backlog_dir = root.join(".story_kit").join("work").join("1_backlog");
fs::create_dir_all(&upcoming_dir) fs::create_dir_all(&backlog_dir)
.map_err(|e| format!("Failed to create upcoming directory: {e}"))?; .map_err(|e| format!("Failed to create backlog directory: {e}"))?;
let filepath = upcoming_dir.join(&filename); let filepath = backlog_dir.join(&filename);
if filepath.exists() { if filepath.exists() {
return Err(format!("Spike file already exists: {filename}")); return Err(format!("Spike file already exists: {filename}"));
} }
@@ -338,7 +338,7 @@ pub fn create_spike_file(
Ok(spike_id) Ok(spike_id)
} }
/// Create a refactor work item file in `work/1_upcoming/`. /// Create a refactor work item file in `work/1_backlog/`.
/// ///
/// Returns the refactor_id (e.g. `"5_refactor_split_agents_rs"`). /// Returns the refactor_id (e.g. `"5_refactor_split_agents_rs"`).
pub fn create_refactor_file( pub fn create_refactor_file(
@@ -355,11 +355,11 @@ pub fn create_refactor_file(
} }
let filename = format!("{refactor_number}_refactor_{slug}.md"); let filename = format!("{refactor_number}_refactor_{slug}.md");
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming"); let backlog_dir = root.join(".story_kit").join("work").join("1_backlog");
fs::create_dir_all(&upcoming_dir) fs::create_dir_all(&backlog_dir)
.map_err(|e| format!("Failed to create upcoming directory: {e}"))?; .map_err(|e| format!("Failed to create backlog directory: {e}"))?;
let filepath = upcoming_dir.join(&filename); let filepath = backlog_dir.join(&filename);
if filepath.exists() { if filepath.exists() {
return Err(format!("Refactor file already exists: {filename}")); return Err(format!("Refactor file already exists: {filename}"));
} }
@@ -427,18 +427,18 @@ fn extract_bug_name(path: &Path) -> Option<String> {
None None
} }
/// List all open bugs — files in `work/1_upcoming/` matching the `_bug_` naming pattern. /// List all open bugs — files in `work/1_backlog/` matching the `_bug_` naming pattern.
/// ///
/// Returns a sorted list of `(bug_id, name)` pairs. /// Returns a sorted list of `(bug_id, name)` pairs.
pub fn list_bug_files(root: &Path) -> Result<Vec<(String, String)>, String> { pub fn list_bug_files(root: &Path) -> Result<Vec<(String, String)>, String> {
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming"); let backlog_dir = root.join(".story_kit").join("work").join("1_backlog");
if !upcoming_dir.exists() { if !backlog_dir.exists() {
return Ok(Vec::new()); return Ok(Vec::new());
} }
let mut bugs = Vec::new(); let mut bugs = Vec::new();
for entry in for entry in
fs::read_dir(&upcoming_dir).map_err(|e| format!("Failed to read upcoming directory: {e}"))? fs::read_dir(&backlog_dir).map_err(|e| format!("Failed to read backlog directory: {e}"))?
{ {
let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?; let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
let path = entry.path(); let path = entry.path();
@@ -477,18 +477,18 @@ fn is_refactor_item(stem: &str) -> bool {
after_num.starts_with("_refactor_") after_num.starts_with("_refactor_")
} }
/// List all open refactors — files in `work/1_upcoming/` matching the `_refactor_` naming pattern. /// List all open refactors — files in `work/1_backlog/` matching the `_refactor_` naming pattern.
/// ///
/// Returns a sorted list of `(refactor_id, name)` pairs. /// Returns a sorted list of `(refactor_id, name)` pairs.
pub fn list_refactor_files(root: &Path) -> Result<Vec<(String, String)>, String> { pub fn list_refactor_files(root: &Path) -> Result<Vec<(String, String)>, String> {
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming"); let backlog_dir = root.join(".story_kit").join("work").join("1_backlog");
if !upcoming_dir.exists() { if !backlog_dir.exists() {
return Ok(Vec::new()); return Ok(Vec::new());
} }
let mut refactors = Vec::new(); let mut refactors = Vec::new();
for entry in fs::read_dir(&upcoming_dir) for entry in fs::read_dir(&backlog_dir)
.map_err(|e| format!("Failed to read upcoming directory: {e}"))? .map_err(|e| format!("Failed to read backlog directory: {e}"))?
{ {
let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?; let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
let path = entry.path(); let path = entry.path();
@@ -525,11 +525,11 @@ pub fn list_refactor_files(root: &Path) -> Result<Vec<(String, String)>, String>
/// Locate a work item file by searching all active pipeline stages. /// Locate a work item file by searching all active pipeline stages.
/// ///
/// Searches in priority order: 2_current, 1_upcoming, 3_qa, 4_merge, 5_done, 6_archived. /// Searches in priority order: 2_current, 1_backlog, 3_qa, 4_merge, 5_done, 6_archived.
fn find_story_file(project_root: &Path, story_id: &str) -> Result<PathBuf, String> { fn find_story_file(project_root: &Path, story_id: &str) -> Result<PathBuf, String> {
let filename = format!("{story_id}.md"); let filename = format!("{story_id}.md");
let sk = project_root.join(".story_kit").join("work"); let sk = project_root.join(".story_kit").join("work");
for stage in &["2_current", "1_upcoming", "3_qa", "4_merge", "5_done", "6_archived"] { for stage in &["2_current", "1_backlog", "3_qa", "4_merge", "5_done", "6_archived"] {
let path = sk.join(stage).join(&filename); let path = sk.join(stage).join(&filename);
if path.exists() { if path.exists() {
return Ok(path); return Ok(path);
@@ -778,7 +778,7 @@ fn next_item_number(root: &std::path::Path) -> Result<u32, String> {
let work_base = root.join(".story_kit").join("work"); let work_base = root.join(".story_kit").join("work");
let mut max_num: u32 = 0; let mut max_num: u32 = 0;
for subdir in &["1_upcoming", "2_current", "3_qa", "4_merge", "5_done", "6_archived"] { for subdir in &["1_backlog", "2_current", "3_qa", "4_merge", "5_done", "6_archived"] {
let dir = work_base.join(subdir); let dir = work_base.join(subdir);
if !dir.exists() { if !dir.exists() {
continue; continue;
@@ -973,10 +973,10 @@ pub fn validate_story_dirs(
) -> Result<Vec<StoryValidationResult>, String> { ) -> Result<Vec<StoryValidationResult>, String> {
let mut results = Vec::new(); let mut results = Vec::new();
// Directories to validate: work/2_current/ + work/1_upcoming/ // Directories to validate: work/2_current/ + work/1_backlog/
let dirs_to_validate: Vec<PathBuf> = vec![ let dirs_to_validate: Vec<PathBuf> = vec![
root.join(".story_kit").join("work").join("2_current"), root.join(".story_kit").join("work").join("2_current"),
root.join(".story_kit").join("work").join("1_upcoming"), root.join(".story_kit").join("work").join("1_backlog"),
]; ];
for dir in &dirs_to_validate { for dir in &dirs_to_validate {
@@ -1042,7 +1042,7 @@ mod tests {
let root = tmp.path().to_path_buf(); let root = tmp.path().to_path_buf();
for (stage, id) in &[ for (stage, id) in &[
("1_upcoming", "10_story_upcoming"), ("1_backlog", "10_story_upcoming"),
("2_current", "20_story_current"), ("2_current", "20_story_current"),
("3_qa", "30_story_qa"), ("3_qa", "30_story_qa"),
("4_merge", "40_story_merge"), ("4_merge", "40_story_merge"),
@@ -1060,8 +1060,8 @@ mod tests {
let ctx = crate::http::context::AppContext::new_test(root); let ctx = crate::http::context::AppContext::new_test(root);
let state = load_pipeline_state(&ctx).unwrap(); let state = load_pipeline_state(&ctx).unwrap();
assert_eq!(state.upcoming.len(), 1); assert_eq!(state.backlog.len(), 1);
assert_eq!(state.upcoming[0].story_id, "10_story_upcoming"); assert_eq!(state.backlog[0].story_id, "10_story_upcoming");
assert_eq!(state.current.len(), 1); assert_eq!(state.current.len(), 1);
assert_eq!(state.current[0].story_id, "20_story_current"); assert_eq!(state.current[0].story_id, "20_story_current");
@@ -1164,15 +1164,15 @@ mod tests {
#[test] #[test]
fn load_upcoming_parses_metadata() { fn load_upcoming_parses_metadata() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming"); let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&upcoming).unwrap(); fs::create_dir_all(&backlog).unwrap();
fs::write( fs::write(
upcoming.join("31_story_view_upcoming.md"), backlog.join("31_story_view_upcoming.md"),
"---\nname: View Upcoming\n---\n# Story\n", "---\nname: View Upcoming\n---\n# Story\n",
) )
.unwrap(); .unwrap();
fs::write( fs::write(
upcoming.join("32_story_worktree.md"), backlog.join("32_story_worktree.md"),
"---\nname: Worktree Orchestration\n---\n# Story\n", "---\nname: Worktree Orchestration\n---\n# Story\n",
) )
.unwrap(); .unwrap();
@@ -1189,11 +1189,11 @@ mod tests {
#[test] #[test]
fn load_upcoming_skips_non_md_files() { fn load_upcoming_skips_non_md_files() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming"); let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&upcoming).unwrap(); fs::create_dir_all(&backlog).unwrap();
fs::write(upcoming.join(".gitkeep"), "").unwrap(); fs::write(backlog.join(".gitkeep"), "").unwrap();
fs::write( fs::write(
upcoming.join("31_story_example.md"), backlog.join("31_story_example.md"),
"---\nname: A Story\n---\n", "---\nname: A Story\n---\n",
) )
.unwrap(); .unwrap();
@@ -1208,16 +1208,16 @@ mod tests {
fn validate_story_dirs_valid_files() { fn validate_story_dirs_valid_files() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join(".story_kit/work/2_current"); let current = tmp.path().join(".story_kit/work/2_current");
let upcoming = tmp.path().join(".story_kit/work/1_upcoming"); let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&current).unwrap(); fs::create_dir_all(&current).unwrap();
fs::create_dir_all(&upcoming).unwrap(); fs::create_dir_all(&backlog).unwrap();
fs::write( fs::write(
current.join("28_story_todos.md"), current.join("28_story_todos.md"),
"---\nname: Show TODOs\n---\n# Story\n", "---\nname: Show TODOs\n---\n# Story\n",
) )
.unwrap(); .unwrap();
fs::write( fs::write(
upcoming.join("36_story_front_matter.md"), backlog.join("36_story_front_matter.md"),
"---\nname: Enforce Front Matter\n---\n# Story\n", "---\nname: Enforce Front Matter\n---\n# Story\n",
) )
.unwrap(); .unwrap();
@@ -1302,7 +1302,7 @@ mod tests {
#[test] #[test]
fn next_item_number_empty_dirs() { fn next_item_number_empty_dirs() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let base = tmp.path().join(".story_kit/work/1_upcoming"); let base = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&base).unwrap(); fs::create_dir_all(&base).unwrap();
assert_eq!(next_item_number(tmp.path()).unwrap(), 1); assert_eq!(next_item_number(tmp.path()).unwrap(), 1);
} }
@@ -1310,13 +1310,13 @@ mod tests {
#[test] #[test]
fn next_item_number_scans_all_dirs() { fn next_item_number_scans_all_dirs() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming"); let backlog = tmp.path().join(".story_kit/work/1_backlog");
let current = tmp.path().join(".story_kit/work/2_current"); let current = tmp.path().join(".story_kit/work/2_current");
let archived = tmp.path().join(".story_kit/work/5_done"); let archived = tmp.path().join(".story_kit/work/5_done");
fs::create_dir_all(&upcoming).unwrap(); fs::create_dir_all(&backlog).unwrap();
fs::create_dir_all(&current).unwrap(); fs::create_dir_all(&current).unwrap();
fs::create_dir_all(&archived).unwrap(); fs::create_dir_all(&archived).unwrap();
fs::write(upcoming.join("10_story_foo.md"), "").unwrap(); fs::write(backlog.join("10_story_foo.md"), "").unwrap();
fs::write(current.join("20_story_bar.md"), "").unwrap(); fs::write(current.join("20_story_bar.md"), "").unwrap();
fs::write(archived.join("15_story_baz.md"), "").unwrap(); fs::write(archived.join("15_story_baz.md"), "").unwrap();
assert_eq!(next_item_number(tmp.path()).unwrap(), 21); assert_eq!(next_item_number(tmp.path()).unwrap(), 21);
@@ -1334,9 +1334,9 @@ mod tests {
#[test] #[test]
fn create_story_writes_correct_content() { fn create_story_writes_correct_content() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming"); let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&upcoming).unwrap(); fs::create_dir_all(&backlog).unwrap();
fs::write(upcoming.join("36_story_existing.md"), "").unwrap(); fs::write(backlog.join("36_story_existing.md"), "").unwrap();
let number = next_item_number(tmp.path()).unwrap(); let number = next_item_number(tmp.path()).unwrap();
assert_eq!(number, 37); assert_eq!(number, 37);
@@ -1345,7 +1345,7 @@ mod tests {
assert_eq!(slug, "my_new_feature"); assert_eq!(slug, "my_new_feature");
let filename = format!("{number}_{slug}.md"); let filename = format!("{number}_{slug}.md");
let filepath = upcoming.join(&filename); let filepath = backlog.join(&filename);
let mut content = String::new(); let mut content = String::new();
content.push_str("---\n"); content.push_str("---\n");
@@ -1377,10 +1377,10 @@ mod tests {
let result = create_story_file(tmp.path(), name, None, None, false); let result = create_story_file(tmp.path(), name, None, None, false);
assert!(result.is_ok(), "create_story_file failed: {result:?}"); assert!(result.is_ok(), "create_story_file failed: {result:?}");
let upcoming = tmp.path().join(".story_kit/work/1_upcoming"); let backlog = tmp.path().join(".story_kit/work/1_backlog");
let story_id = result.unwrap(); let story_id = result.unwrap();
let filename = format!("{story_id}.md"); let filename = format!("{story_id}.md");
let contents = fs::read_to_string(upcoming.join(&filename)).unwrap(); let contents = fs::read_to_string(backlog.join(&filename)).unwrap();
let meta = parse_front_matter(&contents).expect("front matter should be valid YAML"); let meta = parse_front_matter(&contents).expect("front matter should be valid YAML");
assert_eq!(meta.name.as_deref(), Some(name)); assert_eq!(meta.name.as_deref(), Some(name));
@@ -1389,10 +1389,10 @@ mod tests {
#[test] #[test]
fn create_story_rejects_duplicate() { fn create_story_rejects_duplicate() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming"); let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&upcoming).unwrap(); fs::create_dir_all(&backlog).unwrap();
let filepath = upcoming.join("1_story_my_feature.md"); let filepath = backlog.join("1_story_my_feature.md");
fs::write(&filepath, "existing").unwrap(); fs::write(&filepath, "existing").unwrap();
// Simulate the check // Simulate the check
@@ -1511,17 +1511,17 @@ mod tests {
} }
#[test] #[test]
fn find_story_file_searches_current_then_upcoming() { fn find_story_file_searches_current_then_backlog() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let current = tmp.path().join(".story_kit/work/2_current"); let current = tmp.path().join(".story_kit/work/2_current");
let upcoming = tmp.path().join(".story_kit/work/1_upcoming"); let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&current).unwrap(); fs::create_dir_all(&current).unwrap();
fs::create_dir_all(&upcoming).unwrap(); fs::create_dir_all(&backlog).unwrap();
// Only in upcoming // Only in backlog
fs::write(upcoming.join("6_test.md"), "").unwrap(); fs::write(backlog.join("6_test.md"), "").unwrap();
let found = find_story_file(tmp.path(), "6_test").unwrap(); let found = find_story_file(tmp.path(), "6_test").unwrap();
assert!(found.ends_with("1_upcoming/6_test.md") || found.ends_with("1_upcoming\\6_test.md")); assert!(found.ends_with("1_backlog/6_test.md") || found.ends_with("1_backlog\\6_test.md"));
// Also in current — current should win // Also in current — current should win
fs::write(current.join("6_test.md"), "").unwrap(); fs::write(current.join("6_test.md"), "").unwrap();
@@ -1724,19 +1724,19 @@ mod tests {
#[test] #[test]
fn next_item_number_increments_from_existing_bugs() { fn next_item_number_increments_from_existing_bugs() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming"); let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&upcoming).unwrap(); fs::create_dir_all(&backlog).unwrap();
fs::write(upcoming.join("1_bug_crash.md"), "").unwrap(); fs::write(backlog.join("1_bug_crash.md"), "").unwrap();
fs::write(upcoming.join("3_bug_another.md"), "").unwrap(); fs::write(backlog.join("3_bug_another.md"), "").unwrap();
assert_eq!(next_item_number(tmp.path()).unwrap(), 4); assert_eq!(next_item_number(tmp.path()).unwrap(), 4);
} }
#[test] #[test]
fn next_item_number_scans_archived_too() { fn next_item_number_scans_archived_too() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming"); let backlog = tmp.path().join(".story_kit/work/1_backlog");
let archived = tmp.path().join(".story_kit/work/5_done"); let archived = tmp.path().join(".story_kit/work/5_done");
fs::create_dir_all(&upcoming).unwrap(); fs::create_dir_all(&backlog).unwrap();
fs::create_dir_all(&archived).unwrap(); fs::create_dir_all(&archived).unwrap();
fs::write(archived.join("5_bug_old.md"), "").unwrap(); fs::write(archived.join("5_bug_old.md"), "").unwrap();
assert_eq!(next_item_number(tmp.path()).unwrap(), 6); assert_eq!(next_item_number(tmp.path()).unwrap(), 6);
@@ -1752,11 +1752,11 @@ mod tests {
#[test] #[test]
fn list_bug_files_excludes_archive_subdir() { fn list_bug_files_excludes_archive_subdir() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let upcoming_dir = tmp.path().join(".story_kit/work/1_upcoming"); let backlog_dir = tmp.path().join(".story_kit/work/1_backlog");
let archived_dir = tmp.path().join(".story_kit/work/5_done"); let archived_dir = tmp.path().join(".story_kit/work/5_done");
fs::create_dir_all(&upcoming_dir).unwrap(); fs::create_dir_all(&backlog_dir).unwrap();
fs::create_dir_all(&archived_dir).unwrap(); fs::create_dir_all(&archived_dir).unwrap();
fs::write(upcoming_dir.join("1_bug_open.md"), "# Bug 1: Open Bug\n").unwrap(); fs::write(backlog_dir.join("1_bug_open.md"), "# Bug 1: Open Bug\n").unwrap();
fs::write(archived_dir.join("2_bug_closed.md"), "# Bug 2: Closed Bug\n").unwrap(); fs::write(archived_dir.join("2_bug_closed.md"), "# Bug 2: Closed Bug\n").unwrap();
let result = list_bug_files(tmp.path()).unwrap(); let result = list_bug_files(tmp.path()).unwrap();
@@ -1768,11 +1768,11 @@ mod tests {
#[test] #[test]
fn list_bug_files_sorted_by_id() { fn list_bug_files_sorted_by_id() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let upcoming_dir = tmp.path().join(".story_kit/work/1_upcoming"); let backlog_dir = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&upcoming_dir).unwrap(); fs::create_dir_all(&backlog_dir).unwrap();
fs::write(upcoming_dir.join("3_bug_third.md"), "# Bug 3: Third\n").unwrap(); fs::write(backlog_dir.join("3_bug_third.md"), "# Bug 3: Third\n").unwrap();
fs::write(upcoming_dir.join("1_bug_first.md"), "# Bug 1: First\n").unwrap(); fs::write(backlog_dir.join("1_bug_first.md"), "# Bug 1: First\n").unwrap();
fs::write(upcoming_dir.join("2_bug_second.md"), "# Bug 2: Second\n").unwrap(); fs::write(backlog_dir.join("2_bug_second.md"), "# Bug 2: Second\n").unwrap();
let result = list_bug_files(tmp.path()).unwrap(); let result = list_bug_files(tmp.path()).unwrap();
assert_eq!(result.len(), 3); assert_eq!(result.len(), 3);
@@ -1810,7 +1810,7 @@ mod tests {
let filepath = tmp let filepath = tmp
.path() .path()
.join(".story_kit/work/1_upcoming/1_bug_login_crash.md"); .join(".story_kit/work/1_backlog/1_bug_login_crash.md");
assert!(filepath.exists()); assert!(filepath.exists());
let contents = fs::read_to_string(&filepath).unwrap(); let contents = fs::read_to_string(&filepath).unwrap();
assert!( assert!(
@@ -1854,7 +1854,7 @@ mod tests {
) )
.unwrap(); .unwrap();
let filepath = tmp.path().join(".story_kit/work/1_upcoming/1_bug_some_bug.md"); let filepath = tmp.path().join(".story_kit/work/1_backlog/1_bug_some_bug.md");
let contents = fs::read_to_string(&filepath).unwrap(); let contents = fs::read_to_string(&filepath).unwrap();
assert!( assert!(
contents.starts_with("---\nname: \"Some Bug\"\n---"), contents.starts_with("---\nname: \"Some Bug\"\n---"),
@@ -1876,7 +1876,7 @@ mod tests {
let filepath = tmp let filepath = tmp
.path() .path()
.join(".story_kit/work/1_upcoming/1_spike_filesystem_watcher_architecture.md"); .join(".story_kit/work/1_backlog/1_spike_filesystem_watcher_architecture.md");
assert!(filepath.exists()); assert!(filepath.exists());
let contents = fs::read_to_string(&filepath).unwrap(); let contents = fs::read_to_string(&filepath).unwrap();
assert!( assert!(
@@ -1900,7 +1900,7 @@ mod tests {
create_spike_file(tmp.path(), "FS Watcher Spike", Some(description)).unwrap(); create_spike_file(tmp.path(), "FS Watcher Spike", Some(description)).unwrap();
let filepath = let filepath =
tmp.path().join(".story_kit/work/1_upcoming/1_spike_fs_watcher_spike.md"); tmp.path().join(".story_kit/work/1_backlog/1_spike_fs_watcher_spike.md");
let contents = fs::read_to_string(&filepath).unwrap(); let contents = fs::read_to_string(&filepath).unwrap();
assert!(contents.contains(description)); assert!(contents.contains(description));
} }
@@ -1910,7 +1910,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
create_spike_file(tmp.path(), "My Spike", None).unwrap(); create_spike_file(tmp.path(), "My Spike", None).unwrap();
let filepath = tmp.path().join(".story_kit/work/1_upcoming/1_spike_my_spike.md"); let filepath = tmp.path().join(".story_kit/work/1_backlog/1_spike_my_spike.md");
let contents = fs::read_to_string(&filepath).unwrap(); let contents = fs::read_to_string(&filepath).unwrap();
// Should have placeholder TBD in Question section // Should have placeholder TBD in Question section
assert!(contents.contains("## Question\n\n- TBD\n")); assert!(contents.contains("## Question\n\n- TBD\n"));
@@ -1931,10 +1931,10 @@ mod tests {
let result = create_spike_file(tmp.path(), name, None); let result = create_spike_file(tmp.path(), name, None);
assert!(result.is_ok(), "create_spike_file failed: {result:?}"); assert!(result.is_ok(), "create_spike_file failed: {result:?}");
let upcoming = tmp.path().join(".story_kit/work/1_upcoming"); let backlog = tmp.path().join(".story_kit/work/1_backlog");
let spike_id = result.unwrap(); let spike_id = result.unwrap();
let filename = format!("{spike_id}.md"); let filename = format!("{spike_id}.md");
let contents = fs::read_to_string(upcoming.join(&filename)).unwrap(); let contents = fs::read_to_string(backlog.join(&filename)).unwrap();
let meta = parse_front_matter(&contents).expect("front matter should be valid YAML"); let meta = parse_front_matter(&contents).expect("front matter should be valid YAML");
assert_eq!(meta.name.as_deref(), Some(name)); assert_eq!(meta.name.as_deref(), Some(name));
@@ -1943,9 +1943,9 @@ mod tests {
#[test] #[test]
fn create_spike_file_increments_from_existing_items() { fn create_spike_file_increments_from_existing_items() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let upcoming = tmp.path().join(".story_kit/work/1_upcoming"); let backlog = tmp.path().join(".story_kit/work/1_backlog");
fs::create_dir_all(&upcoming).unwrap(); fs::create_dir_all(&backlog).unwrap();
fs::write(upcoming.join("5_story_existing.md"), "").unwrap(); fs::write(backlog.join("5_story_existing.md"), "").unwrap();
let spike_id = create_spike_file(tmp.path(), "My Spike", None).unwrap(); let spike_id = create_spike_file(tmp.path(), "My Spike", None).unwrap();
assert!(spike_id.starts_with("6_spike_"), "expected spike number 6, got: {spike_id}"); assert!(spike_id.starts_with("6_spike_"), "expected spike number 6, got: {spike_id}");

View File

@@ -79,7 +79,7 @@ enum WsResponse {
}, },
/// Full pipeline state pushed on connect and after every work-item watcher event. /// Full pipeline state pushed on connect and after every work-item watcher event.
PipelineState { PipelineState {
upcoming: Vec<crate::http::workflow::UpcomingStory>, backlog: Vec<crate::http::workflow::UpcomingStory>,
current: Vec<crate::http::workflow::UpcomingStory>, current: Vec<crate::http::workflow::UpcomingStory>,
qa: Vec<crate::http::workflow::UpcomingStory>, qa: Vec<crate::http::workflow::UpcomingStory>,
merge: Vec<crate::http::workflow::UpcomingStory>, merge: Vec<crate::http::workflow::UpcomingStory>,
@@ -160,7 +160,7 @@ impl From<WatcherEvent> for Option<WsResponse> {
impl From<PipelineState> for WsResponse { impl From<PipelineState> for WsResponse {
fn from(s: PipelineState) -> Self { fn from(s: PipelineState) -> Self {
WsResponse::PipelineState { WsResponse::PipelineState {
upcoming: s.upcoming, backlog: s.backlog,
current: s.current, current: s.current,
qa: s.qa, qa: s.qa,
merge: s.merge, merge: s.merge,
@@ -695,7 +695,7 @@ mod tests {
agent: None, agent: None,
}; };
let resp = WsResponse::PipelineState { let resp = WsResponse::PipelineState {
upcoming: vec![story], backlog: vec![story],
current: vec![], current: vec![],
qa: vec![], qa: vec![],
merge: vec![], merge: vec![],
@@ -703,8 +703,8 @@ mod tests {
}; };
let json = serde_json::to_value(&resp).unwrap(); let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["type"], "pipeline_state"); assert_eq!(json["type"], "pipeline_state");
assert_eq!(json["upcoming"].as_array().unwrap().len(), 1); assert_eq!(json["backlog"].as_array().unwrap().len(), 1);
assert_eq!(json["upcoming"][0]["story_id"], "10_story_test"); assert_eq!(json["backlog"][0]["story_id"], "10_story_test");
assert!(json["current"].as_array().unwrap().is_empty()); assert!(json["current"].as_array().unwrap().is_empty());
assert!(json["done"].as_array().unwrap().is_empty()); assert!(json["done"].as_array().unwrap().is_empty());
} }
@@ -824,7 +824,7 @@ mod tests {
#[test] #[test]
fn pipeline_state_converts_to_ws_response() { fn pipeline_state_converts_to_ws_response() {
let state = PipelineState { let state = PipelineState {
upcoming: vec![UpcomingStory { backlog: vec![UpcomingStory {
story_id: "1_story_a".to_string(), story_id: "1_story_a".to_string(),
name: Some("Story A".to_string()), name: Some("Story A".to_string()),
error: None, error: None,
@@ -851,8 +851,8 @@ mod tests {
let resp: WsResponse = state.into(); let resp: WsResponse = state.into();
let json = serde_json::to_value(&resp).unwrap(); let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["type"], "pipeline_state"); assert_eq!(json["type"], "pipeline_state");
assert_eq!(json["upcoming"].as_array().unwrap().len(), 1); assert_eq!(json["backlog"].as_array().unwrap().len(), 1);
assert_eq!(json["upcoming"][0]["story_id"], "1_story_a"); assert_eq!(json["backlog"][0]["story_id"], "1_story_a");
assert_eq!(json["current"].as_array().unwrap().len(), 1); assert_eq!(json["current"].as_array().unwrap().len(), 1);
assert_eq!(json["current"][0]["story_id"], "2_story_b"); assert_eq!(json["current"][0]["story_id"], "2_story_b");
assert!(json["qa"].as_array().unwrap().is_empty()); assert!(json["qa"].as_array().unwrap().is_empty());
@@ -864,7 +864,7 @@ mod tests {
#[test] #[test]
fn empty_pipeline_state_converts_to_ws_response() { fn empty_pipeline_state_converts_to_ws_response() {
let state = PipelineState { let state = PipelineState {
upcoming: vec![], backlog: vec![],
current: vec![], current: vec![],
qa: vec![], qa: vec![],
merge: vec![], merge: vec![],
@@ -873,7 +873,7 @@ mod tests {
let resp: WsResponse = state.into(); let resp: WsResponse = state.into();
let json = serde_json::to_value(&resp).unwrap(); let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["type"], "pipeline_state"); assert_eq!(json["type"], "pipeline_state");
assert!(json["upcoming"].as_array().unwrap().is_empty()); assert!(json["backlog"].as_array().unwrap().is_empty());
assert!(json["current"].as_array().unwrap().is_empty()); assert!(json["current"].as_array().unwrap().is_empty());
assert!(json["qa"].as_array().unwrap().is_empty()); assert!(json["qa"].as_array().unwrap().is_empty());
assert!(json["merge"].as_array().unwrap().is_empty()); assert!(json["merge"].as_array().unwrap().is_empty());
@@ -991,7 +991,7 @@ mod tests {
#[test] #[test]
fn pipeline_state_with_agent_converts_correctly() { fn pipeline_state_with_agent_converts_correctly() {
let state = PipelineState { let state = PipelineState {
upcoming: vec![], backlog: vec![],
current: vec![UpcomingStory { current: vec![UpcomingStory {
story_id: "10_story_x".to_string(), story_id: "10_story_x".to_string(),
name: Some("Story X".to_string()), name: Some("Story X".to_string()),
@@ -1046,7 +1046,7 @@ mod tests {
let root = tmp.path().to_path_buf(); let root = tmp.path().to_path_buf();
// Create minimal pipeline dirs so load_pipeline_state succeeds. // Create minimal pipeline dirs so load_pipeline_state succeeds.
for stage in &["1_upcoming", "2_current", "3_qa", "4_merge"] { for stage in &["1_backlog", "2_current", "3_qa", "4_merge"] {
std::fs::create_dir_all(root.join(".story_kit").join("work").join(stage)).unwrap(); std::fs::create_dir_all(root.join(".story_kit").join("work").join(stage)).unwrap();
} }
@@ -1155,7 +1155,7 @@ mod tests {
assert_eq!(initial["type"], "pipeline_state"); assert_eq!(initial["type"], "pipeline_state");
// All stages should be empty arrays since no .md files were created. // All stages should be empty arrays since no .md files were created.
assert!(initial["upcoming"].as_array().unwrap().is_empty()); assert!(initial["backlog"].as_array().unwrap().is_empty());
assert!(initial["current"].as_array().unwrap().is_empty()); assert!(initial["current"].as_array().unwrap().is_empty());
assert!(initial["qa"].as_array().unwrap().is_empty()); assert!(initial["qa"].as_array().unwrap().is_empty());
assert!(initial["merge"].as_array().unwrap().is_empty()); assert!(initial["merge"].as_array().unwrap().is_empty());

View File

@@ -409,7 +409,7 @@ fn scaffold_story_kit(root: &Path) -> Result<(), String> {
// Create the work/ pipeline directories, each with a .gitkeep so empty dirs survive git clone // Create the work/ pipeline directories, each with a .gitkeep so empty dirs survive git clone
let work_stages = [ let work_stages = [
"1_upcoming", "1_backlog",
"2_current", "2_current",
"3_qa", "3_qa",
"4_merge", "4_merge",
@@ -1085,7 +1085,7 @@ mod tests {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
scaffold_story_kit(dir.path()).unwrap(); scaffold_story_kit(dir.path()).unwrap();
let stages = ["1_upcoming", "2_current", "3_qa", "4_merge", "5_done", "6_archived"]; let stages = ["1_backlog", "2_current", "3_qa", "4_merge", "5_done", "6_archived"];
for stage in &stages { for stage in &stages {
let path = dir.path().join(".story_kit/work").join(stage); let path = dir.path().join(".story_kit/work").join(stage);
assert!(path.is_dir(), "work/{} should be a directory", stage); assert!(path.is_dir(), "work/{} should be a directory", stage);

View File

@@ -78,7 +78,7 @@ pub fn is_config_file(path: &Path, git_root: &Path) -> bool {
/// Map a pipeline directory name to a (action, commit-message-prefix) pair. /// Map a pipeline directory name to a (action, commit-message-prefix) pair.
fn stage_metadata(stage: &str, item_id: &str) -> Option<(&'static str, String)> { fn stage_metadata(stage: &str, item_id: &str) -> Option<(&'static str, String)> {
let (action, prefix) = match stage { let (action, prefix) = match stage {
"1_upcoming" => ("create", format!("story-kit: create {item_id}")), "1_backlog" => ("create", format!("story-kit: create {item_id}")),
"2_current" => ("start", format!("story-kit: start {item_id}")), "2_current" => ("start", format!("story-kit: start {item_id}")),
"3_qa" => ("qa", format!("story-kit: queue {item_id} for QA")), "3_qa" => ("qa", format!("story-kit: queue {item_id} for QA")),
"4_merge" => ("merge", format!("story-kit: queue {item_id} for merge")), "4_merge" => ("merge", format!("story-kit: queue {item_id} for merge")),
@@ -111,7 +111,7 @@ fn stage_for_path(path: &Path) -> Option<String> {
.parent() .parent()
.and_then(|p| p.file_name()) .and_then(|p| p.file_name())
.and_then(|n| n.to_str())?; .and_then(|n| n.to_str())?;
matches!(stage, "1_upcoming" | "2_current" | "3_qa" | "4_merge" | "5_done" | "6_archived") matches!(stage, "1_backlog" | "2_current" | "3_qa" | "4_merge" | "5_done" | "6_archived")
.then(|| stage.to_string()) .then(|| stage.to_string())
} }
@@ -159,7 +159,7 @@ fn git_add_work_and_commit(git_root: &Path, message: &str) -> Result<bool, Strin
/// Intermediate stages (current, qa, merge, done) are transient pipeline state /// Intermediate stages (current, qa, merge, done) are transient pipeline state
/// that don't need to be committed — they're only relevant while the server is /// that don't need to be committed — they're only relevant while the server is
/// running and are broadcast to WebSocket clients for real-time UI updates. /// running and are broadcast to WebSocket clients for real-time UI updates.
const COMMIT_WORTHY_STAGES: &[&str] = &["1_upcoming", "5_done", "6_archived"]; const COMMIT_WORTHY_STAGES: &[&str] = &["1_backlog", "5_done", "6_archived"];
/// Return `true` if changes in `stage` should be committed to git. /// Return `true` if changes in `stage` should be committed to git.
fn should_commit_stage(stage: &str) -> bool { fn should_commit_stage(stage: &str) -> bool {
@@ -172,7 +172,7 @@ fn should_commit_stage(stage: &str) -> bool {
/// (they represent the destination of a move or a new file). Deletions are /// (they represent the destination of a move or a new file). Deletions are
/// captured by `git add -A .story_kit/work/` automatically. /// captured by `git add -A .story_kit/work/` automatically.
/// ///
/// Only terminal stages (`1_upcoming` and `6_archived`) trigger git commits. /// Only terminal stages (`1_backlog` and `6_archived`) trigger git commits.
/// All stages broadcast a [`WatcherEvent`] so the frontend stays in sync. /// All stages broadcast a [`WatcherEvent`] so the frontend stays in sync.
fn flush_pending( fn flush_pending(
pending: &HashMap<PathBuf, String>, pending: &HashMap<PathBuf, String>,
@@ -574,13 +574,13 @@ mod tests {
fn flush_pending_commits_and_broadcasts_for_terminal_stage() { fn flush_pending_commits_and_broadcasts_for_terminal_stage() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
init_git_repo(tmp.path()); init_git_repo(tmp.path());
let stage_dir = make_stage_dir(tmp.path(), "1_upcoming"); let stage_dir = make_stage_dir(tmp.path(), "1_backlog");
let story_path = stage_dir.join("42_story_foo.md"); let story_path = stage_dir.join("42_story_foo.md");
fs::write(&story_path, "---\nname: test\n---\n").unwrap(); fs::write(&story_path, "---\nname: test\n---\n").unwrap();
let (tx, mut rx) = tokio::sync::broadcast::channel(16); let (tx, mut rx) = tokio::sync::broadcast::channel(16);
let mut pending = HashMap::new(); let mut pending = HashMap::new();
pending.insert(story_path, "1_upcoming".to_string()); pending.insert(story_path, "1_backlog".to_string());
flush_pending(&pending, tmp.path(), &tx); flush_pending(&pending, tmp.path(), &tx);
@@ -592,7 +592,7 @@ mod tests {
action, action,
commit_msg, commit_msg,
} => { } => {
assert_eq!(stage, "1_upcoming"); assert_eq!(stage, "1_backlog");
assert_eq!(item_id, "42_story_foo"); assert_eq!(item_id, "42_story_foo");
assert_eq!(action, "create"); assert_eq!(action, "create");
assert_eq!(commit_msg, "story-kit: create 42_story_foo"); assert_eq!(commit_msg, "story-kit: create 42_story_foo");
@@ -660,7 +660,7 @@ mod tests {
#[test] #[test]
fn flush_pending_broadcasts_for_all_pipeline_stages() { fn flush_pending_broadcasts_for_all_pipeline_stages() {
let stages = [ let stages = [
("1_upcoming", "create", "story-kit: create 10_story_x"), ("1_backlog", "create", "story-kit: create 10_story_x"),
("3_qa", "qa", "story-kit: queue 10_story_x for QA"), ("3_qa", "qa", "story-kit: queue 10_story_x for QA"),
("4_merge", "merge", "story-kit: queue 10_story_x for merge"), ("4_merge", "merge", "story-kit: queue 10_story_x for merge"),
("5_done", "done", "story-kit: done 10_story_x"), ("5_done", "done", "story-kit: done 10_story_x"),
@@ -792,10 +792,10 @@ mod tests {
} }
#[test] #[test]
fn flush_pending_clears_merge_failure_when_moving_to_upcoming() { fn flush_pending_clears_merge_failure_when_moving_to_backlog() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
init_git_repo(tmp.path()); init_git_repo(tmp.path());
let stage_dir = make_stage_dir(tmp.path(), "1_upcoming"); let stage_dir = make_stage_dir(tmp.path(), "1_backlog");
let story_path = stage_dir.join("51_story_reset.md"); let story_path = stage_dir.join("51_story_reset.md");
fs::write( fs::write(
&story_path, &story_path,
@@ -805,14 +805,14 @@ mod tests {
let (tx, _rx) = tokio::sync::broadcast::channel(16); let (tx, _rx) = tokio::sync::broadcast::channel(16);
let mut pending = HashMap::new(); let mut pending = HashMap::new();
pending.insert(story_path.clone(), "1_upcoming".to_string()); pending.insert(story_path.clone(), "1_backlog".to_string());
flush_pending(&pending, tmp.path(), &tx); flush_pending(&pending, tmp.path(), &tx);
let contents = fs::read_to_string(&story_path).unwrap(); let contents = fs::read_to_string(&story_path).unwrap();
assert!( assert!(
!contents.contains("merge_failure"), !contents.contains("merge_failure"),
"merge_failure should be stripped when story lands in 1_upcoming" "merge_failure should be stripped when story lands in 1_backlog"
); );
} }
@@ -937,7 +937,7 @@ mod tests {
#[test] #[test]
fn should_commit_stage_only_for_terminal_stages() { fn should_commit_stage_only_for_terminal_stages() {
// Terminal stages — should commit. // Terminal stages — should commit.
assert!(should_commit_stage("1_upcoming")); assert!(should_commit_stage("1_backlog"));
assert!(should_commit_stage("5_done")); assert!(should_commit_stage("5_done"));
assert!(should_commit_stage("6_archived")); assert!(should_commit_stage("6_archived"));
// Intermediate stages — broadcast-only, no commit. // Intermediate stages — broadcast-only, no commit.

View File

@@ -15,7 +15,7 @@ use tokio::sync::broadcast;
/// Human-readable display name for a pipeline stage directory. /// Human-readable display name for a pipeline stage directory.
pub fn stage_display_name(stage: &str) -> &'static str { pub fn stage_display_name(stage: &str) -> &'static str {
match stage { match stage {
"1_upcoming" => "Upcoming", "1_backlog" => "Backlog",
"2_current" => "Current", "2_current" => "Current",
"3_qa" => "QA", "3_qa" => "QA",
"4_merge" => "Merge", "4_merge" => "Merge",
@@ -27,11 +27,11 @@ pub fn stage_display_name(stage: &str) -> &'static str {
/// Infer the previous pipeline stage for a given destination stage. /// Infer the previous pipeline stage for a given destination stage.
/// ///
/// Returns `None` for `1_upcoming` since items are created there (not /// Returns `None` for `1_backlog` since items are created there (not
/// transitioned from another stage). /// transitioned from another stage).
pub fn inferred_from_stage(to_stage: &str) -> Option<&'static str> { pub fn inferred_from_stage(to_stage: &str) -> Option<&'static str> {
match to_stage { match to_stage {
"2_current" => Some("Upcoming"), "2_current" => Some("Backlog"),
"3_qa" => Some("Current"), "3_qa" => Some("Current"),
"4_merge" => Some("QA"), "4_merge" => Some("QA"),
"5_done" => Some("Merge"), "5_done" => Some("Merge"),
@@ -195,7 +195,7 @@ mod tests {
#[test] #[test]
fn stage_display_name_maps_all_known_stages() { fn stage_display_name_maps_all_known_stages() {
assert_eq!(stage_display_name("1_upcoming"), "Upcoming"); assert_eq!(stage_display_name("1_backlog"), "Backlog");
assert_eq!(stage_display_name("2_current"), "Current"); assert_eq!(stage_display_name("2_current"), "Current");
assert_eq!(stage_display_name("3_qa"), "QA"); assert_eq!(stage_display_name("3_qa"), "QA");
assert_eq!(stage_display_name("4_merge"), "Merge"); assert_eq!(stage_display_name("4_merge"), "Merge");
@@ -208,7 +208,7 @@ mod tests {
#[test] #[test]
fn inferred_from_stage_returns_previous_stage() { fn inferred_from_stage_returns_previous_stage() {
assert_eq!(inferred_from_stage("2_current"), Some("Upcoming")); assert_eq!(inferred_from_stage("2_current"), Some("Backlog"));
assert_eq!(inferred_from_stage("3_qa"), Some("Current")); assert_eq!(inferred_from_stage("3_qa"), Some("Current"));
assert_eq!(inferred_from_stage("4_merge"), Some("QA")); assert_eq!(inferred_from_stage("4_merge"), Some("QA"));
assert_eq!(inferred_from_stage("5_done"), Some("Merge")); assert_eq!(inferred_from_stage("5_done"), Some("Merge"));
@@ -216,8 +216,8 @@ mod tests {
} }
#[test] #[test]
fn inferred_from_stage_returns_none_for_upcoming() { fn inferred_from_stage_returns_none_for_backlog() {
assert_eq!(inferred_from_stage("1_upcoming"), None); assert_eq!(inferred_from_stage("1_backlog"), None);
} }
#[test] #[test]