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,143 @@
1
+ # Reference YAML adapter config: GitHub Issues via MCP
2
+ #
3
+ # This file demonstrates how to integrate an MCP server with the
4
+ # declarative adapter framework. Copy and adapt for your own server.
5
+ #
6
+ # Architecture: ADR-041, E337
7
+
8
+ # --- Adapter identity ---
9
+ # name: Used with --adapter flag (e.g., rai backlog search --adapter github)
10
+ # protocol: "pm" (ProjectManagement) or "docs" (Documentation)
11
+ # description: Human-readable, shown in `rai adapter list`
12
+ adapter:
13
+ name: github
14
+ protocol: pm
15
+ description: "GitHub Issues via mcp-github"
16
+
17
+ # --- MCP server connection ---
18
+ # command: Executable to launch the MCP server subprocess
19
+ # args: Command-line arguments passed to the server
20
+ # env: Environment variable names to forward to the subprocess
21
+ # (values read from the current environment at runtime)
22
+ server:
23
+ command: uvx
24
+ args: [mcp-github]
25
+ env: [GITHUB_TOKEN]
26
+
27
+ # --- Protocol method mappings ---
28
+ # Each key is a protocol method name from AsyncProjectManagementAdapter.
29
+ # Set to null for methods the MCP server doesn't support.
30
+ #
31
+ # Per method:
32
+ # tool: MCP tool name to call
33
+ # args: Argument name → expression template
34
+ # Templates use {{ var }} syntax with dot-access and filters:
35
+ # {{ issue.summary }} — dot-access into nested objects
36
+ # {{ data.number | str }} — type coercion filter
37
+ # {{ data.url | default:"" }} — default value filter
38
+ # {{ data.labels | pluck:"name" }} — extract field from list items
39
+ # response: How to parse the MCP tool response
40
+ # fields: Field name → expression template (for single-item responses)
41
+ # items_path: Dot-path to list in response data (for list responses)
42
+ methods:
43
+
44
+ # --- CRUD ---
45
+
46
+ create_issue:
47
+ tool: github_create_issue
48
+ args:
49
+ title: "{{ issue.summary }}"
50
+ body: "{{ issue.description | default:'' }}"
51
+ repo: "{{ project_key }}"
52
+ labels: "{{ issue.labels | default:'' }}"
53
+ response:
54
+ fields:
55
+ key: "{{ data.number | str }}"
56
+ url: "{{ data.html_url }}"
57
+
58
+ get_issue:
59
+ tool: github_get_issue
60
+ args:
61
+ issue_number: "{{ key }}"
62
+ response:
63
+ fields:
64
+ key: "{{ data.number | str }}"
65
+ summary: "{{ data.title }}"
66
+ status: "{{ data.state }}"
67
+ issue_type: "{{ data.type | default:'Issue' }}"
68
+ description: "{{ data.body | default:'' }}"
69
+
70
+ update_issue:
71
+ tool: github_update_issue
72
+ args:
73
+ issue_number: "{{ key }}"
74
+ title: "{{ fields.summary | default:'' }}"
75
+ body: "{{ fields.description | default:'' }}"
76
+ response:
77
+ fields:
78
+ key: "{{ data.number | str }}"
79
+ url: "{{ data.html_url }}"
80
+
81
+ transition_issue:
82
+ tool: github_update_issue
83
+ args:
84
+ issue_number: "{{ key }}"
85
+ state: "{{ status }}"
86
+ response:
87
+ fields:
88
+ key: "{{ data.number | str }}"
89
+ url: "{{ data.html_url }}"
90
+
91
+ # --- Batch ---
92
+ # batch_transition is auto-looped by the adapter (calls transition_issue N times).
93
+ # No MCP tool needed — set to null.
94
+ batch_transition: null
95
+
96
+ # --- Relationships ---
97
+ # GitHub doesn't have native issue linking via MCP tools.
98
+ link_to_parent: null
99
+ link_issues: null
100
+
101
+ # --- Comments ---
102
+
103
+ add_comment:
104
+ tool: github_add_comment
105
+ args:
106
+ issue_number: "{{ key }}"
107
+ body: "{{ body }}"
108
+ response:
109
+ fields:
110
+ id: "{{ data.id | str }}"
111
+ url: "{{ data.html_url }}"
112
+
113
+ get_comments:
114
+ tool: github_list_comments
115
+ args:
116
+ issue_number: "{{ key }}"
117
+ per_page: "{{ limit }}"
118
+ response:
119
+ items_path: data
120
+ fields:
121
+ id: "{{ item.id | str }}"
122
+ author: "{{ item.user.login }}"
123
+ body: "{{ item.body }}"
124
+ created: "{{ item.created_at }}"
125
+
126
+ # --- Query ---
127
+
128
+ search:
129
+ tool: github_search_issues
130
+ args:
131
+ query: "{{ query }}"
132
+ per_page: "{{ limit }}"
133
+ response:
134
+ items_path: data.items
135
+ fields:
136
+ key: "{{ item.number | str }}"
137
+ summary: "{{ item.title }}"
138
+ status: "{{ item.state }}"
139
+
140
+ # --- Lifecycle ---
141
+ # health is handled by the adapter framework (pings the MCP bridge).
142
+ # No MCP tool needed — set to null.
143
+ health: null
@@ -0,0 +1,98 @@
1
+ """Pydantic models for declarative MCP adapter YAML config.
2
+
3
+ Validates the YAML structure used by DeclarativeMcpAdapter.
4
+ No ``model`` field in ResponseMapping — the adapter infers
5
+ the return type from the protocol method name (AR-R1).
6
+
7
+ Architecture: ADR-041, E337, E338 (AR-C2: shared ServerConnection)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Literal
13
+
14
+ from pydantic import BaseModel, Field, model_validator
15
+
16
+
17
+ class AdapterMeta(BaseModel):
18
+ """Top-level adapter identity and protocol selection."""
19
+
20
+ name: str = Field(..., description="Adapter name (used in --adapter flag)")
21
+ protocol: Literal["pm", "docs"] = Field(
22
+ ..., description="Protocol: 'pm' (ProjectManagement) or 'docs' (Documentation)"
23
+ )
24
+ description: str | None = Field(
25
+ default=None, description="Human-readable description"
26
+ )
27
+
28
+
29
+ class ResponseMapping(BaseModel):
30
+ """How to parse MCP tool response into adapter models.
31
+
32
+ No ``model`` field — adapter infers return type from method name (AR-R1).
33
+ """
34
+
35
+ fields: dict[str, str] = Field(
36
+ ..., description="Field name → expression template mapping"
37
+ )
38
+ items_path: str | None = Field(
39
+ default=None,
40
+ description="Dot-path to list in response (e.g. 'data.items')",
41
+ )
42
+
43
+
44
+ class MethodMapping(BaseModel):
45
+ """Maps a protocol method to an MCP tool call."""
46
+
47
+ tool: str = Field(..., description="MCP tool name to call")
48
+ args: dict[str, str] = Field(
49
+ default_factory=dict,
50
+ description="Argument name → expression template mapping",
51
+ )
52
+ response: ResponseMapping | None = Field(
53
+ default=None, description="Response parsing config (None = raw result)"
54
+ )
55
+
56
+
57
+ class ServerRef(BaseModel):
58
+ """Server config: reference MCP registry OR inline connection.
59
+
60
+ Either ``ref`` (name in ``.raise/mcp/`` registry) or ``command``
61
+ (inline connection) must be provided. Both may be set but ref
62
+ takes precedence at resolution time.
63
+
64
+ Architecture: E338 S338.5 — Option B (separate from ServerConnection).
65
+ """
66
+
67
+ ref: str | None = Field(default=None, description="Name in .raise/mcp/ registry")
68
+ command: str | None = Field(
69
+ default=None, description="Server command (e.g. 'uvx', 'npx')"
70
+ )
71
+ args: list[str] = Field(
72
+ default_factory=list, description="Server command arguments"
73
+ )
74
+ env: list[str] | None = Field(
75
+ default=None,
76
+ description="Env var names to pass to server subprocess",
77
+ )
78
+
79
+ @model_validator(mode="after")
80
+ def _ref_or_command(self) -> ServerRef:
81
+ if self.ref is None and self.command is None:
82
+ msg = "Either 'ref' or 'command' must be provided"
83
+ raise ValueError(msg)
84
+ return self
85
+
86
+
87
+ class DeclarativeAdapterConfig(BaseModel):
88
+ """Root config model for a declarative MCP adapter.
89
+
90
+ Parsed from ``.raise/adapters/<name>.yaml``.
91
+ """
92
+
93
+ adapter: AdapterMeta
94
+ server: ServerRef
95
+ methods: dict[str, MethodMapping | None] = Field(
96
+ default_factory=dict,
97
+ description="Protocol method → MCP tool mapping. None = unsupported.",
98
+ )
@@ -0,0 +1,299 @@
1
+ """Filesystem-based PM adapter with YAML file store.
2
+
3
+ Open-core fallback: provides read + write PM functionality without
4
+ external services. Each issue is a YAML file at
5
+ ``.raise/backlog/items/{KEY}.yaml`` validated by Pydantic on load/dump.
6
+
7
+ Architecture: S347.2 (E347 Backlog Automation)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from datetime import UTC, datetime
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ import yaml
18
+
19
+ from raise_cli.adapters.models import (
20
+ AdapterHealth,
21
+ BacklogComment,
22
+ BacklogItem,
23
+ BacklogLink,
24
+ BatchResult,
25
+ Comment,
26
+ CommentRef,
27
+ FailureDetail,
28
+ IssueDetail,
29
+ IssueRef,
30
+ IssueSpec,
31
+ IssueSummary,
32
+ )
33
+
34
+
35
+ class FilesystemPMAdapter:
36
+ """PM adapter backed by YAML file store.
37
+
38
+ Each issue lives at ``.raise/backlog/items/{KEY}.yaml``.
39
+ """
40
+
41
+ def __init__(self, project_root: Path | None = None) -> None:
42
+ self._root = project_root or Path.cwd()
43
+ self._items_dir = self._root / ".raise" / "backlog" / "items"
44
+
45
+ # -- YAML I/O helpers ---------------------------------------------------
46
+
47
+ def _item_path(self, key: str) -> Path:
48
+ """Path to a YAML item file."""
49
+ return self._items_dir / f"{key}.yaml"
50
+
51
+ def _load_item(self, key: str) -> BacklogItem:
52
+ """Load and validate a single YAML item. Raises KeyError if missing."""
53
+ path = self._item_path(key)
54
+ if not path.exists():
55
+ raise KeyError(key)
56
+ raw = yaml.safe_load(path.read_text(encoding="utf-8"))
57
+ return BacklogItem.model_validate(raw)
58
+
59
+ def _save_item(self, item: BacklogItem) -> None:
60
+ """Dump a BacklogItem to YAML, excluding None values for clean files."""
61
+ self._items_dir.mkdir(parents=True, exist_ok=True)
62
+ data = item.model_dump(exclude_none=True)
63
+ # Remove empty collections for clean YAML
64
+ for field in ("comments", "links", "labels"):
65
+ if field in data and not data[field]:
66
+ del data[field]
67
+ if "description" in data and not data["description"]:
68
+ del data["description"]
69
+ path = self._item_path(item.key)
70
+ path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8")
71
+
72
+ def _load_all_items(self) -> list[BacklogItem]:
73
+ """Load all YAML items from the store."""
74
+ if not self._items_dir.is_dir():
75
+ return []
76
+ items: list[BacklogItem] = []
77
+ for path in sorted(self._items_dir.glob("*.yaml")):
78
+ raw = yaml.safe_load(path.read_text(encoding="utf-8"))
79
+ items.append(BacklogItem.model_validate(raw))
80
+ return items
81
+
82
+ # -- Key generation helpers -----------------------------------------------
83
+
84
+ def _next_epic_key(self) -> str:
85
+ """Scan existing E{N}.yaml files and return E{N+1}."""
86
+ max_n = 0
87
+ if self._items_dir.is_dir():
88
+ for path in self._items_dir.glob("E*.yaml"):
89
+ m = re.match(r"E(\d+)\.yaml$", path.name)
90
+ if m:
91
+ max_n = max(max_n, int(m.group(1)))
92
+ return f"E{max_n + 1}"
93
+
94
+ def _next_story_key(self, parent_key: str) -> str:
95
+ """Scan existing S{epic_num}.{M}.yaml and return S{epic_num}.{M+1}.
96
+
97
+ Requires the parent epic key (e.g., 'E1') to derive the epic number.
98
+ """
99
+ m = re.match(r"E(\d+)", parent_key)
100
+ if not m:
101
+ raise ValueError(f"Cannot derive epic number from parent key: {parent_key}")
102
+ epic_num = m.group(1)
103
+ max_m = 0
104
+ if self._items_dir.is_dir():
105
+ for path in self._items_dir.glob(f"S{epic_num}.*.yaml"):
106
+ sm = re.match(rf"S{epic_num}\.(\d+)\.yaml$", path.name)
107
+ if sm:
108
+ max_m = max(max_m, int(sm.group(1)))
109
+ return f"S{epic_num}.{max_m + 1}"
110
+
111
+ # -- Search helper -------------------------------------------------------
112
+
113
+ def _match(self, item: IssueSummary, query: str) -> bool:
114
+ """Check if an item matches a search query.
115
+
116
+ Supports:
117
+ - Empty query: matches all
118
+ - field = value: exact match on status, key
119
+ - bare text: case-insensitive substring match on key + summary
120
+ """
121
+ query = query.strip()
122
+ if not query:
123
+ return True
124
+
125
+ # field = value
126
+ m = re.match(r"(\w+)\s*=\s*(.+)", query)
127
+ if m:
128
+ field, value = m.group(1).lower(), m.group(2).strip().lower()
129
+ if field == "status":
130
+ return item.status.lower() == value
131
+ if field == "name":
132
+ return value in item.summary.lower()
133
+ if field == "priority":
134
+ return False
135
+ return False
136
+
137
+ # bare text -> substring match on key + summary
138
+ q = query.lower()
139
+ return q in item.key.lower() or q in item.summary.lower()
140
+
141
+ # -- Read operations ----------------------------------------------------
142
+
143
+ def get_issue(self, key: str) -> IssueDetail:
144
+ """Get issue detail by key."""
145
+ item = self._load_item(key)
146
+ return IssueDetail(
147
+ key=item.key,
148
+ summary=item.summary,
149
+ status=item.status,
150
+ issue_type=item.issue_type,
151
+ description=item.description,
152
+ labels=item.labels,
153
+ parent_key=item.parent,
154
+ priority=item.priority,
155
+ assignee=item.assignee,
156
+ created=item.created,
157
+ updated=item.updated,
158
+ )
159
+
160
+ def search(self, query: str, limit: int = 50) -> list[IssueSummary]:
161
+ """Search issues."""
162
+ items = self._load_all_items()
163
+ summaries = [
164
+ IssueSummary(
165
+ key=it.key,
166
+ summary=it.summary,
167
+ status=it.status,
168
+ issue_type=it.issue_type,
169
+ parent_key=it.parent,
170
+ )
171
+ for it in items
172
+ ]
173
+ matched = [s for s in summaries if self._match(s, query)]
174
+ return matched[:limit]
175
+
176
+ def get_comments(self, key: str, limit: int = 10) -> list[Comment]:
177
+ """Get comments for an issue. Returns [] if issue not found."""
178
+ try:
179
+ item = self._load_item(key)
180
+ except KeyError:
181
+ return []
182
+ comments = [
183
+ Comment(id=c.id, body=c.body, author=c.author, created=c.created)
184
+ for c in item.comments
185
+ ]
186
+ return comments[:limit]
187
+
188
+ def health(self) -> AdapterHealth:
189
+ """Check adapter health."""
190
+ if self._items_dir.is_dir():
191
+ count = len(list(self._items_dir.glob("*.yaml")))
192
+ return AdapterHealth(
193
+ name="filesystem",
194
+ healthy=True,
195
+ message=f".raise/backlog/items/ ({count} items)",
196
+ )
197
+ return AdapterHealth(
198
+ name="filesystem",
199
+ healthy=False,
200
+ message="YAML store not found (.raise/backlog/items/)",
201
+ )
202
+
203
+ # -- Write operations ---------------------------------------------------
204
+
205
+ def create_issue(self, project_key: str, issue: IssueSpec) -> IssueRef:
206
+ """Create a new issue."""
207
+ meta = issue.metadata or {}
208
+ now = datetime.now(UTC).isoformat()
209
+ itype = issue.issue_type.lower()
210
+ if itype == "epic":
211
+ key = self._next_epic_key()
212
+ elif itype in ("story", "subtask"):
213
+ parent_key = meta.get("parent_key")
214
+ if not parent_key:
215
+ raise KeyError("Story creation requires parent_key")
216
+ key = self._next_story_key(parent_key)
217
+ else:
218
+ # Default to epic-style key for Task and other types
219
+ key = self._next_epic_key()
220
+ new_item = BacklogItem(
221
+ key=key,
222
+ summary=issue.summary,
223
+ issue_type=issue.issue_type,
224
+ status="pending",
225
+ parent=meta.get("parent_key"),
226
+ description=issue.description,
227
+ labels=issue.labels,
228
+ priority=meta.get("priority"),
229
+ created=now,
230
+ updated=now,
231
+ )
232
+ self._save_item(new_item)
233
+ return IssueRef(key=new_item.key)
234
+
235
+ # Fields that must not be mutated via update_issue()
236
+ _IMMUTABLE_FIELDS: frozenset[str] = frozenset(
237
+ {"key", "created", "comments", "links"}
238
+ )
239
+
240
+ def update_issue(self, key: str, fields: dict[str, Any]) -> IssueRef:
241
+ """Update fields on an existing issue."""
242
+ item = self._load_item(key)
243
+ for field_name, value in fields.items():
244
+ if field_name in self._IMMUTABLE_FIELDS:
245
+ continue
246
+ if hasattr(item, field_name):
247
+ setattr(item, field_name, value)
248
+ item.updated = datetime.now(UTC).isoformat()
249
+ self._save_item(item)
250
+ return IssueRef(key=key)
251
+
252
+ def transition_issue(self, key: str, status: str) -> IssueRef:
253
+ """Update issue status."""
254
+ item = self._load_item(key)
255
+ item.status = status
256
+ item.updated = datetime.now(UTC).isoformat()
257
+ self._save_item(item)
258
+ return IssueRef(key=key)
259
+
260
+ def batch_transition(self, keys: list[str], status: str) -> BatchResult:
261
+ """Transition multiple issues."""
262
+ succeeded: list[IssueRef] = []
263
+ failed: list[FailureDetail] = []
264
+ for key in keys:
265
+ try:
266
+ ref = self.transition_issue(key, status)
267
+ succeeded.append(ref)
268
+ except KeyError:
269
+ failed.append(FailureDetail(key=key, error=f"{key} not found"))
270
+ return BatchResult(succeeded=succeeded, failed=failed)
271
+
272
+ # -- Relationship & comment operations ----------------------------------
273
+
274
+ def link_to_parent(self, child_key: str, parent_key: str) -> None:
275
+ """Set parent field on child issue."""
276
+ item = self._load_item(child_key)
277
+ item.parent = parent_key
278
+ item.updated = datetime.now(UTC).isoformat()
279
+ self._save_item(item)
280
+
281
+ def link_issues(self, source: str, target: str, link_type: str) -> None:
282
+ """Add a link from source to target."""
283
+ item = self._load_item(source)
284
+ item.links.append(BacklogLink(target=target, link_type=link_type))
285
+ item.updated = datetime.now(UTC).isoformat()
286
+ self._save_item(item)
287
+
288
+ def add_comment(self, key: str, body: str) -> CommentRef:
289
+ """Add a comment to an issue."""
290
+ item = self._load_item(key)
291
+ next_n = len(item.comments) + 1
292
+ comment_id = f"{key}-{next_n}"
293
+ now = datetime.now(UTC).isoformat()
294
+ item.comments.append(
295
+ BacklogComment(id=comment_id, body=body, author="rai", created=now)
296
+ )
297
+ item.updated = now
298
+ self._save_item(item)
299
+ return CommentRef(id=comment_id)
@@ -0,0 +1,10 @@
1
+ """Backwards-compat re-export. Import from raise_cli.mcp.bridge instead.
2
+
3
+ Architecture: ADR-042, E338 — McpBridge moved to raise_cli.mcp.bridge.
4
+ This shim exists for backwards compatibility with external consumers.
5
+ """
6
+
7
+ from raise_cli.mcp.bridge import McpBridge, McpBridgeError # noqa: F401
8
+ from raise_cli.mcp.models import McpToolInfo, McpToolResult # noqa: F401
9
+
10
+ __all__ = ["McpBridge", "McpBridgeError", "McpToolInfo", "McpToolResult"]