raise-cli 2.2.1__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 (264) hide show
  1. raise_cli/__init__.py +38 -0
  2. raise_cli/__main__.py +30 -0
  3. raise_cli/adapters/__init__.py +91 -0
  4. raise_cli/adapters/declarative/__init__.py +26 -0
  5. raise_cli/adapters/declarative/adapter.py +267 -0
  6. raise_cli/adapters/declarative/discovery.py +94 -0
  7. raise_cli/adapters/declarative/expressions.py +150 -0
  8. raise_cli/adapters/declarative/reference/__init__.py +1 -0
  9. raise_cli/adapters/declarative/reference/github.yaml +143 -0
  10. raise_cli/adapters/declarative/schema.py +98 -0
  11. raise_cli/adapters/filesystem.py +299 -0
  12. raise_cli/adapters/mcp_bridge.py +10 -0
  13. raise_cli/adapters/mcp_confluence.py +246 -0
  14. raise_cli/adapters/mcp_jira.py +405 -0
  15. raise_cli/adapters/models.py +205 -0
  16. raise_cli/adapters/protocols.py +180 -0
  17. raise_cli/adapters/registry.py +90 -0
  18. raise_cli/adapters/sync.py +149 -0
  19. raise_cli/agents/__init__.py +14 -0
  20. raise_cli/agents/antigravity.yaml +8 -0
  21. raise_cli/agents/claude.yaml +8 -0
  22. raise_cli/agents/copilot.yaml +8 -0
  23. raise_cli/agents/copilot_plugin.py +124 -0
  24. raise_cli/agents/cursor.yaml +7 -0
  25. raise_cli/agents/roo.yaml +8 -0
  26. raise_cli/agents/windsurf.yaml +8 -0
  27. raise_cli/artifacts/__init__.py +30 -0
  28. raise_cli/artifacts/models.py +43 -0
  29. raise_cli/artifacts/reader.py +55 -0
  30. raise_cli/artifacts/renderer.py +104 -0
  31. raise_cli/artifacts/story_design.py +69 -0
  32. raise_cli/artifacts/writer.py +45 -0
  33. raise_cli/backlog/__init__.py +1 -0
  34. raise_cli/backlog/sync.py +115 -0
  35. raise_cli/cli/__init__.py +3 -0
  36. raise_cli/cli/commands/__init__.py +3 -0
  37. raise_cli/cli/commands/_resolve.py +153 -0
  38. raise_cli/cli/commands/adapters.py +362 -0
  39. raise_cli/cli/commands/artifact.py +137 -0
  40. raise_cli/cli/commands/backlog.py +333 -0
  41. raise_cli/cli/commands/base.py +31 -0
  42. raise_cli/cli/commands/discover.py +551 -0
  43. raise_cli/cli/commands/docs.py +130 -0
  44. raise_cli/cli/commands/doctor.py +177 -0
  45. raise_cli/cli/commands/gate.py +223 -0
  46. raise_cli/cli/commands/graph.py +1086 -0
  47. raise_cli/cli/commands/info.py +81 -0
  48. raise_cli/cli/commands/init.py +746 -0
  49. raise_cli/cli/commands/journal.py +167 -0
  50. raise_cli/cli/commands/mcp.py +524 -0
  51. raise_cli/cli/commands/memory.py +467 -0
  52. raise_cli/cli/commands/pattern.py +348 -0
  53. raise_cli/cli/commands/profile.py +59 -0
  54. raise_cli/cli/commands/publish.py +80 -0
  55. raise_cli/cli/commands/release.py +338 -0
  56. raise_cli/cli/commands/session.py +528 -0
  57. raise_cli/cli/commands/signal.py +410 -0
  58. raise_cli/cli/commands/skill.py +350 -0
  59. raise_cli/cli/commands/skill_set.py +145 -0
  60. raise_cli/cli/error_handler.py +158 -0
  61. raise_cli/cli/main.py +163 -0
  62. raise_cli/compat.py +66 -0
  63. raise_cli/config/__init__.py +41 -0
  64. raise_cli/config/agent_plugin.py +105 -0
  65. raise_cli/config/agent_registry.py +233 -0
  66. raise_cli/config/agents.py +120 -0
  67. raise_cli/config/ide.py +32 -0
  68. raise_cli/config/paths.py +379 -0
  69. raise_cli/config/settings.py +180 -0
  70. raise_cli/context/__init__.py +42 -0
  71. raise_cli/context/analyzers/__init__.py +16 -0
  72. raise_cli/context/analyzers/models.py +36 -0
  73. raise_cli/context/analyzers/protocol.py +43 -0
  74. raise_cli/context/analyzers/python.py +292 -0
  75. raise_cli/context/builder.py +1569 -0
  76. raise_cli/context/diff.py +213 -0
  77. raise_cli/context/extractors/__init__.py +13 -0
  78. raise_cli/context/extractors/skills.py +121 -0
  79. raise_cli/core/__init__.py +37 -0
  80. raise_cli/core/files.py +66 -0
  81. raise_cli/core/text.py +174 -0
  82. raise_cli/core/tools.py +441 -0
  83. raise_cli/discovery/__init__.py +50 -0
  84. raise_cli/discovery/analyzer.py +691 -0
  85. raise_cli/discovery/drift.py +355 -0
  86. raise_cli/discovery/scanner.py +1687 -0
  87. raise_cli/doctor/__init__.py +4 -0
  88. raise_cli/doctor/checks/__init__.py +1 -0
  89. raise_cli/doctor/checks/environment.py +110 -0
  90. raise_cli/doctor/checks/project.py +238 -0
  91. raise_cli/doctor/fix.py +80 -0
  92. raise_cli/doctor/models.py +56 -0
  93. raise_cli/doctor/protocol.py +43 -0
  94. raise_cli/doctor/registry.py +100 -0
  95. raise_cli/doctor/report.py +141 -0
  96. raise_cli/doctor/runner.py +95 -0
  97. raise_cli/engines/__init__.py +3 -0
  98. raise_cli/exceptions.py +215 -0
  99. raise_cli/gates/__init__.py +19 -0
  100. raise_cli/gates/builtin/__init__.py +1 -0
  101. raise_cli/gates/builtin/coverage.py +52 -0
  102. raise_cli/gates/builtin/lint.py +48 -0
  103. raise_cli/gates/builtin/tests.py +48 -0
  104. raise_cli/gates/builtin/types.py +48 -0
  105. raise_cli/gates/models.py +40 -0
  106. raise_cli/gates/protocol.py +41 -0
  107. raise_cli/gates/registry.py +141 -0
  108. raise_cli/governance/__init__.py +11 -0
  109. raise_cli/governance/extractor.py +412 -0
  110. raise_cli/governance/models.py +134 -0
  111. raise_cli/governance/parsers/__init__.py +35 -0
  112. raise_cli/governance/parsers/_convert.py +38 -0
  113. raise_cli/governance/parsers/adr.py +274 -0
  114. raise_cli/governance/parsers/backlog.py +356 -0
  115. raise_cli/governance/parsers/constitution.py +119 -0
  116. raise_cli/governance/parsers/epic.py +323 -0
  117. raise_cli/governance/parsers/glossary.py +316 -0
  118. raise_cli/governance/parsers/guardrails.py +345 -0
  119. raise_cli/governance/parsers/prd.py +112 -0
  120. raise_cli/governance/parsers/roadmap.py +118 -0
  121. raise_cli/governance/parsers/vision.py +116 -0
  122. raise_cli/graph/__init__.py +1 -0
  123. raise_cli/graph/backends/__init__.py +57 -0
  124. raise_cli/graph/backends/api.py +137 -0
  125. raise_cli/graph/backends/dual.py +139 -0
  126. raise_cli/graph/backends/pending.py +84 -0
  127. raise_cli/handlers/__init__.py +3 -0
  128. raise_cli/hooks/__init__.py +54 -0
  129. raise_cli/hooks/builtin/__init__.py +1 -0
  130. raise_cli/hooks/builtin/backlog.py +216 -0
  131. raise_cli/hooks/builtin/gate_bridge.py +83 -0
  132. raise_cli/hooks/builtin/jira_sync.py +127 -0
  133. raise_cli/hooks/builtin/memory.py +117 -0
  134. raise_cli/hooks/builtin/telemetry.py +72 -0
  135. raise_cli/hooks/emitter.py +184 -0
  136. raise_cli/hooks/events.py +262 -0
  137. raise_cli/hooks/protocol.py +38 -0
  138. raise_cli/hooks/registry.py +117 -0
  139. raise_cli/mcp/__init__.py +33 -0
  140. raise_cli/mcp/bridge.py +218 -0
  141. raise_cli/mcp/models.py +43 -0
  142. raise_cli/mcp/registry.py +77 -0
  143. raise_cli/mcp/schema.py +41 -0
  144. raise_cli/memory/__init__.py +58 -0
  145. raise_cli/memory/loader.py +247 -0
  146. raise_cli/memory/migration.py +241 -0
  147. raise_cli/memory/models.py +169 -0
  148. raise_cli/memory/writer.py +598 -0
  149. raise_cli/onboarding/__init__.py +103 -0
  150. raise_cli/onboarding/bootstrap.py +324 -0
  151. raise_cli/onboarding/claudemd.py +17 -0
  152. raise_cli/onboarding/conventions.py +742 -0
  153. raise_cli/onboarding/detection.py +374 -0
  154. raise_cli/onboarding/governance.py +443 -0
  155. raise_cli/onboarding/instructions.py +672 -0
  156. raise_cli/onboarding/manifest.py +201 -0
  157. raise_cli/onboarding/memory_md.py +399 -0
  158. raise_cli/onboarding/migration.py +207 -0
  159. raise_cli/onboarding/profile.py +624 -0
  160. raise_cli/onboarding/skill_conflict.py +100 -0
  161. raise_cli/onboarding/skill_manifest.py +176 -0
  162. raise_cli/onboarding/skills.py +437 -0
  163. raise_cli/onboarding/workflows.py +101 -0
  164. raise_cli/output/__init__.py +28 -0
  165. raise_cli/output/console.py +394 -0
  166. raise_cli/output/formatters/__init__.py +9 -0
  167. raise_cli/output/formatters/adapters.py +135 -0
  168. raise_cli/output/formatters/discover.py +439 -0
  169. raise_cli/output/formatters/skill.py +298 -0
  170. raise_cli/publish/__init__.py +3 -0
  171. raise_cli/publish/changelog.py +80 -0
  172. raise_cli/publish/check.py +179 -0
  173. raise_cli/publish/version.py +172 -0
  174. raise_cli/rai_base/__init__.py +22 -0
  175. raise_cli/rai_base/framework/__init__.py +7 -0
  176. raise_cli/rai_base/framework/methodology.yaml +233 -0
  177. raise_cli/rai_base/governance/__init__.py +1 -0
  178. raise_cli/rai_base/governance/architecture/__init__.py +1 -0
  179. raise_cli/rai_base/governance/architecture/domain-model.md +20 -0
  180. raise_cli/rai_base/governance/architecture/system-context.md +34 -0
  181. raise_cli/rai_base/governance/architecture/system-design.md +24 -0
  182. raise_cli/rai_base/governance/backlog.md +8 -0
  183. raise_cli/rai_base/governance/guardrails.md +17 -0
  184. raise_cli/rai_base/governance/prd.md +25 -0
  185. raise_cli/rai_base/governance/vision.md +16 -0
  186. raise_cli/rai_base/identity/__init__.py +8 -0
  187. raise_cli/rai_base/identity/core.md +119 -0
  188. raise_cli/rai_base/identity/perspective.md +119 -0
  189. raise_cli/rai_base/memory/__init__.py +7 -0
  190. raise_cli/rai_base/memory/patterns-base.jsonl +55 -0
  191. raise_cli/schemas/__init__.py +3 -0
  192. raise_cli/schemas/journal.py +49 -0
  193. raise_cli/schemas/session_state.py +117 -0
  194. raise_cli/session/__init__.py +5 -0
  195. raise_cli/session/bundle.py +820 -0
  196. raise_cli/session/close.py +268 -0
  197. raise_cli/session/journal.py +119 -0
  198. raise_cli/session/resolver.py +126 -0
  199. raise_cli/session/state.py +187 -0
  200. raise_cli/skills/__init__.py +44 -0
  201. raise_cli/skills/locator.py +141 -0
  202. raise_cli/skills/name_checker.py +199 -0
  203. raise_cli/skills/parser.py +145 -0
  204. raise_cli/skills/scaffold.py +212 -0
  205. raise_cli/skills/schema.py +132 -0
  206. raise_cli/skills/skillsets.py +195 -0
  207. raise_cli/skills/validator.py +197 -0
  208. raise_cli/skills_base/__init__.py +80 -0
  209. raise_cli/skills_base/contract-template.md +60 -0
  210. raise_cli/skills_base/preamble.md +37 -0
  211. raise_cli/skills_base/rai-architecture-review/SKILL.md +137 -0
  212. raise_cli/skills_base/rai-debug/SKILL.md +171 -0
  213. raise_cli/skills_base/rai-discover/SKILL.md +167 -0
  214. raise_cli/skills_base/rai-discover-document/SKILL.md +128 -0
  215. raise_cli/skills_base/rai-discover-scan/SKILL.md +147 -0
  216. raise_cli/skills_base/rai-discover-start/SKILL.md +145 -0
  217. raise_cli/skills_base/rai-discover-validate/SKILL.md +142 -0
  218. raise_cli/skills_base/rai-docs-update/SKILL.md +142 -0
  219. raise_cli/skills_base/rai-doctor/SKILL.md +120 -0
  220. raise_cli/skills_base/rai-epic-close/SKILL.md +165 -0
  221. raise_cli/skills_base/rai-epic-close/templates/retrospective.md +68 -0
  222. raise_cli/skills_base/rai-epic-design/SKILL.md +146 -0
  223. raise_cli/skills_base/rai-epic-design/templates/design.md +24 -0
  224. raise_cli/skills_base/rai-epic-design/templates/scope.md +76 -0
  225. raise_cli/skills_base/rai-epic-plan/SKILL.md +153 -0
  226. raise_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
  227. raise_cli/skills_base/rai-epic-plan/templates/plan-section.md +49 -0
  228. raise_cli/skills_base/rai-epic-run/SKILL.md +208 -0
  229. raise_cli/skills_base/rai-epic-start/SKILL.md +136 -0
  230. raise_cli/skills_base/rai-epic-start/templates/brief.md +34 -0
  231. raise_cli/skills_base/rai-mcp-add/SKILL.md +176 -0
  232. raise_cli/skills_base/rai-mcp-remove/SKILL.md +120 -0
  233. raise_cli/skills_base/rai-mcp-status/SKILL.md +147 -0
  234. raise_cli/skills_base/rai-problem-shape/SKILL.md +138 -0
  235. raise_cli/skills_base/rai-project-create/SKILL.md +144 -0
  236. raise_cli/skills_base/rai-project-onboard/SKILL.md +162 -0
  237. raise_cli/skills_base/rai-quality-review/SKILL.md +189 -0
  238. raise_cli/skills_base/rai-research/SKILL.md +143 -0
  239. raise_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
  240. raise_cli/skills_base/rai-session-close/SKILL.md +176 -0
  241. raise_cli/skills_base/rai-session-start/SKILL.md +110 -0
  242. raise_cli/skills_base/rai-story-close/SKILL.md +198 -0
  243. raise_cli/skills_base/rai-story-design/SKILL.md +203 -0
  244. raise_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
  245. raise_cli/skills_base/rai-story-implement/SKILL.md +115 -0
  246. raise_cli/skills_base/rai-story-plan/SKILL.md +135 -0
  247. raise_cli/skills_base/rai-story-review/SKILL.md +178 -0
  248. raise_cli/skills_base/rai-story-run/SKILL.md +282 -0
  249. raise_cli/skills_base/rai-story-start/SKILL.md +166 -0
  250. raise_cli/skills_base/rai-story-start/templates/story.md +38 -0
  251. raise_cli/skills_base/rai-welcome/SKILL.md +134 -0
  252. raise_cli/telemetry/__init__.py +42 -0
  253. raise_cli/telemetry/schemas.py +285 -0
  254. raise_cli/telemetry/writer.py +217 -0
  255. raise_cli/tier/__init__.py +0 -0
  256. raise_cli/tier/context.py +134 -0
  257. raise_cli/viz/__init__.py +7 -0
  258. raise_cli/viz/generator.py +406 -0
  259. raise_cli-2.2.1.dist-info/METADATA +433 -0
  260. raise_cli-2.2.1.dist-info/RECORD +264 -0
  261. raise_cli-2.2.1.dist-info/WHEEL +4 -0
  262. raise_cli-2.2.1.dist-info/entry_points.txt +40 -0
  263. raise_cli-2.2.1.dist-info/licenses/LICENSE +190 -0
  264. raise_cli-2.2.1.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,44 @@
