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.
- raise_cli/__init__.py +38 -0
- raise_cli/__main__.py +30 -0
- raise_cli/adapters/__init__.py +91 -0
- raise_cli/adapters/declarative/__init__.py +26 -0
- raise_cli/adapters/declarative/adapter.py +267 -0
- raise_cli/adapters/declarative/discovery.py +94 -0
- raise_cli/adapters/declarative/expressions.py +150 -0
- raise_cli/adapters/declarative/reference/__init__.py +1 -0
- raise_cli/adapters/declarative/reference/github.yaml +143 -0
- raise_cli/adapters/declarative/schema.py +98 -0
- raise_cli/adapters/filesystem.py +299 -0
- raise_cli/adapters/mcp_bridge.py +10 -0
- raise_cli/adapters/mcp_confluence.py +246 -0
- raise_cli/adapters/mcp_jira.py +405 -0
- raise_cli/adapters/models.py +205 -0
- raise_cli/adapters/protocols.py +180 -0
- raise_cli/adapters/registry.py +90 -0
- raise_cli/adapters/sync.py +149 -0
- raise_cli/agents/__init__.py +14 -0
- raise_cli/agents/antigravity.yaml +8 -0
- raise_cli/agents/claude.yaml +8 -0
- raise_cli/agents/copilot.yaml +8 -0
- raise_cli/agents/copilot_plugin.py +124 -0
- raise_cli/agents/cursor.yaml +7 -0
- raise_cli/agents/roo.yaml +8 -0
- raise_cli/agents/windsurf.yaml +8 -0
- raise_cli/artifacts/__init__.py +30 -0
- raise_cli/artifacts/models.py +43 -0
- raise_cli/artifacts/reader.py +55 -0
- raise_cli/artifacts/renderer.py +104 -0
- raise_cli/artifacts/story_design.py +69 -0
- raise_cli/artifacts/writer.py +45 -0
- raise_cli/backlog/__init__.py +1 -0
- raise_cli/backlog/sync.py +115 -0
- raise_cli/cli/__init__.py +3 -0
- raise_cli/cli/commands/__init__.py +3 -0
- raise_cli/cli/commands/_resolve.py +153 -0
- raise_cli/cli/commands/adapters.py +362 -0
- raise_cli/cli/commands/artifact.py +137 -0
- raise_cli/cli/commands/backlog.py +333 -0
- raise_cli/cli/commands/base.py +31 -0
- raise_cli/cli/commands/discover.py +551 -0
- raise_cli/cli/commands/docs.py +130 -0
- raise_cli/cli/commands/doctor.py +177 -0
- raise_cli/cli/commands/gate.py +223 -0
- raise_cli/cli/commands/graph.py +1086 -0
- raise_cli/cli/commands/info.py +81 -0
- raise_cli/cli/commands/init.py +746 -0
- raise_cli/cli/commands/journal.py +167 -0
- raise_cli/cli/commands/mcp.py +524 -0
- raise_cli/cli/commands/memory.py +467 -0
- raise_cli/cli/commands/pattern.py +348 -0
- raise_cli/cli/commands/profile.py +59 -0
- raise_cli/cli/commands/publish.py +80 -0
- raise_cli/cli/commands/release.py +338 -0
- raise_cli/cli/commands/session.py +528 -0
- raise_cli/cli/commands/signal.py +410 -0
- raise_cli/cli/commands/skill.py +350 -0
- raise_cli/cli/commands/skill_set.py +145 -0
- raise_cli/cli/error_handler.py +158 -0
- raise_cli/cli/main.py +163 -0
- raise_cli/compat.py +66 -0
- raise_cli/config/__init__.py +41 -0
- raise_cli/config/agent_plugin.py +105 -0
- raise_cli/config/agent_registry.py +233 -0
- raise_cli/config/agents.py +120 -0
- raise_cli/config/ide.py +32 -0
- raise_cli/config/paths.py +379 -0
- raise_cli/config/settings.py +180 -0
- raise_cli/context/__init__.py +42 -0
- raise_cli/context/analyzers/__init__.py +16 -0
- raise_cli/context/analyzers/models.py +36 -0
- raise_cli/context/analyzers/protocol.py +43 -0
- raise_cli/context/analyzers/python.py +292 -0
- raise_cli/context/builder.py +1569 -0
- raise_cli/context/diff.py +213 -0
- raise_cli/context/extractors/__init__.py +13 -0
- raise_cli/context/extractors/skills.py +121 -0
- raise_cli/core/__init__.py +37 -0
- raise_cli/core/files.py +66 -0
- raise_cli/core/text.py +174 -0
- raise_cli/core/tools.py +441 -0
- raise_cli/discovery/__init__.py +50 -0
- raise_cli/discovery/analyzer.py +691 -0
- raise_cli/discovery/drift.py +355 -0
- raise_cli/discovery/scanner.py +1687 -0
- raise_cli/doctor/__init__.py +4 -0
- raise_cli/doctor/checks/__init__.py +1 -0
- raise_cli/doctor/checks/environment.py +110 -0
- raise_cli/doctor/checks/project.py +238 -0
- raise_cli/doctor/fix.py +80 -0
- raise_cli/doctor/models.py +56 -0
- raise_cli/doctor/protocol.py +43 -0
- raise_cli/doctor/registry.py +100 -0
- raise_cli/doctor/report.py +141 -0
- raise_cli/doctor/runner.py +95 -0
- raise_cli/engines/__init__.py +3 -0
- raise_cli/exceptions.py +215 -0
- raise_cli/gates/__init__.py +19 -0
- raise_cli/gates/builtin/__init__.py +1 -0
- raise_cli/gates/builtin/coverage.py +52 -0
- raise_cli/gates/builtin/lint.py +48 -0
- raise_cli/gates/builtin/tests.py +48 -0
- raise_cli/gates/builtin/types.py +48 -0
- raise_cli/gates/models.py +40 -0
- raise_cli/gates/protocol.py +41 -0
- raise_cli/gates/registry.py +141 -0
- raise_cli/governance/__init__.py +11 -0
- raise_cli/governance/extractor.py +412 -0
- raise_cli/governance/models.py +134 -0
- raise_cli/governance/parsers/__init__.py +35 -0
- raise_cli/governance/parsers/_convert.py +38 -0
- raise_cli/governance/parsers/adr.py +274 -0
- raise_cli/governance/parsers/backlog.py +356 -0
- raise_cli/governance/parsers/constitution.py +119 -0
- raise_cli/governance/parsers/epic.py +323 -0
- raise_cli/governance/parsers/glossary.py +316 -0
- raise_cli/governance/parsers/guardrails.py +345 -0
- raise_cli/governance/parsers/prd.py +112 -0
- raise_cli/governance/parsers/roadmap.py +118 -0
- raise_cli/governance/parsers/vision.py +116 -0
- raise_cli/graph/__init__.py +1 -0
- raise_cli/graph/backends/__init__.py +57 -0
- raise_cli/graph/backends/api.py +137 -0
- raise_cli/graph/backends/dual.py +139 -0
- raise_cli/graph/backends/pending.py +84 -0
- raise_cli/handlers/__init__.py +3 -0
- raise_cli/hooks/__init__.py +54 -0
- raise_cli/hooks/builtin/__init__.py +1 -0
- raise_cli/hooks/builtin/backlog.py +216 -0
- raise_cli/hooks/builtin/gate_bridge.py +83 -0
- raise_cli/hooks/builtin/jira_sync.py +127 -0
- raise_cli/hooks/builtin/memory.py +117 -0
- raise_cli/hooks/builtin/telemetry.py +72 -0
- raise_cli/hooks/emitter.py +184 -0
- raise_cli/hooks/events.py +262 -0
- raise_cli/hooks/protocol.py +38 -0
- raise_cli/hooks/registry.py +117 -0
- raise_cli/mcp/__init__.py +33 -0
- raise_cli/mcp/bridge.py +218 -0
- raise_cli/mcp/models.py +43 -0
- raise_cli/mcp/registry.py +77 -0
- raise_cli/mcp/schema.py +41 -0
- raise_cli/memory/__init__.py +58 -0
- raise_cli/memory/loader.py +247 -0
- raise_cli/memory/migration.py +241 -0
- raise_cli/memory/models.py +169 -0
- raise_cli/memory/writer.py +598 -0
- raise_cli/onboarding/__init__.py +103 -0
- raise_cli/onboarding/bootstrap.py +324 -0
- raise_cli/onboarding/claudemd.py +17 -0
- raise_cli/onboarding/conventions.py +742 -0
- raise_cli/onboarding/detection.py +374 -0
- raise_cli/onboarding/governance.py +443 -0
- raise_cli/onboarding/instructions.py +672 -0
- raise_cli/onboarding/manifest.py +201 -0
- raise_cli/onboarding/memory_md.py +399 -0
- raise_cli/onboarding/migration.py +207 -0
- raise_cli/onboarding/profile.py +624 -0
- raise_cli/onboarding/skill_conflict.py +100 -0
- raise_cli/onboarding/skill_manifest.py +176 -0
- raise_cli/onboarding/skills.py +437 -0
- raise_cli/onboarding/workflows.py +101 -0
- raise_cli/output/__init__.py +28 -0
- raise_cli/output/console.py +394 -0
- raise_cli/output/formatters/__init__.py +9 -0
- raise_cli/output/formatters/adapters.py +135 -0
- raise_cli/output/formatters/discover.py +439 -0
- raise_cli/output/formatters/skill.py +298 -0
- raise_cli/publish/__init__.py +3 -0
- raise_cli/publish/changelog.py +80 -0
- raise_cli/publish/check.py +179 -0
- raise_cli/publish/version.py +172 -0
- raise_cli/rai_base/__init__.py +22 -0
- raise_cli/rai_base/framework/__init__.py +7 -0
- raise_cli/rai_base/framework/methodology.yaml +233 -0
- raise_cli/rai_base/governance/__init__.py +1 -0
- raise_cli/rai_base/governance/architecture/__init__.py +1 -0
- raise_cli/rai_base/governance/architecture/domain-model.md +20 -0
- raise_cli/rai_base/governance/architecture/system-context.md +34 -0
- raise_cli/rai_base/governance/architecture/system-design.md +24 -0
- raise_cli/rai_base/governance/backlog.md +8 -0
- raise_cli/rai_base/governance/guardrails.md +17 -0
- raise_cli/rai_base/governance/prd.md +25 -0
- raise_cli/rai_base/governance/vision.md +16 -0
- raise_cli/rai_base/identity/__init__.py +8 -0
- raise_cli/rai_base/identity/core.md +119 -0
- raise_cli/rai_base/identity/perspective.md +119 -0
- raise_cli/rai_base/memory/__init__.py +7 -0
- raise_cli/rai_base/memory/patterns-base.jsonl +55 -0
- raise_cli/schemas/__init__.py +3 -0
- raise_cli/schemas/journal.py +49 -0
- raise_cli/schemas/session_state.py +117 -0
- raise_cli/session/__init__.py +5 -0
- raise_cli/session/bundle.py +820 -0
- raise_cli/session/close.py +268 -0
- raise_cli/session/journal.py +119 -0
- raise_cli/session/resolver.py +126 -0
- raise_cli/session/state.py +187 -0
- raise_cli/skills/__init__.py +44 -0
- raise_cli/skills/locator.py +141 -0
- raise_cli/skills/name_checker.py +199 -0
- raise_cli/skills/parser.py +145 -0
- raise_cli/skills/scaffold.py +212 -0
- raise_cli/skills/schema.py +132 -0
- raise_cli/skills/skillsets.py +195 -0
- raise_cli/skills/validator.py +197 -0
- raise_cli/skills_base/__init__.py +80 -0
- raise_cli/skills_base/contract-template.md +60 -0
- raise_cli/skills_base/preamble.md +37 -0
- raise_cli/skills_base/rai-architecture-review/SKILL.md +137 -0
- raise_cli/skills_base/rai-debug/SKILL.md +171 -0
- raise_cli/skills_base/rai-discover/SKILL.md +167 -0
- raise_cli/skills_base/rai-discover-document/SKILL.md +128 -0
- raise_cli/skills_base/rai-discover-scan/SKILL.md +147 -0
- raise_cli/skills_base/rai-discover-start/SKILL.md +145 -0
- raise_cli/skills_base/rai-discover-validate/SKILL.md +142 -0
- raise_cli/skills_base/rai-docs-update/SKILL.md +142 -0
- raise_cli/skills_base/rai-doctor/SKILL.md +120 -0
- raise_cli/skills_base/rai-epic-close/SKILL.md +165 -0
- raise_cli/skills_base/rai-epic-close/templates/retrospective.md +68 -0
- raise_cli/skills_base/rai-epic-design/SKILL.md +146 -0
- raise_cli/skills_base/rai-epic-design/templates/design.md +24 -0
- raise_cli/skills_base/rai-epic-design/templates/scope.md +76 -0
- raise_cli/skills_base/rai-epic-plan/SKILL.md +153 -0
- raise_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
- raise_cli/skills_base/rai-epic-plan/templates/plan-section.md +49 -0
- raise_cli/skills_base/rai-epic-run/SKILL.md +208 -0
- raise_cli/skills_base/rai-epic-start/SKILL.md +136 -0
- raise_cli/skills_base/rai-epic-start/templates/brief.md +34 -0
- raise_cli/skills_base/rai-mcp-add/SKILL.md +176 -0
- raise_cli/skills_base/rai-mcp-remove/SKILL.md +120 -0
- raise_cli/skills_base/rai-mcp-status/SKILL.md +147 -0
- raise_cli/skills_base/rai-problem-shape/SKILL.md +138 -0
- raise_cli/skills_base/rai-project-create/SKILL.md +144 -0
- raise_cli/skills_base/rai-project-onboard/SKILL.md +162 -0
- raise_cli/skills_base/rai-quality-review/SKILL.md +189 -0
- raise_cli/skills_base/rai-research/SKILL.md +143 -0
- raise_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
- raise_cli/skills_base/rai-session-close/SKILL.md +176 -0
- raise_cli/skills_base/rai-session-start/SKILL.md +110 -0
- raise_cli/skills_base/rai-story-close/SKILL.md +198 -0
- raise_cli/skills_base/rai-story-design/SKILL.md +203 -0
- raise_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
- raise_cli/skills_base/rai-story-implement/SKILL.md +115 -0
- raise_cli/skills_base/rai-story-plan/SKILL.md +135 -0
- raise_cli/skills_base/rai-story-review/SKILL.md +178 -0
- raise_cli/skills_base/rai-story-run/SKILL.md +282 -0
- raise_cli/skills_base/rai-story-start/SKILL.md +166 -0
- raise_cli/skills_base/rai-story-start/templates/story.md +38 -0
- raise_cli/skills_base/rai-welcome/SKILL.md +134 -0
- raise_cli/telemetry/__init__.py +42 -0
- raise_cli/telemetry/schemas.py +285 -0
- raise_cli/telemetry/writer.py +217 -0
- raise_cli/tier/__init__.py +0 -0
- raise_cli/tier/context.py +134 -0
- raise_cli/viz/__init__.py +7 -0
- raise_cli/viz/generator.py +406 -0
- raise_cli-2.2.1.dist-info/METADATA +433 -0
- raise_cli-2.2.1.dist-info/RECORD +264 -0
- raise_cli-2.2.1.dist-info/WHEEL +4 -0
- raise_cli-2.2.1.dist-info/entry_points.txt +40 -0
- raise_cli-2.2.1.dist-info/licenses/LICENSE +190 -0
- raise_cli-2.2.1.dist-info/licenses/NOTICE +4 -0
|
@@ -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
|
+
)
|
raise_cli/doctor/fix.py
ADDED
|
@@ -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]
|