Skip to main content

speccoder_runtime/format/speckit/
texts.rs

1//! Spec Kit's paths, rules and words: what the runtime says and protects
2//! for a Spec Kit project, and nothing the judge decides.
3
4use std::path::Path;
5
6use crate::format::{Assess, Messages, Planning, Rules, Wording};
7
8/// The marker directory of a Spec Kit project.
9pub const MARKER: &str = ".specify";
10/// Spec Kit's pointer to the current feature (per checkout, git-ignored).
11pub const POINTER: &str = ".specify/feature.json";
12
13/// Spec Kit's selection override, read by the caller of the parser.
14pub const FEATURE_ENV: &str = "SPECIFY_FEATURE_DIRECTORY";
15
16/// The constitution, relative to the project root.
17pub const CONSTITUTION: &str = ".specify/memory/constitution.md";
18
19/// Spec Kit's intent and planning rules.
20pub static RULES: Rules = Rules {
21    intent: &[".specify", ".specify/**", "specs/**", "**/.specify/**"],
22    intent_reason: "it is Spec Kit intent (.specify/, specs/): the spec is not edited during implementation",
23    authoring_path: is_authoring_path,
24    command,
25    env: &[],
26    // The Python Spec Kit's scripts resolve preset templates with.
27    found_env: super::python::command_env,
28    authoring_refusal,
29    judged,
30    plan: "plan.md",
31};
32
33/// A feature's judged files: its spec, its task list, the person's review
34/// and the checklists.
35pub(crate) fn judged(rel: &str) -> bool {
36    matches!(rel, "spec.md" | "tasks.md" | "review.json")
37        || rel == "checklists"
38        || rel.starts_with("checklists/")
39}
40
41/// What a hosted planning skill writes: the feature's artifacts, the
42/// feature selection and the constitution — Spec Kit's own outputs.
43fn is_authoring_path(p: &str) -> bool {
44    p.starts_with("specs/") || p == ".specify/feature.json" || p.starts_with(".specify/memory/")
45}
46
47fn authoring_refusal(p: &str) -> String {
48    format!(
49        "a planning skill writes Spec Kit artifacts (specs/, .specify/feature.json, .specify/memory/); `{p}` is not one"
50    )
51}
52
53/// A planning skill runs Spec Kit's own scripts without asking.
54fn command(argv: &[String], authoring: bool) -> Option<Result<(), String>> {
55    (authoring && is_script(argv)).then_some(Ok(()))
56}
57
58/// One of Spec Kit's own scripts, run directly or by `bash`/`sh`.
59fn is_script(argv: &[String]) -> bool {
60    let script = |a: &str| {
61        let a = a.strip_prefix("./").unwrap_or(a);
62        // Spec Kit's scripts and those of installed extensions
63        // (`.specify/extensions/<ext>/scripts/…`): the person installed them,
64        // and the model can write neither.
65        let ext_script = a
66            .strip_prefix(".specify/extensions/")
67            .is_some_and(|rest| rest.split('/').nth(1) == Some("scripts"));
68        !a.contains("..")
69            && (a.starts_with(".specify/scripts/bash/") && a.ends_with(".sh")
70                || a.starts_with(".specify/scripts/powershell/") && a.ends_with(".ps1")
71                || ext_script && (a.ends_with(".sh") || a.ends_with(".ps1")))
72    };
73    match argv {
74        [first, ..] if script(first) => true,
75        [shell, second, ..] if matches!(shell.as_str(), "bash" | "sh") => script(second),
76        _ => false,
77    }
78}
79
80/// What the model is told about Spec Kit.
81pub static WORDING: Wording = Wording {
82    product: "GitHub Spec Kit",
83    name: "Spec Kit",
84    unit: "feature",
85    intent_paths: ".specify/, specs/",
86    intent_code: "`.specify/`, `specs/`",
87    documents: "spec.md, plan.md and tasks.md",
88    authoring: "write Spec Kit artifacts (specs/, .specify/feature.json, .specify/memory/) and run Spec Kit's own scripts under .specify/scripts/",
89    authoring_tool: "a Spec Kit script",
90    story: "User story",
91    constitution: "Project constitution",
92    plan: "Feature plan",
93    plan_short: "Plan",
94    examples: "the acceptance examples of the task's user story",
95    convergence_task: "A task in a Convergence phase",
96    files: ["spec.md", "plan.md", "tasks.md"],
97    assess: Assess {
98        rules: "the constitution",
99        targets: "functional requirement (FR-###), success criterion that needs buildable work (SC-###), user story acceptance scenario (USn/ACm), plan decision and constitution MUST principle",
100        must: "a constitution MUST",
101        sources: "spec, plan and tasks",
102        critical: "constitution MUST violation, or missing/contradicts that blocks a P1 story",
103        refs: "FR-003, US1/AC2, plan: storage, Constitution II",
104    },
105};
106
107/// What a person is told about a Spec Kit project.
108pub static MESSAGES: Messages = Messages {
109    tasks_file: "tasks.md",
110    make_tasks: "`speccoder spec tasks` writes it",
111    not_a_project,
112    no_project: "not a Spec Kit project (no .specify/ found)",
113    no_project_in_worktree: "the worktree has no Spec Kit project",
114    not_in_git: "the Spec Kit project is not inside a git repository",
115    no_skills: "no skills installed (Spec Kit installs them with `specify init --here --integration claude`, under .claude/skills/)",
116    checklists,
117    unit: "feature",
118    no_current: "",
119    current_shared: "Spec Kit's scripts and the /speckit-* commands in your editor read it too",
120    make_project: (
121        "specify init --here --integration claude",
122        "make it a Spec Kit project (the skills speccoder runs come with it)",
123    ),
124};
125
126fn not_a_project(path: &Path) -> String {
127    format!(
128        "no Spec Kit project (.specify/) at or above {}; run `specify init --here --integration claude` first (speccoder runs Spec Kit's skills from .claude/skills/)",
129        path.display()
130    )
131}
132
133fn checklists(open: &str) -> String {
134    format!(
135        "unfinished checklists: {open}; Spec Kit treats an unchecked item as a stop — finish them, or run with --ignore-checklists"
136    )
137}
138
139/// Spec Kit's planning skills.
140pub static PLANNING: Planning = Planning {
141    prefix: "speckit-",
142    // Spec Kit's core skills, whose `before_<name>` / `after_<name>` hooks
143    // speccoder runs (a hook's own skill gets none: no recursion).
144    core: &[
145        "specify",
146        "clarify",
147        "plan",
148        "tasks",
149        "analyze",
150        "checklist",
151        "constitution",
152    ],
153    refused,
154    creates: &["specify"],
155    current_env: Some(FEATURE_ENV),
156    toward_tasks,
157    revises: Some("clarify"),
158    checks: Some("analyze"),
159};
160
161fn toward_tasks(has_plan: bool) -> &'static [&'static str] {
162    if has_plan {
163        &["tasks"]
164    } else {
165        &["plan", "tasks"]
166    }
167}
168
169/// Skills speccoder does not host, and what to use instead.
170fn refused(name: &str) -> Option<String> {
171    match name {
172        "speckit-implement" => Some(
173            "speccoder implements tasks itself, with evidence: use `speccoder run`".to_owned(),
174        ),
175        "speckit-converge" => Some(
176            "speccoder converges with a read-only assessment and a guarded append: use `speccoder converge`"
177                .to_owned(),
178        ),
179        "speckit-taskstoissues" => Some(
180            "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(),
181        ),
182        _ => None,
183    }
184}