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.
- rai_cli/__init__.py +38 -0
- rai_cli/__main__.py +30 -0
- rai_cli/cli/__init__.py +3 -0
- rai_cli/cli/commands/__init__.py +3 -0
- rai_cli/cli/commands/base.py +101 -0
- rai_cli/cli/commands/discover.py +547 -0
- rai_cli/cli/commands/init.py +460 -0
- rai_cli/cli/commands/memory.py +1626 -0
- rai_cli/cli/commands/profile.py +51 -0
- rai_cli/cli/commands/session.py +264 -0
- rai_cli/cli/commands/skill.py +226 -0
- rai_cli/cli/error_handler.py +158 -0
- rai_cli/cli/main.py +137 -0
- rai_cli/config/__init__.py +11 -0
- rai_cli/config/paths.py +309 -0
- rai_cli/config/settings.py +180 -0
- rai_cli/context/__init__.py +42 -0
- rai_cli/context/analyzers/__init__.py +16 -0
- rai_cli/context/analyzers/models.py +36 -0
- rai_cli/context/analyzers/protocol.py +43 -0
- rai_cli/context/analyzers/python.py +291 -0
- rai_cli/context/builder.py +1566 -0
- rai_cli/context/diff.py +213 -0
- rai_cli/context/extractors/__init__.py +13 -0
- rai_cli/context/extractors/skills.py +121 -0
- rai_cli/context/graph.py +300 -0
- rai_cli/context/models.py +134 -0
- rai_cli/context/query.py +507 -0
- rai_cli/core/__init__.py +37 -0
- rai_cli/core/files.py +66 -0
- rai_cli/core/text.py +174 -0
- rai_cli/core/tools.py +441 -0
- rai_cli/discovery/__init__.py +50 -0
- rai_cli/discovery/analyzer.py +601 -0
- rai_cli/discovery/drift.py +355 -0
- rai_cli/discovery/scanner.py +1200 -0
- rai_cli/engines/__init__.py +3 -0
- rai_cli/exceptions.py +200 -0
- rai_cli/governance/__init__.py +11 -0
- rai_cli/governance/extractor.py +311 -0
- rai_cli/governance/models.py +132 -0
- rai_cli/governance/parsers/__init__.py +35 -0
- rai_cli/governance/parsers/adr.py +255 -0
- rai_cli/governance/parsers/backlog.py +302 -0
- rai_cli/governance/parsers/constitution.py +100 -0
- rai_cli/governance/parsers/epic.py +299 -0
- rai_cli/governance/parsers/glossary.py +297 -0
- rai_cli/governance/parsers/guardrails.py +326 -0
- rai_cli/governance/parsers/prd.py +93 -0
- rai_cli/governance/parsers/vision.py +97 -0
- rai_cli/handlers/__init__.py +3 -0
- rai_cli/memory/__init__.py +58 -0
- rai_cli/memory/loader.py +247 -0
- rai_cli/memory/migration.py +247 -0
- rai_cli/memory/models.py +169 -0
- rai_cli/memory/writer.py +485 -0
- rai_cli/onboarding/__init__.py +96 -0
- rai_cli/onboarding/bootstrap.py +164 -0
- rai_cli/onboarding/claudemd.py +209 -0
- rai_cli/onboarding/conventions.py +742 -0
- rai_cli/onboarding/detection.py +155 -0
- rai_cli/onboarding/governance.py +443 -0
- rai_cli/onboarding/manifest.py +101 -0
- rai_cli/onboarding/memory_md.py +387 -0
- rai_cli/onboarding/migration.py +207 -0
- rai_cli/onboarding/profile.py +457 -0
- rai_cli/onboarding/skills.py +114 -0
- rai_cli/output/__init__.py +28 -0
- rai_cli/output/console.py +394 -0
- rai_cli/output/formatters/__init__.py +9 -0
- rai_cli/output/formatters/discover.py +442 -0
- rai_cli/output/formatters/skill.py +293 -0
- rai_cli/rai_base/__init__.py +22 -0
- rai_cli/rai_base/framework/__init__.py +7 -0
- rai_cli/rai_base/framework/methodology.yaml +235 -0
- rai_cli/rai_base/governance/__init__.py +1 -0
- rai_cli/rai_base/governance/architecture/__init__.py +1 -0
- rai_cli/rai_base/governance/architecture/domain-model.md +20 -0
- rai_cli/rai_base/governance/architecture/system-context.md +34 -0
- rai_cli/rai_base/governance/architecture/system-design.md +24 -0
- rai_cli/rai_base/governance/backlog.md +8 -0
- rai_cli/rai_base/governance/guardrails.md +18 -0
- rai_cli/rai_base/governance/prd.md +25 -0
- rai_cli/rai_base/governance/vision.md +16 -0
- rai_cli/rai_base/identity/__init__.py +8 -0
- rai_cli/rai_base/identity/core.md +119 -0
- rai_cli/rai_base/identity/perspective.md +119 -0
- rai_cli/rai_base/memory/__init__.py +7 -0
- rai_cli/rai_base/memory/patterns-base.jsonl +20 -0
- rai_cli/schemas/__init__.py +3 -0
- rai_cli/schemas/session_state.py +106 -0
- rai_cli/session/__init__.py +5 -0
- rai_cli/session/bundle.py +389 -0
- rai_cli/session/close.py +255 -0
- rai_cli/session/state.py +108 -0
- rai_cli/skills/__init__.py +44 -0
- rai_cli/skills/locator.py +129 -0
- rai_cli/skills/name_checker.py +203 -0
- rai_cli/skills/parser.py +145 -0
- rai_cli/skills/scaffold.py +185 -0
- rai_cli/skills/schema.py +130 -0
- rai_cli/skills/validator.py +172 -0
- rai_cli/skills_base/__init__.py +59 -0
- rai_cli/skills_base/rai-debug/SKILL.md +296 -0
- rai_cli/skills_base/rai-discover-document/SKILL.md +292 -0
- rai_cli/skills_base/rai-discover-scan/SKILL.md +325 -0
- rai_cli/skills_base/rai-discover-start/SKILL.md +213 -0
- rai_cli/skills_base/rai-discover-validate/SKILL.md +310 -0
- rai_cli/skills_base/rai-epic-close/SKILL.md +369 -0
- rai_cli/skills_base/rai-epic-design/SKILL.md +622 -0
- rai_cli/skills_base/rai-epic-plan/SKILL.md +672 -0
- rai_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
- rai_cli/skills_base/rai-epic-start/SKILL.md +217 -0
- rai_cli/skills_base/rai-project-create/SKILL.md +455 -0
- rai_cli/skills_base/rai-project-onboard/SKILL.md +503 -0
- rai_cli/skills_base/rai-research/SKILL.md +264 -0
- rai_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
- rai_cli/skills_base/rai-session-close/SKILL.md +151 -0
- rai_cli/skills_base/rai-session-start/SKILL.md +110 -0
- rai_cli/skills_base/rai-story-close/SKILL.md +367 -0
- rai_cli/skills_base/rai-story-design/SKILL.md +339 -0
- rai_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
- rai_cli/skills_base/rai-story-implement/SKILL.md +256 -0
- rai_cli/skills_base/rai-story-plan/SKILL.md +307 -0
- rai_cli/skills_base/rai-story-review/SKILL.md +276 -0
- rai_cli/skills_base/rai-story-start/SKILL.md +288 -0
- rai_cli/telemetry/__init__.py +42 -0
- rai_cli/telemetry/schemas.py +285 -0
- rai_cli/telemetry/writer.py +210 -0
- rai_cli/viz/__init__.py +7 -0
- rai_cli/viz/generator.py +404 -0
- rai_cli-2.0.0a1.dist-info/METADATA +289 -0
- rai_cli-2.0.0a1.dist-info/RECORD +137 -0
- rai_cli-2.0.0a1.dist-info/WHEEL +4 -0
- rai_cli-2.0.0a1.dist-info/entry_points.txt +2 -0
- rai_cli-2.0.0a1.dist-info/licenses/LICENSE +190 -0
- 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
|
+
)
|
rai_cli/skills/schema.py
ADDED
|
@@ -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
|
+
]
|