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,624 @@
1
+ """Developer profile schema and persistence.
2
+
3
+ Personal memory stored in ~/.rai/developer.yaml - cross-project relationship
4
+ between Rai and individual developers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from datetime import UTC, date, datetime
11
+ from enum import StrEnum
12
+ from pathlib import Path
13
+
14
+ import yaml
15
+ from pydantic import BaseModel, Field, ValidationError
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class DelegationLevel(StrEnum):
21
+ """Delegation level for orchestrator HITL decisions.
22
+
23
+ Controls when the orchestrator pauses for human review.
24
+
25
+ Levels:
26
+ REVIEW: Pause and show work for approval before continuing.
27
+ NOTIFY: Show summary and continue unless human intervenes.
28
+ AUTO: Continue without pausing (only hard gates stop execution).
29
+ """
30
+
31
+ REVIEW = "review"
32
+ NOTIFY = "notify"
33
+ AUTO = "auto"
34
+
35
+
36
+ class ExperienceLevel(StrEnum):
37
+ """Developer experience level with RaiSE (Shu-Ha-Ri model).
38
+
39
+ Determines interaction verbosity and explanation depth.
40
+
41
+ Levels:
42
+ SHU: Beginner (sessions 0-5) - explain everything, guide each step
43
+ HA: Intermediate (sessions 6-20) - explain new concepts, efficient on known
44
+ RI: Expert (sessions 21+) - minimal ceremony, maximum efficiency
45
+ """
46
+
47
+ SHU = "shu"
48
+ HA = "ha"
49
+ RI = "ri"
50
+
51
+
52
+ class CommunicationStyle(StrEnum):
53
+ """Communication style preference.
54
+
55
+ Determines how much explanation Rai provides by default.
56
+
57
+ Styles:
58
+ EXPLANATORY: Detailed explanations, good for learning
59
+ BALANCED: Mix of explanation and efficiency
60
+ DIRECT: Minimal explanation, maximum efficiency
61
+ """
62
+
63
+ EXPLANATORY = "explanatory"
64
+ BALANCED = "balanced"
65
+ DIRECT = "direct"
66
+
67
+
68
+ class CommunicationPreferences(BaseModel):
69
+ """Communication preferences for a developer.
70
+
71
+ Controls how Rai interacts with this developer.
72
+
73
+ Attributes:
74
+ style: Explanation verbosity (explanatory/balanced/direct).
75
+ language: Preferred language code (e.g., "en", "es").
76
+ skip_praise: Avoid unnecessary praise or validation.
77
+ detailed_explanations: Provide thorough explanations (overrides style).
78
+ redirect_when_dispersing: Permission to gently redirect off-topic.
79
+ """
80
+
81
+ style: CommunicationStyle = CommunicationStyle.BALANCED
82
+ language: str = "en"
83
+ skip_praise: bool = False
84
+ detailed_explanations: bool = True
85
+ redirect_when_dispersing: bool = False
86
+
87
+
88
+ class DelegationConfig(BaseModel):
89
+ """Delegation preferences for orchestrator HITL control.
90
+
91
+ Stored in developer.yaml under the 'delegation' key. When absent,
92
+ defaults are derived from the developer's ShuHaRi experience level.
93
+
94
+ Attributes:
95
+ default_level: Default delegation level for all skills.
96
+ overrides: Per-skill overrides (skill name → delegation level).
97
+ """
98
+
99
+ default_level: DelegationLevel
100
+ overrides: dict[str, DelegationLevel] = Field(default_factory=dict)
101
+
102
+
103
+ class CurrentSession(BaseModel):
104
+ """Active session state for detecting orphaned sessions.
105
+
106
+ **DEPRECATED:** Use ActiveSession instead. This model is kept for
107
+ backward compatibility during migration from single-session to multi-session.
108
+
109
+ Tracks when a session started and in which project, enabling detection
110
+ of sessions that were started but never closed (e.g., due to interruption).
111
+
112
+ Attributes:
113
+ started_at: UTC timestamp when session began.
114
+ project: Absolute path to the project directory.
115
+ """
116
+
117
+ started_at: datetime
118
+ project: str
119
+
120
+ def is_stale(self, hours: int = 24) -> bool:
121
+ """Check if session is stale (started more than N hours ago).
122
+
123
+ Args:
124
+ hours: Number of hours after which a session is considered stale.
125
+
126
+ Returns:
127
+ True if session started more than `hours` ago.
128
+ """
129
+ now = datetime.now(UTC)
130
+ age = now - self.started_at
131
+ return age.total_seconds() > hours * 3600
132
+
133
+
134
+ class ActiveSession(BaseModel):
135
+ """Active session instance for multi-session support.
136
+
137
+ Replaces CurrentSession to support concurrent sessions on the same project.
138
+ Multiple AI agents/terminals can run simultaneously without state corruption.
139
+
140
+ Attributes:
141
+ session_id: Unique session identifier (e.g., "SES-177").
142
+ started_at: UTC timestamp when session began.
143
+ project: Absolute path to the project directory.
144
+ agent: Agent type metadata (e.g., "claude-code", "cursor"). Default: "unknown".
145
+ """
146
+
147
+ session_id: str
148
+ started_at: datetime
149
+ project: str
150
+ agent: str = "unknown"
151
+
152
+ def is_stale(self, hours: int = 24) -> bool:
153
+ """Check if session is stale (started more than N hours ago).
154
+
155
+ Args:
156
+ hours: Number of hours after which a session is considered stale.
157
+
158
+ Returns:
159
+ True if session started more than `hours` ago.
160
+ """
161
+ now = datetime.now(UTC)
162
+ age = now - self.started_at
163
+ return age.total_seconds() > hours * 3600
164
+
165
+
166
+ class Correction(BaseModel):
167
+ """A coaching correction episode.
168
+
169
+ Records when Rai observed a behavioral pattern that needed adjustment
170
+ and the lesson learned from it.
171
+
172
+ Attributes:
173
+ session: Session ID where correction occurred (e.g., "SES-097").
174
+ what: Description of the behavior observed.
175
+ lesson: The lesson or principle derived from the correction.
176
+ """
177
+
178
+ session: str
179
+ what: str
180
+ lesson: str
181
+
182
+
183
+ class Deadline(BaseModel):
184
+ """An operational deadline Rai tracks.
185
+
186
+ Deadlines modulate Rai's behavior — urgency, focus, pushback.
187
+ Not governance artifacts; these are Rai's operational context.
188
+
189
+ Attributes:
190
+ name: Short name for the deadline (e.g., "F&F").
191
+ date: Target date.
192
+ notes: Additional context about the deadline.
193
+ """
194
+
195
+ name: str
196
+ date: date
197
+ notes: str = ""
198
+
199
+
200
+ class RelationshipState(BaseModel):
201
+ """State of the Rai-developer relationship.
202
+
203
+ Attributes:
204
+ quality: Relationship quality level.
205
+ since: Date when the relationship started.
206
+ trajectory: Direction of relationship development.
207
+ """
208
+
209
+ quality: str = "new"
210
+ since: date | None = None
211
+ trajectory: str = "starting"
212
+
213
+
214
+ class CoachingContext(BaseModel):
215
+ """Rai's coaching observations about a developer.
216
+
217
+ Accumulates over time in ~/.rai/developer.yaml. Corrections
218
+ are capped at 10 (FIFO — oldest drops when new ones are added).
219
+
220
+ Attributes:
221
+ strengths: Observed developer strengths.
222
+ growth_edge: Current primary growth area.
223
+ trust_level: Trust level in the relationship.
224
+ autonomy: Autonomy observation notes.
225
+ corrections: Recent corrections (max 10, FIFO).
226
+ communication_notes: Notes about communication patterns.
227
+ relationship: State of the Rai-developer relationship.
228
+ """
229
+
230
+ strengths: list[str] = Field(default_factory=list)
231
+ growth_edge: str = ""
232
+ trust_level: str = "new"
233
+ autonomy: str = ""
234
+ corrections: list[Correction] = Field(default_factory=lambda: list[Correction]())
235
+ communication_notes: list[str] = Field(default_factory=list)
236
+ relationship: RelationshipState = Field(default_factory=RelationshipState)
237
+
238
+
239
+ CORRECTIONS_MAX = 10
240
+
241
+
242
+ class DeveloperProfile(BaseModel):
243
+ """Personal profile for a developer using RaiSE.
244
+
245
+ Stored in ~/.rai/developer.yaml and persists across projects.
246
+ Enables Rai to adapt interaction style based on experience.
247
+
248
+ Attributes:
249
+ name: Developer's name for personalized interaction.
250
+ pattern_prefix: Single-letter prefix for pattern IDs (e.g., 'E' for Emilio).
251
+ Used to prevent pattern ID collisions in multi-developer repos.
252
+ Defaults to first letter of name if not set.
253
+ experience_level: Current Shu-Ha-Ri level (affects verbosity).
254
+ communication: Communication style preferences.
255
+ skills_mastered: List of skill names the developer has mastered.
256
+ universal_patterns: Patterns that apply across all projects.
257
+ first_session: Date of first RaiSE session.
258
+ last_session: Date of most recent session.
259
+ projects: List of project paths worked on.
260
+ current_session: Active session state, or None if no session active.
261
+ coaching: Coaching context with corrections and relationship state.
262
+ deadlines: Operational deadlines Rai tracks.
263
+ """
264
+
265
+ name: str
266
+ pattern_prefix: str | None = Field(
267
+ default=None,
268
+ description="Single-letter prefix for pattern IDs (e.g., 'E'). "
269
+ "Defaults to first letter of name.",
270
+ )
271
+ experience_level: ExperienceLevel = ExperienceLevel.SHU
272
+ communication: CommunicationPreferences = Field(
273
+ default_factory=CommunicationPreferences
274
+ )
275
+ skills_mastered: list[str] = Field(default_factory=list)
276
+ universal_patterns: list[str] = Field(default_factory=list)
277
+ first_session: date | None = None
278
+ last_session: date | None = None
279
+ projects: list[str] = Field(default_factory=list)
280
+ current_session: CurrentSession | None = (
281
+ None # DEPRECATED: migrated to active_sessions
282
+ )
283
+ active_sessions: list[ActiveSession] = Field(
284
+ default_factory=lambda: list[ActiveSession]()
285
+ )
286
+ coaching: CoachingContext = Field(default_factory=CoachingContext)
287
+ delegation: DelegationConfig | None = None
288
+ deadlines: list[Deadline] = Field(default_factory=lambda: list[Deadline]())
289
+
290
+ def get_pattern_prefix(self) -> str:
291
+ """Get the developer's pattern prefix.
292
+
293
+ Returns explicit pattern_prefix if set, otherwise first letter of name (uppercased).
294
+ """
295
+ if self.pattern_prefix:
296
+ return self.pattern_prefix.upper()
297
+ return self.name[0].upper() if self.name else "X"
298
+
299
+
300
+ _SHUHARI_DELEGATION: dict[ExperienceLevel, DelegationLevel] = {
301
+ ExperienceLevel.SHU: DelegationLevel.REVIEW,
302
+ ExperienceLevel.HA: DelegationLevel.NOTIFY,
303
+ ExperienceLevel.RI: DelegationLevel.AUTO,
304
+ }
305
+
306
+
307
+ def resolve_delegation(profile: DeveloperProfile, skill_name: str) -> DelegationLevel:
308
+ """Resolve the effective delegation level for a skill.
309
+
310
+ Precedence: per-skill override > explicit default_level > ShuHaRi derivation.
311
+
312
+ Args:
313
+ profile: Developer profile with optional delegation config.
314
+ skill_name: Name of the skill (e.g., "rai-story-design").
315
+
316
+ Returns:
317
+ The effective delegation level for the given skill.
318
+ """
319
+ if profile.delegation is not None:
320
+ if skill_name in profile.delegation.overrides:
321
+ return profile.delegation.overrides[skill_name]
322
+ return profile.delegation.default_level
323
+ return _SHUHARI_DELEGATION[profile.experience_level]
324
+
325
+
326
+ # Constants
327
+ RAI_HOME_DIR = ".rai"
328
+ DEVELOPER_PROFILE_FILE = "developer.yaml"
329
+
330
+
331
+ def get_rai_home() -> Path:
332
+ """Get the path to ~/.rai/ directory.
333
+
334
+ Returns:
335
+ Path to the user's .rai directory in their home folder.
336
+ """
337
+ return Path.home() / RAI_HOME_DIR
338
+
339
+
340
+ def _migrate_current_session(profile: DeveloperProfile) -> DeveloperProfile:
341
+ """Migrate old current_session format to active_sessions list.
342
+
343
+ Backward compatibility migration for RAISE-137. Converts single
344
+ current_session (dict) to active_sessions (list) with generated session ID.
345
+
346
+ Args:
347
+ profile: Profile to migrate.
348
+
349
+ Returns:
350
+ Profile with migration applied (may be same instance if no migration needed).
351
+ """
352
+ if profile.current_session is None:
353
+ # No current session to migrate
354
+ return profile
355
+
356
+ if len(profile.active_sessions) > 0:
357
+ # Already migrated or has active sessions — don't re-migrate
358
+ logger.debug("Profile already has active_sessions, skipping migration")
359
+ return profile
360
+
361
+ # The old current_session is stale — it was never properly closed
362
+ # under the old format. Clear it instead of converting to a zombie
363
+ # SES-MIGRATED entry that blocks future session closes.
364
+ updated = profile.model_copy(deep=True)
365
+ updated.active_sessions = []
366
+ updated.current_session = None
367
+
368
+ logger.info("Migrated current_session: cleared stale session (old format)")
369
+ return updated
370
+
371
+
372
+ def load_developer_profile() -> DeveloperProfile | None:
373
+ """Load developer profile from ~/.rai/developer.yaml.
374
+
375
+ Automatically migrates old current_session format to active_sessions
376
+ if needed (backward compatibility for RAISE-137).
377
+
378
+ Returns:
379
+ DeveloperProfile if file exists and is valid, None otherwise.
380
+ """
381
+ rai_home = get_rai_home()
382
+ profile_path = rai_home / DEVELOPER_PROFILE_FILE
383
+
384
+ if not profile_path.exists():
385
+ logger.debug("Developer profile not found: %s", profile_path)
386
+ return None
387
+
388
+ try:
389
+ content = profile_path.read_text(encoding="utf-8")
390
+ data = yaml.safe_load(content)
391
+ if data is None:
392
+ logger.warning("Empty developer profile: %s", profile_path)
393
+ return None
394
+
395
+ profile = DeveloperProfile.model_validate(data)
396
+
397
+ # Migrate if needed
398
+ migrated = _migrate_current_session(profile)
399
+ if migrated is not profile:
400
+ # Migration occurred — save immediately
401
+ save_developer_profile(migrated)
402
+ return migrated
403
+
404
+ return profile
405
+ except yaml.YAMLError as e:
406
+ logger.warning("Invalid YAML in developer profile: %s", e)
407
+ return None
408
+ except ValidationError as e:
409
+ logger.warning("Invalid developer profile schema: %s", e)
410
+ return None
411
+
412
+
413
+ def save_developer_profile(profile: DeveloperProfile) -> None:
414
+ """Save developer profile to ~/.rai/developer.yaml.
415
+
416
+ Creates ~/.rai/ directory if it doesn't exist.
417
+
418
+ Args:
419
+ profile: The developer profile to save.
420
+ """
421
+ rai_home = get_rai_home()
422
+ rai_home.mkdir(parents=True, exist_ok=True)
423
+
424
+ profile_path = rai_home / DEVELOPER_PROFILE_FILE
425
+
426
+ # Convert to dict with proper serialization
427
+ data = profile.model_dump(mode="json")
428
+
429
+ content = yaml.dump(
430
+ data, default_flow_style=False, allow_unicode=True, sort_keys=False
431
+ )
432
+ profile_path.write_text(content, encoding="utf-8")
433
+ logger.debug("Saved developer profile: %s", profile_path)
434
+
435
+
436
+ def increment_session(
437
+ profile: DeveloperProfile, project_path: str | None = None
438
+ ) -> DeveloperProfile:
439
+ """Update session metadata (last_session date and projects list).
440
+
441
+ Pure function that returns a new profile instance without modifying
442
+ the original. Does NOT persist to disk - caller is responsible for saving.
443
+
444
+ Note: Session count is derived from sessions/index.jsonl, not tracked here.
445
+
446
+ Args:
447
+ profile: The developer profile to update.
448
+ project_path: Optional project path to add to projects list.
449
+
450
+ Returns:
451
+ Updated profile with session metadata.
452
+ """
453
+ updates: dict[str, object] = {
454
+ "last_session": date.today(),
455
+ }
456
+
457
+ # Add project path if provided and not already present
458
+ if project_path is not None and project_path not in profile.projects:
459
+ updates["projects"] = [*profile.projects, project_path]
460
+
461
+ return profile.model_copy(update=updates)
462
+
463
+
464
+ def start_session(
465
+ profile: DeveloperProfile,
466
+ session_id: str,
467
+ project_path: str,
468
+ agent: str = "unknown",
469
+ ) -> tuple[DeveloperProfile, list[ActiveSession]]:
470
+ """Mark a session as active by adding to active_sessions list.
471
+
472
+ Adds an ActiveSession to the profile's active_sessions list. Also detects
473
+ stale sessions (started >24h ago) and returns them for warning.
474
+
475
+ Args:
476
+ profile: The developer profile to update.
477
+ session_id: Unique session identifier (e.g., "SES-177").
478
+ project_path: Absolute path to the project directory.
479
+ agent: Agent type (e.g., "claude-code", "cursor"). Default: "unknown".
480
+
481
+ Returns:
482
+ Tuple of (updated profile, list of stale sessions for warning).
483
+ """
484
+ # Detect stale sessions before adding new one
485
+ stale_sessions = [s for s in profile.active_sessions if s.is_stale(hours=24)]
486
+
487
+ # Create new active session
488
+ new_session = ActiveSession(
489
+ session_id=session_id,
490
+ started_at=datetime.now(UTC),
491
+ project=project_path,
492
+ agent=agent,
493
+ )
494
+
495
+ # Remove existing session for same project (idempotency — RAISE-155)
496
+ updated_sessions = [
497
+ s for s in profile.active_sessions if s.project != project_path
498
+ ]
499
+ updated_sessions.append(new_session)
500
+ updated = profile.model_copy(update={"active_sessions": updated_sessions})
501
+
502
+ return updated, stale_sessions
503
+
504
+
505
+ def end_session(profile: DeveloperProfile, session_id: str) -> DeveloperProfile:
506
+ """Remove a session from active_sessions list.
507
+
508
+ Removes the specified session from the profile's active_sessions list.
509
+ If session_id doesn't exist, returns profile unchanged (no-op).
510
+
511
+ Args:
512
+ profile: The developer profile to update.
513
+ session_id: Session identifier to remove (e.g., "SES-177").
514
+
515
+ Returns:
516
+ Updated profile with session removed from active_sessions.
517
+ """
518
+ # Filter out the specified session
519
+ updated_sessions = [
520
+ s for s in profile.active_sessions if s.session_id != session_id
521
+ ]
522
+ return profile.model_copy(update={"active_sessions": updated_sessions})
523
+
524
+
525
+ def add_correction(
526
+ profile: DeveloperProfile, session_id: str, what: str, lesson: str
527
+ ) -> DeveloperProfile:
528
+ """Add a coaching correction to the profile.
529
+
530
+ Maintains FIFO cap of CORRECTIONS_MAX — oldest correction is dropped
531
+ when a new one is added and the list is at capacity.
532
+
533
+ Args:
534
+ profile: The developer profile to update.
535
+ session_id: Session ID where correction occurred.
536
+ what: Description of the behavior observed.
537
+ lesson: The lesson derived from the correction.
538
+
539
+ Returns:
540
+ Updated profile with new correction added.
541
+ """
542
+ correction = Correction(session=session_id, what=what, lesson=lesson)
543
+ corrections = [*profile.coaching.corrections, correction]
544
+ if len(corrections) > CORRECTIONS_MAX:
545
+ corrections = corrections[-CORRECTIONS_MAX:]
546
+ coaching = profile.coaching.model_copy(update={"corrections": corrections})
547
+ return profile.model_copy(update={"coaching": coaching})
548
+
549
+
550
+ def add_deadline(
551
+ profile: DeveloperProfile, name: str, deadline_date: date, notes: str = ""
552
+ ) -> DeveloperProfile:
553
+ """Add an operational deadline to the profile.
554
+
555
+ If a deadline with the same name exists, it is replaced.
556
+
557
+ Args:
558
+ profile: The developer profile to update.
559
+ name: Short name for the deadline.
560
+ deadline_date: Target date.
561
+ notes: Additional context.
562
+
563
+ Returns:
564
+ Updated profile with deadline added or updated.
565
+ """
566
+ deadline = Deadline(name=name, date=deadline_date, notes=notes)
567
+ # Replace existing deadline with same name, or append
568
+ deadlines = [d for d in profile.deadlines if d.name != name]
569
+ deadlines.append(deadline)
570
+ return profile.model_copy(update={"deadlines": deadlines})
571
+
572
+
573
+ def update_coaching(
574
+ profile: DeveloperProfile,
575
+ strengths: list[str] | None = None,
576
+ growth_edge: str | None = None,
577
+ trust_level: str | None = None,
578
+ autonomy: str | None = None,
579
+ relationship: dict[str, str] | None = None,
580
+ communication_notes: list[str] | None = None,
581
+ ) -> DeveloperProfile:
582
+ """Update coaching context fields.
583
+
584
+ Only updates fields that are explicitly provided (not None).
585
+
586
+ Args:
587
+ profile: The developer profile to update.
588
+ strengths: New strengths list (replaces existing).
589
+ growth_edge: New growth edge description.
590
+ trust_level: New trust level.
591
+ autonomy: New autonomy observation.
592
+ relationship: Dict with optional keys (quality, trajectory).
593
+ Updates RelationshipState fields individually.
594
+ communication_notes: Notes about communication patterns (replaces existing).
595
+
596
+ Returns:
597
+ Updated profile with coaching changes.
598
+ """
599
+ updates: dict[str, object] = {}
600
+ if strengths is not None:
601
+ updates["strengths"] = strengths
602
+ if growth_edge is not None:
603
+ updates["growth_edge"] = growth_edge
604
+ if trust_level is not None:
605
+ updates["trust_level"] = trust_level
606
+ if autonomy is not None:
607
+ updates["autonomy"] = autonomy
608
+ if communication_notes is not None:
609
+ updates["communication_notes"] = communication_notes
610
+ if relationship is not None:
611
+ rel_updates: dict[str, object] = {}
612
+ if "quality" in relationship:
613
+ rel_updates["quality"] = relationship["quality"]
614
+ if "trajectory" in relationship:
615
+ rel_updates["trajectory"] = relationship["trajectory"]
616
+ if rel_updates:
617
+ updated_rel = profile.coaching.relationship.model_copy(update=rel_updates)
618
+ updates["relationship"] = updated_rel
619
+
620
+ if not updates:
621
+ return profile
622
+
623
+ coaching = profile.coaching.model_copy(update=updates)
624
+ return profile.model_copy(update={"coaching": coaching})