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
raise_cli/__init__.py ADDED
@@ -0,0 +1,38 @@
1
+ """RaiSE CLI - Reliable AI Software Engineering.
2
+
3
+ A governance framework for AI-assisted software development.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from raise_cli.exceptions import (
9
+ ArtifactNotFoundError,
10
+ ConfigurationError,
11
+ DependencyError,
12
+ GateFailedError,
13
+ GateNotFoundError,
14
+ KataNotFoundError,
15
+ RaiError,
16
+ StateError,
17
+ ValidationError,
18
+ )
19
+
20
+ __version__ = "2.2.1"
21
+ __author__ = "Emilio Osorio"
22
+ __license__ = "MIT"
23
+
24
+ __all__ = [
25
+ "__version__",
26
+ "__author__",
27
+ "__license__",
28
+ # Exceptions
29
+ "RaiError",
30
+ "ConfigurationError",
31
+ "KataNotFoundError",
32
+ "GateNotFoundError",
33
+ "ArtifactNotFoundError",
34
+ "DependencyError",
35
+ "StateError",
36
+ "ValidationError",
37
+ "GateFailedError",
38
+ ]
raise_cli/__main__.py ADDED
@@ -0,0 +1,30 @@
1
+ """Entry point for python -m raise_cli."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from raise_cli.cli.main import app
8
+ from raise_cli.exceptions import RaiError
9
+
10
+
11
+ def main() -> None:
12
+ """Run the CLI application with error handling.
13
+
14
+ Catches RaiError exceptions and displays them with proper formatting,
15
+ then exits with the appropriate exit code.
16
+ """
17
+ try:
18
+ app()
19
+ except RaiError as error:
20
+ # Import here to avoid circular imports and for lazy loading
21
+ from raise_cli.cli.error_handler import handle_error
22
+ from raise_cli.cli.main import get_output_format
23
+
24
+ output_format = get_output_format()
25
+ exit_code = handle_error(error, output_format=output_format)
26
+ sys.exit(exit_code)
27
+
28
+
29
+ if __name__ == "__main__":
30
+ main()
@@ -0,0 +1,91 @@
1
+ """Adapter contracts and registry for raise-cli extensibility.
2
+
3
+ Public API: 7 Protocols + 15 boundary models + 2 sync wrappers
4
+ + 5 registry functions + 5 group constants.
5
+ KnowledgeGraphBackend + BackendHealth moved to raise_core (E275).
6
+
7
+ Architecture: ADR-033 (PM), ADR-034 (Governance)
8
+ """
9
+
10
+ from raise_cli.adapters.models import (
11
+ AdapterHealth,
12
+ ArtifactLocator,
13
+ BatchResult,
14
+ Comment,
15
+ CommentRef,
16
+ CoreArtifactType,
17
+ FailureDetail,
18
+ IssueDetail,
19
+ IssueRef,
20
+ IssueSpec,
21
+ IssueSummary,
22
+ PageContent,
23
+ PageSummary,
24
+ PublishResult,
25
+ )
26
+ from raise_cli.adapters.protocols import (
27
+ AsyncDocumentationTarget,
28
+ AsyncProjectManagementAdapter,
29
+ DocumentationTarget,
30
+ GovernanceParser,
31
+ GovernanceSchemaProvider,
32
+ ProjectManagementAdapter,
33
+ )
34
+ from raise_cli.adapters.registry import (
35
+ EP_DOC_TARGETS,
36
+ EP_GOVERNANCE_PARSERS,
37
+ EP_GOVERNANCE_SCHEMAS,
38
+ EP_GRAPH_BACKENDS,
39
+ EP_PM_ADAPTERS,
40
+ get_doc_targets,
41
+ get_governance_parsers,
42
+ get_governance_schemas,
43
+ get_graph_backends,
44
+ get_pm_adapters,
45
+ )
46
+ from raise_cli.adapters.sync import SyncDocsAdapter, SyncPMAdapter
47
+ from raise_core.graph.backends.models import BackendHealth
48
+ from raise_core.graph.backends.protocol import KnowledgeGraphBackend
49
+
50
+ __all__ = [
51
+ # Protocols — sync (4 local + 1 from raise_core)
52
+ "DocumentationTarget",
53
+ "GovernanceParser",
54
+ "GovernanceSchemaProvider",
55
+ "KnowledgeGraphBackend",
56
+ "ProjectManagementAdapter",
57
+ # Protocols — async (2)
58
+ "AsyncDocumentationTarget",
59
+ "AsyncProjectManagementAdapter",
60
+ # Sync wrappers (2)
61
+ "SyncDocsAdapter",
62
+ "SyncPMAdapter",
63
+ # Models (14 local + 1 from raise_core)
64
+ "AdapterHealth",
65
+ "ArtifactLocator",
66
+ "BackendHealth",
67
+ "BatchResult",
68
+ "Comment",
69
+ "CommentRef",
70
+ "CoreArtifactType",
71
+ "FailureDetail",
72
+ "IssueDetail",
73
+ "IssueRef",
74
+ "IssueSpec",
75
+ "IssueSummary",
76
+ "PageContent",
77
+ "PageSummary",
78
+ "PublishResult",
79
+ # Registry functions
80
+ "get_doc_targets",
81
+ "get_governance_parsers",
82
+ "get_governance_schemas",
83
+ "get_graph_backends",
84
+ "get_pm_adapters",
85
+ # Entry point group constants
86
+ "EP_DOC_TARGETS",
87
+ "EP_GOVERNANCE_PARSERS",
88
+ "EP_GOVERNANCE_SCHEMAS",
89
+ "EP_GRAPH_BACKENDS",
90
+ "EP_PM_ADAPTERS",
91
+ ]
@@ -0,0 +1,26 @@
1
+ """Declarative MCP adapter framework.
2
+
3
+ Enables integration of any MCP server via YAML config instead of Python code.
4
+
5
+ Architecture: ADR-041, E337
6
+ """
7
+
8
+ from raise_cli.adapters.declarative.adapter import DeclarativeMcpAdapter
9
+ from raise_cli.adapters.declarative.expressions import ExpressionEvaluator
10
+ from raise_cli.adapters.declarative.schema import (
11
+ AdapterMeta,
12
+ DeclarativeAdapterConfig,
13
+ MethodMapping,
14
+ ResponseMapping,
15
+ )
16
+ from raise_cli.mcp.schema import ServerConnection
17
+
18
+ __all__ = [
19
+ "AdapterMeta",
20
+ "DeclarativeAdapterConfig",
21
+ "DeclarativeMcpAdapter",
22
+ "ExpressionEvaluator",
23
+ "MethodMapping",
24
+ "ResponseMapping",
25
+ "ServerConnection",
26
+ ]
@@ -0,0 +1,267 @@
1
+ """Generic declarative MCP adapter driven by YAML config.
2
+
3
+ Implements ``AsyncProjectManagementAdapter`` and ``AsyncDocumentationTarget``
4
+ by dispatching each protocol method to the MCP tool specified in the YAML
5
+ config. Expression templates in args are evaluated against a context dict
6
+ built from method parameters. One class serves both protocols (AR-Q1).
7
+
8
+ Architecture: ADR-041, E337, AR-C1/C2/R1/Q1
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import os
15
+ from typing import Any, cast
16
+
17
+ from raise_cli.adapters.declarative.expressions import ExpressionEvaluator
18
+ from raise_cli.adapters.declarative.schema import (
19
+ DeclarativeAdapterConfig,
20
+ MethodMapping,
21
+ )
22
+ from raise_cli.adapters.models import (
23
+ AdapterHealth,
24
+ BatchResult,
25
+ Comment,
26
+ CommentRef,
27
+ FailureDetail,
28
+ IssueDetail,
29
+ IssueRef,
30
+ IssueSpec,
31
+ IssueSummary,
32
+ PageContent,
33
+ PageSummary,
34
+ PublishResult,
35
+ )
36
+ from raise_cli.mcp.bridge import McpBridge, McpBridgeError, McpToolResult
37
+ from raise_cli.mcp.registry import discover_mcp_servers
38
+ from raise_cli.mcp.schema import ServerConnection
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ class DeclarativeMcpAdapter:
44
+ """Async adapter driven by YAML config — serves PM or Docs protocol.
45
+
46
+ Implements ``AsyncProjectManagementAdapter`` and ``AsyncDocumentationTarget``
47
+ via structural typing. Each protocol method dispatches to the MCP tool
48
+ declared in config.methods. One class, both protocols (AR-Q1).
49
+
50
+ Args:
51
+ config: Parsed YAML adapter configuration.
52
+ """
53
+
54
+ def __init__(self, config: DeclarativeAdapterConfig) -> None:
55
+ self._config = config
56
+ self._evaluator = ExpressionEvaluator()
57
+ self._bridge = self._create_bridge()
58
+
59
+ def _create_bridge(self) -> McpBridge:
60
+ """Create McpBridge from server config.
61
+
62
+ Resolves ``server.ref`` via MCP registry if set, otherwise
63
+ uses inline connection fields. (S338.5, AR-C2)
64
+ """
65
+ server = self._config.server
66
+ if server.ref is not None:
67
+ registry = discover_mcp_servers()
68
+ if server.ref not in registry:
69
+ msg = f"Server ref '{server.ref}' not found in MCP registry"
70
+ raise McpBridgeError(msg)
71
+ conn = registry[server.ref].server
72
+ else:
73
+ assert server.command is not None # guaranteed by ServerRef validator
74
+ conn = ServerConnection(
75
+ command=server.command,
76
+ args=server.args,
77
+ env=server.env,
78
+ )
79
+ env: dict[str, str] | None = None
80
+ if conn.env:
81
+ env = {
82
+ **os.environ,
83
+ **{k: os.environ.get(k, "") for k in conn.env},
84
+ }
85
+ return McpBridge(
86
+ server_command=conn.command,
87
+ server_args=conn.args,
88
+ env=env,
89
+ )
90
+
91
+ def _get_method(self, name: str) -> MethodMapping:
92
+ """Get method mapping, raising NotImplementedError if null or missing."""
93
+ mapping = self._config.methods.get(name)
94
+ if mapping is None:
95
+ msg = f"Method '{name}' not supported by adapter '{self._config.adapter.name}'"
96
+ raise NotImplementedError(msg)
97
+ return mapping
98
+
99
+ async def _dispatch(
100
+ self, method_name: str, context: dict[str, Any]
101
+ ) -> McpToolResult:
102
+ """Dispatch a protocol method call to the configured MCP tool."""
103
+ mapping = self._get_method(method_name)
104
+ args = self._evaluator.evaluate_args(mapping.args, context)
105
+ return await self._bridge.call(mapping.tool, args)
106
+
107
+ def _parse_single(
108
+ self, method_name: str, result: McpToolResult, context: dict[str, Any]
109
+ ) -> dict[str, Any]:
110
+ """Parse a single-item response using the method's response mapping."""
111
+ mapping = self._get_method(method_name)
112
+ if mapping.response is None:
113
+ return result.data
114
+
115
+ parse_ctx = {**context, "data": result.data}
116
+ return {
117
+ field: self._evaluator.evaluate(tmpl, parse_ctx)
118
+ for field, tmpl in mapping.response.fields.items()
119
+ }
120
+
121
+ def _parse_list(
122
+ self, method_name: str, result: McpToolResult
123
+ ) -> list[dict[str, Any]]:
124
+ """Parse a list response using items_path and field mappings."""
125
+ mapping = self._get_method(method_name)
126
+ if mapping.response is None:
127
+ return result.data.get("items", [])
128
+
129
+ # Navigate to list via items_path
130
+ items: Any = result.data
131
+ if mapping.response.items_path:
132
+ for part in mapping.response.items_path.split("."):
133
+ if isinstance(items, dict):
134
+ items = cast(Any, items.get(part, []))
135
+
136
+ if not isinstance(items, list):
137
+ items = []
138
+
139
+ parsed: list[dict[str, Any]] = []
140
+ for raw_item in cast(list[Any], items):
141
+ item_ctx: dict[str, Any] = {"item": raw_item}
142
+ row = {
143
+ field: self._evaluator.evaluate(tmpl, item_ctx)
144
+ for field, tmpl in mapping.response.fields.items()
145
+ }
146
+ parsed.append(row)
147
+ return parsed
148
+
149
+ # ----- CRUD -----
150
+
151
+ async def create_issue(self, project_key: str, issue: IssueSpec) -> IssueRef:
152
+ ctx = {"project_key": project_key, "issue": issue.model_dump()}
153
+ result = await self._dispatch("create_issue", ctx)
154
+ fields = self._parse_single("create_issue", result, ctx)
155
+ return IssueRef(**fields)
156
+
157
+ async def get_issue(self, key: str) -> IssueDetail:
158
+ ctx = {"key": key}
159
+ result = await self._dispatch("get_issue", ctx)
160
+ fields = self._parse_single("get_issue", result, ctx)
161
+ return IssueDetail(**fields)
162
+
163
+ async def update_issue(self, key: str, fields: dict[str, Any]) -> IssueRef:
164
+ ctx = {"key": key, "fields": fields}
165
+ result = await self._dispatch("update_issue", ctx)
166
+ parsed = self._parse_single("update_issue", result, ctx)
167
+ return IssueRef(**parsed)
168
+
169
+ async def transition_issue(self, key: str, status: str) -> IssueRef:
170
+ ctx = {"key": key, "status": status}
171
+ result = await self._dispatch("transition_issue", ctx)
172
+ parsed = self._parse_single("transition_issue", result, ctx)
173
+ return IssueRef(**parsed)
174
+
175
+ # ----- Batch -----
176
+
177
+ async def batch_transition(self, keys: list[str], status: str) -> BatchResult:
178
+ """Auto-loops over transition_issue (AR design D6)."""
179
+ succeeded: list[IssueRef] = []
180
+ failed: list[FailureDetail] = []
181
+
182
+ for key in keys:
183
+ try:
184
+ ref = await self.transition_issue(key, status)
185
+ succeeded.append(ref)
186
+ except (McpBridgeError, NotImplementedError, Exception) as exc:
187
+ failed.append(FailureDetail(key=key, error=str(exc)))
188
+
189
+ return BatchResult(succeeded=succeeded, failed=failed)
190
+
191
+ # ----- Relationships -----
192
+
193
+ async def link_to_parent(self, child_key: str, parent_key: str) -> None:
194
+ ctx = {"child_key": child_key, "parent_key": parent_key}
195
+ await self._dispatch("link_to_parent", ctx)
196
+
197
+ async def link_issues(self, source: str, target: str, link_type: str) -> None:
198
+ ctx = {"source": source, "target": target, "link_type": link_type}
199
+ await self._dispatch("link_issues", ctx)
200
+
201
+ # ----- Comments -----
202
+
203
+ async def add_comment(self, key: str, body: str) -> CommentRef:
204
+ ctx = {"key": key, "body": body}
205
+ result = await self._dispatch("add_comment", ctx)
206
+ fields = self._parse_single("add_comment", result, ctx)
207
+ return CommentRef(**fields)
208
+
209
+ async def get_comments(self, key: str, limit: int = 10) -> list[Comment]:
210
+ ctx = {"key": key, "limit": limit}
211
+ result = await self._dispatch("get_comments", ctx)
212
+ items = self._parse_list("get_comments", result)
213
+ return [Comment(**item) for item in items]
214
+
215
+ # ----- Query -----
216
+
217
+ async def search(
218
+ self, query: str, limit: int = 50
219
+ ) -> list[IssueSummary] | list[PageSummary]:
220
+ """Protocol-aware search: returns IssueSummary (PM) or PageSummary (docs).
221
+
222
+ SyncDocsAdapter calls .search() — must return the right type based on
223
+ the adapter's configured protocol (QR-C1 fix).
224
+ """
225
+ ctx = {"query": query, "limit": limit}
226
+ result = await self._dispatch("search", ctx)
227
+ items = self._parse_list("search", result)
228
+ if self._config.adapter.protocol == "docs":
229
+ return [PageSummary(**item) for item in items]
230
+ return [IssueSummary(**item) for item in items]
231
+
232
+ # ----- Docs: Documentation Target (AR-Q1) -----
233
+
234
+ async def can_publish(self, doc_type: str, metadata: dict[str, Any]) -> bool:
235
+ ctx = {"doc_type": doc_type, "metadata": metadata}
236
+ result = await self._dispatch("can_publish", ctx)
237
+ fields = self._parse_single("can_publish", result, ctx)
238
+ return bool(fields.get("result", False))
239
+
240
+ async def publish(
241
+ self, doc_type: str, content: str, metadata: dict[str, Any]
242
+ ) -> PublishResult:
243
+ ctx = {"doc_type": doc_type, "content": content, "metadata": metadata}
244
+ result = await self._dispatch("publish", ctx)
245
+ fields = self._parse_single("publish", result, ctx)
246
+ return PublishResult(**fields)
247
+
248
+ async def get_page(self, identifier: str) -> PageContent:
249
+ ctx = {"identifier": identifier}
250
+ result = await self._dispatch("get_page", ctx)
251
+ fields = self._parse_single("get_page", result, ctx)
252
+ return PageContent(**fields)
253
+
254
+ # ----- Lifecycle -----
255
+
256
+ async def health(self) -> AdapterHealth:
257
+ bridge_health = await self._bridge.health()
258
+ return AdapterHealth(
259
+ name=self._config.adapter.name,
260
+ healthy=bridge_health.healthy,
261
+ message=bridge_health.message,
262
+ latency_ms=bridge_health.latency_ms,
263
+ )
264
+
265
+ async def aclose(self) -> None:
266
+ """Close the underlying MCP bridge."""
267
+ await self._bridge.aclose()
@@ -0,0 +1,94 @@
1
+ """YAML adapter discovery for declarative MCP adapters.
2
+
3
+ Scans ``.raise/adapters/*.yaml`` for adapter configs, parses them with
4
+ ``DeclarativeAdapterConfig``, filters by protocol, and returns factory
5
+ closures compatible with ``resolve_entrypoint``.
6
+
7
+ Architecture: ADR-041, E337, S337.3, AR-C1 (factory closure)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from collections.abc import Callable
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ import yaml
18
+ from pydantic import ValidationError
19
+
20
+ from raise_cli.adapters.declarative.schema import DeclarativeAdapterConfig
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def _make_factory(cfg: DeclarativeAdapterConfig) -> Callable[[], Any]:
26
+ """AR-C1: factory closure captures config for no-arg instantiation."""
27
+
28
+ def factory() -> Any:
29
+ from raise_cli.adapters.declarative.adapter import DeclarativeMcpAdapter
30
+
31
+ return DeclarativeMcpAdapter(cfg)
32
+
33
+ return factory
34
+
35
+
36
+ def discover_yaml_adapters(
37
+ protocol: str,
38
+ *,
39
+ adapters_dir: Path | None = None,
40
+ ) -> dict[str, Callable[[], Any]]:
41
+ """Discover YAML-defined adapters for a given protocol.
42
+
43
+ Scans ``adapters_dir`` (default: ``.raise/adapters/``) for ``*.yaml``
44
+ files, parses each as a ``DeclarativeAdapterConfig``, filters by
45
+ ``adapter.protocol``, and returns factory closures.
46
+
47
+ Each factory closure captures its parsed config and returns a new
48
+ ``DeclarativeMcpAdapter`` when called with no arguments (AR-C1).
49
+
50
+ Args:
51
+ protocol: Protocol to filter by (``"pm"`` or ``"docs"``).
52
+ adapters_dir: Directory to scan. Defaults to ``.raise/adapters/``.
53
+
54
+ Returns:
55
+ Mapping of adapter name to no-arg factory callable.
56
+ """
57
+ if adapters_dir is None:
58
+ adapters_dir = Path.cwd() / ".raise" / "adapters"
59
+
60
+ if not adapters_dir.is_dir():
61
+ return {}
62
+
63
+ result: dict[str, Callable[[], Any]] = {}
64
+
65
+ for yaml_path in sorted(adapters_dir.glob("*.yaml")):
66
+ try:
67
+ raw = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
68
+ except Exception as exc: # noqa: BLE001
69
+ logger.warning("Skipping %s: YAML parse error: %s", yaml_path.name, exc)
70
+ continue
71
+
72
+ try:
73
+ config = DeclarativeAdapterConfig.model_validate(raw)
74
+ except ValidationError as exc:
75
+ logger.warning(
76
+ "Skipping %s: schema validation error: %s", yaml_path.name, exc
77
+ )
78
+ continue
79
+
80
+ if config.adapter.protocol != protocol:
81
+ continue
82
+
83
+ name = config.adapter.name
84
+ if name in result:
85
+ logger.warning(
86
+ "Skipping %s: adapter name '%s' already defined by another YAML file",
87
+ yaml_path.name,
88
+ name,
89
+ )
90
+ continue
91
+
92
+ result[name] = _make_factory(config)
93
+
94
+ return result
@@ -0,0 +1,150 @@
1
+ """Mini expression evaluator for declarative adapter templates.
2
+
3
+ Supports: ``{{ var }}``, ``{{ obj.field }}``, ``{{ value | filter }}``.
4
+ Filters: ``str``, ``default('fallback')``, ``pluck('field')``, ``json``.
5
+
6
+ ~100 LOC, zero external dependencies.
7
+
8
+ Architecture: ADR-041 (D2 — mini evaluator, not Jinja2)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import re
15
+ from typing import Any, cast
16
+
17
+ # Matches {{ expression }} with optional whitespace
18
+ _TEMPLATE_RE = re.compile(r"\{\{\s*(.+?)\s*\}\}")
19
+
20
+ # Matches filter with optional quoted argument: filter_name('arg')
21
+ _FILTER_RE = re.compile(r"(\w+)\s*(?:\(\s*'([^']*)'\s*\))?")
22
+
23
+
24
+ class ExpressionEvaluator:
25
+ """Evaluate template expressions against a context dict.
26
+
27
+ Designed for declarative MCP adapter YAML configs. Intentionally
28
+ minimal — covers dot-access and 4 filters. Not a general-purpose
29
+ template engine.
30
+ """
31
+
32
+ def evaluate(self, template: str, context: dict[str, Any]) -> Any:
33
+ """Evaluate a single template string against context.
34
+
35
+ Args:
36
+ template: Template string, e.g. ``"{{ issue.summary | str }}"``.
37
+ context: Variable context dict.
38
+
39
+ Returns:
40
+ Resolved value. Type depends on expression — may be str, int,
41
+ list, dict, None, etc.
42
+
43
+ Raises:
44
+ KeyError: If a referenced variable is not in context.
45
+ """
46
+ match = _TEMPLATE_RE.fullmatch(template.strip()) if template else None
47
+ if match is None:
48
+ # Literal passthrough — no {{ }} found
49
+ return template
50
+
51
+ expr = match.group(1).strip()
52
+ return self._eval_expr(expr, context)
53
+
54
+ def evaluate_args(
55
+ self, args: dict[str, str], context: dict[str, Any]
56
+ ) -> dict[str, Any]:
57
+ """Evaluate all values in an args dict.
58
+
59
+ Args:
60
+ args: Mapping of param name → template string.
61
+ context: Variable context dict.
62
+
63
+ Returns:
64
+ New dict with all values resolved.
65
+ """
66
+ return {key: self.evaluate(tmpl, context) for key, tmpl in args.items()}
67
+
68
+ def _eval_expr(self, expr: str, context: dict[str, Any]) -> Any:
69
+ """Evaluate an expression, optionally with a pipe filter."""
70
+ # Split on pipe: "value | filter('arg')"
71
+ parts = expr.split("|", maxsplit=1)
72
+ var_path = parts[0].strip()
73
+
74
+ try:
75
+ value = self._resolve_path(var_path, context)
76
+ except KeyError:
77
+ # If there's a default filter, use it; otherwise re-raise
78
+ if len(parts) > 1 and "default" in parts[1]:
79
+ value = None
80
+ else:
81
+ raise
82
+
83
+ if len(parts) > 1:
84
+ filter_expr = parts[1].strip()
85
+ value = self._apply_filter(filter_expr, value, context)
86
+
87
+ return value
88
+
89
+ def _resolve_path(self, path: str, context: dict[str, Any]) -> Any:
90
+ """Resolve a dot-separated path against context.
91
+
92
+ ``"issue.summary"`` → ``context["issue"]["summary"]``
93
+ """
94
+ parts = path.split(".")
95
+ current: Any = context
96
+
97
+ for part in parts:
98
+ if isinstance(current, dict):
99
+ if part not in current:
100
+ raise KeyError(part)
101
+ current = cast(Any, current[part])
102
+ else:
103
+ raise KeyError(part)
104
+
105
+ return current
106
+
107
+ def _apply_filter(
108
+ self, filter_expr: str, value: Any, context: dict[str, Any]
109
+ ) -> Any:
110
+ """Apply a filter to a value."""
111
+ match = _FILTER_RE.fullmatch(filter_expr)
112
+ if not match:
113
+ msg = f"Invalid filter expression: {filter_expr}"
114
+ raise ValueError(msg)
115
+
116
+ filter_name = match.group(1)
117
+ filter_arg = match.group(2) # None if no arg
118
+
119
+ if filter_name == "str":
120
+ return str(value)
121
+ if filter_name == "json":
122
+ return json.dumps(value)
123
+ if filter_name == "default":
124
+ if value is None:
125
+ return filter_arg
126
+ # Also handle KeyError case — caller catches and retries with default
127
+ return value
128
+ if filter_name == "pluck":
129
+ if not isinstance(value, list):
130
+ msg = f"pluck requires a list, got {type(value).__name__}"
131
+ raise TypeError(msg)
132
+ if filter_arg is None:
133
+ msg = "pluck requires a field argument"
134
+ raise ValueError(msg)
135
+ return [
136
+ self._pluck_item(item, filter_arg) for item in cast(list[Any], value)
137
+ ]
138
+
139
+ msg = f"Unknown filter: {filter_name}"
140
+ raise ValueError(msg)
141
+
142
+ @staticmethod
143
+ def _pluck_item(item: Any, field: str) -> Any:
144
+ """Extract a field from a dict item, raising KeyError if missing."""
145
+ if isinstance(item, dict):
146
+ if field not in item:
147
+ raise KeyError(field)
148
+ return cast(Any, item[field])
149
+ msg = f"pluck expects dict items, got {type(item).__name__}"
150
+ raise TypeError(msg)
@@ -0,0 +1 @@
1
+ """Reference YAML adapter configs shipped with the package."""