rai-cli 2.0.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. rai_cli/__init__.py +38 -0
  2. rai_cli/__main__.py +30 -0
  3. rai_cli/cli/__init__.py +3 -0
  4. rai_cli/cli/commands/__init__.py +3 -0
  5. rai_cli/cli/commands/base.py +101 -0
  6. rai_cli/cli/commands/discover.py +547 -0
  7. rai_cli/cli/commands/init.py +460 -0
  8. rai_cli/cli/commands/memory.py +1626 -0
  9. rai_cli/cli/commands/profile.py +51 -0
  10. rai_cli/cli/commands/session.py +264 -0
  11. rai_cli/cli/commands/skill.py +226 -0
  12. rai_cli/cli/error_handler.py +158 -0
  13. rai_cli/cli/main.py +137 -0
  14. rai_cli/config/__init__.py +11 -0
  15. rai_cli/config/paths.py +309 -0
  16. rai_cli/config/settings.py +180 -0
  17. rai_cli/context/__init__.py +42 -0
  18. rai_cli/context/analyzers/__init__.py +16 -0
  19. rai_cli/context/analyzers/models.py +36 -0
  20. rai_cli/context/analyzers/protocol.py +43 -0
  21. rai_cli/context/analyzers/python.py +291 -0
  22. rai_cli/context/builder.py +1566 -0
  23. rai_cli/context/diff.py +213 -0
  24. rai_cli/context/extractors/__init__.py +13 -0
  25. rai_cli/context/extractors/skills.py +121 -0
  26. rai_cli/context/graph.py +300 -0
  27. rai_cli/context/models.py +134 -0
  28. rai_cli/context/query.py +507 -0
  29. rai_cli/core/__init__.py +37 -0
  30. rai_cli/core/files.py +66 -0
  31. rai_cli/core/text.py +174 -0
  32. rai_cli/core/tools.py +441 -0
  33. rai_cli/discovery/__init__.py +50 -0
  34. rai_cli/discovery/analyzer.py +601 -0
  35. rai_cli/discovery/drift.py +355 -0
  36. rai_cli/discovery/scanner.py +1200 -0
  37. rai_cli/engines/__init__.py +3 -0
  38. rai_cli/exceptions.py +200 -0
  39. rai_cli/governance/__init__.py +11 -0
  40. rai_cli/governance/extractor.py +311 -0
  41. rai_cli/governance/models.py +132 -0
  42. rai_cli/governance/parsers/__init__.py +35 -0
  43. rai_cli/governance/parsers/adr.py +255 -0
  44. rai_cli/governance/parsers/backlog.py +302 -0
  45. rai_cli/governance/parsers/constitution.py +100 -0
  46. rai_cli/governance/parsers/epic.py +299 -0
  47. rai_cli/governance/parsers/glossary.py +297 -0
  48. rai_cli/governance/parsers/guardrails.py +326 -0
  49. rai_cli/governance/parsers/prd.py +93 -0
  50. rai_cli/governance/parsers/vision.py +97 -0
  51. rai_cli/handlers/__init__.py +3 -0
  52. rai_cli/memory/__init__.py +58 -0
  53. rai_cli/memory/loader.py +247 -0
  54. rai_cli/memory/migration.py +247 -0
  55. rai_cli/memory/models.py +169 -0
  56. rai_cli/memory/writer.py +485 -0
  57. rai_cli/onboarding/__init__.py +96 -0
  58. rai_cli/onboarding/bootstrap.py +164 -0
  59. rai_cli/onboarding/claudemd.py +209 -0
  60. rai_cli/onboarding/conventions.py +742 -0
  61. rai_cli/onboarding/detection.py +155 -0
  62. rai_cli/onboarding/governance.py +443 -0
  63. rai_cli/onboarding/manifest.py +101 -0
  64. rai_cli/onboarding/memory_md.py +387 -0
  65. rai_cli/onboarding/migration.py +207 -0
  66. rai_cli/onboarding/profile.py +457 -0
  67. rai_cli/onboarding/skills.py +114 -0
  68. rai_cli/output/__init__.py +28 -0
  69. rai_cli/output/console.py +394 -0
  70. rai_cli/output/formatters/__init__.py +9 -0
  71. rai_cli/output/formatters/discover.py +442 -0
  72. rai_cli/output/formatters/skill.py +293 -0
  73. rai_cli/rai_base/__init__.py +22 -0
  74. rai_cli/rai_base/framework/__init__.py +7 -0
  75. rai_cli/rai_base/framework/methodology.yaml +235 -0
  76. rai_cli/rai_base/governance/__init__.py +1 -0
  77. rai_cli/rai_base/governance/architecture/__init__.py +1 -0
  78. rai_cli/rai_base/governance/architecture/domain-model.md +20 -0
  79. rai_cli/rai_base/governance/architecture/system-context.md +34 -0
  80. rai_cli/rai_base/governance/architecture/system-design.md +24 -0
  81. rai_cli/rai_base/governance/backlog.md +8 -0
  82. rai_cli/rai_base/governance/guardrails.md +18 -0
  83. rai_cli/rai_base/governance/prd.md +25 -0
  84. rai_cli/rai_base/governance/vision.md +16 -0
  85. rai_cli/rai_base/identity/__init__.py +8 -0
  86. rai_cli/rai_base/identity/core.md +119 -0
  87. rai_cli/rai_base/identity/perspective.md +119 -0
  88. rai_cli/rai_base/memory/__init__.py +7 -0
  89. rai_cli/rai_base/memory/patterns-base.jsonl +20 -0
  90. rai_cli/schemas/__init__.py +3 -0
  91. rai_cli/schemas/session_state.py +106 -0
  92. rai_cli/session/__init__.py +5 -0
  93. rai_cli/session/bundle.py +389 -0
  94. rai_cli/session/close.py +255 -0
  95. rai_cli/session/state.py +108 -0
  96. rai_cli/skills/__init__.py +44 -0
  97. rai_cli/skills/locator.py +129 -0
  98. rai_cli/skills/name_checker.py +203 -0
  99. rai_cli/skills/parser.py +145 -0
  100. rai_cli/skills/scaffold.py +185 -0
  101. rai_cli/skills/schema.py +130 -0
  102. rai_cli/skills/validator.py +172 -0
  103. rai_cli/skills_base/__init__.py +59 -0
  104. rai_cli/skills_base/rai-debug/SKILL.md +296 -0
  105. rai_cli/skills_base/rai-discover-document/SKILL.md +292 -0
  106. rai_cli/skills_base/rai-discover-scan/SKILL.md +325 -0
  107. rai_cli/skills_base/rai-discover-start/SKILL.md +213 -0
  108. rai_cli/skills_base/rai-discover-validate/SKILL.md +310 -0
  109. rai_cli/skills_base/rai-epic-close/SKILL.md +369 -0
  110. rai_cli/skills_base/rai-epic-design/SKILL.md +622 -0
  111. rai_cli/skills_base/rai-epic-plan/SKILL.md +672 -0
  112. rai_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
  113. rai_cli/skills_base/rai-epic-start/SKILL.md +217 -0
  114. rai_cli/skills_base/rai-project-create/SKILL.md +455 -0
  115. rai_cli/skills_base/rai-project-onboard/SKILL.md +503 -0
  116. rai_cli/skills_base/rai-research/SKILL.md +264 -0
  117. rai_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
  118. rai_cli/skills_base/rai-session-close/SKILL.md +151 -0
  119. rai_cli/skills_base/rai-session-start/SKILL.md +110 -0
  120. rai_cli/skills_base/rai-story-close/SKILL.md +367 -0
  121. rai_cli/skills_base/rai-story-design/SKILL.md +339 -0
  122. rai_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
  123. rai_cli/skills_base/rai-story-implement/SKILL.md +256 -0
  124. rai_cli/skills_base/rai-story-plan/SKILL.md +307 -0
  125. rai_cli/skills_base/rai-story-review/SKILL.md +276 -0
  126. rai_cli/skills_base/rai-story-start/SKILL.md +288 -0
  127. rai_cli/telemetry/__init__.py +42 -0
  128. rai_cli/telemetry/schemas.py +285 -0
  129. rai_cli/telemetry/writer.py +210 -0
  130. rai_cli/viz/__init__.py +7 -0
  131. rai_cli/viz/generator.py +404 -0
  132. rai_cli-2.0.0a1.dist-info/METADATA +289 -0
  133. rai_cli-2.0.0a1.dist-info/RECORD +137 -0
  134. rai_cli-2.0.0a1.dist-info/WHEEL +4 -0
  135. rai_cli-2.0.0a1.dist-info/entry_points.txt +2 -0
  136. rai_cli-2.0.0a1.dist-info/licenses/LICENSE +190 -0
  137. rai_cli-2.0.0a1.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,185 @@