1
+ """Skills module for RaiSE skill management.
2
+
3
+ Skills are AI-executed process guides that leverage the RaiSE toolkit.
4
+ This module provides:
5
+ - Schema models for SKILL.md parsing
6
+ - Parser for extracting frontmatter and body
7
+ - Locator for finding skills in the codebase
8
+ - Validator for checking skill compliance
9
+ - Naming utilities for ontology compliance
10
+ - Scaffold for generating new skills
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from raise_cli.skills.locator import (
16
+ SkillLocator,
17
+ get_default_skill_dir,
18
+ list_skills,
19
+ )
20
+ from raise_cli.skills.parser import ParseError, parse_frontmatter, parse_skill
21
+ from raise_cli.skills.schema import (
22
+ Skill,
23
+ SkillFrontmatter,
24
+ SkillHook,
25
+ SkillHookCommand,
26
+ SkillMetadata,
27
+ )
28
+
29
+ __all__ = [
30
+ # Schema
31
+ "Skill",
32
+ "SkillFrontmatter",
33
+ "SkillHook",
34
+ "SkillHookCommand",
35
+ "SkillMetadata",
36
+ # Parser
37
+ "ParseError",
38
+ "parse_frontmatter",
39
+ "parse_skill",
40
+ # Locator
41
+ "SkillLocator",
42
+ "get_default_skill_dir",
43
+ "list_skills",
44
+ ]
@@ -0,0 +1,141 @@
1
+ """Skill locator for finding and loading skills.
2
+
3
+ Discovers skills in the IDE skill directory (e.g. .claude/skills/
4
+ for Claude Code, .agent/skills/ for Antigravity) and provides
5
+ methods for loading and organizing them.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections import defaultdict
11
+ from pathlib import Path
12
+
13
+ from raise_cli.config.agents import AgentConfig, get_agent_config
14
+ from raise_cli.skills.parser import parse_skill
15
+ from raise_cli.skills.schema import Skill
16
+
17
+
18
+ def get_default_skill_dir(
19
+ project_root: Path | None = None,
20
+ *,
21
+ agent_config: AgentConfig | None = None,
22
+ ) -> Path:
23
+ """Get the default skill directory path.
24
+
25
+ Args:
26
+ project_root: Project root directory. Defaults to current directory.
27
+ agent_config: Agent configuration. Defaults to Claude.
28
+
29
+ Returns:
30
+ Path to the agent's skill directory.
31
+ """
32
+ root = project_root or Path.cwd()
33
+ config = agent_config or get_agent_config()
34
+ skills_dir = config.skills_dir or ".claude/skills"
35
+ return root / skills_dir
36
+
37
+
38
+ class SkillLocator:
39
+ """Locates and loads skills from a skill directory.
40
+
41
+ Typical usage:
42
+ locator = SkillLocator()
43
+ skills = locator.load_all_skills()
44
+ grouped = locator.group_by_lifecycle(skills)
45
+ """
46
+
47
+ def __init__(self, skill_dir: Path | None = None) -> None:
48
+ """Initialize the locator.
49
+
50
+ Args:
51
+ skill_dir: Path to skill directory. Defaults to .claude/skills/.
52
+ """
53
+ self.skill_dir = skill_dir or get_default_skill_dir()
54
+
55
+ def find_skill_dirs(self) -> list[Path]:
56
+ """Find all skill directories containing SKILL.md.
57
+
58
+ Returns:
59
+ List of paths to skill directories, sorted by name.
60
+ """
61
+ if not self.skill_dir.exists():
62
+ return []
63
+
64
+ dirs: list[Path] = []
65
+ for item in self.skill_dir.iterdir():
66
+ if item.is_dir() and (item / "SKILL.md").exists():
67
+ dirs.append(item)
68
+
69
+ return sorted(dirs, key=lambda p: p.name)
70
+
71
+ def load_skill(self, name: str) -> Skill | None:
72
+ """Load a skill by name.
73
+
74
+ Args:
75
+ name: Skill name (directory name).
76
+
77
+ Returns:
78
+ Parsed Skill object, or None if not found.
79
+ """
80
+ skill_path = self.skill_dir / name / "SKILL.md"
81
+ if not skill_path.exists():
82
+ return None
83
+
84
+ return parse_skill(skill_path)
85
+
86
+ def load_all_skills(self) -> list[Skill]:
87
+ """Load all skills from the skill directory.
88
+
89
+ Returns:
90
+ List of parsed Skill objects, sorted by name.
91
+ """
92
+ skills: list[Skill] = []
93
+ for skill_dir in self.find_skill_dirs():
94
+ skill_path = skill_dir / "SKILL.md"
95
+ try:
96
+ skill = parse_skill(skill_path)
97
+ skills.append(skill)
98
+ except Exception:
99
+ # Skip skills that fail to parse
100
+ continue
101
+
102
+ return skills
103
+
104
+ def group_by_lifecycle(self, skills: list[Skill]) -> dict[str, list[Skill]]:
105
+ """Group skills by their lifecycle.
106
+
107
+ Args:
108
+ skills: List of skills to group.
109
+
110
+ Returns:
111
+ Dictionary mapping lifecycle name to list of skills.
112
+ """
113
+ grouped: dict[str, list[Skill]] = defaultdict(list)
114
+ for skill in skills:
115
+ lifecycle = skill.lifecycle or "unknown"
116
+ grouped[lifecycle].append(skill)
117
+
118
+ return dict(grouped)
119
+
120
+
121
+ def list_skills(
122
+ skill_dir: Path | None = None,
123
+ project_root: Path | None = None,
124
+ *,
125
+ agent_config: AgentConfig | None = None,
126
+ ) -> list[Skill]:
127
+ """Convenience function to list all skills.
128
+
129
+ Args:
130
+ skill_dir: Direct path to skill directory.
131
+ project_root: Project root (resolves via agent_config).
132
+ agent_config: Agent configuration. Defaults to Claude.
133
+
134
+ Returns:
135
+ List of parsed Skill objects.
136
+ """
137
+ if skill_dir is None and project_root is not None:
138
+ skill_dir = get_default_skill_dir(project_root, agent_config=agent_config)
139
+
140
+ locator = SkillLocator(skill_dir)
141
+ return locator.load_all_skills()
@@ -0,0 +1,199 @@
1
+ """Skill name checker for ontology compliance.
2
+
3
+ Checks proposed skill names against:
4
+ - {domain}-{action} naming pattern
5
+ - Existing skill conflicts
6
+ - CLI command conflicts (PAT-132)
7
+ - Known lifecycle domains
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+
14
+ from pydantic import BaseModel, Field
15
+
16
+ from raise_cli.skills.locator import SkillLocator, get_default_skill_dir
17
+
18
+ # Pattern for {domain}-{action} naming convention
19
+ NAMING_PATTERN = re.compile(r"^[a-z]+-[a-z]+(-[a-z]+)*$")
20
+
21
+ # RaiSE namespace prefix for distributed skills
22
+ RAI_PREFIX = "rai-"
23
+
24
+ # Known lifecycle domains
25
+ KNOWN_LIFECYCLES = {
26
+ "session",
27
+ "epic",
28
+ "story",
29
+ "discover",
30
+ "skill",
31
+ "research",
32
+ "debug",
33
+ "framework",
34
+ "project",
35
+ "docs",
36
+ }
37
+
38
+ # CLI commands that conflict with skill names (PAT-132)
39
+ # Format: {domain: [actions]} for {domain}-{action} pattern
40
+ CLI_COMMANDS = {
41
+ "memory": ["build", "query", "emit-pattern", "emit-calibration"],
42
+ "skill": ["list", "validate", "scaffold", "check-name"],
43
+ "profile": ["show", "update"],
44
+ "session": ["start", "close"],
45
+ "discover": ["scan", "drift", "status"],
46
+ "init": [], # init is a standalone command
47
+ }
48
+
49
+
50
+ class NameCheckResult(BaseModel):
51
+ """Result of checking a skill name."""
52
+
53
+ name: str = Field(description="The name that was checked")
54
+ valid_pattern: bool = Field(description="Whether name follows {domain}-{action}")
55
+ no_skill_conflict: bool = Field(
56
+ description="Whether no existing skill has this name"
57
+ )
58
+ no_cli_conflict: bool = Field(description="Whether no CLI command has this name")
59
+ known_lifecycle: bool = Field(description="Whether domain is a known lifecycle")
60
+ conflicting_skill: str | None = Field(
61
+ default=None, description="Name of conflicting skill if any"
62
+ )
63
+ conflicting_command: str | None = Field(
64
+ default=None, description="CLI command that conflicts if any"
65
+ )
66
+ suggestions: list[str] = Field(
67
+ default_factory=lambda: [], description="Positioning suggestions"
68
+ )
69
+
70
+ @property
71
+ def is_valid(self) -> bool:
72
+ """Name is valid if pattern OK, no skill conflict, and no CLI conflict."""
73
+ return self.valid_pattern and self.no_skill_conflict and self.no_cli_conflict
74
+
75
+
76
+ def _check_pattern(name: str) -> bool:
77
+ """Check if name follows {domain}-{action} pattern."""
78
+ return bool(NAMING_PATTERN.match(name))
79
+
80
+
81
+ def _check_skill_conflict(
82
+ name: str, existing_names: set[str]
83
+ ) -> tuple[bool, str | None]:
84
+ """Check for conflict with existing skills."""
85
+ if name in existing_names:
86
+ return False, name
87
+ return True, None
88
+
89
+
90
+ def _check_cli_conflict(name: str) -> tuple[bool, str | None]:
91
+ """Check for conflict with CLI commands."""
92
+ parts = name.split("-")
93
+ if len(parts) < 2:
94
+ return True, None
95
+
96
+ domain = parts[0]
97
+ action = "-".join(parts[1:]) # Handle multi-part actions like "emit-pattern"
98
+
99
+ # Check if this matches a CLI command
100
+ if domain in CLI_COMMANDS:
101
+ cli_actions = CLI_COMMANDS[domain]
102
+ if action in cli_actions:
103
+ return False, f"{domain} {action}"
104
+ # Also check if domain alone is a command (like "init")
105
+ if not cli_actions and domain == name:
106
+ return False, domain
107
+
108
+ return True, None
109
+
110
+
111
+ def _strip_rai_prefix(name: str) -> str:
112
+ """Strip rai- namespace prefix if present."""
113
+ if name.startswith(RAI_PREFIX):
114
+ return name[len(RAI_PREFIX) :]
115
+ return name
116
+
117
+
118
+ def _check_lifecycle(name: str) -> bool:
119
+ """Check if domain is a known lifecycle."""
120
+ unprefixed = _strip_rai_prefix(name)
121
+ parts = unprefixed.split("-")
122
+ if parts:
123
+ domain = parts[0]
124
+ return domain in KNOWN_LIFECYCLES
125
+ return False
126
+
127
+
128
+ def _get_suggestions(
129
+ name: str, existing_skills: list[str], known_lifecycle: bool
130
+ ) -> list[str]:
131
+ """Generate positioning suggestions."""
132
+ suggestions: list[str] = []
133
+
134
+ unprefixed = _strip_rai_prefix(name)
135
+ parts = unprefixed.split("-")
136
+ if len(parts) < 2:
137
+ return suggestions
138
+
139
+ domain = parts[0]
140
+
141
+ # Find related skills in same domain (check both prefixed and unprefixed)
142
+ related = sorted(
143
+ [s for s in existing_skills if _strip_rai_prefix(s).startswith(f"{domain}-")]
144
+ )
145
+
146
+ if related:
147
+ # Suggest positioning relative to existing skills
148
+ if known_lifecycle:
149
+ suggestions.append(f"Related {domain} skills: {', '.join(related)}")
150
+ elif known_lifecycle:
151
+ suggestions.append(f"First skill in '{domain}' lifecycle")
152
+
153
+ if not known_lifecycle:
154
+ suggestions.append(
155
+ f"Domain '{domain}' is not a standard lifecycle "
156
+ f"(known: {', '.join(sorted(KNOWN_LIFECYCLES))})"
157
+ )
158
+
159
+ return suggestions
160
+
161
+
162
+ def check_name(name: str) -> NameCheckResult:
163
+ """Check a proposed skill name against ontology patterns.
164
+
165
+ Args:
166
+ name: Proposed skill name.
167
+
168
+ Returns:
169
+ NameCheckResult with validation results and suggestions.
170
+ """
171
+ # Check pattern
172
+ valid_pattern = _check_pattern(name)
173
+
174
+ # Get existing skills
175
+ skill_dir = get_default_skill_dir()
176
+ locator = SkillLocator(skill_dir)
177
+ existing_skills = locator.find_skill_dirs()
178
+ existing_names = {s.name for s in existing_skills}
179
+
180
+ # Check conflicts
181
+ no_skill_conflict, conflicting_skill = _check_skill_conflict(name, existing_names)
182
+ no_cli_conflict, conflicting_command = _check_cli_conflict(name)
183
+
184
+ # Check lifecycle
185
+ known_lifecycle = _check_lifecycle(name)
186
+
187
+ # Generate suggestions
188
+ suggestions = _get_suggestions(name, sorted(existing_names), known_lifecycle)
189
+
190
+ return NameCheckResult(
191
+ name=name,
192
+ valid_pattern=valid_pattern,
193
+ no_skill_conflict=no_skill_conflict,
194
+ no_cli_conflict=no_cli_conflict,
195
+ known_lifecycle=known_lifecycle,
196
+ conflicting_skill=conflicting_skill,
197
+ conflicting_command=conflicting_command,
198
+ suggestions=suggestions,
199
+ )
@@ -0,0 +1,145 @@
1
+ """Parser for SKILL.md files.
2
+
3
+ Extracts YAML frontmatter and markdown body from skill files,
4
+ converting them into structured Skill objects.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from pathlib import Path
11
+ from typing import Any, cast
12
+
13
+ import yaml
14
+
15
+ from raise_cli.skills.schema import (
16
+ Skill,
17
+ SkillFrontmatter,
18
+ SkillHook,
19
+ SkillHookCommand,
20
+ SkillMetadata,
21
+ )
22
+
23
+
24
+ class ParseError(Exception):
25
+ """Error parsing a skill file."""
26
+
27
+ pass
28
+
29
+
30
+ # Regex to match YAML frontmatter (--- at start, content, --- to close)
31
+ # Handles empty frontmatter (---\n---) and content with or without trailing newline
32
+ FRONTMATTER_PATTERN = re.compile(
33
+ r"^---[ \t]*\n(.*?)^---[ \t]*\n?",
34
+ re.DOTALL | re.MULTILINE,
35
+ )
36
+
37
+
38
+ def parse_frontmatter(content: str) -> tuple[dict[str, Any], str]:
39
+ """Extract YAML frontmatter and body from markdown content.
40
+
41
+ Args:
42
+ content: Raw markdown content with YAML frontmatter.
43
+
44
+ Returns:
45
+ Tuple of (frontmatter dict, body string).
46
+
47
+ Raises:
48
+ ParseError: If frontmatter is missing, unclosed, or invalid YAML.
49
+ """
50
+ # Check if content starts with ---
51
+ if not content.startswith("---"):
52
+ raise ParseError("No YAML frontmatter found (must start with ---)")
53
+
54
+ # Find the closing ---
55
+ match = FRONTMATTER_PATTERN.match(content)
56
+ if not match:
57
+ # Check if there's an unclosed frontmatter
58
+ # Look for --- at start of a line (not just anywhere in content)
59
+ lines = content.split("\n")
60
+ closing_markers = sum(1 for line in lines[1:] if line.strip() == "---")
61
+ if closing_markers == 0:
62
+ raise ParseError("Unclosed frontmatter (missing closing ---)")
63
+ raise ParseError("No YAML frontmatter found")
64
+
65
+ yaml_content = match.group(1)
66
+ body = content[match.end() :]
67
+
68
+ # Parse YAML
69
+ frontmatter: dict[str, Any] = {}
70
+ try:
71
+ raw_yaml = yaml.safe_load(yaml_content)
72
+ if isinstance(raw_yaml, dict):
73
+ frontmatter = cast(dict[str, Any], raw_yaml)
74
+ except yaml.YAMLError as e:
75
+ raise ParseError(f"Invalid YAML in frontmatter: {e}") from e
76
+
77
+ return frontmatter, body
78
+
79
+
80
+ def _parse_hooks(raw_hooks: dict[str, Any] | None) -> dict[str, list[SkillHook]] | None:
81
+ """Parse hooks from raw frontmatter dict."""
82
+ if not raw_hooks:
83
+ return None
84
+
85
+ result: dict[str, list[SkillHook]] = {}
86
+ for hook_name, hook_list in raw_hooks.items():
87
+ parsed_hooks: list[SkillHook] = []
88
+ for hook_entry in hook_list:
89
+ commands: list[SkillHookCommand] = []
90
+ for cmd in hook_entry.get("hooks", []):
91
+ commands.append(
92
+ SkillHookCommand(
93
+ type=cmd.get("type", "command"),
94
+ command=cmd.get("command", ""),
95
+ )
96
+ )
97
+ parsed_hooks.append(SkillHook(hooks=commands))
98
+ result[hook_name] = parsed_hooks
99
+
100
+ return result
101
+
102
+
103
+ def _parse_frontmatter_to_model(raw: dict[str, Any]) -> SkillFrontmatter:
104
+ """Convert raw frontmatter dict to SkillFrontmatter model."""
105
+ metadata = None
106
+ if "metadata" in raw and raw["metadata"]:
107
+ metadata = SkillMetadata.from_raw(raw["metadata"])
108
+
109
+ hooks = _parse_hooks(raw.get("hooks"))
110
+
111
+ return SkillFrontmatter(
112
+ name=raw.get("name", ""),
113
+ description=raw.get("description", ""),
114
+ license=raw.get("license"),
115
+ metadata=metadata,
116
+ hooks=hooks,
117
+ )
118
+
119
+
120
+ def parse_skill(path: str | Path) -> Skill:
121
+ """Parse a SKILL.md file into a Skill object.
122
+
123
+ Args:
124
+ path: Path to the SKILL.md file.
125
+
126
+ Returns:
127
+ Parsed Skill object.
128
+
129
+ Raises:
130
+ ParseError: If file not found or parsing fails.
131
+ """
132
+ path = Path(path)
133
+
134
+ if not path.exists():
135
+ raise ParseError(f"Skill file not found: {path}")
136
+
137
+ content = path.read_text(encoding="utf-8")
138
+ raw_frontmatter, body = parse_frontmatter(content)
139
+ frontmatter = _parse_frontmatter_to_model(raw_frontmatter)
140
+
141
+ return Skill(
142
+ frontmatter=frontmatter,
143
+ body=body,
144
+ path=str(path),
145
+ )