speccoder_runtime/format/speckit/
texts.rs1use std::path::Path;
5
6use crate::format::{Assess, Messages, Planning, Rules, Wording};
7
8pub const MARKER: &str = ".specify";
10pub const POINTER: &str = ".specify/feature.json";
12
13pub const FEATURE_ENV: &str = "SPECIFY_FEATURE_DIRECTORY";
15
16pub const CONSTITUTION: &str = ".specify/memory/constitution.md";
18
19pub 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 found_env: super::python::command_env,
28 authoring_refusal,
29};
30
31fn 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
43fn command(argv: &[String], authoring: bool) -> Option<Result<(), String>> {
45 (authoring && is_script(argv)).then_some(Ok(()))
46}
47
48fn is_script(argv: &[String]) -> bool {
50 let script = |a: &str| {
51 let a = a.strip_prefix("./").unwrap_or(a);
52 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
70pub 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
97pub 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
129pub static PLANNING: Planning = Planning {
131 prefix: "speckit-",
132 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
159fn 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}