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,212 @@
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 raise_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
+ ## Mastery Levels (ShuHaRi)
96
+
97
+ - **Shu**: [TODO: Beginner behavior]
98
+ - **Ha**: [TODO: Intermediate behavior]
99
+ - **Ri**: [TODO: Expert behavior]
100
+
101
+ ## Context
102
+
103
+ **When to use:** [TODO: Add trigger conditions]
104
+
105
+ **When to skip:** [TODO: Add skip conditions]
106
+
107
+ **Inputs:** [TODO: Add required inputs]
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
+ ## Quality Checklist
128
+
129
+ - [ ] [TODO: Add verification items]
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
+ skill_set: str | None = None,
146
+ from_builtin: bool = False,
147
+ ) -> ScaffoldResult:
148
+ """Scaffold a new skill with proper structure.
149
+
150
+ When ``skill_set`` is provided, creates the skill in
151
+ ``.raise/skills/{skill_set}/{name}/`` instead of the default
152
+ IDE skill directory. When ``from_builtin`` is also True, copies
153
+ the existing deployed skill as a starting point. (S340.2)
154
+
155
+ Args:
156
+ name: Skill name (e.g., 'story-validate').
157
+ lifecycle: Lifecycle category. If not specified, inferred from name.
158
+ after: Skill that should come before this one (prerequisites).
159
+ before: Skill that should come after this one (next).
160
+ skill_set: Skill set name (e.g., 'my-team'). Creates in .raise/skills/{set}/.
161
+ from_builtin: Copy from deployed .claude/skills/{name}/ as starting point.
162
+
163
+ Returns:
164
+ ScaffoldResult with creation status and path or error.
165
+ """
166
+ from pathlib import Path
167
+
168
+ # Determine target directory
169
+ if skill_set is not None:
170
+ skill_dir = Path.cwd() / ".raise" / "skills" / skill_set
171
+ else:
172
+ skill_dir = get_default_skill_dir()
173
+ if not skill_dir.exists():
174
+ skill_dir.mkdir(parents=True)
175
+
176
+ # Check if skill already exists in target
177
+ skill_path = skill_dir / name
178
+ if skill_path.exists():
179
+ return ScaffoldResult(
180
+ created=False,
181
+ error=f"Skill '{name}' already exists at {skill_path}",
182
+ )
183
+
184
+ # "Customize builtin" mode: copy from deployed skill
185
+ if from_builtin and skill_set is not None:
186
+ deployed = get_default_skill_dir() / name / "SKILL.md"
187
+ if deployed.exists():
188
+ skill_path.mkdir(parents=True, exist_ok=True)
189
+ skill_file = skill_path / "SKILL.md"
190
+ skill_file.write_text(deployed.read_text(encoding="utf-8"), encoding="utf-8")
191
+ return ScaffoldResult(created=True, path=str(skill_file))
192
+ return ScaffoldResult(
193
+ created=False,
194
+ error=f"Builtin '{name}' not found at {deployed}",
195
+ )
196
+
197
+ # Infer lifecycle if not specified
198
+ if lifecycle is None:
199
+ lifecycle = _infer_lifecycle(name)
200
+
201
+ # Generate content
202
+ content = _generate_skill_content(name, lifecycle, after, before)
203
+
204
+ # Create skill directory and file
205
+ skill_path.mkdir(parents=True, exist_ok=True)
206
+ skill_file = skill_path / "SKILL.md"
207
+ skill_file.write_text(content, encoding="utf-8")
208
+
209
+ return ScaffoldResult(
210
+ created=True,
211
+ path=str(skill_file),
212
+ )
@@ -0,0 +1,132 @@
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
+ output_type: str | None = Field(
41
+ default=None, description="Artifact type this skill produces (e.g., story-design)"
42
+ )
43
+
44
+ @classmethod
45
+ def from_raw(cls, raw: dict[str, Any]) -> SkillMetadata:
46
+ """Parse metadata from raw YAML dict with 'raise.' prefix.
47
+
48
+ Args:
49
+ raw: Dictionary with keys like 'raise.work_cycle', 'raise.version', etc.
50
+
51
+ Returns:
52
+ SkillMetadata instance with cleaned attributes.
53
+ """
54
+ # Strip 'raise.' prefix and convert to clean dict
55
+ cleaned: dict[str, Any] = {}
56
+ for key, value in raw.items():
57
+ if key.startswith("raise."):
58
+ clean_key = key[6:] # Remove 'raise.' prefix
59
+ # Handle boolean conversion
60
+ if clean_key == "adaptable" and isinstance(value, str):
61
+ value = value.lower() == "true"
62
+ cleaned[clean_key] = value
63
+
64
+ return cls(**cleaned)
65
+
66
+
67
+ class SkillHookCommand(BaseModel):
68
+ """A single hook command in a skill."""
69
+
70
+ type: str = Field(description="Hook type: 'command'")
71
+ command: str = Field(description="Shell command to execute")
72
+
73
+
74
+ class SkillHook(BaseModel):
75
+ """A hook configuration with nested commands."""
76
+
77
+ hooks: list[SkillHookCommand] = Field(
78
+ default_factory=lambda: [], description="List of hook commands"
79
+ )
80
+
81
+
82
+ class SkillFrontmatter(BaseModel):
83
+ """YAML frontmatter for a SKILL.md file.
84
+
85
+ This is the structured data at the top of each skill file,
86
+ containing name, description, metadata, and hooks.
87
+ """
88
+
89
+ name: str = Field(description="Skill name in {domain}-{action} format")
90
+ description: str = Field(description="Brief description of the skill")
91
+ license: str | None = Field(default=None, description="License (typically MIT)")
92
+ metadata: SkillMetadata | None = Field(
93
+ default=None, description="RaiSE-specific metadata"
94
+ )
95
+ hooks: dict[str, list[SkillHook]] | None = Field(
96
+ default=None, description="Claude Code hooks (e.g., Stop)"
97
+ )
98
+
99
+
100
+ class Skill(BaseModel):
101
+ """A complete RaiSE skill with frontmatter and markdown body.
102
+
103
+ Represents a parsed SKILL.md file with all its components.
104
+ """
105
+
106
+ frontmatter: SkillFrontmatter = Field(description="Parsed YAML frontmatter")
107
+ body: str = Field(description="Markdown content after frontmatter")
108
+ path: str = Field(description="Path to the SKILL.md file")
109
+
110
+ @property
111
+ def name(self) -> str:
112
+ """Shortcut to skill name."""
113
+ return self.frontmatter.name
114
+
115
+ @property
116
+ def version(self) -> str | None:
117
+ """Skill version from metadata, or None if no metadata."""
118
+ if self.frontmatter.metadata:
119
+ return self.frontmatter.metadata.version
120
+ return None
121
+
122
+ @property
123
+ def lifecycle(self) -> str | None:
124
+ """Skill lifecycle from metadata work_cycle."""
125
+ if self.frontmatter.metadata:
126
+ return self.frontmatter.metadata.work_cycle
127
+ return None
128
+
129
+ @property
130
+ def description(self) -> str:
131
+ """Shortcut to skill description."""
132
+ return self.frontmatter.description
@@ -0,0 +1,195 @@
1
+ """Skill set management — create, list, and diff skill sets.
2
+
3
+ Skill sets live in ``.raise/skills/{name}/`` and contain skill directories
4
+ with SKILL.md files. They overlay builtins when deployed with
5
+ ``rai init --skill-set``. (S340.4, RAISE-344)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from importlib.resources import files
12
+ from pathlib import Path
13
+
14
+ from pydantic import BaseModel, Field
15
+
16
+ from raise_cli.config.paths import get_raise_dir
17
+ from raise_cli.onboarding.skill_manifest import compute_content_hash
18
+ from raise_cli.onboarding.skills import copy_skill_tree
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class CreateResult(BaseModel):
24
+ """Result of skill set creation."""
25
+
26
+ created: bool
27
+ name: str
28
+ path: str | None = None
29
+ skill_count: int = 0
30
+ error: str | None = None
31
+
32
+
33
+ class SkillSetInfo(BaseModel):
34
+ """Info about an existing skill set."""
35
+
36
+ name: str
37
+ path: str
38
+ skill_count: int = 0
39
+
40
+
41
+ class SkillSetDiff(BaseModel):
42
+ """Diff of a skill set against builtins."""
43
+
44
+ name: str
45
+ added: list[str] = Field(default_factory=list)
46
+ modified: list[str] = Field(default_factory=list)
47
+ unchanged: list[str] = Field(default_factory=list)
48
+
49
+
50
+ def _skills_dir(project_root: Path) -> Path:
51
+ """Get the .raise/skills/ directory."""
52
+ return get_raise_dir(project_root) / "skills"
53
+
54
+
55
+ def _count_skills(set_dir: Path) -> int:
56
+ """Count skill directories containing SKILL.md."""
57
+ if not set_dir.is_dir():
58
+ return 0
59
+ return sum(
60
+ 1 for d in set_dir.iterdir()
61
+ if d.is_dir() and (d / "SKILL.md").exists()
62
+ )
63
+
64
+
65
+ def create_skill_set(
66
+ name: str,
67
+ project_root: Path,
68
+ *,
69
+ empty: bool = False,
70
+ ) -> CreateResult:
71
+ """Create a new skill set directory, optionally from builtins.
72
+
73
+ Args:
74
+ name: Skill set name (e.g., "my-team").
75
+ project_root: Project root directory.
76
+ empty: If True, create empty directory. Otherwise copy all builtins.
77
+
78
+ Returns:
79
+ CreateResult with creation status.
80
+ """
81
+ from raise_cli.onboarding.skills import SkillScaffoldResult
82
+ from raise_cli.skills_base import DISTRIBUTABLE_SKILLS
83
+
84
+ set_dir = _skills_dir(project_root) / name
85
+
86
+ if set_dir.exists():
87
+ return CreateResult(
88
+ created=False,
89
+ name=name,
90
+ error=f"Skill set '{name}' already exists at {set_dir}",
91
+ )
92
+
93
+ set_dir.mkdir(parents=True)
94
+
95
+ if empty:
96
+ return CreateResult(created=True, name=name, path=str(set_dir), skill_count=0)
97
+
98
+ # Copy builtins
99
+ base = files("raise_cli.skills_base")
100
+ result = SkillScaffoldResult()
101
+ for skill_name in DISTRIBUTABLE_SKILLS:
102
+ source = base / skill_name
103
+ dest = set_dir / skill_name
104
+ copy_skill_tree(source, dest, result, overwrite=True)
105
+
106
+ return CreateResult(
107
+ created=True,
108
+ name=name,
109
+ path=str(set_dir),
110
+ skill_count=len(DISTRIBUTABLE_SKILLS),
111
+ )
112
+
113
+
114
+ def list_skill_sets(project_root: Path) -> list[SkillSetInfo]:
115
+ """List all skill sets in .raise/skills/.
116
+
117
+ Args:
118
+ project_root: Project root directory.
119
+
120
+ Returns:
121
+ List of SkillSetInfo, sorted by name.
122
+ """
123
+ skills_root = _skills_dir(project_root)
124
+ if not skills_root.is_dir():
125
+ return []
126
+
127
+ sets: list[SkillSetInfo] = []
128
+ for item in sorted(skills_root.iterdir()):
129
+ if not item.is_dir():
130
+ continue
131
+ sets.append(SkillSetInfo(
132
+ name=item.name,
133
+ path=str(item),
134
+ skill_count=_count_skills(item),
135
+ ))
136
+
137
+ return sets
138
+
139
+
140
+ def diff_skill_set(
141
+ name: str,
142
+ project_root: Path,
143
+ ) -> SkillSetDiff | None:
144
+ """Compare a skill set against builtins.
145
+
146
+ Args:
147
+ name: Skill set name.
148
+ project_root: Project root directory.
149
+
150
+ Returns:
151
+ SkillSetDiff with added/modified/unchanged, or None if set doesn't exist.
152
+ """
153
+ from raise_cli.skills_base import DISTRIBUTABLE_SKILLS
154
+
155
+ set_dir = _skills_dir(project_root) / name
156
+ if not set_dir.is_dir():
157
+ return None
158
+
159
+ base = files("raise_cli.skills_base")
160
+ builtin_names = set(DISTRIBUTABLE_SKILLS)
161
+
162
+ added: list[str] = []
163
+ modified: list[str] = []
164
+ unchanged: list[str] = []
165
+
166
+ # Check each skill in the set
167
+ for skill_dir in sorted(set_dir.iterdir()):
168
+ if not skill_dir.is_dir():
169
+ continue
170
+ skill_md = skill_dir / "SKILL.md"
171
+ if not skill_md.exists():
172
+ continue
173
+
174
+ skill_name = skill_dir.name
175
+
176
+ if skill_name not in builtin_names:
177
+ added.append(skill_name)
178
+ continue
179
+
180
+ # Compare against builtin
181
+ set_hash = compute_content_hash(skill_md.read_text(encoding="utf-8"))
182
+ builtin_content = (base / skill_name / "SKILL.md").read_text(encoding="utf-8")
183
+ builtin_hash = compute_content_hash(builtin_content)
184
+
185
+ if set_hash == builtin_hash:
186
+ unchanged.append(skill_name)
187
+ else:
188
+ modified.append(skill_name)
189
+
190
+ return SkillSetDiff(
191
+ name=name,
192
+ added=added,
193
+ modified=modified,
194
+ unchanged=unchanged,
195
+ )