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