Skip to main content

speccoder_runtime/format/speckit/
workflow.rs

1//! `speccoder workflow install`: speccoder as a Spec Kit workflow
2//! step type, and a workflow that uses it.
3//!
4//! Spec Kit owns the phase level (`specify workflow run`, gates, resume);
5//! speccoder owns the task level. The step type `speccoder` maps speccoder's
6//! exit code onto Spec Kit's step status — 23 (a person is needed) becomes
7//! `paused`, so `specify workflow resume` continues after the decision.
8
9use std::path::{Path, PathBuf};
10
11use speccoder_core::{CommandData, CommandResult, CommandStatus};
12
13/// The files, by project-relative path.
14pub const FILES: [(&str, &str); 3] = [
15    (
16        ".specify/workflows/steps/speccoder/step.yml",
17        include_str!("../../../assets/workflow/step.yml"),
18    ),
19    (
20        ".specify/workflows/steps/speccoder/__init__.py",
21        include_str!("../../../assets/workflow/__init__.py"),
22    ),
23    (
24        ".specify/workflows/speccoder/workflow.yml",
25        include_str!("../../../assets/workflow/workflow.yml"),
26    ),
27];
28
29/// Writes the step type and the workflow into the Spec Kit project at `path`.
30#[must_use]
31pub fn install(path: &Path, force: bool) -> CommandResult {
32    let root = match crate::inspect(path, None).data() {
33        Some(CommandData::Inspect(s)) => PathBuf::from(&s.root),
34        _ => {
35            return CommandResult::new(
36                "workflow",
37                CommandStatus::NotAProject,
38                "not a Spec Kit project (no .specify/ found); run `specify init --here --integration claude` first",
39            );
40        }
41    };
42    let existing: Vec<&str> = FILES
43        .iter()
44        .filter(|(p, _)| root.join(p).exists())
45        .map(|(p, _)| *p)
46        .collect();
47    if !existing.is_empty() && !force {
48        return CommandResult::new(
49            "workflow",
50            CommandStatus::ConfigError,
51            &format!("{} exist(s); pass --force to replace", existing.join(", ")),
52        );
53    }
54    for (rel, text) in FILES {
55        let file = root.join(rel);
56        if let Some(dir) = file.parent()
57            && let Err(e) = std::fs::create_dir_all(dir)
58        {
59            return CommandResult::new(
60                "workflow",
61                CommandStatus::Error,
62                &format!("{}: {e}", dir.display()),
63            );
64        }
65        if let Err(e) = std::fs::write(&file, text) {
66            return CommandResult::new(
67                "workflow",
68                CommandStatus::Error,
69                &format!("{}: {e}", file.display()),
70            );
71        }
72    }
73    CommandResult::new(
74        "workflow",
75        CommandStatus::Ok,
76        "installed the `speccoder` step type and workflow; run it with `specify workflow run .specify/workflows/speccoder/workflow.yml --input spec=\"…\"` (resume after a pause with `specify workflow resume <run>`)",
77    )
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn the_assets_declare_the_step_type_and_use_it() {
86        assert!(FILES[0].1.contains("type_key: \"speccoder\""));
87        assert!(
88            FILES[1].1.contains("type_key = \"speccoder\"")
89                && FILES[1].1.contains("NEEDS_HUMAN = 23")
90        );
91        assert!(FILES[2].1.contains("type: speccoder") && FILES[2].1.contains("converge: true"));
92    }
93}