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