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