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,177 @@
1
+ """rai doctor — self-diagnostic and bug reporting.
2
+
3
+ Architecture: ADR-045 (DoctorCheck protocol, E352).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ from typing import Annotated
10
+
11
+ import typer
12
+ from rich.console import Console
13
+
14
+ from raise_cli.doctor.models import CheckStatus, DoctorContext
15
+ from raise_cli.doctor.registry import CheckRegistry
16
+ from raise_cli.doctor.runner import PIPELINE_ORDER, run_checks, summarize
17
+
18
+ doctor_app = typer.Typer(
19
+ name="doctor",
20
+ help="Diagnose RaiSE setup, report problems, and auto-fix common issues.",
21
+ invoke_without_command=True,
22
+ )
23
+
24
+ console = Console(stderr=True)
25
+
26
+ _STATUS_STYLE: dict[CheckStatus, tuple[str, str]] = {
27
+ CheckStatus.PASS: ("[green]OK[/green]", " "),
28
+ CheckStatus.WARN: ("[yellow]!![/yellow]", "[yellow]!![/yellow]"),
29
+ CheckStatus.ERROR: ("[red]XX[/red]", "[red]XX[/red]"),
30
+ }
31
+
32
+
33
+ def _format_result_line(category: str, message: str, status: CheckStatus) -> str:
34
+ """Format a single result line for human output."""
35
+ _, prefix = _STATUS_STYLE[status]
36
+ return f" {prefix} {category:14s} {message}"
37
+
38
+
39
+ @doctor_app.callback()
40
+ def doctor(
41
+ ctx: typer.Context,
42
+ verbose: Annotated[
43
+ bool,
44
+ typer.Option("--verbose", "-v", help="Show all checks including passing"),
45
+ ] = False,
46
+ json_output: Annotated[
47
+ bool,
48
+ typer.Option("--json", help="JSON output for CI"),
49
+ ] = False,
50
+ fix: Annotated[
51
+ bool,
52
+ typer.Option("--fix", help="Auto-fix common issues (with backup)"),
53
+ ] = False,
54
+ online: Annotated[
55
+ bool,
56
+ typer.Option("--online", help="Include online checks (MCP, adapter connectivity)"),
57
+ ] = False,
58
+ category: Annotated[
59
+ str | None,
60
+ typer.Option("--category", "-c", help="Run only this check category"),
61
+ ] = None,
62
+ ) -> None:
63
+ """Run diagnostic checks on your RaiSE setup."""
64
+ if ctx.invoked_subcommand is not None:
65
+ return
66
+
67
+ registry = CheckRegistry()
68
+ registry.discover()
69
+
70
+ categories = [category] if category else None
71
+ if category and category not in PIPELINE_ORDER:
72
+ available = ", ".join(PIPELINE_ORDER)
73
+ console.print(f"Unknown category '{category}'. Available: {available}")
74
+ raise typer.Exit(1)
75
+
76
+ context = DoctorContext(
77
+ working_dir=Path.cwd(),
78
+ online=online,
79
+ verbose=verbose,
80
+ )
81
+
82
+ results = run_checks(registry, context, categories)
83
+ passes, warns, errors = summarize(results)
84
+
85
+ if json_output:
86
+ import json
87
+
88
+ out = Console()
89
+ data = {
90
+ "results": [
91
+ {
92
+ "check_id": r.check_id,
93
+ "category": r.category,
94
+ "status": r.status.value,
95
+ "message": r.message,
96
+ "fix_hint": r.fix_hint,
97
+ "details": list(r.details),
98
+ }
99
+ for r in results
100
+ ],
101
+ "summary": {"pass": passes, "warn": warns, "error": errors},
102
+ }
103
+ out.print_json(json.dumps(data))
104
+ else:
105
+ out = Console()
106
+ if not results:
107
+ out.print("No checks registered. Install raise-cli with extras for diagnostics.")
108
+ return
109
+
110
+ for r in results:
111
+ if not verbose and r.status == CheckStatus.PASS:
112
+ continue
113
+ out.print(_format_result_line(r.category, r.message, r.status))
114
+ if r.fix_hint:
115
+ from rich.markup import escape
116
+
117
+ out.print(f" hint: {escape(r.fix_hint)}")
118
+
119
+ if warns == 0 and errors == 0:
120
+ out.print("\n[green]All checks passed.[/green]")
121
+ else:
122
+ parts: list[str] = []
123
+ if warns:
124
+ parts.append(f"{warns} warning{'s' if warns > 1 else ''}")
125
+ if errors:
126
+ parts.append(f"{errors} error{'s' if errors > 1 else ''}")
127
+ out.print(f"\n{', '.join(parts)}.")
128
+
129
+ if fix:
130
+ fixable = [r for r in results if r.fix_id and r.status != CheckStatus.PASS]
131
+ if fixable:
132
+ from raise_cli.doctor.fix import run_fixes
133
+
134
+ out = Console()
135
+ outcomes = run_fixes(fixable, Path.cwd())
136
+ for fix_id, success in outcomes:
137
+ status_label = "[green]fixed[/green]" if success else "[red]failed[/red]"
138
+ out.print(f" fix: {fix_id} -- {status_label}")
139
+
140
+ if errors > 0:
141
+ raise typer.Exit(1)
142
+
143
+
144
+ @doctor_app.command()
145
+ def report(
146
+ send: Annotated[
147
+ bool, typer.Option("--send", help="Open email client with report"),
148
+ ] = False,
149
+ ) -> None:
150
+ """Generate diagnostic report, optionally send via email."""
151
+ from raise_cli.doctor.report import (
152
+ SUPPORT_EMAIL,
153
+ generate_report,
154
+ open_mailto,
155
+ save_report,
156
+ )
157
+
158
+ registry = CheckRegistry()
159
+ registry.discover()
160
+ context = DoctorContext(working_dir=Path.cwd())
161
+ results = run_checks(registry, context)
162
+
163
+ report_data = generate_report(results, Path.cwd())
164
+ saved = save_report(report_data, Path.cwd())
165
+ out = Console()
166
+ out.print(f"Report saved to {saved}")
167
+
168
+ if send:
169
+ opened = open_mailto(report_data)
170
+ if opened:
171
+ out.print("Email client opened. Review and send.")
172
+ else:
173
+ out.print("Could not open email client.")
174
+ out.print(f"Email to: {SUPPORT_EMAIL}")
175
+ out.print(f"Attach or paste the report from: {saved}")
176
+ else:
177
+ out.print("To send: rai doctor report --send")
@@ -0,0 +1,223 @@
1
+ """CLI commands for workflow gate discovery and execution.
2
+
3
+ Provides ``rai gate check`` and ``rai gate list`` for discovering
4
+ and invoking registered WorkflowGate implementations.
5
+
6
+ Architecture: ADR-039 §1 (WorkflowGate Protocol), §5 (Standalone gates)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ from typing import Annotated
14
+
15
+ import typer
16
+ from rich.console import Console
17
+
18
+ from raise_cli.gates.models import GateContext, GateResult
19
+ from raise_cli.gates.protocol import WorkflowGate
20
+ from raise_cli.gates.registry import GateRegistry
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ gate_app = typer.Typer(
25
+ name="gate",
26
+ help="Discover and run workflow gates",
27
+ no_args_is_help=True,
28
+ )
29
+
30
+ console = Console()
31
+
32
+
33
+ def _get_registry() -> GateRegistry:
34
+ """Create and populate a gate registry from entry points."""
35
+ reg = GateRegistry()
36
+ reg.discover()
37
+ return reg
38
+
39
+
40
+ def _run_gate(gate: WorkflowGate, context: GateContext) -> GateResult:
41
+ """Run a single gate with error isolation.
42
+
43
+ Gate exceptions are caught and converted to a failed GateResult.
44
+ Gates never crash the CLI.
45
+ """
46
+ try:
47
+ return gate.evaluate(context)
48
+ except Exception as exc: # noqa: BLE001
49
+ msg = f"{type(exc).__name__}: {exc}"
50
+ logger.warning("Gate '%s' raised: %s", context.gate_id, msg)
51
+ return GateResult(
52
+ passed=False,
53
+ gate_id=context.gate_id,
54
+ message=msg,
55
+ )
56
+
57
+
58
+ def _print_result(result: GateResult) -> None:
59
+ """Print a single gate result in human format."""
60
+ marker = "[green]✓[/green]" if result.passed else "[red]✗[/red]"
61
+ console.print(f" {marker} {result.gate_id}: {result.message}")
62
+ for detail in result.details:
63
+ console.print(f" {detail}")
64
+
65
+
66
+ @gate_app.command("list")
67
+ def list_command(
68
+ format: Annotated[
69
+ str,
70
+ typer.Option("--format", "-f", help="Output format: human or json"),
71
+ ] = "human",
72
+ ) -> None:
73
+ """List all discovered workflow gates.
74
+
75
+ Shows each gate's ID, description, and workflow point.
76
+
77
+ Examples:
78
+ $ rai gate list
79
+ $ rai gate list --format json
80
+ """
81
+ registry = _get_registry()
82
+ gates = registry.gates
83
+
84
+ if not gates:
85
+ if format == "json":
86
+ typer.echo(json.dumps({"gates": []}, indent=2))
87
+ else:
88
+ console.print("No gates discovered.")
89
+ return
90
+
91
+ if format == "json":
92
+ data = [
93
+ {
94
+ "gate_id": g.gate_id,
95
+ "description": g.description,
96
+ "workflow_point": g.workflow_point,
97
+ }
98
+ for g in gates
99
+ ]
100
+ typer.echo(json.dumps({"gates": data}, indent=2))
101
+ else:
102
+ console.print("[bold]Discovered gates:[/bold]\n")
103
+ for g in gates:
104
+ console.print(f" {g.gate_id:<20s} {g.description:<30s} {g.workflow_point}")
105
+
106
+
107
+ @gate_app.command("check")
108
+ def check_command(
109
+ gate_id: Annotated[
110
+ str | None,
111
+ typer.Argument(help="Gate ID to check (omit for --all)"),
112
+ ] = None,
113
+ all_gates: Annotated[
114
+ bool,
115
+ typer.Option("--all", "-a", help="Run all discovered gates"),
116
+ ] = False,
117
+ format: Annotated[
118
+ str,
119
+ typer.Option("--format", "-f", help="Output format: human or json"),
120
+ ] = "human",
121
+ ) -> None:
122
+ """Run workflow gates and report results.
123
+
124
+ Check a specific gate by ID, or use --all to run every discovered gate.
125
+ Exit code 0 when all pass, 1 when any fail.
126
+
127
+ Examples:
128
+ $ rai gate check gate-tests
129
+ $ rai gate check --all
130
+ $ rai gate check --all --format json
131
+ """
132
+ registry = _get_registry()
133
+
134
+ if gate_id and all_gates:
135
+ console.print("[red]Error:[/red] Provide gate_id OR --all, not both.")
136
+ raise typer.Exit(1)
137
+
138
+ if not gate_id and not all_gates:
139
+ console.print("[red]Error:[/red] Provide a gate_id or use --all.")
140
+ raise typer.Exit(1)
141
+
142
+ if gate_id:
143
+ _check_single(registry, gate_id, format)
144
+ else:
145
+ _check_all(registry, format)
146
+
147
+
148
+ def _check_single(registry: GateRegistry, gate_id: str, fmt: str) -> None:
149
+ """Check a single gate by ID."""
150
+ gate = registry.get_gate(gate_id)
151
+ if gate is None:
152
+ if fmt == "json":
153
+ typer.echo(json.dumps({"error": f"Gate '{gate_id}' not found"}))
154
+ else:
155
+ console.print(f"[red]Error:[/red] Gate '{gate_id}' not found.")
156
+ raise typer.Exit(1)
157
+
158
+ context = GateContext(gate_id=gate_id)
159
+ result = _run_gate(gate, context)
160
+
161
+ if fmt == "json":
162
+ typer.echo(
163
+ json.dumps(
164
+ {
165
+ "gate_id": result.gate_id,
166
+ "passed": result.passed,
167
+ "message": result.message,
168
+ "details": list(result.details),
169
+ },
170
+ indent=2,
171
+ )
172
+ )
173
+ else:
174
+ _print_result(result)
175
+
176
+ raise typer.Exit(0 if result.passed else 1)
177
+
178
+
179
+ def _check_all(registry: GateRegistry, fmt: str) -> None:
180
+ """Check all discovered gates."""
181
+ gates = registry.gates
182
+ if not gates:
183
+ if fmt == "json":
184
+ typer.echo(json.dumps({"gates": [], "summary": "No gates discovered"}))
185
+ else:
186
+ console.print("No gates discovered.")
187
+ raise typer.Exit(0)
188
+
189
+ results: list[GateResult] = []
190
+ for gate in gates:
191
+ context = GateContext(gate_id=gate.gate_id)
192
+ result = _run_gate(gate, context)
193
+ results.append(result)
194
+
195
+ failed = [r for r in results if not r.passed]
196
+
197
+ if fmt == "json":
198
+ data = [
199
+ {
200
+ "gate_id": r.gate_id,
201
+ "passed": r.passed,
202
+ "message": r.message,
203
+ "details": list(r.details),
204
+ }
205
+ for r in results
206
+ ]
207
+ typer.echo(
208
+ json.dumps({"gates": data, "all_passed": len(failed) == 0}, indent=2)
209
+ )
210
+ else:
211
+ for r in results:
212
+ _print_result(r)
213
+ console.print()
214
+ if failed:
215
+ console.print(
216
+ f"[red bold]FAILED:[/red bold] {len(failed)} of {len(results)} gates failed"
217
+ )
218
+ else:
219
+ console.print(
220
+ f"[green bold]PASSED:[/green bold] {len(results)} gates passed"
221
+ )
222
+
223
+ raise typer.Exit(1 if failed else 0)