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,298 @@
1
+ """Output formatters for skill commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from raise_cli.skills.name_checker import NameCheckResult
12
+ from raise_cli.skills.scaffold import ScaffoldResult
13
+ from raise_cli.skills.schema import Skill
14
+ from raise_cli.skills.validator import ValidationResult
15
+
16
+
17
+ def format_skill_list_human(
18
+ skills: list[Skill],
19
+ grouped: dict[str, list[Skill]],
20
+ console: Console,
21
+ ) -> None:
22
+ """Format skill list for human output.
23
+
24
+ Args:
25
+ skills: All skills (for count).
26
+ grouped: Skills grouped by lifecycle.
27
+ console: Rich console for output.
28
+ """
29
+ if not skills:
30
+ console.print("No skills found in .claude/skills/")
31
+ return
32
+
33
+ console.print(f"[bold]Skills[/bold] ({len(skills)} found)\n")
34
+
35
+ # Define lifecycle order for consistent output
36
+ lifecycle_order = [
37
+ "session",
38
+ "epic",
39
+ "story",
40
+ "discovery",
41
+ "utility",
42
+ "meta",
43
+ "unknown",
44
+ ]
45
+
46
+ # Show all lifecycles: ordered first, then any unknown ones alphabetically
47
+ ordered_set = set(lifecycle_order)
48
+ extra_lifecycles = sorted(lc for lc in grouped if lc not in ordered_set)
49
+ display_order = lifecycle_order + extra_lifecycles
50
+
51
+ for lifecycle in display_order:
52
+ if lifecycle not in grouped:
53
+ continue
54
+
55
+ lifecycle_skills = grouped[lifecycle]
56
+ console.print(f"[bold cyan]{lifecycle.capitalize()}[/bold cyan]")
57
+
58
+ table = Table(show_header=False, box=None, padding=(0, 2, 0, 0))
59
+ table.add_column("Name", style="green")
60
+ table.add_column("Version", style="dim")
61
+ table.add_column("Description")
62
+
63
+ for skill in sorted(lifecycle_skills, key=lambda s: s.name):
64
+ # Truncate description if too long
65
+ desc = skill.description
66
+ if len(desc) > 50:
67
+ desc = desc[:47] + "..."
68
+ table.add_row(
69
+ skill.name,
70
+ skill.version or "-",
71
+ desc,
72
+ )
73
+
74
+ console.print(table)
75
+ console.print()
76
+
77
+
78
+ def format_skill_list_json(
79
+ skills: list[Skill],
80
+ skill_dir: str,
81
+ ) -> str:
82
+ """Format skill list as JSON.
83
+
84
+ Args:
85
+ skills: List of skills to format.
86
+ skill_dir: Path to skill directory.
87
+
88
+ Returns:
89
+ JSON string.
90
+ """
91
+ skill_data: list[dict[str, Any]] = []
92
+ for skill in sorted(skills, key=lambda s: s.name):
93
+ skill_data.append(
94
+ {
95
+ "name": skill.name,
96
+ "version": skill.version,
97
+ "lifecycle": skill.lifecycle,
98
+ "description": skill.description,
99
+ "path": skill.path,
100
+ }
101
+ )
102
+
103
+ output = {
104
+ "skills": skill_data,
105
+ "skill_dir": skill_dir,
106
+ "count": len(skills),
107
+ }
108
+
109
+ return json.dumps(output, indent=2)
110
+
111
+
112
+ def format_validation_human(
113
+ results: list[ValidationResult],
114
+ console: Console,
115
+ ) -> None:
116
+ """Format validation results for human output.
117
+
118
+ Args:
119
+ results: List of validation results.
120
+ console: Rich console for output.
121
+ """
122
+ total_errors = sum(r.error_count for r in results)
123
+ total_warnings = sum(r.warning_count for r in results)
124
+
125
+ for result in results:
126
+ console.print(f"\n[bold]Validating:[/bold] {result.path}")
127
+
128
+ if result.is_valid and result.warning_count == 0:
129
+ console.print("[green]✓ All checks passed[/green]")
130
+ continue
131
+
132
+ # Show errors
133
+ for error in result.errors:
134
+ console.print(f"[red]✗ {error}[/red]")
135
+
136
+ # Show warnings
137
+ for warning in result.warnings:
138
+ console.print(f"[yellow]⚠ {warning}[/yellow]")
139
+
140
+ # Summary
141
+ console.print()
142
+ if total_errors == 0 and total_warnings == 0:
143
+ console.print(f"[green]All {len(results)} skill(s) valid[/green]")
144
+ else:
145
+ parts: list[str] = []
146
+ if total_errors > 0:
147
+ parts.append(f"[red]{total_errors} error(s)[/red]")
148
+ if total_warnings > 0:
149
+ parts.append(f"[yellow]{total_warnings} warning(s)[/yellow]")
150
+ console.print(f"{len(results)} skill(s) checked: {', '.join(parts)}")
151
+
152
+
153
+ def format_validation_json(results: list[ValidationResult]) -> str:
154
+ """Format validation results as JSON.
155
+
156
+ Args:
157
+ results: List of validation results.
158
+
159
+ Returns:
160
+ JSON string.
161
+ """
162
+ output: list[dict[str, Any]] = []
163
+ for result in results:
164
+ output.append(
165
+ {
166
+ "path": result.path,
167
+ "valid": result.is_valid,
168
+ "errors": result.errors,
169
+ "warnings": result.warnings,
170
+ }
171
+ )
172
+
173
+ return json.dumps(
174
+ {
175
+ "results": output,
176
+ "total_errors": sum(r.error_count for r in results),
177
+ "total_warnings": sum(r.warning_count for r in results),
178
+ "all_valid": all(r.is_valid for r in results),
179
+ },
180
+ indent=2,
181
+ )
182
+
183
+
184
+ def format_name_check_human(result: NameCheckResult, console: Console) -> None:
185
+ """Format name check result for human output.
186
+
187
+ Args:
188
+ result: Name check result.
189
+ console: Rich console for output.
190
+ """
191
+ console.print(f"\n[bold]Checking name:[/bold] {result.name}\n")
192
+
193
+ # Pattern check
194
+ if result.valid_pattern:
195
+ console.print("[green]✓ Follows {domain}-{action} pattern[/green]")
196
+ else:
197
+ console.print("[red]✗ Does not follow {domain}-{action} pattern[/red]")
198
+
199
+ # Skill conflict
200
+ if result.no_skill_conflict:
201
+ console.print("[green]✓ No conflict with existing skills[/green]")
202
+ else:
203
+ console.print(
204
+ f"[red]✗ Conflicts with existing skill: {result.conflicting_skill}[/red]"
205
+ )
206
+
207
+ # CLI conflict
208
+ if result.no_cli_conflict:
209
+ console.print("[green]✓ No CLI command conflict[/green]")
210
+ else:
211
+ console.print(
212
+ f"[red]✗ Conflicts with CLI command: {result.conflicting_command}[/red]"
213
+ )
214
+
215
+ # Lifecycle check
216
+ if result.known_lifecycle:
217
+ console.print("[green]✓ Domain is a known lifecycle[/green]")
218
+ else:
219
+ console.print("[yellow]⚠ Domain is not a standard lifecycle[/yellow]")
220
+
221
+ # Final verdict
222
+ console.print()
223
+ if result.is_valid:
224
+ console.print(f"[bold green]Name '{result.name}' is valid.[/bold green]")
225
+ else:
226
+ console.print(f"[bold red]Name '{result.name}' is not valid.[/bold red]")
227
+
228
+ # Suggestions
229
+ if result.suggestions:
230
+ console.print()
231
+ for suggestion in result.suggestions:
232
+ console.print(f"[dim]→ {suggestion}[/dim]")
233
+
234
+
235
+ def format_name_check_json(result: NameCheckResult) -> str:
236
+ """Format name check result as JSON.
237
+
238
+ Args:
239
+ result: Name check result.
240
+
241
+ Returns:
242
+ JSON string.
243
+ """
244
+ return json.dumps(
245
+ {
246
+ "name": result.name,
247
+ "valid": result.is_valid,
248
+ "checks": {
249
+ "valid_pattern": result.valid_pattern,
250
+ "no_skill_conflict": result.no_skill_conflict,
251
+ "no_cli_conflict": result.no_cli_conflict,
252
+ "known_lifecycle": result.known_lifecycle,
253
+ },
254
+ "conflicts": {
255
+ "skill": result.conflicting_skill,
256
+ "command": result.conflicting_command,
257
+ },
258
+ "suggestions": result.suggestions,
259
+ },
260
+ indent=2,
261
+ )
262
+
263
+
264
+ def format_scaffold_human(result: ScaffoldResult, console: Console) -> None:
265
+ """Format scaffold result for human output.
266
+
267
+ Args:
268
+ result: Scaffold result.
269
+ console: Rich console for output.
270
+ """
271
+ if result.created:
272
+ console.print(f"\n[green]✓ Created skill at:[/green] {result.path}")
273
+ console.print("\n[dim]Next steps:[/dim]")
274
+ console.print(" 1. Edit the SKILL.md to add description and steps")
275
+ console.print(" 2. Run [cyan]raise skill validate[/cyan] to check structure")
276
+ console.print(" 3. Test the skill with Claude Code")
277
+ else:
278
+ console.print("\n[red]✗ Failed to create skill[/red]")
279
+ console.print(f"[red] {result.error}[/red]")
280
+
281
+
282
+ def format_scaffold_json(result: ScaffoldResult) -> str:
283
+ """Format scaffold result as JSON.
284
+
285
+ Args:
286
+ result: Scaffold result.
287
+
288
+ Returns:
289
+ JSON string.
290
+ """
291
+ return json.dumps(
292
+ {
293
+ "created": result.created,
294
+ "path": result.path,
295
+ "error": result.error,
296
+ },
297
+ indent=2,
298
+ )
@@ -0,0 +1,3 @@
1
+ """Publish module — quality gates, version management, and release workflow."""
2
+
3
+ from __future__ import annotations
@@ -0,0 +1,80 @@
1
+ """Changelog parsing and updating for Keep a Changelog format."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+
8
+ def has_unreleased_entries(content: str) -> bool:
9
+ """Check if the changelog has entries under the [Unreleased] section.
10
+
11
+ Args:
12
+ content: Full changelog text.
13
+
14
+ Returns:
15
+ True if there are non-whitespace entries between [Unreleased] and the next section.
16
+ """
17
+ match = re.search(
18
+ r"^## \[Unreleased\]\s*$(.*?)(?=^## \[|\Z)",
19
+ content,
20
+ re.DOTALL | re.MULTILINE,
21
+ )
22
+ if not match:
23
+ return False
24
+ return len(match.group(1).strip()) > 0
25
+
26
+
27
+ def promote_unreleased(content: str, version: str, date: str) -> str:
28
+ """Move unreleased entries into a new versioned section.
29
+
30
+ Args:
31
+ content: Full changelog text.
32
+ version: New version string (e.g. "2.0.0").
33
+ date: Release date string (e.g. "2026-02-14").
34
+
35
+ Returns:
36
+ Updated changelog text.
37
+
38
+ Raises:
39
+ ValueError: If there are no unreleased entries to promote.
40
+ """
41
+ if not has_unreleased_entries(content):
42
+ msg = "No unreleased entries to promote"
43
+ raise ValueError(msg)
44
+
45
+ # Extract the unreleased body
46
+ match = re.search(
47
+ r"(^## \[Unreleased\])\s*$(.*?)(?=^## \[)",
48
+ content,
49
+ re.DOTALL | re.MULTILINE,
50
+ )
51
+ if not match:
52
+ msg = "No unreleased entries to promote"
53
+ raise ValueError(msg)
54
+
55
+ unreleased_header = match.group(1)
56
+ unreleased_body = match.group(2).rstrip()
57
+
58
+ # Build replacement: empty Unreleased + new version section
59
+ replacement = f"{unreleased_header}\n\n## [{version}] - {date}\n{unreleased_body}"
60
+ content = content[: match.start()] + replacement + content[match.end() :]
61
+
62
+ # Update link references if they exist
63
+ # Replace: [Unreleased]: .../compare/vOLD...HEAD
64
+ # With: [Unreleased]: .../compare/vNEW...HEAD
65
+ # [NEW]: .../compare/vOLD...vNEW
66
+ old_link_match = re.search(
67
+ r"\[Unreleased\]:\s*(https?://\S+/compare/)v([\d.]+\S*)\.\.\.HEAD",
68
+ content,
69
+ )
70
+ if old_link_match:
71
+ base_url = old_link_match.group(1)
72
+ old_version = old_link_match.group(2)
73
+ new_unreleased_link = f"[Unreleased]: {base_url}v{version}...HEAD"
74
+ new_version_link = f"[{version}]: {base_url}v{old_version}...v{version}"
75
+ content = content.replace(
76
+ old_link_match.group(0),
77
+ f"{new_unreleased_link}\n{new_version_link}",
78
+ )
79
+
80
+ return content
@@ -0,0 +1,179 @@
1
+ """Quality gate runner for pre-publish checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import subprocess
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Gate:
13
+ """Definition of a quality gate."""
14
+
15
+ name: str
16
+ command: str
17
+ required: bool = True
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class CheckResult:
22
+ """Result of a single quality gate check."""
23
+
24
+ gate: str
25
+ passed: bool
26
+ message: str
27
+
28
+
29
+ # Subprocess gates (executed via shell)
30
+ _COMMAND_GATES: list[Gate] = [
31
+ Gate(name="Tests pass", command="uv run pytest --cov --tb=no -q"),
32
+ Gate(name="Type checks clean", command="uv run pyright src/"),
33
+ Gate(name="Lint clean", command="uv run ruff check src/"),
34
+ Gate(name="Security scan", command="uv run bandit -r src/ -q -ll"),
35
+ Gate(name="Build succeeds", command="uv build"),
36
+ Gate(name="Package validates", command="uv run twine check dist/*"),
37
+ ]
38
+
39
+
40
+ def _run_command(command: str, cwd: Path) -> tuple[bool, str]:
41
+ """Run a shell command and return (success, output).
42
+
43
+ Args:
44
+ command: Shell command string.
45
+ cwd: Working directory.
46
+
47
+ Returns:
48
+ Tuple of (passed, message).
49
+ """
50
+ try:
51
+ import shlex
52
+
53
+ result = subprocess.run(
54
+ shlex.split(command),
55
+ cwd=cwd,
56
+ capture_output=True,
57
+ text=True,
58
+ timeout=300,
59
+ )
60
+ output = result.stdout.strip() or result.stderr.strip()
61
+ if result.returncode == 0:
62
+ return (True, output or "OK")
63
+ return (False, output or f"Exit code {result.returncode}")
64
+ except subprocess.TimeoutExpired:
65
+ return (False, "Timed out after 300s")
66
+ except FileNotFoundError:
67
+ return (False, f"Command not found: {command}")
68
+
69
+
70
+ def _extract_version(path: Path, pattern: str) -> str | None:
71
+ """Extract a version string from a file using a regex pattern.
72
+
73
+ Args:
74
+ path: File to read.
75
+ pattern: Regex with a capture group for the version.
76
+
77
+ Returns:
78
+ Extracted version string or None.
79
+ """
80
+ if not path.exists():
81
+ return None
82
+ content = path.read_text(encoding="utf-8")
83
+ match = re.search(pattern, content)
84
+ if match:
85
+ return match.group(1)
86
+ return None
87
+
88
+
89
+ def run_checks(
90
+ *,
91
+ project_root: Path,
92
+ pyproject_path: Path,
93
+ init_path: Path,
94
+ changelog_path: Path,
95
+ ) -> list[CheckResult]:
96
+ """Run all quality gates and return results.
97
+
98
+ Args:
99
+ project_root: Project root directory.
100
+ pyproject_path: Path to pyproject.toml.
101
+ init_path: Path to __init__.py with __version__.
102
+ changelog_path: Path to CHANGELOG.md.
103
+
104
+ Returns:
105
+ List of CheckResult for each gate.
106
+ """
107
+ from raise_cli.publish.changelog import has_unreleased_entries
108
+ from raise_cli.publish.version import is_pep440
109
+
110
+ results: list[CheckResult] = []
111
+
112
+ # 1-7: Command-based gates
113
+ for gate in _COMMAND_GATES:
114
+ passed, message = _run_command(gate.command, project_root)
115
+ results.append(CheckResult(gate=gate.name, passed=passed, message=message))
116
+
117
+ # 8: Changelog has unreleased entries
118
+ if changelog_path.exists():
119
+ content = changelog_path.read_text(encoding="utf-8")
120
+ has_entries = has_unreleased_entries(content)
121
+ results.append(
122
+ CheckResult(
123
+ gate="CHANGELOG has unreleased entries",
124
+ passed=has_entries,
125
+ message="Unreleased entries found"
126
+ if has_entries
127
+ else "No unreleased entries",
128
+ )
129
+ )
130
+ else:
131
+ results.append(
132
+ CheckResult(
133
+ gate="CHANGELOG has unreleased entries",
134
+ passed=False,
135
+ message=f"File not found: {changelog_path}",
136
+ )
137
+ )
138
+
139
+ # 9: Version is PEP 440 compliant
140
+ pyproject_version = _extract_version(pyproject_path, r'version\s*=\s*"([^"]*)"')
141
+ if pyproject_version and is_pep440(pyproject_version):
142
+ results.append(
143
+ CheckResult(
144
+ gate="Version PEP 440 compliant",
145
+ passed=True,
146
+ message=f"{pyproject_version} is valid PEP 440",
147
+ )
148
+ )
149
+ else:
150
+ results.append(
151
+ CheckResult(
152
+ gate="Version PEP 440 compliant",
153
+ passed=False,
154
+ message=f"'{pyproject_version}' is not valid PEP 440"
155
+ if pyproject_version
156
+ else "Could not read version from pyproject.toml",
157
+ )
158
+ )
159
+
160
+ # 10: Version sync between pyproject.toml and __init__.py
161
+ init_version = _extract_version(init_path, r'__version__\s*=\s*"([^"]*)"')
162
+ if pyproject_version and init_version and pyproject_version == init_version:
163
+ results.append(
164
+ CheckResult(
165
+ gate="Version sync",
166
+ passed=True,
167
+ message=f"Both files: {pyproject_version}",
168
+ )
169
+ )
170
+ else:
171
+ results.append(
172
+ CheckResult(
173
+ gate="Version sync",
174
+ passed=False,
175
+ message=f"pyproject.toml={pyproject_version}, __init__.py={init_version}",
176
+ )
177
+ )
178
+
179
+ return results