Skip to main content

aida_core/scaffolding/
mod.rs

1// trace:FR-0152,FR-0226 | ai:claude:high
2//! AI Project Scaffolding Module
3//!
4//! Provides functionality to generate AI coding agent integration artifacts:
5//! - CLAUDE.md project instructions
6//! - AGENTS.md project instructions
7//! - .claude/commands/ directory with project-specific slash commands
8//! - .claude/skills/ directory with requirements-driven development skills
9//! - .codex/skills/ directory with requirements-driven development skills
10//! - .git/hooks/ directory with traceability validation hooks
11//! - Code traceability configuration
12
13mod claude_md;
14mod codex_md;
15mod hooks;
16mod settings;
17
18use std::collections::HashSet;
19use std::fs;
20use std::path::PathBuf;
21
22use serde::{Deserialize, Serialize};
23
24use crate::models::RequirementsStore;
25use crate::templates::TemplateLoader;
26
27/// Current scaffolding version - increment when templates change significantly
28pub const SCAFFOLD_VERSION: &str = "2.0.0";
29
30/// Compute a simple checksum for content (first 8 chars of hex-encoded hash)
31fn compute_checksum(content: &str) -> String {
32    use std::collections::hash_map::DefaultHasher;
33    use std::hash::{Hash, Hasher};
34    let mut hasher = DefaultHasher::new();
35    content.hash(&mut hasher);
36    format!("{:016x}", hasher.finish())[..8].to_string()
37}
38
39/// Generate the AIDA header for a markdown file
40fn generate_aida_header(content: &str) -> String {
41    let checksum = compute_checksum(content);
42    format!(
43        "<!-- AIDA Generated: v{} | checksum:{} | DO NOT EDIT DIRECTLY -->\n\
44         <!-- To customize: copy this file and modify the copy -->\n\n",
45        SCAFFOLD_VERSION, checksum
46    )
47}
48
49/// Generate the AIDA header for a shell script file
50fn generate_aida_header_shell(content: &str) -> String {
51    let checksum = compute_checksum(content);
52    format!(
53        "# AIDA Generated: v{} | checksum:{}\n\
54         # To customize: copy this file and modify the copy\n",
55        SCAFFOLD_VERSION, checksum
56    )
57}
58
59/// Find the AIDA header line in file content, skipping YAML frontmatter if present
60fn find_aida_header_line(content: &str) -> Option<&str> {
61    // If content starts with YAML frontmatter, skip past it
62    if content.starts_with("---\n") || content.starts_with("---\r\n") {
63        // Find the closing --- after the opening one
64        let after_open = if content.starts_with("---\r\n") { 5 } else { 4 };
65        if let Some(close_pos) = content[after_open..].find("\n---") {
66            let after_close = after_open + close_pos + 4; // past "\n---"
67                                                          // Skip the newline after closing ---
68            let rest = content[after_close..]
69                .trim_start_matches('\r')
70                .trim_start_matches('\n');
71            return rest.lines().next();
72        }
73    }
74
75    // No frontmatter - header should be on the first line
76    content.lines().next()
77}
78
79/// Parse an existing file to determine its status relative to expected content
80fn check_file_status(file_path: &PathBuf, expected_content: &str) -> FileStatus {
81    let content = match fs::read_to_string(file_path) {
82        Ok(c) => c,
83        Err(_) => return FileStatus::New,
84    };
85
86    // Try to parse AIDA header (markdown format)
87    // Format: <!-- AIDA Generated: v{version} | checksum:{hash} | DO NOT EDIT DIRECTLY -->
88    let md_header_pattern = regex::Regex::new(
89        r"^<!-- AIDA Generated: v([0-9.]+) \| checksum:([a-f0-9]+) \| DO NOT EDIT DIRECTLY -->",
90    )
91    .unwrap();
92
93    // Try to parse AIDA header (shell format)
94    // Format: # AIDA Generated: v{version} | checksum:{hash}
95    let shell_header_pattern =
96        regex::Regex::new(r"^# AIDA Generated: v([0-9.]+) \| checksum:([a-f0-9]+)").unwrap();
97
98    // Find the header line, skipping frontmatter if present
99    let header_line = find_aida_header_line(&content).unwrap_or("");
100
101    // Check markdown header
102    if let Some(caps) = md_header_pattern.captures(header_line) {
103        let file_version = caps.get(1).map(|m| m.as_str()).unwrap_or("");
104        let stored_checksum = caps.get(2).map(|m| m.as_str()).unwrap_or("");
105
106        // Check version first
107        if file_version != SCAFFOLD_VERSION {
108            return FileStatus::OlderVersion {
109                file_version: file_version.to_string(),
110            };
111        }
112
113        // Compute checksum of the expected content (without header)
114        let expected_checksum = compute_checksum(expected_content);
115
116        if stored_checksum == expected_checksum {
117            return FileStatus::Unmodified;
118        } else {
119            return FileStatus::Modified {
120                expected_checksum,
121                actual_checksum: stored_checksum.to_string(),
122            };
123        }
124    }
125
126    // Check shell header
127    if let Some(caps) = shell_header_pattern.captures(header_line) {
128        let file_version = caps.get(1).map(|m| m.as_str()).unwrap_or("");
129        let stored_checksum = caps.get(2).map(|m| m.as_str()).unwrap_or("");
130
131        // Check version first
132        if file_version != SCAFFOLD_VERSION {
133            return FileStatus::OlderVersion {
134                file_version: file_version.to_string(),
135            };
136        }
137
138        // Compute checksum of the expected content (without header)
139        let expected_checksum = compute_checksum(expected_content);
140
141        if stored_checksum == expected_checksum {
142            return FileStatus::Unmodified;
143        } else {
144            return FileStatus::Modified {
145                expected_checksum,
146                actual_checksum: stored_checksum.to_string(),
147            };
148        }
149    }
150
151    // No AIDA header found - file exists but wasn't generated by AIDA
152    FileStatus::NoHeader
153}
154
155/// Status of an existing scaffolded file
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum FileStatus {
158    /// File doesn't exist yet
159    New,
160    /// File exists and matches expected checksum (safe to overwrite)
161    Unmodified,
162    /// File exists but checksum differs (user modified)
163    Modified {
164        expected_checksum: String,
165        actual_checksum: String,
166    },
167    /// File exists but has no AIDA header (unknown origin)
168    NoHeader,
169    /// File exists with older version (can be upgraded)
170    OlderVersion { file_version: String },
171}
172
173/// Configuration for what scaffolding artifacts to generate
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct ScaffoldConfig {
176    /// Generate CLAUDE.md project instructions
177    pub generate_claude_md: bool,
178    /// Generate AGENTS.md project instructions for Codex-compatible agents
179    pub generate_agents_md: bool,
180    /// Generate .claude/commands/ directory with slash commands
181    pub generate_commands: bool,
182    /// Generate .claude/skills/ directory with skills
183    pub generate_skills: bool,
184    /// Generate .codex/skills/ directory with Codex-compatible skills
185    pub generate_codex_skills: bool,
186    /// Include aida-req skill for requirement creation
187    pub include_aida_req_skill: bool,
188    /// Include aida-plan skill for implementation planning
189    pub include_aida_plan_skill: bool,
190    /// Include aida-implement skill for requirement implementation
191    pub include_aida_implement_skill: bool,
192    /// Include aida-capture skill for session review
193    pub include_aida_capture_skill: bool,
194    /// Include aida-docs skill for documentation management
195    pub include_aida_docs_skill: bool,
196    /// Include aida-release skill for release management
197    pub include_aida_release_skill: bool,
198    /// Include aida-evaluate skill for requirement quality evaluation
199    pub include_aida_evaluate_skill: bool,
200    /// Include aida-commit skill for commit with requirement linking
201    pub include_aida_commit_skill: bool,
202    /// Include aida-sync skill for template synchronization
203    pub include_aida_sync_skill: bool,
204    /// Include aida-test skill for test generation linked to requirements
205    pub include_aida_test_skill: bool,
206    /// Include aida-review skill for code review against specs
207    pub include_aida_review_skill: bool,
208    /// Include aida-onboard skill for project onboarding
209    pub include_aida_onboard_skill: bool,
210    /// Include aida-sprint skill for sprint planning
211    pub include_aida_sprint_skill: bool,
212    /// Include aida-search skill for unified search
213    pub include_aida_search_skill: bool,
214    /// Include aida-standup skill for daily standup generation
215    pub include_aida_standup_skill: bool,
216    /// Generate git hooks for traceability validation
217    pub generate_git_hooks: bool,
218    /// Include commit-msg hook for AI attribution validation
219    pub include_commit_msg_hook: bool,
220    /// Include pre-commit hook for trace comment validation
221    pub include_pre_commit_hook: bool,
222    /// Generate Claude Code hooks for AIDA integration
223    pub generate_claude_code_hooks: bool,
224    /// Include commit validation hook (PreToolUse)
225    pub include_validate_commit_hook: bool,
226    /// Include commit tracking hook (PostToolUse)
227    pub include_track_commits_hook: bool,
228    /// Custom project type for specialized scaffolding
229    pub project_type: ProjectType,
230    /// Tech stack hints for context generation
231    pub tech_stack: Vec<String>,
232}
233
234impl Default for ScaffoldConfig {
235    fn default() -> Self {
236        Self {
237            generate_claude_md: true,
238            generate_agents_md: true,
239            generate_commands: true,
240            generate_skills: true,
241            generate_codex_skills: true,
242            include_aida_req_skill: true,
243            include_aida_plan_skill: true,
244            include_aida_implement_skill: true,
245            include_aida_capture_skill: true,
246            include_aida_docs_skill: true,
247            include_aida_release_skill: true,
248            include_aida_evaluate_skill: true,
249            include_aida_commit_skill: true,
250            include_aida_sync_skill: true,
251            include_aida_test_skill: true,
252            include_aida_review_skill: true,
253            include_aida_onboard_skill: true,
254            include_aida_sprint_skill: true,
255            include_aida_search_skill: true,
256            include_aida_standup_skill: true,
257            generate_git_hooks: true,
258            include_commit_msg_hook: true,
259            include_pre_commit_hook: false, // Optional, disabled by default
260            generate_claude_code_hooks: true,
261            include_validate_commit_hook: true,
262            include_track_commits_hook: true,
263            project_type: ProjectType::Generic,
264            tech_stack: Vec::new(),
265        }
266    }
267}
268
269/// Project type for specialized scaffolding
270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
271pub enum ProjectType {
272    #[default]
273    Generic,
274    Rust,
275    Python,
276    TypeScript,
277    Web,
278    Api,
279    Cli,
280}
281
282impl ProjectType {
283    /// Get all project types for UI selection
284    pub fn all() -> &'static [ProjectType] {
285        &[
286            ProjectType::Generic,
287            ProjectType::Rust,
288            ProjectType::Python,
289            ProjectType::TypeScript,
290            ProjectType::Web,
291            ProjectType::Api,
292            ProjectType::Cli,
293        ]
294    }
295
296    /// Get display label for the project type
297    pub fn label(&self) -> &'static str {
298        match self {
299            ProjectType::Generic => "Generic",
300            ProjectType::Rust => "Rust",
301            ProjectType::Python => "Python",
302            ProjectType::TypeScript => "TypeScript",
303            ProjectType::Web => "Web Application",
304            ProjectType::Api => "API/Backend",
305            ProjectType::Cli => "CLI Tool",
306        }
307    }
308}
309
310/// Represents a scaffolding artifact to be generated
311#[derive(Debug, Clone)]
312pub struct ScaffoldArtifact {
313    /// Relative path from project root
314    pub path: PathBuf,
315    /// Content of the artifact (with AIDA header)
316    pub content: String,
317    /// Description of what this artifact does
318    pub description: String,
319    /// Whether the file already exists
320    pub exists: bool,
321    /// Status of existing file (if any)
322    pub file_status: FileStatus,
323}
324
325/// Result of scaffolding preview
326#[derive(Debug, Clone)]
327pub struct ScaffoldPreview {
328    /// Artifacts to be generated
329    pub artifacts: Vec<ScaffoldArtifact>,
330    /// Files that would be overwritten
331    pub overwrites: Vec<PathBuf>,
332    /// New files that would be created
333    pub new_files: Vec<PathBuf>,
334    /// Directories that would be created
335    pub new_dirs: Vec<PathBuf>,
336    /// Files that have been modified by user (need confirmation to overwrite)
337    pub modified_files: Vec<PathBuf>,
338    /// Files with older AIDA versions (safe to upgrade)
339    pub upgradeable_files: Vec<PathBuf>,
340}
341
342/// Options for applying scaffolding
343#[derive(Debug, Clone, Default)]
344pub struct ApplyOptions {
345    /// Force overwrite of modified files (ignores user modifications)
346    pub force: bool,
347}
348
349/// Scaffolding generator
350pub struct Scaffolder {
351    /// Project root directory
352    project_root: PathBuf,
353    /// Scaffolding configuration
354    config: ScaffoldConfig,
355    /// Database path (to determine backend type)
356    database_path: Option<PathBuf>,
357    /// Template loader for external/embedded templates (used for customization fallback chain)
358    #[allow(dead_code)]
359    template_loader: TemplateLoader,
360}
361
362impl Scaffolder {
363    /// Create a new scaffolder for the given project directory
364    pub fn new(project_root: PathBuf, config: ScaffoldConfig) -> Self {
365        let template_loader = TemplateLoader::with_project_root(&project_root);
366        Self {
367            project_root,
368            config,
369            database_path: None,
370            template_loader,
371        }
372    }
373
374    /// Create a new scaffolder with database path for backend-aware scaffolding
375    pub fn with_database(
376        project_root: PathBuf,
377        config: ScaffoldConfig,
378        database_path: PathBuf,
379    ) -> Self {
380        let template_loader = TemplateLoader::with_project_root(&project_root);
381        Self {
382            project_root,
383            config,
384            database_path: Some(database_path),
385            template_loader,
386        }
387    }
388
389    /// Check if the database is SQLite based on path extension
390    fn is_sqlite_database(&self) -> bool {
391        self.database_path
392            .as_ref()
393            .map(|p| p.extension().map(|e| e == "db").unwrap_or(false))
394            .unwrap_or(false)
395    }
396
397    /// Get the database filename for display
398    fn database_filename(&self) -> String {
399        self.database_path
400            .as_ref()
401            .and_then(|p| p.file_name())
402            .map(|n| n.to_string_lossy().to_string())
403            .unwrap_or_else(|| "requirements.yaml".to_string())
404    }
405
406    /// Load a template from external sources or embedded, with fallback chain
407    #[allow(dead_code)]
408    fn load_template(&mut self, key: &str) -> Option<String> {
409        self.template_loader.load(key)
410    }
411
412    /// Helper to create an artifact with version/checksum header and file status checking
413    fn create_artifact(
414        &self,
415        path: PathBuf,
416        raw_content: String,
417        description: String,
418        is_shell: bool,
419    ) -> ScaffoldArtifact {
420        let full_path = self.project_root.join(&path);
421        let exists = full_path.exists();
422
423        // Check file status against the raw content (what we're comparing against)
424        let file_status = if exists {
425            check_file_status(&full_path, &raw_content)
426        } else {
427            FileStatus::New
428        };
429
430        // Generate content with appropriate header
431        // For files with YAML frontmatter (---), insert header AFTER the closing ---
432        let content = if is_shell {
433            format!(
434                "{}{}",
435                generate_aida_header_shell(&raw_content),
436                raw_content
437            )
438        } else if raw_content.starts_with("---\n") {
439            // Split at the closing --- and insert header after frontmatter
440            let after_open = 4; // past "---\n"
441            if let Some(close_pos) = raw_content[after_open..].find("\n---\n") {
442                let fm_end = after_open + close_pos + 5; // past "\n---\n"
443                let (frontmatter, body) = raw_content.split_at(fm_end);
444                format!("{}{}{}", frontmatter, generate_aida_header(body), body)
445            } else {
446                format!("{}{}", generate_aida_header(&raw_content), raw_content)
447            }
448        } else {
449            format!("{}{}", generate_aida_header(&raw_content), raw_content)
450        };
451
452        ScaffoldArtifact {
453            path,
454            content,
455            description,
456            exists,
457            file_status,
458        }
459    }
460
461    /// Generate a preview of what would be scaffolded
462    pub fn preview(&mut self, store: &RequirementsStore) -> ScaffoldPreview {
463        let mut artifacts = Vec::new();
464        let mut overwrites = Vec::new();
465        let mut new_files = Vec::new();
466        let mut new_dirs = HashSet::new();
467        let mut modified_files = Vec::new();
468        let mut upgradeable_files = Vec::new();
469
470        // CLAUDE.md - Note: CLAUDE.md is user-edited, so no AIDA header
471        if self.config.generate_claude_md {
472            let path = PathBuf::from("CLAUDE.md");
473            let full_path = self.project_root.join(&path);
474            let exists = full_path.exists();
475            let content = self.generate_claude_md(store);
476
477            if exists {
478                overwrites.push(path.clone());
479            } else {
480                new_files.push(path.clone());
481            }
482
483            artifacts.push(ScaffoldArtifact {
484                path,
485                content,
486                description: "Project instructions for Claude Code".to_string(),
487                exists,
488                file_status: if exists {
489                    FileStatus::NoHeader
490                } else {
491                    FileStatus::New
492                },
493            });
494        }
495
496        // AGENTS.md - Note: AGENTS.md is user-edited, so no AIDA header
497        if self.config.generate_agents_md {
498            let path = PathBuf::from("AGENTS.md");
499            let full_path = self.project_root.join(&path);
500            let exists = full_path.exists();
501            let content = self.generate_agents_md(store);
502
503            if exists {
504                overwrites.push(path.clone());
505            } else {
506                new_files.push(path.clone());
507            }
508
509            artifacts.push(ScaffoldArtifact {
510                path,
511                content,
512                description: "Project instructions for Codex-compatible agents".to_string(),
513                exists,
514                file_status: if exists {
515                    FileStatus::NoHeader
516                } else {
517                    FileStatus::New
518                },
519            });
520        }
521
522        // .claude/commands/ directory
523        if self.config.generate_commands {
524            new_dirs.insert(PathBuf::from(".claude/commands"));
525
526            // Add default commands
527            let commands = self.generate_commands(store);
528            for (name, content, desc) in commands {
529                let path = PathBuf::from(format!(".claude/commands/{}.md", name));
530                let artifact = self.create_artifact(path.clone(), content, desc, false);
531
532                match &artifact.file_status {
533                    FileStatus::New => new_files.push(path),
534                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
535                        modified_files.push(artifact.path.clone())
536                    }
537                    FileStatus::OlderVersion { .. } => {
538                        upgradeable_files.push(artifact.path.clone())
539                    }
540                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
541                }
542
543                artifacts.push(artifact);
544            }
545        }
546
547        // .claude/skills/ directory
548        if self.config.generate_skills {
549            new_dirs.insert(PathBuf::from(".claude/skills"));
550
551            // Add aida-req skill
552            if self.config.include_aida_req_skill {
553                let path = PathBuf::from(".claude/skills/aida-req.md");
554                let artifact = self.create_artifact(
555                    path.clone(),
556                    self.generate_aida_req_skill(),
557                    "Skill for adding requirements with AI evaluation".to_string(),
558                    false,
559                );
560
561                match &artifact.file_status {
562                    FileStatus::New => new_files.push(path),
563                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
564                        modified_files.push(artifact.path.clone())
565                    }
566                    FileStatus::OlderVersion { .. } => {
567                        upgradeable_files.push(artifact.path.clone())
568                    }
569                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
570                }
571
572                artifacts.push(artifact);
573            }
574
575            // Add aida-plan skill
576            if self.config.include_aida_plan_skill {
577                let path = PathBuf::from(".claude/skills/aida-plan.md");
578                let artifact = self.create_artifact(
579                    path.clone(),
580                    self.generate_aida_plan_skill(),
581                    "Skill for planning requirement implementation".to_string(),
582                    false,
583                );
584
585                match &artifact.file_status {
586                    FileStatus::New => new_files.push(path),
587                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
588                        modified_files.push(artifact.path.clone())
589                    }
590                    FileStatus::OlderVersion { .. } => {
591                        upgradeable_files.push(artifact.path.clone())
592                    }
593                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
594                }
595
596                artifacts.push(artifact);
597            }
598
599            // Add aida-implement skill
600            if self.config.include_aida_implement_skill {
601                let path = PathBuf::from(".claude/skills/aida-implement.md");
602                let artifact = self.create_artifact(
603                    path.clone(),
604                    self.generate_aida_implement_skill(),
605                    "Skill for implementing requirements with traceability".to_string(),
606                    false,
607                );
608
609                match &artifact.file_status {
610                    FileStatus::New => new_files.push(path),
611                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
612                        modified_files.push(artifact.path.clone())
613                    }
614                    FileStatus::OlderVersion { .. } => {
615                        upgradeable_files.push(artifact.path.clone())
616                    }
617                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
618                }
619
620                artifacts.push(artifact);
621            }
622
623            // Add aida-capture skill
624            if self.config.include_aida_capture_skill {
625                let path = PathBuf::from(".claude/skills/aida-capture.md");
626                let artifact = self.create_artifact(
627                    path.clone(),
628                    self.generate_aida_capture_skill(),
629                    "Skill for capturing missed requirements from session".to_string(),
630                    false,
631                );
632
633                match &artifact.file_status {
634                    FileStatus::New => new_files.push(path),
635                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
636                        modified_files.push(artifact.path.clone())
637                    }
638                    FileStatus::OlderVersion { .. } => {
639                        upgradeable_files.push(artifact.path.clone())
640                    }
641                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
642                }
643
644                artifacts.push(artifact);
645            }
646
647            // Add aida-docs skill
648            if self.config.include_aida_docs_skill {
649                let path = PathBuf::from(".claude/skills/aida-docs.md");
650                let artifact = self.create_artifact(
651                    path.clone(),
652                    self.generate_aida_docs_skill(),
653                    "Skill for documentation management and generation".to_string(),
654                    false,
655                );
656
657                match &artifact.file_status {
658                    FileStatus::New => new_files.push(path),
659                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
660                        modified_files.push(artifact.path.clone())
661                    }
662                    FileStatus::OlderVersion { .. } => {
663                        upgradeable_files.push(artifact.path.clone())
664                    }
665                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
666                }
667
668                artifacts.push(artifact);
669            }
670
671            // Add aida-release skill
672            if self.config.include_aida_release_skill {
673                let path = PathBuf::from(".claude/skills/aida-release.md");
674                let artifact = self.create_artifact(
675                    path.clone(),
676                    self.generate_aida_release_skill(),
677                    "Skill for release management and version bumping".to_string(),
678                    false,
679                );
680
681                match &artifact.file_status {
682                    FileStatus::New => new_files.push(path),
683                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
684                        modified_files.push(artifact.path.clone())
685                    }
686                    FileStatus::OlderVersion { .. } => {
687                        upgradeable_files.push(artifact.path.clone())
688                    }
689                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
690                }
691
692                artifacts.push(artifact);
693            }
694
695            // Add aida-evaluate skill
696            if self.config.include_aida_evaluate_skill {
697                let path = PathBuf::from(".claude/skills/aida-evaluate.md");
698                let artifact = self.create_artifact(
699                    path.clone(),
700                    self.generate_aida_evaluate_skill(),
701                    "Skill for evaluating requirement quality".to_string(),
702                    false,
703                );
704
705                match &artifact.file_status {
706                    FileStatus::New => new_files.push(path),
707                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
708                        modified_files.push(artifact.path.clone())
709                    }
710                    FileStatus::OlderVersion { .. } => {
711                        upgradeable_files.push(artifact.path.clone())
712                    }
713                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
714                }
715
716                artifacts.push(artifact);
717            }
718
719            // Add aida-commit skill
720            if self.config.include_aida_commit_skill {
721                let path = PathBuf::from(".claude/skills/aida-commit.md");
722                let artifact = self.create_artifact(
723                    path.clone(),
724                    self.generate_aida_commit_skill(),
725                    "Skill for committing with requirement linking".to_string(),
726                    false,
727                );
728
729                match &artifact.file_status {
730                    FileStatus::New => new_files.push(path),
731                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
732                        modified_files.push(artifact.path.clone())
733                    }
734                    FileStatus::OlderVersion { .. } => {
735                        upgradeable_files.push(artifact.path.clone())
736                    }
737                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
738                }
739
740                artifacts.push(artifact);
741            }
742
743            // Add aida-sync skill
744            if self.config.include_aida_sync_skill {
745                let path = PathBuf::from(".claude/skills/aida-sync.md");
746                let artifact = self.create_artifact(
747                    path.clone(),
748                    self.generate_aida_sync_skill(),
749                    "Skill for template synchronization".to_string(),
750                    false,
751                );
752
753                match &artifact.file_status {
754                    FileStatus::New => new_files.push(path),
755                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
756                        modified_files.push(artifact.path.clone())
757                    }
758                    FileStatus::OlderVersion { .. } => {
759                        upgradeable_files.push(artifact.path.clone())
760                    }
761                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
762                }
763
764                artifacts.push(artifact);
765            }
766
767            // Add aida-test skill
768            if self.config.include_aida_test_skill {
769                let path = PathBuf::from(".claude/skills/aida-test.md");
770                let artifact = self.create_artifact(
771                    path.clone(),
772                    self.generate_aida_test_skill(),
773                    "Skill for generating tests linked to requirements".to_string(),
774                    false,
775                );
776
777                match &artifact.file_status {
778                    FileStatus::New => new_files.push(path),
779                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
780                        modified_files.push(artifact.path.clone())
781                    }
782                    FileStatus::OlderVersion { .. } => {
783                        upgradeable_files.push(artifact.path.clone())
784                    }
785                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
786                }
787
788                artifacts.push(artifact);
789            }
790
791            // Add aida-review skill
792            if self.config.include_aida_review_skill {
793                let path = PathBuf::from(".claude/skills/aida-review.md");
794                let artifact = self.create_artifact(
795                    path.clone(),
796                    self.generate_aida_review_skill(),
797                    "Skill for reviewing code changes against specs".to_string(),
798                    false,
799                );
800
801                match &artifact.file_status {
802                    FileStatus::New => new_files.push(path),
803                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
804                        modified_files.push(artifact.path.clone())
805                    }
806                    FileStatus::OlderVersion { .. } => {
807                        upgradeable_files.push(artifact.path.clone())
808                    }
809                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
810                }
811
812                artifacts.push(artifact);
813            }
814
815            // Add aida-onboard skill
816            if self.config.include_aida_onboard_skill {
817                let path = PathBuf::from(".claude/skills/aida-onboard.md");
818                let artifact = self.create_artifact(
819                    path.clone(),
820                    self.generate_aida_onboard_skill(),
821                    "Skill for project onboarding".to_string(),
822                    false,
823                );
824
825                match &artifact.file_status {
826                    FileStatus::New => new_files.push(path),
827                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
828                        modified_files.push(artifact.path.clone())
829                    }
830                    FileStatus::OlderVersion { .. } => {
831                        upgradeable_files.push(artifact.path.clone())
832                    }
833                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
834                }
835
836                artifacts.push(artifact);
837            }
838
839            // Add aida-sprint skill
840            if self.config.include_aida_sprint_skill {
841                let path = PathBuf::from(".claude/skills/aida-sprint.md");
842                let artifact = self.create_artifact(
843                    path.clone(),
844                    self.generate_aida_sprint_skill(),
845                    "Skill for sprint planning".to_string(),
846                    false,
847                );
848
849                match &artifact.file_status {
850                    FileStatus::New => new_files.push(path),
851                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
852                        modified_files.push(artifact.path.clone())
853                    }
854                    FileStatus::OlderVersion { .. } => {
855                        upgradeable_files.push(artifact.path.clone())
856                    }
857                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
858                }
859
860                artifacts.push(artifact);
861            }
862
863            // Add aida-search skill
864            if self.config.include_aida_search_skill {
865                let path = PathBuf::from(".claude/skills/aida-search.md");
866                let artifact = self.create_artifact(
867                    path.clone(),
868                    self.generate_aida_search_skill(),
869                    "Skill for unified search across requirements and code".to_string(),
870                    false,
871                );
872
873                match &artifact.file_status {
874                    FileStatus::New => new_files.push(path),
875                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
876                        modified_files.push(artifact.path.clone())
877                    }
878                    FileStatus::OlderVersion { .. } => {
879                        upgradeable_files.push(artifact.path.clone())
880                    }
881                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
882                }
883
884                artifacts.push(artifact);
885            }
886
887            // Add aida-standup skill
888            if self.config.include_aida_standup_skill {
889                let path = PathBuf::from(".claude/skills/aida-standup.md");
890                let artifact = self.create_artifact(
891                    path.clone(),
892                    self.generate_aida_standup_skill(),
893                    "Skill for daily standup generation".to_string(),
894                    false,
895                );
896
897                match &artifact.file_status {
898                    FileStatus::New => new_files.push(path),
899                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
900                        modified_files.push(artifact.path.clone())
901                    }
902                    FileStatus::OlderVersion { .. } => {
903                        upgradeable_files.push(artifact.path.clone())
904                    }
905                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
906                }
907
908                artifacts.push(artifact);
909            }
910        }
911
912        // .codex/skills/ directory
913        if self.config.generate_codex_skills {
914            new_dirs.insert(PathBuf::from(".codex/skills"));
915
916            let codex_skill_defs = [
917                ("aida-req", self.config.include_aida_req_skill),
918                ("aida-plan", self.config.include_aida_plan_skill),
919                ("aida-implement", self.config.include_aida_implement_skill),
920                ("aida-capture", self.config.include_aida_capture_skill),
921                ("aida-docs", self.config.include_aida_docs_skill),
922                ("aida-release", self.config.include_aida_release_skill),
923                ("aida-evaluate", self.config.include_aida_evaluate_skill),
924                ("aida-commit", self.config.include_aida_commit_skill),
925                ("aida-sync", self.config.include_aida_sync_skill),
926                ("aida-test", self.config.include_aida_test_skill),
927                ("aida-review", self.config.include_aida_review_skill),
928                ("aida-onboard", self.config.include_aida_onboard_skill),
929                ("aida-sprint", self.config.include_aida_sprint_skill),
930                ("aida-search", self.config.include_aida_search_skill),
931                ("aida-standup", self.config.include_aida_standup_skill),
932            ];
933
934            for (name, enabled) in codex_skill_defs {
935                if !enabled {
936                    continue;
937                }
938                let path = PathBuf::from(format!(".codex/skills/{}/SKILL.md", name));
939                let artifact = self.create_artifact(
940                    path.clone(),
941                    self.generate_codex_skill(name),
942                    format!("Codex-compatible skill {}", name),
943                    false,
944                );
945
946                match &artifact.file_status {
947                    FileStatus::New => new_files.push(path),
948                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
949                        modified_files.push(artifact.path.clone())
950                    }
951                    FileStatus::OlderVersion { .. } => {
952                        upgradeable_files.push(artifact.path.clone())
953                    }
954                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
955                }
956
957                artifacts.push(artifact);
958            }
959        }
960
961        // .mcp.json — MCP server configuration for Claude Code
962        {
963            let mcp_content = r#"{
964  "mcpServers": {
965    "aida": {
966      "type": "stdio",
967      "command": "aida",
968      "args": ["mcp-serve"]
969    }
970  }
971}"#
972            .to_string();
973            let path = PathBuf::from(".mcp.json");
974            let artifact = self.create_artifact(
975                path.clone(),
976                mcp_content,
977                "MCP server configuration for Claude Code".to_string(),
978                false,
979            );
980
981            match &artifact.file_status {
982                FileStatus::New => new_files.push(path),
983                FileStatus::Modified { .. } | FileStatus::NoHeader => {
984                    modified_files.push(artifact.path.clone())
985                }
986                FileStatus::OlderVersion { .. } => upgradeable_files.push(artifact.path.clone()),
987                FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
988            }
989
990            artifacts.push(artifact);
991        }
992
993        // .git/hooks/ directory (only if .git exists)
994        if self.config.generate_git_hooks && self.project_root.join(".git").exists() {
995            new_dirs.insert(PathBuf::from(".git/hooks"));
996
997            // commit-msg hook
998            if self.config.include_commit_msg_hook {
999                let path = PathBuf::from(".git/hooks/commit-msg");
1000                let artifact = self.create_artifact(
1001                    path.clone(),
1002                    self.generate_commit_msg_hook(),
1003                    "Git hook for validating AI attribution in commit messages".to_string(),
1004                    true, // shell script
1005                );
1006
1007                match &artifact.file_status {
1008                    FileStatus::New => new_files.push(path),
1009                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
1010                        modified_files.push(artifact.path.clone())
1011                    }
1012                    FileStatus::OlderVersion { .. } => {
1013                        upgradeable_files.push(artifact.path.clone())
1014                    }
1015                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
1016                }
1017
1018                artifacts.push(artifact);
1019            }
1020
1021            // pre-commit hook
1022            if self.config.include_pre_commit_hook {
1023                let path = PathBuf::from(".git/hooks/pre-commit");
1024                let artifact = self.create_artifact(
1025                    path.clone(),
1026                    self.generate_pre_commit_hook(),
1027                    "Git hook for validating trace comments before commit".to_string(),
1028                    true, // shell script
1029                );
1030
1031                match &artifact.file_status {
1032                    FileStatus::New => new_files.push(path),
1033                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
1034                        modified_files.push(artifact.path.clone())
1035                    }
1036                    FileStatus::OlderVersion { .. } => {
1037                        upgradeable_files.push(artifact.path.clone())
1038                    }
1039                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
1040                }
1041
1042                artifacts.push(artifact);
1043            }
1044        }
1045
1046        // Claude Code hooks (in .claude/hooks/)
1047        if self.config.generate_claude_code_hooks {
1048            new_dirs.insert(PathBuf::from(".claude/hooks"));
1049
1050            // Validate commit hook (PreToolUse)
1051            if self.config.include_validate_commit_hook {
1052                let path = PathBuf::from(".claude/hooks/aida-validate-commit.sh");
1053                let artifact = self.create_artifact(
1054                    path.clone(),
1055                    self.generate_validate_commit_hook(),
1056                    "Claude Code hook for validating commit messages reference requirements"
1057                        .to_string(),
1058                    true, // shell script
1059                );
1060
1061                match &artifact.file_status {
1062                    FileStatus::New => new_files.push(path),
1063                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
1064                        modified_files.push(artifact.path.clone())
1065                    }
1066                    FileStatus::OlderVersion { .. } => {
1067                        upgradeable_files.push(artifact.path.clone())
1068                    }
1069                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
1070                }
1071
1072                artifacts.push(artifact);
1073            }
1074
1075            // Track commits hook (PostToolUse)
1076            if self.config.include_track_commits_hook {
1077                let path = PathBuf::from(".claude/hooks/aida-track-commits.sh");
1078                let artifact = self.create_artifact(
1079                    path.clone(),
1080                    self.generate_track_commits_hook(),
1081                    "Claude Code hook for updating requirement status after commits".to_string(),
1082                    true, // shell script
1083                );
1084
1085                match &artifact.file_status {
1086                    FileStatus::New => new_files.push(path),
1087                    FileStatus::Modified { .. } | FileStatus::NoHeader => {
1088                        modified_files.push(artifact.path.clone())
1089                    }
1090                    FileStatus::OlderVersion { .. } => {
1091                        upgradeable_files.push(artifact.path.clone())
1092                    }
1093                    FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
1094                }
1095
1096                artifacts.push(artifact);
1097            }
1098
1099            // Generate settings.json with hook configuration
1100            let path = PathBuf::from(".claude/settings.json");
1101            let artifact = self.create_artifact(
1102                path.clone(),
1103                self.generate_claude_settings_json(),
1104                "Claude Code settings with AIDA hook configuration".to_string(),
1105                false, // JSON file
1106            );
1107
1108            match &artifact.file_status {
1109                FileStatus::New => new_files.push(path),
1110                FileStatus::Modified { .. } | FileStatus::NoHeader => {
1111                    modified_files.push(artifact.path.clone())
1112                }
1113                FileStatus::OlderVersion { .. } => upgradeable_files.push(artifact.path.clone()),
1114                FileStatus::Unmodified => overwrites.push(artifact.path.clone()),
1115            }
1116
1117            artifacts.push(artifact);
1118        }
1119
1120        // Filter new_dirs to only include those that don't exist
1121        let new_dirs: Vec<PathBuf> = new_dirs
1122            .into_iter()
1123            .filter(|d| !self.project_root.join(d).exists())
1124            .collect();
1125
1126        ScaffoldPreview {
1127            artifacts,
1128            overwrites,
1129            new_files,
1130            new_dirs,
1131            modified_files,
1132            upgradeable_files,
1133        }
1134    }
1135
1136    /// Apply the scaffolding (write files) - writes all files regardless of status
1137    /// For more control, use `apply_with_options`
1138    pub fn apply(&self, preview: &ScaffoldPreview) -> Result<Vec<PathBuf>, ScaffoldError> {
1139        self.apply_with_options(preview, &ApplyOptions::default())
1140    }
1141
1142    /// Apply the scaffolding with options to control behavior for modified files
1143    pub fn apply_with_options(
1144        &self,
1145        preview: &ScaffoldPreview,
1146        options: &ApplyOptions,
1147    ) -> Result<Vec<PathBuf>, ScaffoldError> {
1148        let mut written_files = Vec::new();
1149        let mut skipped_files = Vec::new();
1150
1151        // Create directories first
1152        for dir in &preview.new_dirs {
1153            let full_path = self.project_root.join(dir);
1154            fs::create_dir_all(&full_path).map_err(|e| ScaffoldError::IoError {
1155                path: full_path.clone(),
1156                message: e.to_string(),
1157            })?;
1158        }
1159
1160        // Also ensure parent directories exist for all artifacts
1161        for artifact in &preview.artifacts {
1162            if let Some(parent) = artifact.path.parent() {
1163                let full_parent = self.project_root.join(parent);
1164                if !full_parent.exists() {
1165                    fs::create_dir_all(&full_parent).map_err(|e| ScaffoldError::IoError {
1166                        path: full_parent.clone(),
1167                        message: e.to_string(),
1168                    })?;
1169                }
1170            }
1171        }
1172
1173        // Write artifacts based on their status and options
1174        for artifact in &preview.artifacts {
1175            let should_write = match &artifact.file_status {
1176                FileStatus::New => true,
1177                FileStatus::Unmodified => true,
1178                FileStatus::OlderVersion { .. } => true, // Always upgrade
1179                FileStatus::Modified { .. } => {
1180                    // User modified - only write if --force
1181                    options.force
1182                }
1183                FileStatus::NoHeader => {
1184                    // No header means unknown origin - only write if --force
1185                    options.force
1186                }
1187            };
1188
1189            if !should_write {
1190                skipped_files.push(artifact.path.clone());
1191                continue;
1192            }
1193
1194            let full_path = self.project_root.join(&artifact.path);
1195            fs::write(&full_path, &artifact.content).map_err(|e| ScaffoldError::IoError {
1196                path: full_path.clone(),
1197                message: e.to_string(),
1198            })?;
1199
1200            // Make git hooks and Claude Code hooks executable on Unix
1201            #[cfg(unix)]
1202            if artifact.path.starts_with(".git/hooks/")
1203                || artifact.path.starts_with(".claude/hooks/")
1204            {
1205                use std::os::unix::fs::PermissionsExt;
1206                let mut perms = fs::metadata(&full_path)
1207                    .map_err(|e| ScaffoldError::IoError {
1208                        path: full_path.clone(),
1209                        message: e.to_string(),
1210                    })?
1211                    .permissions();
1212                perms.set_mode(0o755);
1213                fs::set_permissions(&full_path, perms).map_err(|e| ScaffoldError::IoError {
1214                    path: full_path.clone(),
1215                    message: e.to_string(),
1216                })?;
1217            }
1218
1219            written_files.push(artifact.path.clone());
1220        }
1221
1222        Ok(written_files)
1223    }
1224
1225    /// Generate slash commands
1226    fn generate_commands(&self, _store: &RequirementsStore) -> Vec<(String, String, String)> {
1227        use crate::templates::EMBEDDED_TEMPLATES;
1228
1229        // Command definitions: (template_key, output_name, description)
1230        let command_defs = [
1231            (
1232                "commands/aida-status.md",
1233                "aida-status",
1234                "Show project requirements status",
1235            ),
1236            (
1237                "commands/aida-review.md",
1238                "aida-review",
1239                "Review a requirement for quality",
1240            ),
1241            (
1242                "commands/aida-req.md",
1243                "aida-req",
1244                "Add a new requirement with AI evaluation",
1245            ),
1246            (
1247                "commands/aida-implement.md",
1248                "aida-implement",
1249                "Implement a requirement with traceability",
1250            ),
1251            (
1252                "commands/aida-capture.md",
1253                "aida-capture",
1254                "Capture missed requirements from session",
1255            ),
1256            (
1257                "commands/aida-evaluate.md",
1258                "aida-evaluate",
1259                "Evaluate requirement quality with AI",
1260            ),
1261            (
1262                "commands/aida-commit.md",
1263                "aida-commit",
1264                "Commit with requirement linking",
1265            ),
1266            (
1267                "commands/aida-sync.md",
1268                "aida-sync",
1269                "Sync templates and scaffolding",
1270            ),
1271            (
1272                "commands/aida-test.md",
1273                "aida-test",
1274                "Generate tests linked to requirements",
1275            ),
1276            (
1277                "commands/aida-onboard.md",
1278                "aida-onboard",
1279                "Project onboarding for new team members",
1280            ),
1281            (
1282                "commands/aida-sprint.md",
1283                "aida-sprint",
1284                "Sprint planning from approved requirements",
1285            ),
1286            (
1287                "commands/aida-search.md",
1288                "aida-search",
1289                "Unified search across requirements and code",
1290            ),
1291            (
1292                "commands/aida-standup.md",
1293                "aida-standup",
1294                "Daily standup summary from recent activity",
1295            ),
1296        ];
1297
1298        command_defs
1299            .iter()
1300            .filter_map(|(key, name, desc)| {
1301                EMBEDDED_TEMPLATES
1302                    .get(key)
1303                    .map(|content| (name.to_string(), content.to_string(), desc.to_string()))
1304            })
1305            .collect()
1306    }
1307
1308    /// Generate aida-req skill content (loads from embedded template)
1309    fn generate_aida_req_skill(&self) -> String {
1310        use crate::templates::EMBEDDED_TEMPLATES;
1311        EMBEDDED_TEMPLATES
1312            .get("skills/aida-req.md")
1313            .map(|s| s.to_string())
1314            .unwrap_or_else(|| {
1315                "# AIDA Requirement Creation Skill\n\n(template not found)".to_string()
1316            })
1317    }
1318
1319    /// Generate aida-implement skill content (loads from embedded template)
1320    fn generate_aida_implement_skill(&self) -> String {
1321        use crate::templates::EMBEDDED_TEMPLATES;
1322        EMBEDDED_TEMPLATES
1323            .get("skills/aida-implement.md")
1324            .map(|s| s.to_string())
1325            .unwrap_or_else(|| "# AIDA Implementation Skill\n\n(template not found)".to_string())
1326    }
1327
1328    /// Generate aida-plan skill content (loads from embedded template)
1329    fn generate_aida_plan_skill(&self) -> String {
1330        use crate::templates::EMBEDDED_TEMPLATES;
1331        EMBEDDED_TEMPLATES
1332            .get("skills/aida-plan.md")
1333            .map(|s| s.to_string())
1334            .unwrap_or_else(|| "# AIDA Planning Skill\n\n(template not found)".to_string())
1335    }
1336
1337    /// Generate aida-capture skill content (loads from embedded template)
1338    fn generate_aida_capture_skill(&self) -> String {
1339        use crate::templates::EMBEDDED_TEMPLATES;
1340        EMBEDDED_TEMPLATES
1341            .get("skills/aida-capture.md")
1342            .map(|s| s.to_string())
1343            .unwrap_or_else(|| "# AIDA Session Capture Skill\n\n(template not found)".to_string())
1344    }
1345
1346    /// Generate aida-docs skill content (loads from embedded template)
1347    fn generate_aida_docs_skill(&self) -> String {
1348        use crate::templates::EMBEDDED_TEMPLATES;
1349        EMBEDDED_TEMPLATES
1350            .get("skills/aida-docs.md")
1351            .map(|s| s.to_string())
1352            .unwrap_or_else(|| "# AIDA Documentation Skill\n\n(template not found)".to_string())
1353    }
1354
1355    /// Generate aida-release skill content (loads from embedded template)
1356    fn generate_aida_release_skill(&self) -> String {
1357        use crate::templates::EMBEDDED_TEMPLATES;
1358        EMBEDDED_TEMPLATES
1359            .get("skills/aida-release.md")
1360            .map(|s| s.to_string())
1361            .unwrap_or_else(|| {
1362                "# AIDA Release Management Skill\n\n(template not found)".to_string()
1363            })
1364    }
1365
1366    /// Generate aida-evaluate skill content (loads from embedded template)
1367    fn generate_aida_evaluate_skill(&self) -> String {
1368        // Load from embedded templates at compile time
1369        use crate::templates::EMBEDDED_TEMPLATES;
1370        EMBEDDED_TEMPLATES
1371            .get("skills/aida-evaluate.md")
1372            .map(|s| s.to_string())
1373            .unwrap_or_else(|| {
1374                r#"# AIDA Requirement Evaluation Skill
1375
1376## Purpose
1377
1378Evaluate a requirement's quality using AI analysis.
1379
1380## When to Use
1381
1382Use this skill when:
1383- User wants to evaluate a specific requirement's quality
1384- User asks to "evaluate", "assess", or "review" a requirement
1385
1386## Workflow
1387
13881. Load the requirement from database: `aida show <SPEC-ID>`
13892. Run AI evaluation for clarity, testability, completeness, consistency
13903. Display quality score and issues found
13914. Offer follow-up actions: improve, split, or accept
1392"#
1393                .to_string()
1394            })
1395    }
1396
1397    /// Generate aida-commit skill content (loads from embedded template)
1398    fn generate_aida_commit_skill(&self) -> String {
1399        // Load from embedded templates at compile time
1400        use crate::templates::EMBEDDED_TEMPLATES;
1401        EMBEDDED_TEMPLATES
1402            .get("skills/aida-commit.md")
1403            .map(|s| s.to_string())
1404            .unwrap_or_else(|| {
1405                r#"# AIDA Commit Skill
1406
1407## Purpose
1408
1409Create git commits with automatic requirement linkage.
1410
1411## When to Use
1412
1413Use this skill when:
1414- User wants to commit changes with requirement traceability
1415- User says "commit" after implementing features
1416
1417## Workflow
1418
14191. Analyze staged changes and extract requirement traces
14202. Check for untraced implementation code
14213. Offer to create requirements for untraced work
14224. Create commit with requirement links
14235. Update linked requirement statuses
1424"#
1425                .to_string()
1426            })
1427    }
1428
1429    /// Generate aida-sync skill content (loads from embedded template)
1430    fn generate_aida_sync_skill(&self) -> String {
1431        // Load from embedded templates at compile time
1432        use crate::templates::EMBEDDED_TEMPLATES;
1433        EMBEDDED_TEMPLATES
1434            .get("skills/aida-sync.md")
1435            .map(|s| s.to_string())
1436            .unwrap_or_else(|| {
1437                r#"# AIDA Sync Skill
1438
1439## Purpose
1440
1441Maintain consistency between AIDA templates and scaffolded projects.
1442
1443## When to Use
1444
1445Use this skill when:
1446- You've modified templates in `aida-core/templates/`
1447- You want to check scaffold status
1448- At the end of an AIDA development session
1449
1450## Workflow
1451
14521. Detect environment (AIDA repo vs scaffolded project)
14532. For AIDA repo: Check template integrity
14543. For other projects: Check scaffold status
14554. Ensure templates and skills are consistent
1456"#
1457                .to_string()
1458            })
1459    }
1460
1461    /// Generate aida-test skill content (loads from embedded template)
1462    fn generate_aida_test_skill(&self) -> String {
1463        use crate::templates::EMBEDDED_TEMPLATES;
1464        EMBEDDED_TEMPLATES
1465            .get("skills/aida-test.md")
1466            .map(|s| s.to_string())
1467            .unwrap_or_else(|| "# AIDA Test Generation Skill\n\n(template not found)".to_string())
1468    }
1469
1470    /// Generate aida-review skill content (loads from embedded template)
1471    fn generate_aida_review_skill(&self) -> String {
1472        use crate::templates::EMBEDDED_TEMPLATES;
1473        EMBEDDED_TEMPLATES
1474            .get("skills/aida-review.md")
1475            .map(|s| s.to_string())
1476            .unwrap_or_else(|| "# AIDA Code Review Skill\n\n(template not found)".to_string())
1477    }
1478
1479    /// Generate aida-onboard skill content (loads from embedded template)
1480    fn generate_aida_onboard_skill(&self) -> String {
1481        use crate::templates::EMBEDDED_TEMPLATES;
1482        EMBEDDED_TEMPLATES
1483            .get("skills/aida-onboard.md")
1484            .map(|s| s.to_string())
1485            .unwrap_or_else(|| {
1486                "# AIDA Project Onboarding Skill\n\n(template not found)".to_string()
1487            })
1488    }
1489
1490    /// Generate aida-sprint skill content (loads from embedded template)
1491    fn generate_aida_sprint_skill(&self) -> String {
1492        use crate::templates::EMBEDDED_TEMPLATES;
1493        EMBEDDED_TEMPLATES
1494            .get("skills/aida-sprint.md")
1495            .map(|s| s.to_string())
1496            .unwrap_or_else(|| "# AIDA Sprint Planning Skill\n\n(template not found)".to_string())
1497    }
1498
1499    /// Generate aida-search skill content (loads from embedded template)
1500    fn generate_aida_search_skill(&self) -> String {
1501        use crate::templates::EMBEDDED_TEMPLATES;
1502        EMBEDDED_TEMPLATES
1503            .get("skills/aida-search.md")
1504            .map(|s| s.to_string())
1505            .unwrap_or_else(|| "# AIDA Unified Search Skill\n\n(template not found)".to_string())
1506    }
1507
1508    /// Generate aida-standup skill content (loads from embedded template)
1509    fn generate_aida_standup_skill(&self) -> String {
1510        use crate::templates::EMBEDDED_TEMPLATES;
1511        EMBEDDED_TEMPLATES
1512            .get("skills/aida-standup.md")
1513            .map(|s| s.to_string())
1514            .unwrap_or_else(|| "# AIDA Standup Skill\n\n(template not found)".to_string())
1515    }
1516
1517    /// Generate Codex skill content from an embedded Claude skill template.
1518    /// Converts frontmatter-style skill files into plain SKILL.md content.
1519    fn generate_codex_skill(&self, skill_name: &str) -> String {
1520        use crate::templates::EMBEDDED_TEMPLATES;
1521
1522        let key = format!("skills/{}.md", skill_name);
1523        let raw = EMBEDDED_TEMPLATES
1524            .get(key.as_str())
1525            .map(|s| s.to_string())
1526            .unwrap_or_else(|| format!("# {}\n\n(template not found)", skill_name));
1527
1528        strip_yaml_frontmatter(&raw)
1529    }
1530}
1531
1532fn strip_yaml_frontmatter(content: &str) -> String {
1533    if !content.starts_with("---\n") && !content.starts_with("---\r\n") {
1534        return content.to_string();
1535    }
1536
1537    let after_open = if content.starts_with("---\r\n") { 5 } else { 4 };
1538    let rest = &content[after_open..];
1539    if let Some(close_pos) = rest.find("\n---\n") {
1540        let body_start = after_open + close_pos + 5;
1541        return content[body_start..].to_string();
1542    }
1543    if let Some(close_pos) = rest.find("\n---\r\n") {
1544        let body_start = after_open + close_pos + 6;
1545        return content[body_start..].to_string();
1546    }
1547
1548    content.to_string()
1549}
1550
1551/// Errors that can occur during scaffolding
1552#[derive(Debug)]
1553pub enum ScaffoldError {
1554    /// IO error while reading/writing files
1555    IoError { path: PathBuf, message: String },
1556}
1557
1558impl std::fmt::Display for ScaffoldError {
1559    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1560        match self {
1561            ScaffoldError::IoError { path, message } => {
1562                write!(f, "IO error at {}: {}", path.display(), message)
1563            }
1564        }
1565    }
1566}
1567
1568impl std::error::Error for ScaffoldError {}
1569
1570#[cfg(test)]
1571mod tests {
1572    use super::*;
1573    use tempfile::TempDir;
1574
1575    fn create_test_store() -> RequirementsStore {
1576        RequirementsStore {
1577            name: "test-project".to_string(),
1578            title: "Test Project".to_string(),
1579            description: "A test project for scaffolding".to_string(),
1580            ..Default::default()
1581        }
1582    }
1583
1584    #[test]
1585    fn test_default_config() {
1586        let config = ScaffoldConfig::default();
1587        assert!(config.generate_claude_md);
1588        assert!(config.generate_agents_md);
1589        assert!(config.generate_commands);
1590        assert!(config.generate_skills);
1591        assert!(config.generate_codex_skills);
1592        assert!(config.include_aida_req_skill);
1593        assert!(config.include_aida_implement_skill);
1594        assert!(config.include_aida_capture_skill);
1595        assert_eq!(config.project_type, ProjectType::Generic);
1596    }
1597
1598    #[test]
1599    fn test_preview_generates_expected_artifacts() {
1600        let temp_dir = TempDir::new().unwrap();
1601        let config = ScaffoldConfig::default();
1602        let mut scaffolder = Scaffolder::new(temp_dir.path().to_path_buf(), config);
1603        let store = create_test_store();
1604
1605        let preview = scaffolder.preview(&store);
1606
1607        // Should have CLAUDE.md, 2 commands, and 2 skills
1608        assert!(!preview.artifacts.is_empty());
1609
1610        // Check that CLAUDE.md is generated
1611        let claude_md = preview
1612            .artifacts
1613            .iter()
1614            .find(|a| a.path == PathBuf::from("CLAUDE.md"));
1615        assert!(claude_md.is_some());
1616        assert!(claude_md.unwrap().content.contains("Test Project"));
1617    }
1618
1619    #[test]
1620    fn test_apply_creates_files() {
1621        let temp_dir = TempDir::new().unwrap();
1622        let config = ScaffoldConfig::default();
1623        let mut scaffolder = Scaffolder::new(temp_dir.path().to_path_buf(), config);
1624        let store = create_test_store();
1625
1626        let preview = scaffolder.preview(&store);
1627        let result = scaffolder.apply(&preview);
1628
1629        assert!(result.is_ok());
1630
1631        // Check that CLAUDE.md was created
1632        assert!(temp_dir.path().join("CLAUDE.md").exists());
1633        assert!(temp_dir.path().join("AGENTS.md").exists());
1634
1635        // Check that .claude directories were created
1636        assert!(temp_dir.path().join(".claude/commands").exists());
1637        assert!(temp_dir.path().join(".claude/skills").exists());
1638        assert!(temp_dir.path().join(".codex/skills").exists());
1639    }
1640
1641    #[test]
1642    fn test_project_type_labels() {
1643        assert_eq!(ProjectType::Rust.label(), "Rust");
1644        assert_eq!(ProjectType::Python.label(), "Python");
1645        assert_eq!(ProjectType::Generic.label(), "Generic");
1646    }
1647}