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,4 @@
1
+ """rai doctor — self-diagnostic, coherence audit, and bug reporting.
2
+
3
+ Architecture: ADR-045 (DoctorCheck protocol, separate from WorkflowGates).
4
+ """
@@ -0,0 +1 @@
1
+ """Built-in doctor checks, registered via rai.doctor.checks entry points."""
@@ -0,0 +1,110 @@
1
+ """Environment diagnostic check — Python, raise-cli, OS, optional extras.
2
+
3
+ Reports Python version, raise-cli version, OS platform, and whether optional
4
+ extras (mcp, api) are installed.
5
+
6
+ Architecture: ADR-045.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib
12
+ import platform
13
+ import sys
14
+ from typing import ClassVar
15
+
16
+ from raise_cli.doctor.models import CheckResult, CheckStatus, DoctorContext
17
+
18
+ _MIN_PYTHON: tuple[int, int] = (3, 11)
19
+
20
+ _OPTIONAL_EXTRAS: tuple[tuple[str, str, str], ...] = (
21
+ ("mcp", "mcp", "pip install raise-cli[mcp]"),
22
+ ("httpx", "httpx", "pip install raise-cli[api]"),
23
+ )
24
+
25
+
26
+ class EnvironmentCheck:
27
+ """Validates Python version, raise-cli version, OS, and installed extras."""
28
+
29
+ check_id: ClassVar[str] = "environment"
30
+ category: ClassVar[str] = "environment"
31
+ description: ClassVar[str] = "Python version, raise-cli version, OS, installed extras"
32
+ requires_online: ClassVar[bool] = False
33
+
34
+ def evaluate(self, context: DoctorContext) -> list[CheckResult]:
35
+ results: list[CheckResult] = []
36
+ results.append(self._check_python_version())
37
+ results.append(self._check_rai_version())
38
+ results.append(self._check_os_info())
39
+ results.extend(self._check_optional_extras())
40
+ return results
41
+
42
+ # ------------------------------------------------------------------
43
+ # Individual checks
44
+ # ------------------------------------------------------------------
45
+
46
+ @staticmethod
47
+ def _check_python_version() -> CheckResult:
48
+ current = sys.version_info[:2]
49
+ version_str = f"{current[0]}.{current[1]}"
50
+ if current >= _MIN_PYTHON:
51
+ return CheckResult(
52
+ check_id="env-python-version",
53
+ category="environment",
54
+ status=CheckStatus.PASS,
55
+ message=f"Python {version_str}",
56
+ )
57
+ return CheckResult(
58
+ check_id="env-python-version",
59
+ category="environment",
60
+ status=CheckStatus.ERROR,
61
+ message=f"Python {version_str} (>= {_MIN_PYTHON[0]}.{_MIN_PYTHON[1]} required)",
62
+ fix_hint=f"Install Python >= {_MIN_PYTHON[0]}.{_MIN_PYTHON[1]}",
63
+ )
64
+
65
+ @staticmethod
66
+ def _check_rai_version() -> CheckResult:
67
+ from raise_cli import __version__
68
+
69
+ return CheckResult(
70
+ check_id="env-rai-version",
71
+ category="environment",
72
+ status=CheckStatus.PASS,
73
+ message=f"raise-cli {__version__}",
74
+ )
75
+
76
+ @staticmethod
77
+ def _check_os_info() -> CheckResult:
78
+ os_info = f"{platform.system()} {platform.release()} ({platform.machine()})"
79
+ return CheckResult(
80
+ check_id="env-os-info",
81
+ category="environment",
82
+ status=CheckStatus.PASS,
83
+ message=os_info,
84
+ )
85
+
86
+ @staticmethod
87
+ def _check_optional_extras() -> list[CheckResult]:
88
+ results: list[CheckResult] = []
89
+ for extra_name, module_name, fix_hint in _OPTIONAL_EXTRAS:
90
+ try:
91
+ importlib.import_module(module_name)
92
+ results.append(
93
+ CheckResult(
94
+ check_id=f"env-extra-{extra_name}",
95
+ category="environment",
96
+ status=CheckStatus.PASS,
97
+ message=f"Optional extra '{extra_name}' installed",
98
+ )
99
+ )
100
+ except ImportError:
101
+ results.append(
102
+ CheckResult(
103
+ check_id=f"env-extra-{extra_name}",
104
+ category="environment",
105
+ status=CheckStatus.WARN,
106
+ message=f"Optional extra '{extra_name}' not installed",
107
+ fix_hint=fix_hint,
108
+ )
109
+ )
110
+ return results
@@ -0,0 +1,238 @@
1
+ """Built-in ProjectCheck — validates .raise/ structure and project coherence.
2
+
3
+ Checks .raise/ directory, manifest.yaml, graph staleness, adapter config,
4
+ skill deployment, and .gitignore entries.
5
+
6
+ Architecture: ADR-045, S352.3
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import time
12
+ from pathlib import Path
13
+ from typing import ClassVar
14
+
15
+ import yaml
16
+
17
+ from raise_cli.doctor.models import CheckResult, CheckStatus, DoctorContext
18
+
19
+
20
+ class ProjectCheck:
21
+ """Diagnostic check for .raise/ project structure and coherence.
22
+
23
+ Registered via ``rai.doctor.checks`` entry point in pyproject.toml.
24
+ """
25
+
26
+ check_id: ClassVar[str] = "project"
27
+ category: ClassVar[str] = "project"
28
+ description: ClassVar[str] = (
29
+ ".raise/ structure, manifest, graph staleness, config"
30
+ )
31
+ requires_online: ClassVar[bool] = False
32
+
33
+ _GRAPH_STALENESS_DAYS: ClassVar[int] = 7
34
+
35
+ def evaluate(self, context: DoctorContext) -> list[CheckResult]:
36
+ """Run all project coherence checks."""
37
+ root = context.working_dir
38
+ results: list[CheckResult] = []
39
+
40
+ results.append(self._check_raise_dir(root))
41
+ results.append(self._check_manifest(root))
42
+ results.append(self._check_graph_staleness(root))
43
+ results.append(self._check_adapter_config(root))
44
+ results.append(self._check_skills_deployed(root))
45
+ results.append(self._check_gitignore(root))
46
+
47
+ return results
48
+
49
+ def _check_raise_dir(self, root: Path) -> CheckResult:
50
+ """Check that .raise/ directory exists."""
51
+ raise_dir = root / ".raise"
52
+ if raise_dir.is_dir():
53
+ return CheckResult(
54
+ check_id="project-raise-dir",
55
+ category=self.category,
56
+ status=CheckStatus.PASS,
57
+ message=".raise/ directory exists",
58
+ )
59
+ return CheckResult(
60
+ check_id="project-raise-dir",
61
+ category=self.category,
62
+ status=CheckStatus.ERROR,
63
+ message=".raise/ directory missing",
64
+ fix_hint="run: rai init",
65
+ )
66
+
67
+ def _check_manifest(self, root: Path) -> CheckResult:
68
+ """Check that manifest.yaml exists and is valid YAML."""
69
+ manifest = root / ".raise" / "manifest.yaml"
70
+ if not manifest.is_file():
71
+ return CheckResult(
72
+ check_id="project-manifest",
73
+ category=self.category,
74
+ status=CheckStatus.ERROR,
75
+ message="manifest.yaml missing",
76
+ fix_hint="run: rai init",
77
+ )
78
+ try:
79
+ content = manifest.read_text(encoding="utf-8")
80
+ parsed = yaml.safe_load(content)
81
+ if not isinstance(parsed, dict):
82
+ return CheckResult(
83
+ check_id="project-manifest",
84
+ category=self.category,
85
+ status=CheckStatus.ERROR,
86
+ message="manifest.yaml is not a valid YAML mapping",
87
+ )
88
+ except yaml.YAMLError as exc:
89
+ return CheckResult(
90
+ check_id="project-manifest",
91
+ category=self.category,
92
+ status=CheckStatus.ERROR,
93
+ message=f"manifest.yaml has invalid YAML: {exc}",
94
+ )
95
+ return CheckResult(
96
+ check_id="project-manifest",
97
+ category=self.category,
98
+ status=CheckStatus.PASS,
99
+ message="manifest.yaml is valid",
100
+ )
101
+
102
+ def _check_graph_staleness(self, root: Path) -> CheckResult:
103
+ """Check if graph index exists and is not stale."""
104
+ # Graph is stored at .raise/rai/memory/index.json (rai graph build output)
105
+ graph_index = root / ".raise" / "rai" / "memory" / "index.json"
106
+ if not graph_index.is_file():
107
+ return CheckResult(
108
+ check_id="project-graph",
109
+ category=self.category,
110
+ status=CheckStatus.WARN,
111
+ message="graph not built (.raise/rai/memory/index.json missing)",
112
+ fix_hint="run: rai graph build",
113
+ fix_id="rebuild-graph",
114
+ )
115
+
116
+ graph_files = [graph_index]
117
+ # Also check for additional graph files in the directory
118
+ graph_dir = graph_index.parent
119
+ graph_files.extend(f for f in graph_dir.rglob("*") if f.is_file() and f != graph_index)
120
+ if not graph_files:
121
+ return CheckResult(
122
+ check_id="project-graph",
123
+ category=self.category,
124
+ status=CheckStatus.WARN,
125
+ message="graph directory is empty",
126
+ fix_hint="run: rai graph build",
127
+ fix_id="rebuild-graph",
128
+ )
129
+
130
+ newest_graph = max(f.stat().st_mtime for f in graph_files)
131
+ now = time.time()
132
+ days_old = (now - newest_graph) / 86400
133
+
134
+ if days_old > self._GRAPH_STALENESS_DAYS:
135
+ return CheckResult(
136
+ check_id="project-graph",
137
+ category=self.category,
138
+ status=CheckStatus.WARN,
139
+ message=f"graph is {days_old:.0f} days old",
140
+ fix_hint="run: rai graph build",
141
+ fix_id="rebuild-graph",
142
+ )
143
+
144
+ # Check if governance files are newer than graph
145
+ governance_dir = root / "governance"
146
+ if governance_dir.is_dir():
147
+ gov_files = list(governance_dir.rglob("*.md"))
148
+ if gov_files:
149
+ newest_gov = max(f.stat().st_mtime for f in gov_files)
150
+ if newest_gov > newest_graph:
151
+ return CheckResult(
152
+ check_id="project-graph",
153
+ category=self.category,
154
+ status=CheckStatus.WARN,
155
+ message="governance files are newer than graph",
156
+ fix_hint="run: rai graph build",
157
+ fix_id="rebuild-graph",
158
+ )
159
+
160
+ return CheckResult(
161
+ check_id="project-graph",
162
+ category=self.category,
163
+ status=CheckStatus.PASS,
164
+ message="graph is up to date",
165
+ )
166
+
167
+ def _check_adapter_config(self, root: Path) -> CheckResult:
168
+ """Check if adapter config files are present (informational)."""
169
+ jira_config = root / ".raise" / "jira.yaml"
170
+ if jira_config.is_file():
171
+ return CheckResult(
172
+ check_id="project-adapter-config",
173
+ category=self.category,
174
+ status=CheckStatus.PASS,
175
+ message="jira.yaml adapter config found",
176
+ )
177
+ return CheckResult(
178
+ check_id="project-adapter-config",
179
+ category=self.category,
180
+ status=CheckStatus.WARN,
181
+ message="jira.yaml not found (optional — needed for Jira integration)",
182
+ )
183
+
184
+ def _check_skills_deployed(self, root: Path) -> CheckResult:
185
+ """Check if skills are deployed to .claude/skills/."""
186
+ skills_dir = root / ".claude" / "skills"
187
+ if not skills_dir.is_dir():
188
+ return CheckResult(
189
+ check_id="project-skills",
190
+ category=self.category,
191
+ status=CheckStatus.WARN,
192
+ message=".claude/skills/ directory missing",
193
+ fix_hint="run: rai skill sync",
194
+ )
195
+ skill_files = list(skills_dir.rglob("SKILL.md"))
196
+ if not skill_files:
197
+ return CheckResult(
198
+ check_id="project-skills",
199
+ category=self.category,
200
+ status=CheckStatus.WARN,
201
+ message="no skills deployed in .claude/skills/",
202
+ fix_hint="run: rai skill sync",
203
+ )
204
+ return CheckResult(
205
+ check_id="project-skills",
206
+ category=self.category,
207
+ status=CheckStatus.PASS,
208
+ message=f"{len(skill_files)} skills deployed",
209
+ )
210
+
211
+ def _check_gitignore(self, root: Path) -> CheckResult:
212
+ """Check if .raise/rai/personal/ is in .gitignore."""
213
+ gitignore = root / ".gitignore"
214
+ if not gitignore.is_file():
215
+ return CheckResult(
216
+ check_id="project-gitignore",
217
+ category=self.category,
218
+ status=CheckStatus.WARN,
219
+ message=".gitignore missing",
220
+ fix_hint="run: rai init",
221
+ )
222
+ content = gitignore.read_text(encoding="utf-8")
223
+ # Check for the personal directory pattern
224
+ if ".raise/rai/personal/" in content or ".raise/rai/personal" in content:
225
+ return CheckResult(
226
+ check_id="project-gitignore",
227
+ category=self.category,
228
+ status=CheckStatus.PASS,
229
+ message=".raise/rai/personal/ is in .gitignore",
230
+ )
231
+ return CheckResult(
232
+ check_id="project-gitignore",
233
+ category=self.category,
234
+ status=CheckStatus.WARN,
235
+ message=".raise/rai/personal/ not found in .gitignore",
236
+ fix_hint="run: rai init",
237
+ fix_id="add-gitignore-personal",
238
+ )
@@ -0,0 +1,80 @@
1
+ """Auto-fix actions for rai doctor --fix.
2
+
3
+ Each fix is a function registered via @register_fix(fix_id).
4
+ Fix IDs correspond to CheckResult.fix_id values set by checks.
5
+
6
+ Before any mutation, backups are created (.bak suffix).
7
+
8
+ Architecture: S352.4
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import shutil
14
+ import subprocess
15
+ import sys
16
+ from collections.abc import Callable
17
+ from pathlib import Path
18
+
19
+ from raise_cli.doctor.models import CheckResult
20
+
21
+ FIX_REGISTRY: dict[str, Callable[[Path], bool]] = {}
22
+
23
+
24
+ def register_fix(fix_id: str) -> Callable[[Callable[[Path], bool]], Callable[[Path], bool]]:
25
+ """Decorator to register a fix function by ID."""
26
+
27
+ def decorator(fn: Callable[[Path], bool]) -> Callable[[Path], bool]:
28
+ FIX_REGISTRY[fix_id] = fn
29
+ return fn
30
+
31
+ return decorator
32
+
33
+
34
+ @register_fix("rebuild-graph")
35
+ def rebuild_graph(working_dir: Path) -> bool:
36
+ """Rebuild the knowledge graph by invoking ``rai graph build``."""
37
+ result = subprocess.run(
38
+ [sys.executable, "-m", "raise_cli", "graph", "build"],
39
+ cwd=working_dir,
40
+ capture_output=True,
41
+ text=True,
42
+ )
43
+ return result.returncode == 0
44
+
45
+
46
+ @register_fix("add-gitignore-personal")
47
+ def add_gitignore_personal(working_dir: Path) -> bool:
48
+ """Add .raise/rai/personal/ to .gitignore with backup."""
49
+ gitignore = working_dir / ".gitignore"
50
+ entry = ".raise/rai/personal/"
51
+
52
+ if gitignore.exists():
53
+ content = gitignore.read_text(encoding="utf-8")
54
+ if entry in content:
55
+ return True # already present, no-op
56
+ # Backup before mutation
57
+ shutil.copy2(gitignore, working_dir / ".gitignore.bak")
58
+ else:
59
+ content = ""
60
+
61
+ with open(gitignore, "a", encoding="utf-8") as f:
62
+ f.write(f"\n# RaiSE personal data (sessions, telemetry)\n{entry}\n")
63
+ return True
64
+
65
+
66
+ def run_fixes(
67
+ results: list[CheckResult],
68
+ working_dir: Path,
69
+ ) -> list[tuple[str, bool]]:
70
+ """Run fixes for all results that have a fix_id.
71
+
72
+ Returns:
73
+ List of (fix_id, success) pairs for each attempted fix.
74
+ """
75
+ outcomes: list[tuple[str, bool]] = []
76
+ for r in results:
77
+ if r.fix_id and r.fix_id in FIX_REGISTRY:
78
+ success = FIX_REGISTRY[r.fix_id](working_dir)
79
+ outcomes.append((r.fix_id, success))
80
+ return outcomes
@@ -0,0 +1,56 @@
1
+ """Doctor check result types.
2
+
3
+ Frozen dataclasses (not Pydantic) — internal infrastructure, not boundary
4
+ objects. Same rationale as gate models (ADR-039 S2).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+ from pathlib import Path
12
+
13
+
14
+ class CheckStatus(Enum):
15
+ """Three-level severity for doctor checks."""
16
+
17
+ PASS = "pass"
18
+ WARN = "warn"
19
+ ERROR = "error"
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class CheckResult:
24
+ """Result from a single diagnostic check.
25
+
26
+ Attributes:
27
+ check_id: Unique identifier (e.g. ``"env-python-version"``).
28
+ category: Grouping key for output (e.g. ``"environment"``).
29
+ status: Pass, warning, or error.
30
+ message: Human-readable summary.
31
+ fix_hint: Actionable suggestion (e.g. ``"run: pip install raise-cli[mcp]"``).
32
+ details: Additional detail lines.
33
+ """
34
+
35
+ check_id: str
36
+ category: str
37
+ status: CheckStatus
38
+ message: str
39
+ fix_hint: str = ""
40
+ fix_id: str = ""
41
+ details: tuple[str, ...] = ()
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class DoctorContext:
46
+ """Context passed to each check's ``evaluate()`` method.
47
+
48
+ Attributes:
49
+ working_dir: Project root directory.
50
+ online: Whether online checks (MCP, adapters) should run.
51
+ verbose: Whether to include extra detail.
52
+ """
53
+
54
+ working_dir: Path = field(default_factory=Path.cwd)
55
+ online: bool = False
56
+ verbose: bool = False
@@ -0,0 +1,43 @@
1
+ """DoctorCheck Protocol — contract for diagnostic check implementations.
2
+
3
+ Doctor checks diagnose RaiSE's own health. They inform and suggest fixes,
4
+ but never block operations (unlike WorkflowGates which guard transitions).
5
+
6
+ Architecture: ADR-045 (DoctorCheck protocol).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import ClassVar, Protocol, runtime_checkable
12
+
13
+ from raise_cli.doctor.models import CheckResult, DoctorContext
14
+
15
+
16
+ @runtime_checkable
17
+ class DoctorCheck(Protocol):
18
+ """Contract for diagnostic check implementations.
19
+
20
+ Attributes:
21
+ check_id: Unique identifier (e.g. ``"environment"``).
22
+ category: Grouping key for output and pipeline ordering.
23
+ description: Human-readable purpose.
24
+ requires_online: If True, skipped unless ``--online`` flag is set.
25
+
26
+ Example::
27
+
28
+ class EnvironmentCheck:
29
+ check_id = "environment"
30
+ category = "environment"
31
+ description = "Python version, raise-cli version, OS, installed extras"
32
+ requires_online = False
33
+
34
+ def evaluate(self, context: DoctorContext) -> list[CheckResult]:
35
+ ...
36
+ """
37
+
38
+ check_id: ClassVar[str]
39
+ category: ClassVar[str]
40
+ description: ClassVar[str]
41
+ requires_online: ClassVar[bool]
42
+
43
+ def evaluate(self, context: DoctorContext) -> list[CheckResult]: ...
@@ -0,0 +1,100 @@
1
+ """Check registry with entry point discovery.
2
+
3
+ Discovers DoctorCheck implementations registered via Python entry points
4
+ (``[project.entry-points."rai.doctor.checks"]`` in pyproject.toml).
5
+
6
+ Architecture: ADR-045, same discovery pattern as gates (ADR-039 S3).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import inspect
12
+ import logging
13
+ from importlib.metadata import entry_points
14
+ from typing import Any
15
+
16
+ from raise_cli.doctor.protocol import DoctorCheck
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ EP_DOCTOR: str = "rai.doctor.checks"
21
+
22
+
23
+ def _dist_name(ep: Any) -> str:
24
+ """Best-effort extraction of the distribution name for an entry point."""
25
+ try:
26
+ return ep.dist.name # type: ignore[union-attr]
27
+ except AttributeError:
28
+ return "unknown"
29
+
30
+
31
+ class CheckRegistry:
32
+ """Discovers and manages DoctorCheck implementations.
33
+
34
+ Example::
35
+
36
+ registry = CheckRegistry()
37
+ registry.discover()
38
+
39
+ for check in registry.checks:
40
+ print(f"{check.check_id}: {check.description}")
41
+ """
42
+
43
+ def __init__(self) -> None:
44
+ self._checks: list[DoctorCheck] = []
45
+
46
+ @property
47
+ def checks(self) -> list[DoctorCheck]:
48
+ """Return a copy of registered checks."""
49
+ return list(self._checks)
50
+
51
+ def discover(self) -> None:
52
+ """Load checks from ``rai.doctor.checks`` entry points.
53
+
54
+ Skips entry points that fail to load, are not classes, or don't
55
+ conform to the DoctorCheck Protocol.
56
+ """
57
+ for ep in entry_points(group=EP_DOCTOR):
58
+ try:
59
+ loaded: Any = ep.load()
60
+ except Exception as exc: # noqa: BLE001
61
+ logger.warning(
62
+ "Skipping doctor check '%s' from '%s': %s",
63
+ ep.name,
64
+ _dist_name(ep),
65
+ exc,
66
+ )
67
+ continue
68
+
69
+ if not inspect.isclass(loaded):
70
+ logger.warning(
71
+ "Skipping doctor check '%s': expected a class, got %s",
72
+ ep.name,
73
+ type(loaded).__name__,
74
+ )
75
+ continue
76
+
77
+ instance = loaded()
78
+ if not isinstance(instance, DoctorCheck):
79
+ logger.warning(
80
+ "Skipping doctor check '%s': does not conform to DoctorCheck Protocol",
81
+ ep.name,
82
+ )
83
+ continue
84
+
85
+ self._checks.append(instance)
86
+ logger.debug("Loaded doctor check '%s' (category=%s)", ep.name, instance.category)
87
+
88
+ def register(self, check: DoctorCheck | Any) -> None:
89
+ """Manually register a check instance (useful for testing)."""
90
+ if not isinstance(check, DoctorCheck):
91
+ logger.warning(
92
+ "Skipping manual check registration: %s does not conform to DoctorCheck",
93
+ type(check).__name__,
94
+ )
95
+ return
96
+ self._checks.append(check)
97
+
98
+ def get_checks_for_category(self, category: str) -> list[DoctorCheck]:
99
+ """Return checks matching the given category."""
100
+ return [c for c in self._checks if c.category == category]