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,141 @@
1
+ """Diagnostic report generation and email submission."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import platform
7
+ import urllib.parse
8
+ import webbrowser
9
+ from dataclasses import dataclass, field
10
+ from datetime import UTC, datetime
11
+ from pathlib import Path
12
+
13
+ from raise_cli.doctor.models import CheckResult, CheckStatus
14
+
15
+ SUPPORT_EMAIL = "support@raise.humansys.ai"
16
+
17
+
18
+ def _str_list() -> list[str]:
19
+ return []
20
+
21
+
22
+ @dataclass
23
+ class DiagnosticReport:
24
+ """Non-sensitive diagnostic snapshot."""
25
+
26
+ timestamp: str
27
+ rai_version: str
28
+ python_version: str
29
+ os_info: str
30
+ check_results: list[CheckResult]
31
+ raise_structure: list[str] = field(default_factory=_str_list)
32
+ installed_extras: list[str] = field(default_factory=_str_list)
33
+
34
+
35
+ def generate_report(
36
+ results: list[CheckResult], working_dir: Path
37
+ ) -> DiagnosticReport:
38
+ """Collect non-sensitive data into report."""
39
+ # Get rai version
40
+ try:
41
+ from importlib.metadata import version
42
+
43
+ rai_ver = version("raise-cli")
44
+ except Exception: # noqa: BLE001
45
+ rai_ver = "unknown"
46
+
47
+ # Collect .raise/ file names (not contents)
48
+ raise_dir = working_dir / ".raise"
49
+ structure: list[str] = []
50
+ if raise_dir.exists():
51
+ for p in sorted(raise_dir.rglob("*")):
52
+ if p.is_file():
53
+ structure.append(str(p.relative_to(working_dir)))
54
+
55
+ # Detect installed extras via find_spec (no actual import)
56
+ extras: list[str] = []
57
+ for pkg in ("mcp", "httpx"):
58
+ if importlib.util.find_spec(pkg) is not None:
59
+ extras.append(pkg)
60
+
61
+ return DiagnosticReport(
62
+ timestamp=datetime.now(UTC).isoformat(),
63
+ rai_version=rai_ver,
64
+ python_version=platform.python_version(),
65
+ os_info=f"{platform.system()} {platform.release()} ({platform.machine()})",
66
+ check_results=results,
67
+ raise_structure=structure[:50], # cap at 50 files
68
+ installed_extras=extras,
69
+ )
70
+
71
+
72
+ def report_to_markdown(report: DiagnosticReport) -> str:
73
+ """Render report as markdown."""
74
+ extras_str = ", ".join(report.installed_extras) if report.installed_extras else "none"
75
+ lines = [
76
+ "# rai doctor report",
77
+ "",
78
+ f"**Timestamp:** {report.timestamp}",
79
+ f"**raise-cli:** {report.rai_version}",
80
+ f"**Python:** {report.python_version}",
81
+ f"**OS:** {report.os_info}",
82
+ f"**Extras:** {extras_str}",
83
+ "",
84
+ "## Check Results",
85
+ "",
86
+ ]
87
+ for r in report.check_results:
88
+ icon = {"pass": "OK", "warn": "!!", "error": "XX"}[r.status.value]
89
+ lines.append(f"- [{icon}] {r.category}: {r.message}")
90
+ if r.fix_hint:
91
+ lines.append(f" - hint: {r.fix_hint}")
92
+
93
+ if report.raise_structure:
94
+ lines.extend(["", "## .raise/ structure", ""])
95
+ for path in report.raise_structure:
96
+ lines.append(f"- {path}")
97
+
98
+ return "\n".join(lines)
99
+
100
+
101
+ def save_report(report: DiagnosticReport, working_dir: Path) -> Path:
102
+ """Save report to .raise/rai/personal/report-{date}.md. Returns path."""
103
+ personal_dir = working_dir / ".raise" / "rai" / "personal"
104
+ personal_dir.mkdir(parents=True, exist_ok=True)
105
+ date_str = datetime.now(UTC).strftime("%Y-%m-%d")
106
+ path = personal_dir / f"report-{date_str}.md"
107
+ path.write_text(report_to_markdown(report))
108
+ return path
109
+
110
+
111
+ def open_mailto(report: DiagnosticReport, to: str = SUPPORT_EMAIL) -> bool:
112
+ """Open default email client via mailto: URI. Returns True if opened."""
113
+ md = report_to_markdown(report)
114
+
115
+ # Count issues for subject
116
+ warns = sum(1 for r in report.check_results if r.status == CheckStatus.WARN)
117
+ errors = sum(1 for r in report.check_results if r.status == CheckStatus.ERROR)
118
+ parts: list[str] = []
119
+ if errors:
120
+ parts.append(f"{errors} error{'s' if errors > 1 else ''}")
121
+ if warns:
122
+ parts.append(f"{warns} warning{'s' if warns > 1 else ''}")
123
+ issue_summary = ", ".join(parts) if parts else "all clear"
124
+
125
+ subject = f"[rai-doctor] {report.rai_version} — {issue_summary}"
126
+
127
+ # Truncate body for mailto (most clients cap at ~2000 chars in URL)
128
+ body = md[:1800]
129
+ if len(md) > 1800:
130
+ body += "\n\n(truncated — full report in local file)"
131
+
132
+ mailto_url = (
133
+ f"mailto:{to}"
134
+ f"?subject={urllib.parse.quote(subject)}"
135
+ f"&body={urllib.parse.quote(body)}"
136
+ )
137
+
138
+ try:
139
+ return webbrowser.open(mailto_url)
140
+ except Exception: # noqa: BLE001
141
+ return False
@@ -0,0 +1,95 @@
1
+ """Doctor check runner — executes checks in pipeline order.
2
+
3
+ Pipeline order ensures dependencies: environment must pass before
4
+ project checks, project before adapters, etc.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+
11
+ from raise_cli.doctor.models import CheckResult, CheckStatus, DoctorContext
12
+ from raise_cli.doctor.registry import CheckRegistry
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ PIPELINE_ORDER: list[str] = [
17
+ "environment",
18
+ "project",
19
+ "adapters",
20
+ "skills",
21
+ "mcp",
22
+ ]
23
+
24
+
25
+ def run_checks(
26
+ registry: CheckRegistry,
27
+ context: DoctorContext,
28
+ categories: list[str] | None = None,
29
+ ) -> list[CheckResult]:
30
+ """Execute checks in pipeline order.
31
+
32
+ Args:
33
+ registry: Check registry with discovered checks.
34
+ context: Execution context (working_dir, online, verbose).
35
+ categories: If provided, only run checks in these categories.
36
+
37
+ Returns:
38
+ All check results in execution order.
39
+ """
40
+ results: list[CheckResult] = []
41
+ critical_failure = False
42
+
43
+ ordered = categories or PIPELINE_ORDER
44
+
45
+ for category in ordered:
46
+ if critical_failure:
47
+ results.append(
48
+ CheckResult(
49
+ check_id=f"{category}-skipped",
50
+ category=category,
51
+ status=CheckStatus.WARN,
52
+ message="Skipped — previous category had critical errors",
53
+ )
54
+ )
55
+ continue
56
+
57
+ checks = registry.get_checks_for_category(category)
58
+ if not checks:
59
+ continue
60
+
61
+ for check in checks:
62
+ if check.requires_online and not context.online:
63
+ logger.debug("Skipping online check '%s'", check.check_id)
64
+ continue
65
+
66
+ try:
67
+ check_results = check.evaluate(context)
68
+ results.extend(check_results)
69
+ except Exception as exc: # noqa: BLE001
70
+ logger.warning("Check '%s' raised: %s", check.check_id, exc)
71
+ results.append(
72
+ CheckResult(
73
+ check_id=check.check_id,
74
+ category=check.category,
75
+ status=CheckStatus.ERROR,
76
+ message=f"Check crashed: {exc}",
77
+ )
78
+ )
79
+
80
+ # If any ERROR in this category, flag for downstream skip
81
+ category_errors = [
82
+ r for r in results if r.category == category and r.status == CheckStatus.ERROR
83
+ ]
84
+ if category_errors and category == "environment":
85
+ critical_failure = True
86
+
87
+ return results
88
+
89
+
90
+ def summarize(results: list[CheckResult]) -> tuple[int, int, int]:
91
+ """Count pass/warn/error results. Returns (pass_count, warn_count, error_count)."""
92
+ passes = sum(1 for r in results if r.status == CheckStatus.PASS)
93
+ warns = sum(1 for r in results if r.status == CheckStatus.WARN)
94
+ errors = sum(1 for r in results if r.status == CheckStatus.ERROR)
95
+ return passes, warns, errors
@@ -0,0 +1,3 @@
1
+ """Engines."""
2
+
3
+ from __future__ import annotations
@@ -0,0 +1,215 @@
1
+ """Centralized exception hierarchy for raise-cli.
2
+
3
+ This module defines all exceptions used throughout raise-cli with:
4
+ - Consistent exit codes for scripting
5
+ - Error codes for documentation/troubleshooting
6
+ - Optional hints and details for user guidance
7
+
8
+ Exit Code Table:
9
+ 0 - Success
10
+ 1 - General error (RaiError)
11
+ 2 - Configuration error
12
+ 3 - Resource not found (kata, gate)
13
+ 4 - Artifact not found
14
+ 5 - Dependency unavailable
15
+ 6 - State corruption
16
+ 7 - Validation error
17
+ 10 - Gate failed
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Any
23
+
24
+
25
+ class RaiError(Exception):
26
+ """Base exception for all raise-cli errors.
27
+
28
+ All raise-cli exceptions inherit from this class, providing:
29
+ - exit_code: Process exit code for scripting
30
+ - error_code: Unique identifier for documentation
31
+ - message: Human-readable error description
32
+ - hint: Optional suggestion for resolution
33
+ - details: Optional structured data for debugging
34
+
35
+ Example:
36
+ >>> raise RaiError("Something went wrong", hint="Try again")
37
+ """
38
+
39
+ exit_code: int = 1
40
+ error_code: str = "E000"
41
+
42
+ def __init__(
43
+ self,
44
+ message: str,
45
+ *,
46
+ hint: str | None = None,
47
+ details: dict[str, Any] | None = None,
48
+ ) -> None:
49
+ """Initialize a RaiError.
50
+
51
+ Args:
52
+ message: Human-readable error description.
53
+ hint: Optional suggestion for resolution.
54
+ details: Optional structured data for debugging.
55
+ """
56
+ self.message = message
57
+ self.hint = hint
58
+ self.details = details or {}
59
+ super().__init__(message)
60
+
61
+ def __str__(self) -> str:
62
+ """Return the error message."""
63
+ return self.message
64
+
65
+ def to_dict(self) -> dict[str, Any]:
66
+ """Convert exception to dictionary for JSON output.
67
+
68
+ Returns:
69
+ Dictionary with error_code, exit_code, message, hint, and details.
70
+ """
71
+ return {
72
+ "error_code": self.error_code,
73
+ "exit_code": self.exit_code,
74
+ "message": self.message,
75
+ "hint": self.hint,
76
+ "details": self.details,
77
+ }
78
+
79
+
80
+ class ConfigurationError(RaiError):
81
+ """Configuration-related errors.
82
+
83
+ Raised when:
84
+ - Config file is malformed
85
+ - Required configuration is missing
86
+ - Configuration values are invalid
87
+
88
+ Exit code: 2
89
+ """
90
+
91
+ exit_code: int = 2
92
+ error_code: str = "E001"
93
+
94
+
95
+ class KataNotFoundError(RaiError):
96
+ """Kata definition not found.
97
+
98
+ Raised when a requested kata ID does not exist in .raise/katas/.
99
+
100
+ Exit code: 3
101
+ """
102
+
103
+ exit_code: int = 3
104
+ error_code: str = "E002"
105
+
106
+
107
+ class GateNotFoundError(RaiError):
108
+ """Gate definition not found.
109
+
110
+ Raised when a requested gate ID does not exist in .raise/gates/.
111
+
112
+ Exit code: 3
113
+ """
114
+
115
+ exit_code: int = 3
116
+ error_code: str = "E003"
117
+
118
+
119
+ class ArtifactNotFoundError(RaiError):
120
+ """Artifact file not found.
121
+
122
+ Raised when a referenced artifact path does not exist.
123
+
124
+ Exit code: 4
125
+ """
126
+
127
+ exit_code: int = 4
128
+ error_code: str = "E004"
129
+
130
+
131
+ class DependencyError(RaiError):
132
+ """External dependency not available.
133
+
134
+ Raised when a required external tool is not installed or accessible:
135
+ - git
136
+ - ast-grep
137
+ - ripgrep
138
+
139
+ Exit code: 5
140
+ """
141
+
142
+ exit_code: int = 5
143
+ error_code: str = "E005"
144
+
145
+
146
+ class StateError(RaiError):
147
+ """State file corrupted or invalid.
148
+
149
+ Raised when:
150
+ - State file cannot be parsed
151
+ - State schema validation fails
152
+ - State version mismatch
153
+
154
+ Exit code: 6
155
+ """
156
+
157
+ exit_code: int = 6
158
+ error_code: str = "E006"
159
+
160
+
161
+ class ValidationError(RaiError):
162
+ """Schema or artifact validation failed.
163
+
164
+ Raised when:
165
+ - Pydantic schema validation fails
166
+ - Artifact content validation fails
167
+ - Input format is invalid
168
+
169
+ Exit code: 7
170
+ """
171
+
172
+ exit_code: int = 7
173
+ error_code: str = "E007"
174
+
175
+
176
+ class GateFailedError(RaiError):
177
+ """Gate validation did not pass.
178
+
179
+ Raised when one or more required gate criteria fail.
180
+ This is a "business logic" failure, not a system error.
181
+
182
+ Exit code: 10
183
+ """
184
+
185
+ exit_code: int = 10
186
+ error_code: str = "E010"
187
+
188
+
189
+ class RaiSessionNotFoundError(RaiError):
190
+ """Session ID not provided or resolvable.
191
+
192
+ Raised when:
193
+ - Neither --session flag nor RAI_SESSION_ID env var is provided
194
+ - Session ID is required but cannot be determined
195
+
196
+ Exit code: 2 (configuration error - user must provide session context)
197
+ """
198
+
199
+ exit_code: int = 2
200
+ error_code: str = "E011"
201
+
202
+
203
+ # All public exceptions
204
+ __all__ = [
205
+ "RaiError",
206
+ "ConfigurationError",
207
+ "KataNotFoundError",
208
+ "GateNotFoundError",
209
+ "ArtifactNotFoundError",
210
+ "DependencyError",
211
+ "StateError",
212
+ "ValidationError",
213
+ "GateFailedError",
214
+ "RaiSessionNotFoundError",
215
+ ]
@@ -0,0 +1,19 @@
1
+ """Workflow gates infrastructure for raise-cli.
2
+
3
+ Provides standalone quality gates that validate workflow transitions.
4
+ Gates are independent of the event emitter (AD-5) and can be invoked
5
+ directly via ``rai gate check``.
6
+
7
+ Architecture: ADR-039 §1 (WorkflowGate Protocol), §5 (Standalone gates)
8
+ """
9
+
10
+ from raise_cli.gates.models import GateContext, GateResult
11
+ from raise_cli.gates.protocol import WorkflowGate
12
+ from raise_cli.gates.registry import GateRegistry
13
+
14
+ __all__ = [
15
+ "GateContext",
16
+ "GateRegistry",
17
+ "GateResult",
18
+ "WorkflowGate",
19
+ ]
@@ -0,0 +1 @@
1
+ """Built-in quality gates for raise-cli (COMMUNITY tier)."""
@@ -0,0 +1,52 @@
1
+ """Built-in CoverageGate — validates test coverage collection succeeds.
2
+
3
+ Runs ``pytest --cov --cov-report=term-missing -q`` and reports pass/fail.
4
+ Per PAT-E-444, coverage is diagnostic — this gate checks that coverage
5
+ collection succeeds, not a specific percentage threshold.
6
+
7
+ Architecture: ADR-039 §5 (Built-in gates), S248.6
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import subprocess
13
+ from typing import ClassVar
14
+
15
+ from raise_cli.gates.models import GateContext, GateResult
16
+
17
+
18
+ class CoverageGate:
19
+ """Quality gate that runs pytest with coverage.
20
+
21
+ Registered via ``rai.gates`` entry point in pyproject.toml.
22
+ """
23
+
24
+ gate_id: ClassVar[str] = "gate-coverage"
25
+ description: ClassVar[str] = "Coverage collection succeeds"
26
+ workflow_point: ClassVar[str] = "before:release:publish"
27
+
28
+ def evaluate(self, context: GateContext) -> GateResult:
29
+ """Run pytest --cov and return pass/fail result."""
30
+ try:
31
+ result = subprocess.run(
32
+ ["pytest", "--cov", "--cov-report=term-missing", "-q"],
33
+ capture_output=True,
34
+ text=True,
35
+ cwd=str(context.working_dir),
36
+ )
37
+ except Exception as exc: # noqa: BLE001
38
+ return GateResult(
39
+ passed=False,
40
+ gate_id=self.gate_id,
41
+ message=f"{type(exc).__name__}: {exc}",
42
+ )
43
+
44
+ passed = result.returncode == 0
45
+ return GateResult(
46
+ passed=passed,
47
+ gate_id=self.gate_id,
48
+ message="Coverage collection succeeds"
49
+ if passed
50
+ else "Coverage check failed",
51
+ details=(result.stdout,) if not passed else (),
52
+ )
@@ -0,0 +1,48 @@
1
+ """Built-in LintGate — validates linting passes.
2
+
3
+ Runs ``ruff check .`` and reports pass/fail.
4
+
5
+ Architecture: ADR-039 §5 (Built-in gates), S248.6
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import subprocess
11
+ from typing import ClassVar
12
+
13
+ from raise_cli.gates.models import GateContext, GateResult
14
+
15
+
16
+ class LintGate:
17
+ """Quality gate that runs ruff.
18
+
19
+ Registered via ``rai.gates`` entry point in pyproject.toml.
20
+ """
21
+
22
+ gate_id: ClassVar[str] = "gate-lint"
23
+ description: ClassVar[str] = "Linting passes"
24
+ workflow_point: ClassVar[str] = "before:release:publish"
25
+
26
+ def evaluate(self, context: GateContext) -> GateResult:
27
+ """Run ruff check and return pass/fail result."""
28
+ try:
29
+ result = subprocess.run(
30
+ ["ruff", "check", "."],
31
+ capture_output=True,
32
+ text=True,
33
+ cwd=str(context.working_dir),
34
+ )
35
+ except Exception as exc: # noqa: BLE001
36
+ return GateResult(
37
+ passed=False,
38
+ gate_id=self.gate_id,
39
+ message=f"{type(exc).__name__}: {exc}",
40
+ )
41
+
42
+ passed = result.returncode == 0
43
+ return GateResult(
44
+ passed=passed,
45
+ gate_id=self.gate_id,
46
+ message="Linting passes" if passed else "Lint errors found",
47
+ details=(result.stdout,) if not passed else (),
48
+ )
@@ -0,0 +1,48 @@
1
+ """Built-in TestGate — validates all tests pass.
2
+
3
+ Runs ``pytest -x --tb=short`` and reports pass/fail.
4
+
5
+ Architecture: ADR-039 §5 (Built-in gates), S248.6
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import subprocess
11
+ from typing import ClassVar
12
+
13
+ from raise_cli.gates.models import GateContext, GateResult
14
+
15
+
16
+ class TestGate:
17
+ """Quality gate that runs pytest.
18
+
19
+ Registered via ``rai.gates`` entry point in pyproject.toml.
20
+ """
21
+
22
+ gate_id: ClassVar[str] = "gate-tests"
23
+ description: ClassVar[str] = "All tests pass"
24
+ workflow_point: ClassVar[str] = "before:release:publish"
25
+
26
+ def evaluate(self, context: GateContext) -> GateResult:
27
+ """Run pytest and return pass/fail result."""
28
+ try:
29
+ result = subprocess.run(
30
+ ["pytest", "-x", "--tb=short"],
31
+ capture_output=True,
32
+ text=True,
33
+ cwd=str(context.working_dir),
34
+ )
35
+ except Exception as exc: # noqa: BLE001
36
+ return GateResult(
37
+ passed=False,
38
+ gate_id=self.gate_id,
39
+ message=f"{type(exc).__name__}: {exc}",
40
+ )
41
+
42
+ passed = result.returncode == 0
43
+ return GateResult(
44
+ passed=passed,
45
+ gate_id=self.gate_id,
46
+ message="Tests pass" if passed else "Tests failing",
47
+ details=(result.stdout,) if not passed else (),
48
+ )
@@ -0,0 +1,48 @@
1
+ """Built-in TypeGate — validates no type errors.
2
+
3
+ Runs ``pyright`` and reports pass/fail.
4
+
5
+ Architecture: ADR-039 §5 (Built-in gates), S248.6
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import subprocess
11
+ from typing import ClassVar
12
+
13
+ from raise_cli.gates.models import GateContext, GateResult
14
+
15
+
16
+ class TypeGate:
17
+ """Quality gate that runs pyright.
18
+
19
+ Registered via ``rai.gates`` entry point in pyproject.toml.
20
+ """
21
+
22
+ gate_id: ClassVar[str] = "gate-types"
23
+ description: ClassVar[str] = "No type errors"
24
+ workflow_point: ClassVar[str] = "before:release:publish"
25
+
26
+ def evaluate(self, context: GateContext) -> GateResult:
27
+ """Run pyright and return pass/fail result."""
28
+ try:
29
+ result = subprocess.run(
30
+ ["pyright"],
31
+ capture_output=True,
32
+ text=True,
33
+ cwd=str(context.working_dir),
34
+ )
35
+ except Exception as exc: # noqa: BLE001
36
+ return GateResult(
37
+ passed=False,
38
+ gate_id=self.gate_id,
39
+ message=f"{type(exc).__name__}: {exc}",
40
+ )
41
+
42
+ passed = result.returncode == 0
43
+ return GateResult(
44
+ passed=passed,
45
+ gate_id=self.gate_id,
46
+ message="No type errors" if passed else "Type errors found",
47
+ details=(result.stdout,) if not passed else (),
48
+ )