1
+ """Skill scaffolding for creating new skills.
2
+
3
+ Generates new skill directories with properly structured SKILL.md files
4
+ following the RaiSE skill template.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from textwrap import dedent
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+ from rai_cli.skills.locator import get_default_skill_dir
14
+
15
+ # Mapping from domain prefix to lifecycle
16
+ DOMAIN_TO_LIFECYCLE = {
17
+ "session": "session",
18
+ "epic": "epic",
19
+ "story": "story",
20
+ "discover": "discovery",
21
+ "skill": "meta",
22
+ "research": "utility",
23
+ "debug": "utility",
24
+ "framework": "meta",
25
+ }
26
+
27
+
28
+ class ScaffoldResult(BaseModel):
29
+ """Result of scaffolding a skill."""
30
+
31
+ created: bool = Field(description="Whether the skill was created")
32
+ path: str | None = Field(default=None, description="Path to created skill")
33
+ error: str | None = Field(default=None, description="Error message if failed")
34
+
35
+
36
+ def _infer_lifecycle(name: str) -> str:
37
+ """Infer lifecycle from skill name domain."""
38
+ parts = name.split("-")
39
+ if parts:
40
+ domain = parts[0]
41
+ return DOMAIN_TO_LIFECYCLE.get(domain, "utility")
42
+ return "utility"
43
+
44
+
45
+ def _generate_skill_content(
46
+ name: str,
47
+ lifecycle: str,
48
+ prerequisites: str | None,
49
+ next_skill: str | None,
50
+ ) -> str:
51
+ """Generate SKILL.md content from template."""
52
+ # Extract title from name (e.g., story-validate -> Story Validate)
53
+ title = " ".join(word.capitalize() for word in name.split("-"))
54
+
55
+ # Build prerequisites and next strings
56
+ prereq_str = prerequisites or ""
57
+ next_str = next_skill or ""
58
+
59
+ # Determine frequency based on lifecycle
60
+ frequency_map = {
61
+ "session": "per-session",
62
+ "epic": "per-epic",
63
+ "story": "per-story",
64
+ "discovery": "per-project",
65
+ "utility": "on-demand",
66
+ "meta": "on-demand",
67
+ }
68
+ frequency = frequency_map.get(lifecycle, "on-demand")
69
+
70
+ content = dedent(f"""\
71
+ ---
72
+ name: {name}
73
+ description: >
74
+ [TODO: Add description of what this skill does]
75
+
76
+ license: MIT
77
+
78
+ metadata:
79
+ raise.work_cycle: {lifecycle}
80
+ raise.frequency: {frequency}
81
+ raise.fase: ""
82
+ raise.prerequisites: "{prereq_str}"
83
+ raise.next: "{next_str}"
84
+ raise.gate: ""
85
+ raise.adaptable: "true"
86
+ raise.version: "1.0.0"
87
+ ---
88
+
89
+ # {title}
90
+
91
+ ## Purpose
92
+
93
+ [TODO: Describe the purpose of this skill]
94
+
95
+ ## Context
96
+
97
+ **When to use:**
98
+ - [TODO: Add trigger conditions]
99
+
100
+ **When to skip:**
101
+ - [TODO: Add skip conditions]
102
+
103
+ **Inputs required:**
104
+ - [TODO: Add required inputs]
105
+
106
+ **Output:**
107
+ - [TODO: Add expected outputs]
108
+
109
+ ## Steps
110
+
111
+ ### Step 1: [TODO: Step Name]
112
+
113
+ [TODO: Describe what to do in this step]
114
+
115
+ ```bash
116
+ # Example command
117
+ ```
118
+
119
+ **Verification:** [TODO: How to verify this step succeeded]
120
+
121
+ ## Output
122
+
123
+ | Item | Destination |
124
+ |------|-------------|
125
+ | [TODO] | [TODO] |
126
+
127
+ ## Notes
128
+
129
+ [TODO: Add any additional notes]
130
+
131
+ ## References
132
+
133
+ - Previous: `/{prereq_str if prereq_str else "[none]"}`
134
+ - Next: `/{next_str if next_str else "[none]"}`
135
+ """)
136
+
137
+ return content
138
+
139
+
140
+ def scaffold_skill(
141
+ name: str,
142
+ lifecycle: str | None = None,
143
+ after: str | None = None,
144
+ before: str | None = None,
145
+ ) -> ScaffoldResult:
146
+ """Scaffold a new skill with proper structure.
147
+
148
+ Args:
149
+ name: Skill name (e.g., 'story-validate').
150
+ lifecycle: Lifecycle category. If not specified, inferred from name.
151
+ after: Skill that should come before this one (prerequisites).
152
+ before: Skill that should come after this one (next).
153
+
154
+ Returns:
155
+ ScaffoldResult with creation status and path or error.
156
+ """
157
+ # Get or create skill directory
158
+ skill_dir = get_default_skill_dir()
159
+ if not skill_dir.exists():
160
+ skill_dir.mkdir(parents=True)
161
+
162
+ # Check if skill already exists
163
+ skill_path = skill_dir / name
164
+ if skill_path.exists():
165
+ return ScaffoldResult(
166
+ created=False,
167
+ error=f"Skill '{name}' already exists at {skill_path}",
168
+ )
169
+
170
+ # Infer lifecycle if not specified
171
+ if lifecycle is None:
172
+ lifecycle = _infer_lifecycle(name)
173
+
174
+ # Generate content
175
+ content = _generate_skill_content(name, lifecycle, after, before)
176
+
177
+ # Create skill directory and file
178
+ skill_path.mkdir(parents=True, exist_ok=True)
179
+ skill_file = skill_path / "SKILL.md"
180
+ skill_file.write_text(content)
181
+
182
+ return ScaffoldResult(
183
+ created=True,
184
+ path=str(skill_file),
185
+ )
@@ -0,0 +1,130 @@
1
+ """Pydantic models for SKILL.md frontmatter and structure.
2
+
3
+ These models define the schema for RaiSE skills, enabling:
4
+ - Parsing of SKILL.md YAML frontmatter
5
+ - Validation of skill structure
6
+ - Type-safe access to skill metadata
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from pydantic import BaseModel, Field
14
+
15
+
16
+ class SkillMetadata(BaseModel):
17
+ """Metadata for a RaiSE skill.
18
+
19
+ Maps from YAML frontmatter with 'raise.' prefix to clean attributes.
20
+ """
21
+
22
+ work_cycle: str = Field(
23
+ description="Lifecycle: session, epic, story, discovery, utility, meta"
24
+ )
25
+ version: str = Field(description="Semantic version of the skill")
26
+ frequency: str | None = Field(
27
+ default=None, description="How often invoked: per-session, per-epic, etc."
28
+ )
29
+ fase: str | None = Field(default=None, description="Phase number or 'meta'")
30
+ prerequisites: str | None = Field(
31
+ default=None, description="Skills that must run before this one"
32
+ )
33
+ next: str | None = Field(
34
+ default=None, description="Skill that typically follows this one"
35
+ )
36
+ gate: str | None = Field(default=None, description="Validation gate for this skill")
37
+ adaptable: bool = Field(
38
+ default=True, description="Whether skill can be adapted by mastery level"
39
+ )
40
+
41
+ @classmethod
42
+ def from_raw(cls, raw: dict[str, Any]) -> SkillMetadata:
43
+ """Parse metadata from raw YAML dict with 'raise.' prefix.
44
+
45
+ Args:
46
+ raw: Dictionary with keys like 'raise.work_cycle', 'raise.version', etc.
47
+
48
+ Returns:
49
+ SkillMetadata instance with cleaned attributes.
50
+ """
51
+ # Strip 'raise.' prefix and convert to clean dict
52
+ cleaned: dict[str, Any] = {}
53
+ for key, value in raw.items():
54
+ if key.startswith("raise."):
55
+ clean_key = key[6:] # Remove 'raise.' prefix
56
+ # Handle boolean conversion
57
+ if clean_key == "adaptable":
58
+ if isinstance(value, str):
59
+ value = value.lower() == "true"
60
+ cleaned[clean_key] = value
61
+
62
+ return cls(**cleaned)
63
+
64
+
65
+ class SkillHookCommand(BaseModel):
66
+ """A single hook command in a skill."""
67
+
68
+ type: str = Field(description="Hook type: 'command'")
69
+ command: str = Field(description="Shell command to execute")
70
+
71
+
72
+ class SkillHook(BaseModel):
73
+ """A hook configuration with nested commands."""
74
+
75
+ hooks: list[SkillHookCommand] = Field(
76
+ default_factory=lambda: [], description="List of hook commands"
77
+ )
78
+
79
+
80
+ class SkillFrontmatter(BaseModel):
81
+ """YAML frontmatter for a SKILL.md file.
82
+
83
+ This is the structured data at the top of each skill file,
84
+ containing name, description, metadata, and hooks.
85
+ """
86
+
87
+ name: str = Field(description="Skill name in {domain}-{action} format")
88
+ description: str = Field(description="Brief description of the skill")
89
+ license: str | None = Field(default=None, description="License (typically MIT)")
90
+ metadata: SkillMetadata | None = Field(
91
+ default=None, description="RaiSE-specific metadata"
92
+ )
93
+ hooks: dict[str, list[SkillHook]] | None = Field(
94
+ default=None, description="Claude Code hooks (e.g., Stop)"
95
+ )
96
+
97
+
98
+ class Skill(BaseModel):
99
+ """A complete RaiSE skill with frontmatter and markdown body.
100
+
101
+ Represents a parsed SKILL.md file with all its components.
102
+ """
103
+
104
+ frontmatter: SkillFrontmatter = Field(description="Parsed YAML frontmatter")
105
+ body: str = Field(description="Markdown content after frontmatter")
106
+ path: str = Field(description="Path to the SKILL.md file")
107
+
108
+ @property
109
+ def name(self) -> str:
110
+ """Shortcut to skill name."""
111
+ return self.frontmatter.name
112
+
113
+ @property
114
+ def version(self) -> str | None:
115
+ """Skill version from metadata, or None if no metadata."""
116
+ if self.frontmatter.metadata:
117
+ return self.frontmatter.metadata.version
118
+ return None
119
+
120
+ @property
121
+ def lifecycle(self) -> str | None:
122
+ """Skill lifecycle from metadata work_cycle."""
123
+ if self.frontmatter.metadata:
124
+ return self.frontmatter.metadata.work_cycle
125
+ return None
126
+
127
+ @property
128
+ def description(self) -> str:
129
+ """Shortcut to skill description."""
130
+ return self.frontmatter.description
@@ -0,0 +1,172 @@
1
+ """Validator for SKILL.md files.
2
+
3
+ Validates skill structure against RaiSE schema including:
4
+ - Required fields (name, description, metadata)
5
+ - Required sections (Purpose, Context, Steps, Output)
6
+ - Naming conventions ({domain}-{action})
7
+ - Hook paths (warns if script not found)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from enum import Enum
14
+ from pathlib import Path
15
+
16
+ from pydantic import BaseModel, Field
17
+
18
+ from rai_cli.skills.parser import ParseError, parse_skill
19
+ from rai_cli.skills.schema import Skill
20
+
21
+
22
+ class ValidationSeverity(Enum):
23
+ """Severity level for validation issues."""
24
+
25
+ ERROR = "error"
26
+ WARNING = "warning"
27
+
28
+
29
+ class ValidationResult(BaseModel):
30
+ """Result of validating a skill file."""
31
+
32
+ path: str = Field(description="Path to the validated file")
33
+ errors: list[str] = Field(
34
+ default_factory=lambda: [], description="Validation errors"
35
+ )
36
+ warnings: list[str] = Field(
37
+ default_factory=lambda: [], description="Validation warnings"
38
+ )
39
+
40
+ @property
41
+ def is_valid(self) -> bool:
42
+ """Skill is valid if there are no errors (warnings OK)."""
43
+ return len(self.errors) == 0
44
+
45
+ @property
46
+ def error_count(self) -> int:
47
+ """Number of errors."""
48
+ return len(self.errors)
49
+
50
+ @property
51
+ def warning_count(self) -> int:
52
+ """Number of warnings."""
53
+ return len(self.warnings)
54
+
55
+
56
+ # Required sections in skill body (case-insensitive match)
57
+ REQUIRED_SECTIONS = ["Purpose", "Context", "Steps", "Output"]
58
+
59
+ # Pattern for {domain}-{action} naming convention
60
+ NAMING_PATTERN = re.compile(r"^[a-z]+-[a-z]+(-[a-z]+)*$")
61
+
62
+
63
+ def _validate_required_fields(skill: Skill, errors: list[str]) -> None:
64
+ """Check required frontmatter fields."""
65
+ if not skill.frontmatter.name:
66
+ errors.append("Missing required field: name")
67
+
68
+ if not skill.frontmatter.description:
69
+ errors.append("Missing required field: description")
70
+
71
+ if not skill.frontmatter.metadata:
72
+ errors.append("Missing required field: metadata")
73
+
74
+
75
+ def _validate_required_sections(skill: Skill, errors: list[str]) -> None:
76
+ """Check required sections in body."""
77
+ body_lower = skill.body.lower()
78
+
79
+ for section in REQUIRED_SECTIONS:
80
+ # Look for ## Section (case-insensitive)
81
+ pattern = f"## {section.lower()}"
82
+ if pattern not in body_lower:
83
+ errors.append(f"Missing required section: {section}")
84
+
85
+
86
+ def _validate_naming_convention(skill: Skill, warnings: list[str]) -> None:
87
+ """Check naming follows {domain}-{action} pattern."""
88
+ name = skill.frontmatter.name
89
+ if name and not NAMING_PATTERN.match(name):
90
+ warnings.append(
91
+ f"Name '{name}' doesn't follow {{domain}}-{{action}} pattern (e.g., session-start)"
92
+ )
93
+
94
+
95
+ def _validate_hook_paths(skill: Skill, warnings: list[str]) -> None:
96
+ """Check that hook script paths exist (warning only)."""
97
+ if not skill.frontmatter.hooks:
98
+ return
99
+
100
+ for hook_name, hook_list in skill.frontmatter.hooks.items():
101
+ for hook in hook_list:
102
+ for cmd in hook.hooks:
103
+ # Extract script path from command
104
+ # Handles: "RAISE_SKILL_NAME=x \"$CLAUDE_PROJECT_DIR\"/.raise/scripts/script.sh"
105
+ # Also handles: "/absolute/path/script.sh"
106
+ command = cmd.command
107
+
108
+ # Skip variable-based paths (we can't resolve them)
109
+ if "$" in command:
110
+ continue
111
+
112
+ # Check if command looks like a path
113
+ if command.startswith("/"):
114
+ path = Path(command.split()[0]) # Get first word (the path)
115
+ if not path.exists():
116
+ warnings.append(f"Hook '{hook_name}' script not found: {path}")
117
+
118
+
119
+ def validate_skill(skill: Skill) -> ValidationResult:
120
+ """Validate a parsed Skill object.
121
+
122
+ Args:
123
+ skill: Parsed Skill object.
124
+
125
+ Returns:
126
+ ValidationResult with errors and warnings.
127
+ """
128
+ errors: list[str] = []
129
+ warnings: list[str] = []
130
+
131
+ _validate_required_fields(skill, errors)
132
+ _validate_required_sections(skill, errors)
133
+ _validate_naming_convention(skill, warnings)
134
+ _validate_hook_paths(skill, warnings)
135
+
136
+ return ValidationResult(
137
+ path=skill.path,
138
+ errors=errors,
139
+ warnings=warnings,
140
+ )
141
+
142
+
143
+ def validate_skill_file(path: str | Path) -> ValidationResult:
144
+ """Validate a SKILL.md file.
145
+
146
+ Args:
147
+ path: Path to the SKILL.md file.
148
+
149
+ Returns:
150
+ ValidationResult with errors and warnings.
151
+ """
152
+ path = Path(path)
153
+ str_path = str(path)
154
+
155
+ # Check file exists
156
+ if not path.exists():
157
+ return ValidationResult(
158
+ path=str_path,
159
+ errors=[f"File not found: {path}"],
160
+ )
161
+
162
+ # Try to parse
163
+ try:
164
+ skill = parse_skill(path)
165
+ except ParseError as e:
166
+ return ValidationResult(
167
+ path=str_path,
168
+ errors=[f"Parse error: {e}"],
169
+ )
170
+
171
+ # Validate the parsed skill
172
+ return validate_skill(skill)
@@ -0,0 +1,59 @@
1
+ """Base skills package for distribution.
2
+
3
+ This package contains the RaiSE skills that ship with rai-cli.
4
+ On `rai init`, skill files are copied to the project's
5
+ `.claude/skills/` directory (Claude Code) or equivalent IDE location.
6
+
7
+ All skills use the `rai-` namespace prefix to prevent collision
8
+ with user-created or third-party skills.
9
+
10
+ Contents:
11
+ Session lifecycle: rai-session-start, rai-session-close
12
+ Story lifecycle: rai-story-start, rai-story-plan, rai-story-design,
13
+ rai-story-implement, rai-story-review, rai-story-close
14
+ Epic lifecycle: rai-epic-start, rai-epic-plan, rai-epic-design, rai-epic-close
15
+ Discovery: rai-discover-start, rai-discover-scan,
16
+ rai-discover-validate, rai-discover-document
17
+ Onboarding: rai-project-create, rai-project-onboard
18
+ Tools: rai-research, rai-debug
19
+
20
+ Usage:
21
+ from importlib.resources import files
22
+
23
+ base_skills = files("rai_cli.skills_base")
24
+ session_start = base_skills / "rai-session-start" / "SKILL.md"
25
+ content = session_start.read_text()
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ __version__ = "2.1.0"
31
+
32
+ DISTRIBUTABLE_SKILLS: list[str] = [
33
+ # Session lifecycle
34
+ "rai-session-start",
35
+ "rai-session-close",
36
+ # Story lifecycle
37
+ "rai-story-start",
38
+ "rai-story-plan",
39
+ "rai-story-design",
40
+ "rai-story-implement",
41
+ "rai-story-review",
42
+ "rai-story-close",
43
+ # Epic lifecycle
44
+ "rai-epic-start",
45
+ "rai-epic-plan",
46
+ "rai-epic-design",
47
+ "rai-epic-close",
48
+ # Discovery
49
+ "rai-discover-start",
50
+ "rai-discover-scan",
51
+ "rai-discover-validate",
52
+ "rai-discover-document",
53
+ # Onboarding
54
+ "rai-project-create",
55
+ "rai-project-onboard",
56
+ # Tools
57
+ "rai-research",
58
+ "rai-debug",
59
+ ]