use std::path::Path;
use crate::format::{Assess, Messages, Planning, Rules, Wording};
pub const MARKER: &str = ".specify";
pub const POINTER: &str = ".specify/feature.json";
pub const FEATURE_ENV: &str = "SPECIFY_FEATURE_DIRECTORY";
pub const CONSTITUTION: &str = ".specify/memory/constitution.md";
pub static RULES: Rules = Rules {
intent: &[".specify", ".specify/**", "specs/**", "**/.specify/**"],
intent_reason: "it is Spec Kit intent (.specify/, specs/): the spec is not edited during implementation",
authoring_path: is_authoring_path,
command,
env: &[],
found_env: super::python::command_env,
authoring_refusal,
judged,
plan: "plan.md",
};
pub(crate) fn judged(rel: &str) -> bool {
matches!(rel, "spec.md" | "tasks.md" | "review.json")
|| rel == "checklists"
|| rel.starts_with("checklists/")
}
fn is_authoring_path(p: &str) -> bool {
p.starts_with("specs/") || p == ".specify/feature.json" || p.starts_with(".specify/memory/")
}
fn authoring_refusal(p: &str) -> String {
format!(
"a planning skill writes Spec Kit artifacts (specs/, .specify/feature.json, .specify/memory/); `{p}` is not one"
)
}
fn command(argv: &[String], authoring: bool) -> Option<Result<(), String>> {
(authoring && is_script(argv)).then_some(Ok(()))
}
fn is_script(argv: &[String]) -> bool {
let script = |a: &str| {
let a = a.strip_prefix("./").unwrap_or(a);
let ext_script = a
.strip_prefix(".specify/extensions/")
.is_some_and(|rest| rest.split('/').nth(1) == Some("scripts"));
!a.contains("..")
&& (a.starts_with(".specify/scripts/bash/") && a.ends_with(".sh")
|| a.starts_with(".specify/scripts/powershell/") && a.ends_with(".ps1")
|| ext_script && (a.ends_with(".sh") || a.ends_with(".ps1")))
};
match argv {
[first, ..] if script(first) => true,
[shell, second, ..] if matches!(shell.as_str(), "bash" | "sh") => script(second),
_ => false,
}
}
pub static WORDING: Wording = Wording {
product: "GitHub Spec Kit",
name: "Spec Kit",
unit: "feature",
intent_paths: ".specify/, specs/",
intent_code: "`.specify/`, `specs/`",
documents: "spec.md, plan.md and tasks.md",
authoring: "write Spec Kit artifacts (specs/, .specify/feature.json, .specify/memory/) and run Spec Kit's own scripts under .specify/scripts/",
authoring_tool: "a Spec Kit script",
story: "User story",
constitution: "Project constitution",
plan: "Feature plan",
plan_short: "Plan",
examples: "the acceptance examples of the task's user story",
convergence_task: "A task in a Convergence phase",
files: ["spec.md", "plan.md", "tasks.md"],
assess: Assess {
rules: "the constitution",
targets: "functional requirement (FR-###), success criterion that needs buildable work (SC-###), user story acceptance scenario (USn/ACm), plan decision and constitution MUST principle",
must: "a constitution MUST",
sources: "spec, plan and tasks",
critical: "constitution MUST violation, or missing/contradicts that blocks a P1 story",
refs: "FR-003, US1/AC2, plan: storage, Constitution II",
},
};
pub static MESSAGES: Messages = Messages {
tasks_file: "tasks.md",
make_tasks: "`speccoder spec tasks` writes it",
not_a_project,
no_project: "not a Spec Kit project (no .specify/ found)",
no_project_in_worktree: "the worktree has no Spec Kit project",
not_in_git: "the Spec Kit project is not inside a git repository",
no_skills: "no skills installed (Spec Kit installs them with `specify init --here --integration claude`, under .claude/skills/)",
checklists,
unit: "feature",
no_current: "",
current_shared: "Spec Kit's scripts and the /speckit-* commands in your editor read it too",
make_project: (
"specify init --here --integration claude",
"make it a Spec Kit project (the skills speccoder runs come with it)",
),
};
fn not_a_project(path: &Path) -> String {
format!(
"no Spec Kit project (.specify/) at or above {}; run `specify init --here --integration claude` first (speccoder runs Spec Kit's skills from .claude/skills/)",
path.display()
)
}
fn checklists(open: &str) -> String {
format!(
"unfinished checklists: {open}; Spec Kit treats an unchecked item as a stop — finish them, or run with --ignore-checklists"
)
}
pub static PLANNING: Planning = Planning {
prefix: "speckit-",
core: &[
"specify",
"clarify",
"plan",
"tasks",
"analyze",
"checklist",
"constitution",
],
refused,
creates: &["specify"],
current_env: Some(FEATURE_ENV),
toward_tasks,
revises: Some("clarify"),
checks: Some("analyze"),
explores: None,
explore_paths: crate::format::writes_nothing,
explore_commands: &[],
};
fn toward_tasks(has_plan: bool) -> &'static [&'static str] {
if has_plan {
&["tasks"]
} else {
&["plan", "tasks"]
}
}
fn refused(name: &str) -> Option<String> {
match name {
"speckit-implement" => Some(
"speccoder implements tasks itself, with evidence: use `speccoder run`".to_owned(),
),
"speckit-converge" => Some(
"speccoder converges with a read-only assessment and a guarded append: use `speccoder converge`"
.to_owned(),
),
"speckit-taskstoissues" => Some(
"it turns tasks into GitHub issues; speccoder goes the other way — `speccoder issue start N` makes a feature from an issue, and `speccoder pr` closes it".to_owned(),
),
_ => None,
}
}