story-kit: merge 287_story_rename_upcoming_pipeline_stage_to_backlog
This commit is contained in:
@@ -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.
|
||||
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.
|
||||
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
|
||||
├── project.toml # Agent configuration (roles, models, prompts)
|
||||
├── 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
|
||||
│ ├── 3_qa/ # QA review
|
||||
│ ├── 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:
|
||||
|
||||
`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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
* 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
|
||||
|
||||
---
|
||||
@@ -156,7 +156,7 @@ Not everything needs to be a full story. Simple bugs can skip the story process:
|
||||
* Performance issues with known fixes
|
||||
|
||||
### 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
|
||||
* **Root Cause:** Technical explanation (if known)
|
||||
* **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
|
||||
|
||||
### 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
|
||||
* **Hypothesis:** What you expect to be true
|
||||
* **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.**
|
||||
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`.
|
||||
* *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.
|
||||
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`.
|
||||
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.
|
||||
6. **Draft Stack:** Write `specs/tech/STACK.md` based on best practices for that language.
|
||||
7. **Wait:** Ask the user for "Story #1".
|
||||
|
||||
@@ -34,7 +34,7 @@ You have these tools via the story-kit MCP server:
|
||||
## Your Workflow
|
||||
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
|
||||
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"
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
@@ -262,7 +262,7 @@ describe("ChatWebSocket", () => {
|
||||
|
||||
// Server pushes pipeline_state on fresh connection
|
||||
const freshState = {
|
||||
upcoming: [{ story_id: "1_story_test", name: "Test", error: null }],
|
||||
backlog: [{ story_id: "1_story_test", name: "Test", error: null }],
|
||||
current: [],
|
||||
qa: [],
|
||||
merge: [],
|
||||
|
||||
@@ -36,7 +36,7 @@ export interface PipelineStageItem {
|
||||
}
|
||||
|
||||
export interface PipelineState {
|
||||
upcoming: PipelineStageItem[];
|
||||
backlog: PipelineStageItem[];
|
||||
current: PipelineStageItem[];
|
||||
qa: PipelineStageItem[];
|
||||
merge: PipelineStageItem[];
|
||||
@@ -50,7 +50,7 @@ export type WsResponse =
|
||||
| { type: "error"; message: string }
|
||||
| {
|
||||
type: "pipeline_state";
|
||||
upcoming: PipelineStageItem[];
|
||||
backlog: PipelineStageItem[];
|
||||
current: PipelineStageItem[];
|
||||
qa: PipelineStageItem[];
|
||||
merge: PipelineStageItem[];
|
||||
@@ -398,7 +398,7 @@ export class ChatWebSocket {
|
||||
if (data.type === "error") this.onError?.(data.message);
|
||||
if (data.type === "pipeline_state")
|
||||
this.onPipelineState?.({
|
||||
upcoming: data.upcoming,
|
||||
backlog: data.backlog,
|
||||
current: data.current,
|
||||
qa: data.qa,
|
||||
merge: data.merge,
|
||||
|
||||
@@ -165,7 +165,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
const [apiKeyInput, setApiKeyInput] = useState("");
|
||||
const [hasAnthropicKey, setHasAnthropicKey] = useState(false);
|
||||
const [pipeline, setPipeline] = useState<PipelineState>({
|
||||
upcoming: [],
|
||||
backlog: [],
|
||||
current: [],
|
||||
qa: [],
|
||||
merge: [],
|
||||
@@ -1017,8 +1017,8 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
|
||||
/>
|
||||
<StagePanel
|
||||
title="Upcoming"
|
||||
items={pipeline.upcoming}
|
||||
title="Backlog"
|
||||
items={pipeline.backlog}
|
||||
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { StagePanel } from "./StagePanel";
|
||||
|
||||
function makePipeline(overrides: Partial<PipelineState> = {}): PipelineState {
|
||||
return {
|
||||
upcoming: [],
|
||||
backlog: [],
|
||||
current: [],
|
||||
qa: [],
|
||||
merge: [],
|
||||
|
||||
@@ -115,7 +115,7 @@ export function LozengeFlyProvider({
|
||||
const assignedAgentNames = useMemo(() => {
|
||||
const names = new Set<string>();
|
||||
for (const item of [
|
||||
...pipeline.upcoming,
|
||||
...pipeline.backlog,
|
||||
...pipeline.current,
|
||||
...pipeline.qa,
|
||||
...pipeline.merge,
|
||||
@@ -165,13 +165,13 @@ export function LozengeFlyProvider({
|
||||
|
||||
const prev = prevPipelineRef.current;
|
||||
const allPrev = [
|
||||
...prev.upcoming,
|
||||
...prev.backlog,
|
||||
...prev.current,
|
||||
...prev.qa,
|
||||
...prev.merge,
|
||||
];
|
||||
const allCurr = [
|
||||
...pipeline.upcoming,
|
||||
...pipeline.backlog,
|
||||
...pipeline.current,
|
||||
...pipeline.qa,
|
||||
...pipeline.merge,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { api } from "../api/client";
|
||||
const { useEffect, useRef, useState } = React;
|
||||
|
||||
const STAGE_LABELS: Record<string, string> = {
|
||||
upcoming: "Upcoming",
|
||||
backlog: "Backlog",
|
||||
current: "Current",
|
||||
qa: "QA",
|
||||
merge: "To Merge",
|
||||
|
||||
@@ -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 {
|
||||
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/).
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// 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> {
|
||||
let sk = project_root.join(".story_kit").join("work");
|
||||
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(())
|
||||
}
|
||||
|
||||
/// 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 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 not found anywhere, an error is returned.
|
||||
pub fn close_bug_to_archive(project_root: &Path, bug_id: &str) -> Result<(), String> {
|
||||
let sk = project_root.join(".story_kit").join("work");
|
||||
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_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() {
|
||||
current_path.clone()
|
||||
} else if upcoming_path.exists() {
|
||||
upcoming_path.clone()
|
||||
} else if backlog_path.exists() {
|
||||
backlog_path.clone()
|
||||
} else {
|
||||
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;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
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");
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::create_dir_all(¤t).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();
|
||||
|
||||
assert!(!upcoming.join("10_story_foo.md").exists());
|
||||
assert!(!backlog.join("10_story_foo.md").exists());
|
||||
assert!(current.join("10_story_foo.md").exists());
|
||||
}
|
||||
|
||||
@@ -295,25 +295,25 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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();
|
||||
assert!(move_story_to_current(tmp.path(), "99_missing").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_bug_to_current_moves_from_upcoming() {
|
||||
fn move_bug_to_current_moves_from_backlog() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
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");
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::create_dir_all(¤t).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();
|
||||
|
||||
assert!(!upcoming.join("1_bug_test.md").exists());
|
||||
assert!(!backlog.join("1_bug_test.md").exists());
|
||||
assert!(current.join("1_bug_test.md").exists());
|
||||
}
|
||||
|
||||
@@ -335,17 +335,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_bug_moves_from_upcoming_when_not_started() {
|
||||
fn close_bug_moves_from_backlog_when_not_started() {
|
||||
use std::fs;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let upcoming = root.join(".story_kit/work/1_upcoming");
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
fs::write(upcoming.join("3_bug_test.md"), "# Bug 3\n").unwrap();
|
||||
let backlog = root.join(".story_kit/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(backlog.join("3_bug_test.md"), "# Bug 3\n").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());
|
||||
}
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ impl AgentPool {
|
||||
let event_log: Arc<Mutex<Vec<AgentEvent>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
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
|
||||
// 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
|
||||
@@ -1430,7 +1430,7 @@ impl AgentPool {
|
||||
///
|
||||
/// 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
|
||||
/// `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
|
||||
/// 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.
|
||||
let stage_dir = match find_active_story_stage(project_root, story_id) {
|
||||
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.
|
||||
@@ -2728,8 +2728,8 @@ mod tests {
|
||||
fs::write(current.join("173_story_test.md"), "test").unwrap();
|
||||
// Ensure 3_qa/ exists for the move target
|
||||
fs::create_dir_all(root.join(".story_kit/work/3_qa")).unwrap();
|
||||
// Ensure 1_upcoming/ exists (start_agent calls move_story_to_current)
|
||||
fs::create_dir_all(root.join(".story_kit/work/1_upcoming")).unwrap();
|
||||
// Ensure 1_backlog/ exists (start_agent calls move_story_to_current)
|
||||
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.
|
||||
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
|
||||
/// 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.
|
||||
#[tokio::test]
|
||||
async fn start_agent_moves_story_to_current_when_coders_busy() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".story_kit");
|
||||
let upcoming = sk.join("work/1_upcoming");
|
||||
std::fs::create_dir_all(&upcoming).unwrap();
|
||||
let backlog = sk.join("work/1_backlog");
|
||||
std::fs::create_dir_all(&backlog).unwrap();
|
||||
std::fs::write(
|
||||
sk.join("project.toml"),
|
||||
r#"
|
||||
@@ -3515,9 +3515,9 @@ stage = "coder"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// Place the story in 1_upcoming/.
|
||||
// Place the story in 1_backlog/.
|
||||
std::fs::write(
|
||||
upcoming.join("story-3.md"),
|
||||
backlog.join("story-3.md"),
|
||||
"---\nname: Story 3\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -3547,10 +3547,10 @@ stage = "coder"
|
||||
current_path.exists(),
|
||||
"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!(
|
||||
!upcoming_path.exists(),
|
||||
"story should no longer be in 1_upcoming/"
|
||||
!backlog_path.exists(),
|
||||
"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,
|
||||
// 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::write(
|
||||
upcoming.join("50_story_test.md"),
|
||||
@@ -3924,7 +3924,7 @@ stage = "coder"
|
||||
let root = tmp.path().to_path_buf();
|
||||
|
||||
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(
|
||||
root.join(".story_kit/project.toml"),
|
||||
"[[agent]]\nname = \"coder-1\"\n",
|
||||
@@ -3933,12 +3933,12 @@ stage = "coder"
|
||||
// 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).
|
||||
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",
|
||||
)
|
||||
.unwrap();
|
||||
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",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -4138,14 +4138,14 @@ stage = "coder"
|
||||
let root = tmp.path();
|
||||
|
||||
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(
|
||||
root.join(".story_kit/project.toml"),
|
||||
"[[agent]]\nname = \"coder-1\"\n\n[[agent]]\nname = \"coder-2\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
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",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -339,7 +339,7 @@ impl AgentsApi {
|
||||
.map_err(bad_request)?;
|
||||
|
||||
let stages = [
|
||||
("1_upcoming", "upcoming"),
|
||||
("1_backlog", "backlog"),
|
||||
("2_current", "current"),
|
||||
("3_qa", "qa"),
|
||||
("4_merge", "merge"),
|
||||
@@ -809,12 +809,12 @@ allowed_tools = ["Read", "Bash"]
|
||||
}
|
||||
|
||||
#[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 root = tmp.path();
|
||||
make_stage_dir(root, "1_upcoming");
|
||||
make_stage_dir(root, "1_backlog");
|
||||
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.",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -828,7 +828,7 @@ allowed_tools = ["Read", "Bash"]
|
||||
.unwrap()
|
||||
.0;
|
||||
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()));
|
||||
}
|
||||
|
||||
@@ -1113,7 +1113,7 @@ allowed_tools = ["Read", "Bash"]
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let root = tmp.path().to_path_buf();
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -672,7 +672,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -690,7 +690,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -725,7 +725,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
@@ -733,7 +733,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -756,7 +756,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
@@ -764,7 +764,7 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1022,7 +1022,7 @@ fn tool_create_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||
.get("acceptance_criteria")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok());
|
||||
// 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 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.done, "done"));
|
||||
|
||||
let upcoming: Vec<Value> = state
|
||||
.upcoming
|
||||
let backlog: Vec<Value> = state
|
||||
.backlog
|
||||
.iter()
|
||||
.map(|s| json!({ "story_id": s.story_id, "name": s.name }))
|
||||
.collect();
|
||||
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"active": active,
|
||||
"upcoming": upcoming,
|
||||
"upcoming_count": upcoming.len(),
|
||||
"backlog": backlog,
|
||||
"backlog_count": backlog.len(),
|
||||
}))
|
||||
.map_err(|e| format!("Serialization error: {e}"))
|
||||
}
|
||||
@@ -2452,7 +2452,7 @@ mod tests {
|
||||
let root = tmp.path();
|
||||
|
||||
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"),
|
||||
("3_qa", "30_story_qa", "QA Story"),
|
||||
("4_merge", "40_story_merge", "Merge Story"),
|
||||
@@ -2481,11 +2481,11 @@ mod tests {
|
||||
assert!(stages.contains(&"merge"));
|
||||
assert!(stages.contains(&"done"));
|
||||
|
||||
// Upcoming backlog
|
||||
let upcoming = parsed["upcoming"].as_array().unwrap();
|
||||
assert_eq!(upcoming.len(), 1);
|
||||
assert_eq!(upcoming[0]["story_id"], "10_story_upcoming");
|
||||
assert_eq!(parsed["upcoming_count"], 1);
|
||||
// Backlog
|
||||
let backlog = parsed["backlog"].as_array().unwrap();
|
||||
assert_eq!(backlog.len(), 1);
|
||||
assert_eq!(backlog[0]["story_id"], "10_story_upcoming");
|
||||
assert_eq!(parsed["backlog_count"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2801,8 +2801,8 @@ mod tests {
|
||||
let t = tool.unwrap();
|
||||
let desc = t["description"].as_str().unwrap();
|
||||
assert!(
|
||||
desc.contains("work/1_upcoming/"),
|
||||
"create_bug description should reference work/1_upcoming/, got: {desc}"
|
||||
desc.contains("work/1_backlog/"),
|
||||
"create_bug description should reference work/1_backlog/, got: {desc}"
|
||||
);
|
||||
assert!(
|
||||
!desc.contains(".story_kit/bugs"),
|
||||
@@ -2826,8 +2826,8 @@ mod tests {
|
||||
let t = tool.unwrap();
|
||||
let desc = t["description"].as_str().unwrap();
|
||||
assert!(
|
||||
desc.contains("work/1_upcoming/"),
|
||||
"list_bugs description should reference work/1_upcoming/, got: {desc}"
|
||||
desc.contains("work/1_backlog/"),
|
||||
"list_bugs description should reference work/1_backlog/, got: {desc}"
|
||||
);
|
||||
assert!(
|
||||
!desc.contains(".story_kit/bugs"),
|
||||
@@ -2911,7 +2911,7 @@ mod tests {
|
||||
assert!(result.contains("1_bug_login_crash"));
|
||||
let bug_file = tmp
|
||||
.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());
|
||||
}
|
||||
|
||||
@@ -2927,15 +2927,15 @@ mod tests {
|
||||
#[test]
|
||||
fn tool_list_bugs_returns_open_bugs() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let upcoming_dir = tmp.path().join(".story_kit/work/1_upcoming");
|
||||
std::fs::create_dir_all(&upcoming_dir).unwrap();
|
||||
let backlog_dir = tmp.path().join(".story_kit/work/1_backlog");
|
||||
std::fs::create_dir_all(&backlog_dir).unwrap();
|
||||
std::fs::write(
|
||||
upcoming_dir.join("1_bug_crash.md"),
|
||||
backlog_dir.join("1_bug_crash.md"),
|
||||
"# Bug 1: App Crash\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
upcoming_dir.join("2_bug_typo.md"),
|
||||
backlog_dir.join("2_bug_typo.md"),
|
||||
"# Bug 2: Typo in Header\n",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -2963,9 +2963,9 @@ mod tests {
|
||||
fn tool_close_bug_moves_to_archive() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo_in(tmp.path());
|
||||
let upcoming_dir = tmp.path().join(".story_kit/work/1_upcoming");
|
||||
std::fs::create_dir_all(&upcoming_dir).unwrap();
|
||||
let bug_file = upcoming_dir.join("1_bug_crash.md");
|
||||
let backlog_dir = tmp.path().join(".story_kit/work/1_backlog");
|
||||
std::fs::create_dir_all(&backlog_dir).unwrap();
|
||||
let bug_file = backlog_dir.join("1_bug_crash.md");
|
||||
std::fs::write(&bug_file, "# Bug 1: Crash\n").unwrap();
|
||||
// Stage the file so it's tracked
|
||||
std::process::Command::new("git")
|
||||
@@ -3035,7 +3035,7 @@ mod tests {
|
||||
assert!(result.contains("1_spike_compare_encoders"));
|
||||
let spike_file = tmp
|
||||
.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());
|
||||
let contents = std::fs::read_to_string(&spike_file).unwrap();
|
||||
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();
|
||||
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());
|
||||
let contents = std::fs::read_to_string(&spike_file).unwrap();
|
||||
assert!(contents.starts_with("---\nname: \"My Spike\"\n---"));
|
||||
|
||||
@@ -35,7 +35,7 @@ pub struct StoryValidationResult {
|
||||
/// Full pipeline state across all stages.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct PipelineState {
|
||||
pub upcoming: Vec<UpcomingStory>,
|
||||
pub backlog: Vec<UpcomingStory>,
|
||||
pub current: Vec<UpcomingStory>,
|
||||
pub qa: Vec<UpcomingStory>,
|
||||
pub merge: Vec<UpcomingStory>,
|
||||
@@ -46,7 +46,7 @@ pub struct PipelineState {
|
||||
pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
|
||||
let agent_map = build_active_agent_map(ctx);
|
||||
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)?,
|
||||
qa: load_stage_items(ctx, "3_qa", &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> {
|
||||
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.
|
||||
@@ -152,11 +152,11 @@ pub fn create_story_file(
|
||||
}
|
||||
|
||||
let filename = format!("{story_number}_story_{slug}.md");
|
||||
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming");
|
||||
fs::create_dir_all(&upcoming_dir)
|
||||
.map_err(|e| format!("Failed to create upcoming directory: {e}"))?;
|
||||
let backlog_dir = root.join(".story_kit").join("work").join("1_backlog");
|
||||
fs::create_dir_all(&backlog_dir)
|
||||
.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() {
|
||||
return Err(format!("Story file already exists: {filename}"));
|
||||
}
|
||||
@@ -206,7 +206,7 @@ pub fn create_story_file(
|
||||
|
||||
// ── 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"`).
|
||||
pub fn create_bug_file(
|
||||
@@ -226,9 +226,9 @@ pub fn create_bug_file(
|
||||
}
|
||||
|
||||
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)
|
||||
.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);
|
||||
if filepath.exists() {
|
||||
@@ -276,7 +276,7 @@ pub fn create_bug_file(
|
||||
|
||||
// ── 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"`).
|
||||
pub fn create_spike_file(
|
||||
@@ -292,11 +292,11 @@ pub fn create_spike_file(
|
||||
}
|
||||
|
||||
let filename = format!("{spike_number}_spike_{slug}.md");
|
||||
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming");
|
||||
fs::create_dir_all(&upcoming_dir)
|
||||
.map_err(|e| format!("Failed to create upcoming directory: {e}"))?;
|
||||
let backlog_dir = root.join(".story_kit").join("work").join("1_backlog");
|
||||
fs::create_dir_all(&backlog_dir)
|
||||
.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() {
|
||||
return Err(format!("Spike file already exists: {filename}"));
|
||||
}
|
||||
@@ -338,7 +338,7 @@ pub fn create_spike_file(
|
||||
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"`).
|
||||
pub fn create_refactor_file(
|
||||
@@ -355,11 +355,11 @@ pub fn create_refactor_file(
|
||||
}
|
||||
|
||||
let filename = format!("{refactor_number}_refactor_{slug}.md");
|
||||
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming");
|
||||
fs::create_dir_all(&upcoming_dir)
|
||||
.map_err(|e| format!("Failed to create upcoming directory: {e}"))?;
|
||||
let backlog_dir = root.join(".story_kit").join("work").join("1_backlog");
|
||||
fs::create_dir_all(&backlog_dir)
|
||||
.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() {
|
||||
return Err(format!("Refactor file already exists: {filename}"));
|
||||
}
|
||||
@@ -427,18 +427,18 @@ fn extract_bug_name(path: &Path) -> Option<String> {
|
||||
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.
|
||||
pub fn list_bug_files(root: &Path) -> Result<Vec<(String, String)>, String> {
|
||||
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming");
|
||||
if !upcoming_dir.exists() {
|
||||
let backlog_dir = root.join(".story_kit").join("work").join("1_backlog");
|
||||
if !backlog_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut bugs = Vec::new();
|
||||
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 path = entry.path();
|
||||
@@ -477,18 +477,18 @@ fn is_refactor_item(stem: &str) -> bool {
|
||||
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.
|
||||
pub fn list_refactor_files(root: &Path) -> Result<Vec<(String, String)>, String> {
|
||||
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming");
|
||||
if !upcoming_dir.exists() {
|
||||
let backlog_dir = root.join(".story_kit").join("work").join("1_backlog");
|
||||
if !backlog_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut refactors = Vec::new();
|
||||
for entry in fs::read_dir(&upcoming_dir)
|
||||
.map_err(|e| format!("Failed to read upcoming directory: {e}"))?
|
||||
for entry in 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 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.
|
||||
///
|
||||
/// 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> {
|
||||
let filename = format!("{story_id}.md");
|
||||
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);
|
||||
if path.exists() {
|
||||
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 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);
|
||||
if !dir.exists() {
|
||||
continue;
|
||||
@@ -973,10 +973,10 @@ pub fn validate_story_dirs(
|
||||
) -> Result<Vec<StoryValidationResult>, String> {
|
||||
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![
|
||||
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 {
|
||||
@@ -1042,7 +1042,7 @@ mod tests {
|
||||
let root = tmp.path().to_path_buf();
|
||||
|
||||
for (stage, id) in &[
|
||||
("1_upcoming", "10_story_upcoming"),
|
||||
("1_backlog", "10_story_upcoming"),
|
||||
("2_current", "20_story_current"),
|
||||
("3_qa", "30_story_qa"),
|
||||
("4_merge", "40_story_merge"),
|
||||
@@ -1060,8 +1060,8 @@ mod tests {
|
||||
let ctx = crate::http::context::AppContext::new_test(root);
|
||||
let state = load_pipeline_state(&ctx).unwrap();
|
||||
|
||||
assert_eq!(state.upcoming.len(), 1);
|
||||
assert_eq!(state.upcoming[0].story_id, "10_story_upcoming");
|
||||
assert_eq!(state.backlog.len(), 1);
|
||||
assert_eq!(state.backlog[0].story_id, "10_story_upcoming");
|
||||
|
||||
assert_eq!(state.current.len(), 1);
|
||||
assert_eq!(state.current[0].story_id, "20_story_current");
|
||||
@@ -1164,15 +1164,15 @@ mod tests {
|
||||
#[test]
|
||||
fn load_upcoming_parses_metadata() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
let backlog = tmp.path().join(".story_kit/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(
|
||||
upcoming.join("31_story_view_upcoming.md"),
|
||||
backlog.join("31_story_view_upcoming.md"),
|
||||
"---\nname: View Upcoming\n---\n# Story\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
upcoming.join("32_story_worktree.md"),
|
||||
backlog.join("32_story_worktree.md"),
|
||||
"---\nname: Worktree Orchestration\n---\n# Story\n",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1189,11 +1189,11 @@ mod tests {
|
||||
#[test]
|
||||
fn load_upcoming_skips_non_md_files() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
fs::write(upcoming.join(".gitkeep"), "").unwrap();
|
||||
let backlog = tmp.path().join(".story_kit/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(backlog.join(".gitkeep"), "").unwrap();
|
||||
fs::write(
|
||||
upcoming.join("31_story_example.md"),
|
||||
backlog.join("31_story_example.md"),
|
||||
"---\nname: A Story\n---\n",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1208,16 +1208,16 @@ mod tests {
|
||||
fn validate_story_dirs_valid_files() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
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(¤t).unwrap();
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(
|
||||
current.join("28_story_todos.md"),
|
||||
"---\nname: Show TODOs\n---\n# Story\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
upcoming.join("36_story_front_matter.md"),
|
||||
backlog.join("36_story_front_matter.md"),
|
||||
"---\nname: Enforce Front Matter\n---\n# Story\n",
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1302,7 +1302,7 @@ mod tests {
|
||||
#[test]
|
||||
fn next_item_number_empty_dirs() {
|
||||
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();
|
||||
assert_eq!(next_item_number(tmp.path()).unwrap(), 1);
|
||||
}
|
||||
@@ -1310,13 +1310,13 @@ mod tests {
|
||||
#[test]
|
||||
fn next_item_number_scans_all_dirs() {
|
||||
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 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(¤t).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(archived.join("15_story_baz.md"), "").unwrap();
|
||||
assert_eq!(next_item_number(tmp.path()).unwrap(), 21);
|
||||
@@ -1334,9 +1334,9 @@ mod tests {
|
||||
#[test]
|
||||
fn create_story_writes_correct_content() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
fs::write(upcoming.join("36_story_existing.md"), "").unwrap();
|
||||
let backlog = tmp.path().join(".story_kit/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(backlog.join("36_story_existing.md"), "").unwrap();
|
||||
|
||||
let number = next_item_number(tmp.path()).unwrap();
|
||||
assert_eq!(number, 37);
|
||||
@@ -1345,7 +1345,7 @@ mod tests {
|
||||
assert_eq!(slug, "my_new_feature");
|
||||
|
||||
let filename = format!("{number}_{slug}.md");
|
||||
let filepath = upcoming.join(&filename);
|
||||
let filepath = backlog.join(&filename);
|
||||
|
||||
let mut content = String::new();
|
||||
content.push_str("---\n");
|
||||
@@ -1377,10 +1377,10 @@ mod tests {
|
||||
let result = create_story_file(tmp.path(), name, None, None, false);
|
||||
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 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");
|
||||
assert_eq!(meta.name.as_deref(), Some(name));
|
||||
@@ -1389,10 +1389,10 @@ mod tests {
|
||||
#[test]
|
||||
fn create_story_rejects_duplicate() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
let backlog = tmp.path().join(".story_kit/work/1_backlog");
|
||||
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();
|
||||
|
||||
// Simulate the check
|
||||
@@ -1511,17 +1511,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_story_file_searches_current_then_upcoming() {
|
||||
fn find_story_file_searches_current_then_backlog() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
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(¤t).unwrap();
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
|
||||
// Only in upcoming
|
||||
fs::write(upcoming.join("6_test.md"), "").unwrap();
|
||||
// Only in backlog
|
||||
fs::write(backlog.join("6_test.md"), "").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
|
||||
fs::write(current.join("6_test.md"), "").unwrap();
|
||||
@@ -1724,19 +1724,19 @@ mod tests {
|
||||
#[test]
|
||||
fn next_item_number_increments_from_existing_bugs() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
fs::write(upcoming.join("1_bug_crash.md"), "").unwrap();
|
||||
fs::write(upcoming.join("3_bug_another.md"), "").unwrap();
|
||||
let backlog = tmp.path().join(".story_kit/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(backlog.join("1_bug_crash.md"), "").unwrap();
|
||||
fs::write(backlog.join("3_bug_another.md"), "").unwrap();
|
||||
assert_eq!(next_item_number(tmp.path()).unwrap(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_item_number_scans_archived_too() {
|
||||
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");
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::create_dir_all(&archived).unwrap();
|
||||
fs::write(archived.join("5_bug_old.md"), "").unwrap();
|
||||
assert_eq!(next_item_number(tmp.path()).unwrap(), 6);
|
||||
@@ -1752,11 +1752,11 @@ mod tests {
|
||||
#[test]
|
||||
fn list_bug_files_excludes_archive_subdir() {
|
||||
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");
|
||||
fs::create_dir_all(&upcoming_dir).unwrap();
|
||||
fs::create_dir_all(&backlog_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();
|
||||
|
||||
let result = list_bug_files(tmp.path()).unwrap();
|
||||
@@ -1768,11 +1768,11 @@ mod tests {
|
||||
#[test]
|
||||
fn list_bug_files_sorted_by_id() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let upcoming_dir = tmp.path().join(".story_kit/work/1_upcoming");
|
||||
fs::create_dir_all(&upcoming_dir).unwrap();
|
||||
fs::write(upcoming_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(upcoming_dir.join("2_bug_second.md"), "# Bug 2: Second\n").unwrap();
|
||||
let backlog_dir = tmp.path().join(".story_kit/work/1_backlog");
|
||||
fs::create_dir_all(&backlog_dir).unwrap();
|
||||
fs::write(backlog_dir.join("3_bug_third.md"), "# Bug 3: Third\n").unwrap();
|
||||
fs::write(backlog_dir.join("1_bug_first.md"), "# Bug 1: First\n").unwrap();
|
||||
fs::write(backlog_dir.join("2_bug_second.md"), "# Bug 2: Second\n").unwrap();
|
||||
|
||||
let result = list_bug_files(tmp.path()).unwrap();
|
||||
assert_eq!(result.len(), 3);
|
||||
@@ -1810,7 +1810,7 @@ mod tests {
|
||||
|
||||
let filepath = tmp
|
||||
.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());
|
||||
let contents = fs::read_to_string(&filepath).unwrap();
|
||||
assert!(
|
||||
@@ -1854,7 +1854,7 @@ mod tests {
|
||||
)
|
||||
.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();
|
||||
assert!(
|
||||
contents.starts_with("---\nname: \"Some Bug\"\n---"),
|
||||
@@ -1876,7 +1876,7 @@ mod tests {
|
||||
|
||||
let filepath = tmp
|
||||
.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());
|
||||
let contents = fs::read_to_string(&filepath).unwrap();
|
||||
assert!(
|
||||
@@ -1900,7 +1900,7 @@ mod tests {
|
||||
create_spike_file(tmp.path(), "FS Watcher Spike", Some(description)).unwrap();
|
||||
|
||||
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();
|
||||
assert!(contents.contains(description));
|
||||
}
|
||||
@@ -1910,7 +1910,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().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();
|
||||
// Should have placeholder TBD in Question section
|
||||
assert!(contents.contains("## Question\n\n- TBD\n"));
|
||||
@@ -1931,10 +1931,10 @@ mod tests {
|
||||
let result = create_spike_file(tmp.path(), name, None);
|
||||
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 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");
|
||||
assert_eq!(meta.name.as_deref(), Some(name));
|
||||
@@ -1943,9 +1943,9 @@ mod tests {
|
||||
#[test]
|
||||
fn create_spike_file_increments_from_existing_items() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let upcoming = tmp.path().join(".story_kit/work/1_upcoming");
|
||||
fs::create_dir_all(&upcoming).unwrap();
|
||||
fs::write(upcoming.join("5_story_existing.md"), "").unwrap();
|
||||
let backlog = tmp.path().join(".story_kit/work/1_backlog");
|
||||
fs::create_dir_all(&backlog).unwrap();
|
||||
fs::write(backlog.join("5_story_existing.md"), "").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}");
|
||||
|
||||
@@ -79,7 +79,7 @@ enum WsResponse {
|
||||
},
|
||||
/// Full pipeline state pushed on connect and after every work-item watcher event.
|
||||
PipelineState {
|
||||
upcoming: Vec<crate::http::workflow::UpcomingStory>,
|
||||
backlog: Vec<crate::http::workflow::UpcomingStory>,
|
||||
current: Vec<crate::http::workflow::UpcomingStory>,
|
||||
qa: 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 {
|
||||
fn from(s: PipelineState) -> Self {
|
||||
WsResponse::PipelineState {
|
||||
upcoming: s.upcoming,
|
||||
backlog: s.backlog,
|
||||
current: s.current,
|
||||
qa: s.qa,
|
||||
merge: s.merge,
|
||||
@@ -695,7 +695,7 @@ mod tests {
|
||||
agent: None,
|
||||
};
|
||||
let resp = WsResponse::PipelineState {
|
||||
upcoming: vec![story],
|
||||
backlog: vec![story],
|
||||
current: vec![],
|
||||
qa: vec![],
|
||||
merge: vec![],
|
||||
@@ -703,8 +703,8 @@ mod tests {
|
||||
};
|
||||
let json = serde_json::to_value(&resp).unwrap();
|
||||
assert_eq!(json["type"], "pipeline_state");
|
||||
assert_eq!(json["upcoming"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(json["upcoming"][0]["story_id"], "10_story_test");
|
||||
assert_eq!(json["backlog"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(json["backlog"][0]["story_id"], "10_story_test");
|
||||
assert!(json["current"].as_array().unwrap().is_empty());
|
||||
assert!(json["done"].as_array().unwrap().is_empty());
|
||||
}
|
||||
@@ -824,7 +824,7 @@ mod tests {
|
||||
#[test]
|
||||
fn pipeline_state_converts_to_ws_response() {
|
||||
let state = PipelineState {
|
||||
upcoming: vec![UpcomingStory {
|
||||
backlog: vec![UpcomingStory {
|
||||
story_id: "1_story_a".to_string(),
|
||||
name: Some("Story A".to_string()),
|
||||
error: None,
|
||||
@@ -851,8 +851,8 @@ mod tests {
|
||||
let resp: WsResponse = state.into();
|
||||
let json = serde_json::to_value(&resp).unwrap();
|
||||
assert_eq!(json["type"], "pipeline_state");
|
||||
assert_eq!(json["upcoming"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(json["upcoming"][0]["story_id"], "1_story_a");
|
||||
assert_eq!(json["backlog"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(json["backlog"][0]["story_id"], "1_story_a");
|
||||
assert_eq!(json["current"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(json["current"][0]["story_id"], "2_story_b");
|
||||
assert!(json["qa"].as_array().unwrap().is_empty());
|
||||
@@ -864,7 +864,7 @@ mod tests {
|
||||
#[test]
|
||||
fn empty_pipeline_state_converts_to_ws_response() {
|
||||
let state = PipelineState {
|
||||
upcoming: vec![],
|
||||
backlog: vec![],
|
||||
current: vec![],
|
||||
qa: vec![],
|
||||
merge: vec![],
|
||||
@@ -873,7 +873,7 @@ mod tests {
|
||||
let resp: WsResponse = state.into();
|
||||
let json = serde_json::to_value(&resp).unwrap();
|
||||
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["qa"].as_array().unwrap().is_empty());
|
||||
assert!(json["merge"].as_array().unwrap().is_empty());
|
||||
@@ -991,7 +991,7 @@ mod tests {
|
||||
#[test]
|
||||
fn pipeline_state_with_agent_converts_correctly() {
|
||||
let state = PipelineState {
|
||||
upcoming: vec![],
|
||||
backlog: vec![],
|
||||
current: vec![UpcomingStory {
|
||||
story_id: "10_story_x".to_string(),
|
||||
name: Some("Story X".to_string()),
|
||||
@@ -1046,7 +1046,7 @@ mod tests {
|
||||
let root = tmp.path().to_path_buf();
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -1155,7 +1155,7 @@ mod tests {
|
||||
|
||||
assert_eq!(initial["type"], "pipeline_state");
|
||||
// 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["qa"].as_array().unwrap().is_empty());
|
||||
assert!(initial["merge"].as_array().unwrap().is_empty());
|
||||
|
||||
@@ -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
|
||||
let work_stages = [
|
||||
"1_upcoming",
|
||||
"1_backlog",
|
||||
"2_current",
|
||||
"3_qa",
|
||||
"4_merge",
|
||||
@@ -1085,7 +1085,7 @@ mod tests {
|
||||
let dir = tempdir().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 {
|
||||
let path = dir.path().join(".story_kit/work").join(stage);
|
||||
assert!(path.is_dir(), "work/{} should be a directory", stage);
|
||||
|
||||
@@ -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.
|
||||
fn stage_metadata(stage: &str, item_id: &str) -> Option<(&'static str, String)> {
|
||||
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}")),
|
||||
"3_qa" => ("qa", format!("story-kit: queue {item_id} for QA")),
|
||||
"4_merge" => ("merge", format!("story-kit: queue {item_id} for merge")),
|
||||
@@ -111,7 +111,7 @@ fn stage_for_path(path: &Path) -> Option<String> {
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// 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.
|
||||
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.
|
||||
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
|
||||
/// 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.
|
||||
fn flush_pending(
|
||||
pending: &HashMap<PathBuf, String>,
|
||||
@@ -574,13 +574,13 @@ mod tests {
|
||||
fn flush_pending_commits_and_broadcasts_for_terminal_stage() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
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");
|
||||
fs::write(&story_path, "---\nname: test\n---\n").unwrap();
|
||||
|
||||
let (tx, mut rx) = tokio::sync::broadcast::channel(16);
|
||||
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);
|
||||
|
||||
@@ -592,7 +592,7 @@ mod tests {
|
||||
action,
|
||||
commit_msg,
|
||||
} => {
|
||||
assert_eq!(stage, "1_upcoming");
|
||||
assert_eq!(stage, "1_backlog");
|
||||
assert_eq!(item_id, "42_story_foo");
|
||||
assert_eq!(action, "create");
|
||||
assert_eq!(commit_msg, "story-kit: create 42_story_foo");
|
||||
@@ -660,7 +660,7 @@ mod tests {
|
||||
#[test]
|
||||
fn flush_pending_broadcasts_for_all_pipeline_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"),
|
||||
("4_merge", "merge", "story-kit: queue 10_story_x for merge"),
|
||||
("5_done", "done", "story-kit: done 10_story_x"),
|
||||
@@ -792,10 +792,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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();
|
||||
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");
|
||||
fs::write(
|
||||
&story_path,
|
||||
@@ -805,14 +805,14 @@ mod tests {
|
||||
|
||||
let (tx, _rx) = tokio::sync::broadcast::channel(16);
|
||||
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);
|
||||
|
||||
let contents = fs::read_to_string(&story_path).unwrap();
|
||||
assert!(
|
||||
!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]
|
||||
fn should_commit_stage_only_for_terminal_stages() {
|
||||
// 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("6_archived"));
|
||||
// Intermediate stages — broadcast-only, no commit.
|
||||
|
||||
@@ -15,7 +15,7 @@ use tokio::sync::broadcast;
|
||||
/// Human-readable display name for a pipeline stage directory.
|
||||
pub fn stage_display_name(stage: &str) -> &'static str {
|
||||
match stage {
|
||||
"1_upcoming" => "Upcoming",
|
||||
"1_backlog" => "Backlog",
|
||||
"2_current" => "Current",
|
||||
"3_qa" => "QA",
|
||||
"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.
|
||||
///
|
||||
/// 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).
|
||||
pub fn inferred_from_stage(to_stage: &str) -> Option<&'static str> {
|
||||
match to_stage {
|
||||
"2_current" => Some("Upcoming"),
|
||||
"2_current" => Some("Backlog"),
|
||||
"3_qa" => Some("Current"),
|
||||
"4_merge" => Some("QA"),
|
||||
"5_done" => Some("Merge"),
|
||||
@@ -195,7 +195,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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("3_qa"), "QA");
|
||||
assert_eq!(stage_display_name("4_merge"), "Merge");
|
||||
@@ -208,7 +208,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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("4_merge"), Some("QA"));
|
||||
assert_eq!(inferred_from_stage("5_done"), Some("Merge"));
|
||||
@@ -216,8 +216,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inferred_from_stage_returns_none_for_upcoming() {
|
||||
assert_eq!(inferred_from_stage("1_upcoming"), None);
|
||||
fn inferred_from_stage_returns_none_for_backlog() {
|
||||
assert_eq!(inferred_from_stage("1_backlog"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user