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,350 @@
1
+ """CLI commands for skill management.
2
+
3
+ Provides deterministic operations for listing, validating, and scaffolding
4
+ RaiSE skills, following the inference economy principle.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Annotated
11
+
12
+ import typer
13
+ from rich.console import Console
14
+
15
+ from raise_cli.cli.commands.skill_set import skill_set_app
16
+ from raise_cli.onboarding.skills import SkillScaffoldResult, scaffold_skills
17
+ from raise_cli.output.formatters.skill import (
18
+ format_name_check_human,
19
+ format_name_check_json,
20
+ format_scaffold_human,
21
+ format_scaffold_json,
22
+ format_skill_list_human,
23
+ format_skill_list_json,
24
+ format_validation_human,
25
+ format_validation_json,
26
+ )
27
+ from raise_cli.skills.locator import SkillLocator, get_default_skill_dir
28
+ from raise_cli.skills.name_checker import check_name
29
+ from raise_cli.skills.scaffold import scaffold_skill
30
+ from raise_cli.skills.validator import (
31
+ ValidationResult,
32
+ validate_skill,
33
+ validate_skill_file,
34
+ )
35
+
36
+ skill_app = typer.Typer(
37
+ name="skill",
38
+ help="Manage RaiSE skills",
39
+ no_args_is_help=True,
40
+ )
41
+ skill_app.add_typer(skill_set_app, name="set")
42
+
43
+ console = Console()
44
+
45
+
46
+ @skill_app.command("list")
47
+ def list_command(
48
+ format: Annotated[
49
+ str,
50
+ typer.Option(
51
+ "--format",
52
+ "-f",
53
+ help="Output format: human or json",
54
+ ),
55
+ ] = "human",
56
+ ) -> None:
57
+ """List all skills in the skill directory.
58
+
59
+ Shows skills grouped by lifecycle with version and description.
60
+ """
61
+ skill_dir = get_default_skill_dir()
62
+ locator = SkillLocator(skill_dir)
63
+ skills = locator.load_all_skills()
64
+ grouped = locator.group_by_lifecycle(skills)
65
+
66
+ if format == "json":
67
+ output = format_skill_list_json(skills, str(skill_dir))
68
+ print(output) # Plain print for valid JSON
69
+ else:
70
+ format_skill_list_human(skills, grouped, console)
71
+
72
+
73
+ @skill_app.command("validate")
74
+ def validate_command(
75
+ path: Annotated[
76
+ str | None,
77
+ typer.Argument(
78
+ help="Path to skill file or directory. Validates all skills if not specified.",
79
+ ),
80
+ ] = None,
81
+ format: Annotated[
82
+ str,
83
+ typer.Option(
84
+ "--format",
85
+ "-f",
86
+ help="Output format: human or json",
87
+ ),
88
+ ] = "human",
89
+ ) -> None:
90
+ """Validate skill structure against RaiSE schema.
91
+
92
+ Checks frontmatter, required fields, sections, and naming conventions.
93
+ """
94
+ results: list[ValidationResult] = []
95
+
96
+ if path:
97
+ # Validate specific file or directory
98
+ target = Path(path)
99
+ if target.is_file():
100
+ results.append(validate_skill_file(target))
101
+ elif target.is_dir():
102
+ # Look for SKILL.md in directory
103
+ skill_file = target / "SKILL.md"
104
+ if skill_file.exists():
105
+ results.append(validate_skill_file(skill_file))
106
+ else:
107
+ results.append(
108
+ ValidationResult(
109
+ path=str(target),
110
+ errors=[f"No SKILL.md found in {target}"],
111
+ )
112
+ )
113
+ else:
114
+ results.append(
115
+ ValidationResult(
116
+ path=str(target),
117
+ errors=[f"Path not found: {target}"],
118
+ )
119
+ )
120
+ else:
121
+ # Validate all skills
122
+ skill_dir = get_default_skill_dir()
123
+ locator = SkillLocator(skill_dir)
124
+ skills = locator.load_all_skills()
125
+
126
+ for skill in skills:
127
+ results.append(validate_skill(skill))
128
+
129
+ # Output results
130
+ if format == "json":
131
+ print(format_validation_json(results))
132
+ else:
133
+ format_validation_human(results, console)
134
+
135
+ # Exit with error code if any validation failed
136
+ if not all(r.is_valid for r in results):
137
+ raise typer.Exit(code=1)
138
+
139
+
140
+ @skill_app.command("check-name")
141
+ def check_name_command(
142
+ name: Annotated[
143
+ str,
144
+ typer.Argument(
145
+ help="Proposed skill name to check (e.g., 'story-validate').",
146
+ ),
147
+ ],
148
+ format: Annotated[
149
+ str,
150
+ typer.Option(
151
+ "--format",
152
+ "-f",
153
+ help="Output format: human or json",
154
+ ),
155
+ ] = "human",
156
+ ) -> None:
157
+ """Check a proposed skill name against naming conventions.
158
+
159
+ Validates that the name follows {domain}-{action} pattern,
160
+ doesn't conflict with existing skills or CLI commands,
161
+ and uses a known lifecycle domain.
162
+ """
163
+ result = check_name(name)
164
+
165
+ # Output results
166
+ if format == "json":
167
+ print(format_name_check_json(result))
168
+ else:
169
+ format_name_check_human(result, console)
170
+
171
+ # Exit with error code if name is invalid
172
+ if not result.is_valid:
173
+ raise typer.Exit(code=1)
174
+
175
+
176
+ @skill_app.command("scaffold")
177
+ def scaffold_command(
178
+ name: Annotated[
179
+ str,
180
+ typer.Argument(
181
+ help="Skill name to create (e.g., 'story-validate').",
182
+ ),
183
+ ],
184
+ lifecycle: Annotated[
185
+ str | None,
186
+ typer.Option(
187
+ "--lifecycle",
188
+ "-l",
189
+ help="Lifecycle: session, epic, story, discovery, utility, meta. Inferred from name if not specified.",
190
+ ),
191
+ ] = None,
192
+ after: Annotated[
193
+ str | None,
194
+ typer.Option(
195
+ "--after",
196
+ help="Skill that should come before this one (prerequisites).",
197
+ ),
198
+ ] = None,
199
+ before: Annotated[
200
+ str | None,
201
+ typer.Option(
202
+ "--before",
203
+ help="Skill that should come after this one (next).",
204
+ ),
205
+ ] = None,
206
+ skill_set: Annotated[
207
+ str | None,
208
+ typer.Option(
209
+ "--set",
210
+ help="Skill set to create in (e.g., 'my-team'). Creates in .raise/skills/{set}/.",
211
+ ),
212
+ ] = None,
213
+ from_builtin: Annotated[
214
+ bool,
215
+ typer.Option(
216
+ "--from-builtin",
217
+ help="Copy from deployed builtin skill as starting point. Requires --set.",
218
+ ),
219
+ ] = False,
220
+ format: Annotated[
221
+ str,
222
+ typer.Option(
223
+ "--format",
224
+ "-f",
225
+ help="Output format: human or json",
226
+ ),
227
+ ] = "human",
228
+ ) -> None:
229
+ """Create a new skill from template.
230
+
231
+ Generates a SKILL.md file with proper structure.
232
+ Without --set: creates in .claude/skills/<name>/.
233
+ With --set: creates in .raise/skills/<set>/<name>/.
234
+ """
235
+ result = scaffold_skill(
236
+ name,
237
+ lifecycle=lifecycle,
238
+ after=after,
239
+ before=before,
240
+ skill_set=skill_set,
241
+ from_builtin=from_builtin,
242
+ )
243
+
244
+ # Output results
245
+ if format == "json":
246
+ print(format_scaffold_json(result))
247
+ else:
248
+ format_scaffold_human(result, console)
249
+
250
+ # Exit with error code if creation failed
251
+ if not result.created:
252
+ raise typer.Exit(code=1)
253
+
254
+
255
+ def _print_sync_table(result: SkillScaffoldResult) -> None:
256
+ """Print a summary table of skill sync status."""
257
+ from rich.table import Table
258
+
259
+ try:
260
+ from raise_cli.skills_base import __version__ as cli_version
261
+ except ImportError:
262
+ cli_version = "unknown"
263
+
264
+ console.print(f"\n[bold]Skill sync check: raise-cli {cli_version}[/bold]\n")
265
+
266
+ rows: list[tuple[str, str]] = []
267
+ for name in result.skills_installed:
268
+ rows.append((name, "[green]new — not deployed[/green]"))
269
+ for name in result.skills_updated:
270
+ rows.append((name, "[yellow]outdated — update available[/yellow]"))
271
+ for name in result.skills_conflicted:
272
+ rows.append((name, "[yellow]conflict — both changed[/yellow]"))
273
+ for name in result.skills_current:
274
+ rows.append((name, "[green]current[/green]"))
275
+
276
+ rows.sort(key=lambda r: r[0])
277
+
278
+ table = Table(show_header=True)
279
+ table.add_column("Skill", style="bold")
280
+ table.add_column("Status")
281
+ for name, status in rows:
282
+ table.add_row(name, status)
283
+
284
+ console.print(table)
285
+
286
+ n_stale = (
287
+ len(result.skills_updated)
288
+ + len(result.skills_installed)
289
+ + len(result.skills_conflicted)
290
+ )
291
+ n_current = len(result.skills_current)
292
+ if n_stale == 0:
293
+ console.print(f"\n All {n_current} skills are current.\n")
294
+ else:
295
+ console.print(
296
+ f"\n {n_stale} skill(s) need attention, "
297
+ f"{n_current} current. Run [bold]rai init[/bold] to update.\n"
298
+ )
299
+
300
+
301
+ @skill_app.command("sync")
302
+ def sync_cmd(
303
+ path: Annotated[
304
+ Path | None, typer.Option("--path", "-p", help="Project path")
305
+ ] = None,
306
+ ) -> None:
307
+ """Check skill freshness against installed package version.
308
+
309
+ Reports which skills are current, outdated, or have conflicts.
310
+ Exit code 0 = all current, 1 = updates available.
311
+
312
+ Examples:
313
+ $ rai skill sync
314
+ $ rai skill sync --path /path/to/project
315
+ """
316
+ from raise_cli.config.agent_registry import load_registry
317
+
318
+ project_path = (path or Path.cwd()).resolve()
319
+ registry = load_registry(project_root=project_path)
320
+
321
+ # Determine agent type from manifest, fall back to "claude"
322
+ agent_type = "claude"
323
+ try:
324
+ from raise_cli.onboarding.manifest import load_manifest
325
+
326
+ manifest = load_manifest(project_path)
327
+ if manifest and manifest.agents.types:
328
+ agent_type = manifest.agents.types[0]
329
+ except Exception:
330
+ pass
331
+
332
+ config = registry.get_config(agent_type)
333
+ plugin = registry.get_plugin(agent_type)
334
+
335
+ result = scaffold_skills(
336
+ project_path,
337
+ agent_config=config,
338
+ plugin=plugin,
339
+ dry_run=True,
340
+ )
341
+
342
+ _print_sync_table(result)
343
+
344
+ has_updates = bool(
345
+ result.skills_updated
346
+ or result.skills_installed
347
+ or result.skills_conflicted
348
+ )
349
+ if has_updates:
350
+ raise typer.Exit(code=1)
@@ -0,0 +1,145 @@
1
+ """CLI commands for skill set management.
2
+
3
+ Provides ``rai skill set create|list|diff`` for observable skill set
4
+ operations. These commands are the foundation for ``/rai-skillset-manage``.
5
+ (S340.4, RAISE-344)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+ from typing import Annotated
13
+
14
+ import typer
15
+ from rich.console import Console
16
+ from rich.table import Table
17
+
18
+ from raise_cli.skills.skillsets import (
19
+ create_skill_set,
20
+ diff_skill_set,
21
+ list_skill_sets,
22
+ )
23
+
24
+ skill_set_app = typer.Typer(
25
+ name="set",
26
+ help="Manage skill sets",
27
+ no_args_is_help=True,
28
+ )
29
+
30
+ console = Console()
31
+
32
+
33
+ @skill_set_app.command("create")
34
+ def create_command(
35
+ name: Annotated[
36
+ str,
37
+ typer.Argument(help="Skill set name (e.g., 'my-team')."),
38
+ ],
39
+ empty: Annotated[
40
+ bool,
41
+ typer.Option("--empty", help="Create empty set (no builtins copied)."),
42
+ ] = False,
43
+ format: Annotated[
44
+ str,
45
+ typer.Option("--format", "-f", help="Output format: human or json"),
46
+ ] = "human",
47
+ ) -> None:
48
+ """Create a new skill set from builtins.
49
+
50
+ Copies all builtin skills to .raise/skills/<name>/ as a starting
51
+ base for customization. Use --empty for a blank set.
52
+ """
53
+ project_root = Path.cwd()
54
+ result = create_skill_set(name, project_root, empty=empty)
55
+
56
+ if format == "json":
57
+ print(result.model_dump_json(indent=2))
58
+ elif result.created:
59
+ console.print(f"[green]✓[/green] Skill set '{name}' created at {result.path}")
60
+ console.print(f" {result.skill_count} skills copied from builtins")
61
+ console.print("\n Next: customize skills, then deploy with:")
62
+ console.print(f" [bold]rai init --skill-set {name}[/bold]")
63
+ else:
64
+ console.print(f"[red]✗[/red] {result.error}")
65
+ raise typer.Exit(code=1)
66
+
67
+
68
+ @skill_set_app.command("list")
69
+ def list_command(
70
+ format: Annotated[
71
+ str,
72
+ typer.Option("--format", "-f", help="Output format: human or json"),
73
+ ] = "human",
74
+ ) -> None:
75
+ """List all skill sets in .raise/skills/."""
76
+ project_root = Path.cwd()
77
+ sets = list_skill_sets(project_root)
78
+
79
+ if format == "json":
80
+ print(json.dumps([s.model_dump() for s in sets], indent=2))
81
+ return
82
+
83
+ if not sets:
84
+ console.print("No skill sets found in .raise/skills/")
85
+ console.print(" Create one with: [bold]rai skill set create <name>[/bold]")
86
+ return
87
+
88
+ table = Table(title="Skill Sets")
89
+ table.add_column("Name", style="bold")
90
+ table.add_column("Skills", justify="right")
91
+ table.add_column("Path")
92
+
93
+ for s in sets:
94
+ table.add_row(s.name, str(s.skill_count), s.path)
95
+
96
+ console.print(table)
97
+
98
+
99
+ @skill_set_app.command("diff")
100
+ def diff_command(
101
+ name: Annotated[
102
+ str,
103
+ typer.Argument(help="Skill set name to compare against builtins."),
104
+ ],
105
+ format: Annotated[
106
+ str,
107
+ typer.Option("--format", "-f", help="Output format: human or json"),
108
+ ] = "human",
109
+ ) -> None:
110
+ """Compare a skill set against builtins.
111
+
112
+ Shows which skills are added, modified, or unchanged relative
113
+ to the builtin skill set.
114
+ """
115
+ project_root = Path.cwd()
116
+ diff = diff_skill_set(name, project_root)
117
+
118
+ if diff is None:
119
+ console.print(f"[red]✗[/red] Skill set '{name}' not found")
120
+ raise typer.Exit(code=1)
121
+
122
+ if format == "json":
123
+ print(diff.model_dump_json(indent=2))
124
+ return
125
+
126
+ console.print(f"[bold]Skill set:[/bold] {name}\n")
127
+
128
+ if diff.added:
129
+ console.print(f"[green]Added ({len(diff.added)}):[/green]")
130
+ for s in diff.added:
131
+ console.print(f" + {s}")
132
+
133
+ if diff.modified:
134
+ console.print(f"[yellow]Modified ({len(diff.modified)}):[/yellow]")
135
+ for s in diff.modified:
136
+ console.print(f" ~ {s}")
137
+
138
+ if diff.unchanged:
139
+ console.print(f"[dim]Unchanged ({len(diff.unchanged)}):[/dim]")
140
+ for s in diff.unchanged:
141
+ console.print(f" = {s}")
142
+
143
+ total = len(diff.added) + len(diff.modified) + len(diff.unchanged)
144
+ added, modified = len(diff.added), len(diff.modified)
145
+ console.print(f"\n[bold]Total:[/bold] {total} skills ({added} added, {modified} modified)")
@@ -0,0 +1,158 @@
1
+ """Error presentation with Rich formatting.
2
+
3
+ This module provides error display for raise-cli, supporting both
4
+ human-friendly Rich output and JSON format for scripting.
5
+
6
+ Example:
7
+ >>> from raise_cli.exceptions import KataNotFoundError
8
+ >>> from raise_cli.cli.error_handler import handle_error
9
+ >>>
10
+ >>> error = KataNotFoundError("Kata 'foo' not found", hint="Check .raise/katas/")
11
+ >>> exit_code = handle_error(error, format="human")
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import sys
18
+ from typing import TYPE_CHECKING, Literal, NoReturn
19
+
20
+ from rich.console import Console
21
+ from rich.panel import Panel
22
+
23
+ if TYPE_CHECKING:
24
+ from raise_cli.exceptions import RaiError
25
+
26
+ # Stderr console for error output
27
+ _error_console: Console | None = None
28
+
29
+
30
+ def get_error_console() -> Console:
31
+ """Get or create the stderr console singleton.
32
+
33
+ Returns:
34
+ Rich Console configured for stderr output.
35
+ """
36
+ global _error_console # noqa: PLW0603
37
+ if _error_console is None:
38
+ _error_console = Console(stderr=True)
39
+ return _error_console
40
+
41
+
42
+ def set_error_console(console: Console | None) -> None:
43
+ """Set the error console (for testing).
44
+
45
+ Args:
46
+ console: Console to use, or None to reset to default.
47
+ """
48
+ global _error_console # noqa: PLW0603
49
+ _error_console = console
50
+
51
+
52
+ def handle_error(
53
+ error: RaiError,
54
+ *,
55
+ output_format: Literal["human", "json", "table"] = "human",
56
+ ) -> int:
57
+ """Format and display error, return exit code.
58
+
59
+ Args:
60
+ error: The RaiError to display.
61
+ output_format: Output format (human uses Rich, json outputs JSON).
62
+
63
+ Returns:
64
+ The error's exit_code for use with sys.exit().
65
+ """
66
+ if output_format == "json":
67
+ _handle_error_json(error)
68
+ else:
69
+ _handle_error_human(error)
70
+
71
+ return error.exit_code
72
+
73
+
74
+ def _handle_error_human(error: RaiError) -> None:
75
+ """Display error with Rich formatting.
76
+
77
+ Args:
78
+ error: The RaiError to display.
79
+ """
80
+ console = get_error_console()
81
+
82
+ # Main error panel
83
+ console.print(
84
+ Panel(
85
+ f"[bold red]{error.message}[/]",
86
+ title=f"[red]Error {error.error_code}[/]",
87
+ border_style="red",
88
+ )
89
+ )
90
+
91
+ # Details section
92
+ if error.details:
93
+ console.print("\n[dim]Details:[/]")
94
+ for key, value in error.details.items():
95
+ console.print(f" [dim]\u2022[/] {key}: {value}")
96
+
97
+ # Hint section
98
+ if error.hint:
99
+ console.print(f"\n[cyan]Hint:[/] {error.hint}")
100
+
101
+
102
+ def _handle_error_json(error: RaiError) -> None:
103
+ """Output error as JSON to stderr.
104
+
105
+ Args:
106
+ error: The RaiError to display.
107
+ """
108
+ output = json.dumps(error.to_dict(), indent=2)
109
+ print(output, file=sys.stderr)
110
+
111
+
112
+ def cli_error(
113
+ message: str,
114
+ *,
115
+ hint: str | None = None,
116
+ exit_code: int = 1,
117
+ ) -> NoReturn:
118
+ """Print error message and exit with code.
119
+
120
+ This is the standard pattern for CLI error handling. Use this instead of
121
+ manually printing errors and calling typer.Exit().
122
+
123
+ Exit codes (from exceptions.py):
124
+ 1 - General error
125
+ 2 - Configuration error
126
+ 3 - Resource not found (kata, gate)
127
+ 4 - Artifact not found
128
+ 5 - Dependency unavailable
129
+ 6 - State corruption
130
+ 7 - Validation error
131
+ 10 - Gate failed
132
+
133
+ Args:
134
+ message: Error message to display.
135
+ hint: Optional suggestion for resolution.
136
+ exit_code: Process exit code (default 1).
137
+
138
+ Raises:
139
+ typer.Exit: Always raises to exit the CLI.
140
+
141
+ Example:
142
+ >>> cli_error("File not found", hint="Check the path", exit_code=4)
143
+ """
144
+ import typer
145
+
146
+ console = get_error_console()
147
+ console.print(f"[red]Error:[/red] {message}")
148
+ if hint:
149
+ console.print(f"[dim]Hint: {hint}[/dim]")
150
+ raise typer.Exit(exit_code)
151
+
152
+
153
+ __all__ = [
154
+ "cli_error",
155
+ "handle_error",
156
+ "get_error_console",
157
+ "set_error_console",
158
+ ]