Story 42: Deterministic worktree management via REST/MCP API

Add REST and MCP endpoints for creating, listing, and removing worktrees.
Includes worktree lifecycle management and cleanup operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-20 14:09:52 +00:00
parent 91534b4a59
commit a9d45bbcd5
7 changed files with 340 additions and 18 deletions

View File

@@ -8,6 +8,7 @@ use poem_openapi::{Object, OpenApi, Tags, payload::Json};
use serde::Deserialize;
use std::collections::BTreeSet;
use std::fs;
use std::process::Command;
use std::sync::Arc;
#[derive(Tags)]
@@ -117,6 +118,8 @@ struct CreateStoryPayload {
pub name: String,
pub user_story: Option<String>,
pub acceptance_criteria: Option<Vec<String>>,
/// If true, git-add and git-commit the new story file to the current branch.
pub commit: Option<bool>,
}
#[derive(Object)]
@@ -556,11 +559,13 @@ impl WorkflowApi {
payload: Json<CreateStoryPayload>,
) -> OpenApiResult<Json<CreateStoryResponse>> {
let root = self.ctx.state.get_project_root().map_err(bad_request)?;
let commit = payload.0.commit.unwrap_or(false);
let story_id = create_story_file(
&root,
&payload.0.name,
payload.0.user_story.as_deref(),
payload.0.acceptance_criteria.as_deref(),
commit,
)
.map_err(bad_request)?;
@@ -597,11 +602,15 @@ impl WorkflowApi {
}
/// Shared create-story logic used by both the OpenApi and MCP handlers.
///
/// When `commit` is `true`, the new story file is git-added and committed to
/// the current branch immediately after creation.
pub fn create_story_file(
root: &std::path::Path,
name: &str,
user_story: Option<&str>,
acceptance_criteria: Option<&[String]>,
commit: bool,
) -> Result<String, String> {
let story_number = next_story_number(root)?;
let slug = slugify_name(name);
@@ -658,9 +667,43 @@ pub fn create_story_file(
fs::write(&filepath, &content)
.map_err(|e| format!("Failed to write story file: {e}"))?;
if commit {
git_commit_story_file(root, &filepath, name)?;
}
Ok(story_id)
}
/// Git-add and git-commit a newly created story file to the current branch.
fn git_commit_story_file(
root: &std::path::Path,
filepath: &std::path::Path,
name: &str,
) -> Result<(), String> {
let output = Command::new("git")
.args(["add", &filepath.to_string_lossy()])
.current_dir(root)
.output()
.map_err(|e| format!("git add: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("git add failed: {stderr}"));
}
let msg = format!("Add story: {name}");
let output = Command::new("git")
.args(["commit", "-m", &msg])
.current_dir(root)
.output()
.map_err(|e| format!("git commit: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("git commit failed: {stderr}"));
}
Ok(())
}
fn slugify_name(name: &str) -> String {
let slug: String = name
.chars()