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,201 @@
1
+ """Project manifest schema and persistence.
2
+
3
+ The manifest file (.raise/manifest.yaml) stores project metadata detected
4
+ during initialization, including project type and code file count.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from datetime import UTC, datetime
11
+ from pathlib import Path
12
+ from typing import Any, cast
13
+
14
+ import yaml
15
+ from pydantic import BaseModel, Field, ValidationError, model_validator
16
+
17
+ from raise_cli.config.ide import IdeType
18
+ from raise_cli.config.paths import MANIFEST_FILE, get_raise_dir
19
+ from raise_cli.onboarding.detection import ProjectType
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class ProjectInfo(BaseModel):
25
+ """Information about the project detected during init.
26
+
27
+ Attributes:
28
+ name: Project name (usually directory name).
29
+ project_type: Whether greenfield or brownfield.
30
+ language: Dominant programming language (auto-detected or user-specified).
31
+ test_command: Command to run tests (configuration over convention).
32
+ lint_command: Command to run linter (configuration over convention).
33
+ type_check_command: Command to run type checker (configuration over convention).
34
+ code_file_count: Number of code files detected.
35
+ detected_at: When the project was initialized.
36
+ """
37
+
38
+ name: str
39
+ project_type: ProjectType
40
+ language: str | None = None
41
+ test_command: str | None = None
42
+ lint_command: str | None = None
43
+ type_check_command: str | None = None
44
+ code_file_count: int = 0
45
+ detected_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
46
+
47
+
48
+ class BranchConfig(BaseModel):
49
+ """Branch naming configuration for the project.
50
+
51
+ Attributes:
52
+ development: The development/integration branch name.
53
+ main: The stable/production branch name.
54
+ """
55
+
56
+ development: str = "main"
57
+ main: str = "main"
58
+
59
+
60
+ class IdeManifest(BaseModel):
61
+ """IDE configuration persisted in manifest (legacy single-IDE format).
62
+
63
+ Attributes:
64
+ type: Which IDE this project uses.
65
+ """
66
+
67
+ type: IdeType = "claude"
68
+
69
+
70
+ class AgentsManifest(BaseModel):
71
+ """Multi-agent configuration persisted in manifest.
72
+
73
+ Replaces IdeManifest with a list to support multiple simultaneous agents.
74
+
75
+ Attributes:
76
+ types: List of active agent types (e.g. ["claude", "cursor", "windsurf"]).
77
+ """
78
+
79
+ types: list[str] = Field(default_factory=lambda: ["claude"])
80
+
81
+
82
+ class BacklogConfig(BaseModel):
83
+ """Backlog configuration from manifest (optional section).
84
+
85
+ Attributes:
86
+ adapter_default: Default PM adapter name (e.g., 'jira', 'filesystem').
87
+ """
88
+
89
+ adapter_default: str | None = None
90
+
91
+
92
+ class TierConfig(BaseModel):
93
+ """Tier configuration from manifest (optional section).
94
+
95
+ Attributes:
96
+ level: Tier level string (community, pro, enterprise).
97
+ backend_url: Backend URL for PRO/Enterprise tiers.
98
+ capabilities: List of capability strings enabled for this tier.
99
+ """
100
+
101
+ level: str = "community"
102
+ backend_url: str | None = None
103
+ capabilities: list[str] = Field(default_factory=list)
104
+
105
+
106
+ class ProjectManifest(BaseModel):
107
+ """Project manifest stored in .raise/manifest.yaml.
108
+
109
+ Attributes:
110
+ version: Manifest schema version.
111
+ project: Project information.
112
+ branches: Branch naming configuration.
113
+ ide: Legacy single-IDE configuration (backward compat — read/write).
114
+ agents: Multi-agent configuration (new format).
115
+ tier: Optional tier configuration (S211.5).
116
+ """
117
+
118
+ version: str = "1.0"
119
+ project: ProjectInfo
120
+ branches: BranchConfig = Field(default_factory=BranchConfig)
121
+ ide: IdeManifest = Field(default_factory=IdeManifest)
122
+ agents: AgentsManifest = Field(default_factory=AgentsManifest)
123
+ tier: TierConfig | None = None
124
+ backlog: BacklogConfig | None = None
125
+
126
+ @model_validator(mode="before")
127
+ @classmethod
128
+ def _migrate_ide_to_agents(cls, data: Any) -> dict[str, Any]:
129
+ """Migrate old ide.type format to agents.types on load.
130
+
131
+ If 'agents' key is absent but 'ide' key is present, derive
132
+ agents.types from ide.type for backward compat.
133
+ """
134
+ if not isinstance(data, dict):
135
+ return cast(dict[str, Any], data)
136
+ typed: dict[str, Any] = cast(dict[str, Any], data)
137
+ if "agents" not in typed and "ide" in typed:
138
+ raw_ide: object = typed["ide"]
139
+ if isinstance(raw_ide, dict):
140
+ raw_type: object = cast(dict[str, object], raw_ide).get(
141
+ "type", "claude"
142
+ )
143
+ ide_type: str = str(raw_type) if raw_type is not None else "claude"
144
+ else:
145
+ ide_type = "claude"
146
+ typed["agents"] = {"types": [ide_type]}
147
+ return typed
148
+
149
+
150
+ def save_manifest(manifest: ProjectManifest, project_root: Path) -> None:
151
+ """Save project manifest to .raise/manifest.yaml.
152
+
153
+ Creates .raise/ directory if it doesn't exist.
154
+
155
+ Args:
156
+ manifest: The manifest to save.
157
+ project_root: Root directory of the project.
158
+ """
159
+ raise_dir = get_raise_dir(project_root)
160
+ raise_dir.mkdir(parents=True, exist_ok=True)
161
+
162
+ manifest_path = raise_dir / MANIFEST_FILE
163
+
164
+ # Convert to dict with proper serialization
165
+ data = manifest.model_dump(mode="json")
166
+
167
+ content = yaml.dump(
168
+ data, default_flow_style=False, allow_unicode=True, sort_keys=False
169
+ )
170
+ manifest_path.write_text(content, encoding="utf-8")
171
+ logger.debug("Saved manifest: %s", manifest_path)
172
+
173
+
174
+ def load_manifest(project_root: Path) -> ProjectManifest | None:
175
+ """Load project manifest from .raise/manifest.yaml.
176
+
177
+ Args:
178
+ project_root: Root directory of the project.
179
+
180
+ Returns:
181
+ ProjectManifest if file exists and is valid, None otherwise.
182
+ """
183
+ manifest_path = get_raise_dir(project_root) / MANIFEST_FILE
184
+
185
+ if not manifest_path.exists():
186
+ logger.debug("Manifest not found: %s", manifest_path)
187
+ return None
188
+
189
+ try:
190
+ content = manifest_path.read_text(encoding="utf-8")
191
+ data = yaml.safe_load(content)
192
+ if data is None:
193
+ logger.warning("Empty manifest: %s", manifest_path)
194
+ return None
195
+ return ProjectManifest.model_validate(data)
196
+ except yaml.YAMLError as e:
197
+ logger.warning("Invalid YAML in manifest: %s", e)
198
+ return None
199
+ except ValidationError as e:
200
+ logger.warning("Invalid manifest schema: %s", e)
201
+ return None
@@ -0,0 +1,399 @@
1
+ """MEMORY.md generation for Claude Code and other AI editors.
2
+
3
+ Agent-agnostic generator: produces markdown string from methodology.yaml
4
+ and patterns.jsonl. Knows nothing about file paths or write destinations.
5
+
6
+ The generator is IDE-independent — placement functions (in CLI commands)
7
+ decide where to write the output.
8
+
9
+ Example:
10
+ from raise_cli.onboarding.memory_md import MemoryMdGenerator
11
+
12
+ gen = MemoryMdGenerator()
13
+ content = gen.generate(
14
+ methodology_path=Path(".raise/rai/framework/methodology.yaml"),
15
+ patterns_path=Path(".raise/rai/memory/patterns.jsonl"),
16
+ project_name="my-project",
17
+ )
18
+ Path("MEMORY.md").write_text(content)
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import logging
25
+ from datetime import date
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ import yaml
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class MemoryMdGenerator:
35
+ """Generates MEMORY.md content from methodology and patterns.
36
+
37
+ Part 1 (static): Lifecycle, skills, gates, principles, branches
38
+ from methodology.yaml.
39
+
40
+ Part 2 (dynamic): Key patterns from patterns.jsonl.
41
+
42
+ The generator returns a string — it never writes to disk.
43
+ This separation enables multi-IDE support: generate once,
44
+ write to Claude Code, Cursor, Windsurf, etc.
45
+ """
46
+
47
+ def generate(
48
+ self,
49
+ methodology_path: Path | None = None,
50
+ patterns_path: Path | None = None,
51
+ project_name: str = "project",
52
+ max_patterns: int = 10,
53
+ development_branch: str = "main",
54
+ ) -> str:
55
+ """Generate MEMORY.md content.
56
+
57
+ Args:
58
+ methodology_path: Path to methodology.yaml (Part 1 source).
59
+ patterns_path: Path to patterns.jsonl (Part 2 source).
60
+ project_name: Project name for the header.
61
+ max_patterns: Maximum patterns to include in Part 2.
62
+ development_branch: Development branch name for placeholder
63
+ substitution in branch model (default: "main").
64
+
65
+ Returns:
66
+ Markdown content for MEMORY.md.
67
+ """
68
+ lines: list[str] = []
69
+
70
+ self._add_header(lines, project_name)
71
+
72
+ # Part 1: Static methodology
73
+ methodology = self._load_methodology(methodology_path)
74
+ if methodology:
75
+ self._add_lifecycle_section(lines, methodology)
76
+ self._add_skills_section(lines, methodology)
77
+ self._add_gates_section(lines, methodology)
78
+ self._add_principles_section(lines, methodology)
79
+ self._add_branches_section(lines, methodology, development_branch)
80
+
81
+ # Part 2: Dynamic patterns
82
+ patterns = self._load_patterns(patterns_path)
83
+ self._add_patterns_section(lines, patterns, max_patterns)
84
+
85
+ self._add_footer(lines)
86
+
87
+ return "\n".join(lines)
88
+
89
+ # =========================================================================
90
+ # Header & Footer
91
+ # =========================================================================
92
+
93
+ def _add_header(self, lines: list[str], project_name: str) -> None:
94
+ """Add document header."""
95
+ lines.append(f"# Rai Memory — {project_name}")
96
+ lines.append("")
97
+ lines.append(
98
+ "> Permanent knowledge for this project. Loaded into system prompt."
99
+ )
100
+ lines.append("")
101
+ lines.append("---")
102
+ lines.append("")
103
+
104
+ def _add_footer(self, lines: list[str]) -> None:
105
+ """Add document footer with timestamp."""
106
+ lines.append(f"*Last updated: {date.today().isoformat()}*")
107
+ lines.append("*Generated by `rai graph build`*")
108
+ lines.append("")
109
+
110
+ # =========================================================================
111
+ # Part 1: Methodology sections
112
+ # =========================================================================
113
+
114
+ def _add_lifecycle_section(
115
+ self, lines: list[str], methodology: dict[str, Any]
116
+ ) -> None:
117
+ """Add lifecycle flows section."""
118
+ lifecycle = methodology.get("lifecycle", {})
119
+ if not lifecycle:
120
+ return
121
+
122
+ lines.append("## RaiSE Framework Process")
123
+ lines.append("")
124
+ lines.append("### Work Lifecycle (Always Follow)")
125
+ lines.append("")
126
+ lines.append("```")
127
+
128
+ for level_name, level_data in lifecycle.items():
129
+ label = level_name.upper() + " LEVEL"
130
+ if level_name == "session":
131
+ label = "SESSION LEVEL"
132
+ elif level_name == "story":
133
+ label = "STORY LEVEL (per story)"
134
+ elif level_name == "epic":
135
+ label = "EPIC LEVEL"
136
+
137
+ lines.append(f"{label}:")
138
+ lines.append(f" {level_data['flow']}")
139
+ lines.append("")
140
+
141
+ # Add note if present
142
+ if "note" in level_data:
143
+ lines.append(f"* {level_data['note']}")
144
+ lines.append("")
145
+
146
+ # Remove trailing blank line inside code block
147
+ if lines and lines[-1] == "":
148
+ lines.pop()
149
+ lines.append("```")
150
+ lines.append("")
151
+
152
+ def _add_skills_section(
153
+ self, lines: list[str], methodology: dict[str, Any]
154
+ ) -> None:
155
+ """Add skills by category."""
156
+ skills = methodology.get("skills", {})
157
+ if not skills:
158
+ return
159
+
160
+ # Count total skills
161
+ total = sum(len(category_skills) for category_skills in skills.values())
162
+
163
+ lines.append(f"## Available Skills ({total} total)")
164
+ lines.append("")
165
+
166
+ # Category display names
167
+ category_names = {
168
+ "session": "Session Skills",
169
+ "epic": "Epic Skills",
170
+ "story": "Story Skills",
171
+ "discovery": "Discovery Skills",
172
+ "meta": "Meta Skills",
173
+ "other": "Other Skills",
174
+ }
175
+
176
+ for category, category_skills in skills.items():
177
+ display_name = category_names.get(category, f"{category.title()} Skills")
178
+ lines.append(f"### {display_name}")
179
+
180
+ for skill in category_skills:
181
+ name = skill["name"]
182
+ purpose = skill["purpose"]
183
+ lines.append(f"- `{name}` — {purpose}")
184
+
185
+ lines.append("")
186
+
187
+ lines.append("---")
188
+ lines.append("")
189
+
190
+ def _add_gates_section(self, lines: list[str], methodology: dict[str, Any]) -> None:
191
+ """Add gate requirements as table."""
192
+ gates = methodology.get("gates", {})
193
+ blocking = gates.get("blocking", [])
194
+ if not blocking:
195
+ return
196
+
197
+ lines.append("### Gate Requirements")
198
+ lines.append("")
199
+ lines.append("| Gate | Required Before |")
200
+ lines.append("|------|-----------------|")
201
+
202
+ for gate in blocking:
203
+ require = gate["require"]
204
+ before = gate["before"]
205
+ enforced_by = gate.get("enforced_by", "")
206
+ enforced_str = f" ({enforced_by})" if enforced_by else ""
207
+ lines.append(f"| **{require}** | **{before}**{enforced_str} |")
208
+
209
+ # Add quality gates as simple rows
210
+ quality = gates.get("quality", [])
211
+ for gate in quality:
212
+ gate_name = gate["gate"]
213
+ when = gate["when"]
214
+ lines.append(f"| {gate_name} | {when} |")
215
+
216
+ lines.append("")
217
+ lines.append("---")
218
+ lines.append("")
219
+
220
+ def _add_principles_section(
221
+ self, lines: list[str], methodology: dict[str, Any]
222
+ ) -> None:
223
+ """Add principles as numbered rules."""
224
+ principles = methodology.get("principles", {})
225
+ if not principles:
226
+ return
227
+
228
+ lines.append("## Critical Process Rules")
229
+ lines.append("")
230
+
231
+ rule_num = 1
232
+ for _category, category_principles in principles.items():
233
+ for principle in category_principles:
234
+ name = principle["name"]
235
+ rule = principle["rule"]
236
+ lines.append(f"{rule_num}. **{name}** — {rule}")
237
+ rule_num += 1
238
+
239
+ lines.append("")
240
+ lines.append("---")
241
+ lines.append("")
242
+
243
+ def _add_branches_section(
244
+ self,
245
+ lines: list[str],
246
+ methodology: dict[str, Any],
247
+ development_branch: str = "main",
248
+ ) -> None:
249
+ """Add branch model section."""
250
+ branches = methodology.get("branches", {})
251
+ if not branches:
252
+ return
253
+
254
+ lines.append("## Branch Model")
255
+ lines.append("")
256
+
257
+ structure = branches.get("structure", "")
258
+ if structure:
259
+ structure = structure.replace("{development_branch}", development_branch)
260
+ lines.append("```")
261
+ # structure may have trailing newline from YAML block scalar
262
+ for line in structure.rstrip().split("\n"):
263
+ lines.append(line)
264
+ lines.append("```")
265
+ lines.append("")
266
+
267
+ flow = branches.get("flow", [])
268
+ for item in flow:
269
+ item = item.replace("{development_branch}", development_branch)
270
+ lines.append(f"- {item}")
271
+
272
+ lines.append("")
273
+ lines.append("---")
274
+ lines.append("")
275
+
276
+ # =========================================================================
277
+ # Part 2: Patterns
278
+ # =========================================================================
279
+
280
+ def _add_patterns_section(
281
+ self,
282
+ lines: list[str],
283
+ patterns: list[dict[str, Any]],
284
+ max_patterns: int,
285
+ ) -> None:
286
+ """Add key patterns from patterns.jsonl."""
287
+ lines.append("## Key Patterns (from memory)")
288
+ lines.append("")
289
+
290
+ if not patterns:
291
+ lines.append("*No patterns yet. Patterns are learned during work.*")
292
+ lines.append("")
293
+ lines.append("---")
294
+ lines.append("")
295
+ return
296
+
297
+ # Take most recent patterns (already sorted by load order = chronological)
298
+ selected = patterns[-max_patterns:]
299
+
300
+ for pattern in selected:
301
+ pat_id = pattern.get("id", "?")
302
+ content = pattern.get("content") or pattern.get("pattern", "")
303
+ lines.append(f"- **{pat_id}:** {content}")
304
+
305
+ lines.append("")
306
+ lines.append("---")
307
+ lines.append("")
308
+
309
+ # =========================================================================
310
+ # Loaders
311
+ # =========================================================================
312
+
313
+ def _load_methodology(self, path: Path | None) -> dict[str, Any] | None:
314
+ """Load and parse methodology.yaml.
315
+
316
+ Args:
317
+ path: Path to methodology.yaml.
318
+
319
+ Returns:
320
+ Parsed YAML dictionary, or None if unavailable.
321
+ """
322
+ if path is None or not path.exists():
323
+ logger.debug("methodology.yaml not found at %s", path)
324
+ return None
325
+
326
+ try:
327
+ content = path.read_text(encoding="utf-8")
328
+ data: dict[str, Any] = yaml.safe_load(content)
329
+ return data
330
+ except (yaml.YAMLError, OSError) as e:
331
+ logger.warning("Failed to parse methodology.yaml: %s", e)
332
+ return None
333
+
334
+ def _load_patterns(self, path: Path | None) -> list[dict[str, Any]]:
335
+ """Load patterns from JSONL file.
336
+
337
+ Args:
338
+ path: Path to patterns.jsonl.
339
+
340
+ Returns:
341
+ List of pattern dictionaries. Empty list if unavailable.
342
+ """
343
+ if path is None or not path.exists():
344
+ return []
345
+
346
+ patterns: list[dict[str, Any]] = []
347
+ try:
348
+ with path.open("r", encoding="utf-8") as f:
349
+ for line in f:
350
+ line = line.strip()
351
+ if not line:
352
+ continue
353
+ try:
354
+ data = json.loads(line)
355
+ patterns.append(data)
356
+ except json.JSONDecodeError:
357
+ continue
358
+ except OSError as e:
359
+ logger.warning("Failed to read patterns.jsonl: %s", e)
360
+
361
+ return patterns
362
+
363
+
364
+ def generate_memory_md(
365
+ methodology_path: Path | None = None,
366
+ patterns_path: Path | None = None,
367
+ project_name: str = "project",
368
+ max_patterns: int = 10,
369
+ development_branch: str = "main",
370
+ ) -> str:
371
+ """Convenience function to generate MEMORY.md content.
372
+
373
+ Args:
374
+ methodology_path: Path to methodology.yaml.
375
+ patterns_path: Path to patterns.jsonl.
376
+ project_name: Project name for header.
377
+ max_patterns: Maximum patterns to include.
378
+ development_branch: Development branch name for placeholder
379
+ substitution in branch model (default: "main").
380
+
381
+ Returns:
382
+ Markdown content for MEMORY.md.
383
+
384
+ Example:
385
+ >>> content = generate_memory_md(
386
+ ... methodology_path=Path(".raise/rai/framework/methodology.yaml"),
387
+ ... patterns_path=Path(".raise/rai/memory/patterns.jsonl"),
388
+ ... project_name="my-api",
389
+ ... )
390
+ >>> Path("MEMORY.md").write_text(content)
391
+ """
392
+ generator = MemoryMdGenerator()
393
+ return generator.generate(
394
+ methodology_path=methodology_path,
395
+ patterns_path=patterns_path,
396
+ project_name=project_name,
397
+ max_patterns=max_patterns,
398
+ development_branch=development_branch,
399
+ )