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,528 @@
1
+ """CLI commands for session management.
2
+
3
+ This module provides the `raise session` command group for managing
4
+ working sessions — the lifecycle of a developer's focused work period.
5
+
6
+ Sessions are first-class workflow state, distinct from:
7
+ - Profile (developer identity)
8
+ - Memory (persistent knowledge)
9
+
10
+ Example:
11
+ $ raise session start # Start a new session
12
+ $ raise session start --context # Start with context bundle
13
+ $ raise session close # End the current session
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from pathlib import Path
19
+ from typing import Annotated
20
+
21
+ import typer
22
+
23
+ from raise_cli.cli.commands.journal import journal_app
24
+ from raise_cli.cli.error_handler import cli_error
25
+ from raise_cli.exceptions import RaiSessionNotFoundError
26
+ from raise_cli.hooks.emitter import create_emitter
27
+ from raise_cli.hooks.events import (
28
+ BeforeSessionCloseEvent,
29
+ SessionCloseEvent,
30
+ SessionStartEvent,
31
+ )
32
+ from raise_cli.memory.writer import get_next_id, validate_session_index
33
+ from raise_cli.onboarding.profile import (
34
+ DeveloperProfile,
35
+ end_session,
36
+ increment_session,
37
+ load_developer_profile,
38
+ save_developer_profile,
39
+ start_session,
40
+ )
41
+ from raise_cli.session.bundle import assemble_context_bundle, assemble_sections
42
+ from raise_cli.session.close import CloseInput, load_state_file, process_session_close
43
+ from raise_cli.session.resolver import resolve_session_id
44
+ from raise_cli.session.state import (
45
+ cleanup_session_dir,
46
+ load_session_state,
47
+ migrate_flat_to_session,
48
+ )
49
+
50
+ session_app = typer.Typer(
51
+ name="session",
52
+ help="Manage working sessions",
53
+ no_args_is_help=True,
54
+ )
55
+ session_app.add_typer(journal_app, name="journal")
56
+
57
+
58
+ def _check_cwd_guard(
59
+ profile: DeveloperProfile,
60
+ session_id: str,
61
+ close_project: Path,
62
+ ) -> None:
63
+ """Poka-yoke: reject session close if CWD project != session project.
64
+
65
+ Compares the resolved absolute path of the close project against the
66
+ project recorded in the ActiveSession. Raises cli_error on mismatch.
67
+
68
+ Args:
69
+ profile: Developer profile with active sessions.
70
+ session_id: The session being closed.
71
+ close_project: Project path from --project flag or CWD.
72
+ """
73
+ for active in profile.active_sessions:
74
+ if active.session_id == session_id and active.project:
75
+ session_path = Path(active.project).resolve()
76
+ close_path = close_project.resolve()
77
+ if session_path != close_path:
78
+ cli_error(
79
+ f"CWD mismatch — session {session_id} started in "
80
+ f"{session_path} but close is running from {close_path}. "
81
+ f"Run from the correct project directory, or use "
82
+ f"--project {session_path}.",
83
+ )
84
+ break
85
+
86
+
87
+ @session_app.command()
88
+ def start(
89
+ name: Annotated[
90
+ str | None,
91
+ typer.Option(
92
+ "--name",
93
+ "-n",
94
+ help="Your name (required for first-time setup)",
95
+ ),
96
+ ] = None,
97
+ project: Annotated[
98
+ str | None,
99
+ typer.Option(
100
+ "--project",
101
+ "-p",
102
+ help="Project path to associate with this session",
103
+ ),
104
+ ] = None,
105
+ agent: Annotated[
106
+ str | None,
107
+ typer.Option(
108
+ "--agent",
109
+ help="Agent type (e.g., claude-code, cursor). Default: unknown",
110
+ ),
111
+ ] = None,
112
+ context: Annotated[
113
+ bool,
114
+ typer.Option(
115
+ "--context",
116
+ help="Output a context bundle for AI consumption",
117
+ ),
118
+ ] = False,
119
+ ) -> None:
120
+ """Start a new working session.
121
+
122
+ Increments the session counter and sets active session state.
123
+ Checks for orphaned sessions (started but not closed) and warns if found.
124
+ For first-time users, creates a new developer profile.
125
+
126
+ With --context, outputs a token-optimized context bundle (~150 tokens)
127
+ assembled from the developer profile, session state, and memory graph.
128
+
129
+ Examples:
130
+ $ raise session start # Start session
131
+ $ raise session start --name "Alice" # First-time setup
132
+ $ raise session start --project /my/proj # Start with project path
133
+ $ raise session start --project . --context # Context bundle
134
+ """
135
+ profile = load_developer_profile()
136
+
137
+ if profile is None:
138
+ # First-time user - need name to create profile
139
+ if name is None:
140
+ cli_error(
141
+ "No developer profile found",
142
+ hint="Provide --name for first-time setup: raise session start --name 'Your Name'",
143
+ )
144
+ return # cli_error raises, but this helps pyright
145
+
146
+ # Create new profile
147
+ profile = DeveloperProfile(name=name)
148
+ typer.echo(f"Welcome to RaiSE, {name}! Creating your developer profile...")
149
+
150
+ # Check for active session
151
+ if profile.current_session is not None:
152
+ prev = profile.current_session
153
+ if prev.is_stale():
154
+ typer.echo(
155
+ f"Warning: Stale session detected (started {prev.started_at.date()}, "
156
+ f"project: {prev.project})\n"
157
+ "Previous session was not closed. Learnings may have been lost.\n"
158
+ "Tip: Use /rai-session-close before ending work."
159
+ )
160
+ else:
161
+ typer.echo(
162
+ f"Note: Session already active (project: {prev.project})\n"
163
+ "Starting new session anyway. Previous session not closed."
164
+ )
165
+
166
+ # Jidoka: Validate session index if project specified
167
+ if project is not None:
168
+ personal_dir = Path(project) / ".raise" / "rai" / "personal"
169
+ if personal_dir.exists():
170
+ validation = validate_session_index(personal_dir)
171
+ if not validation.is_valid:
172
+ typer.echo(f"Warning: {validation.summary()}")
173
+ typer.echo("Run `raise memory validate` to fix data quality issues.\n")
174
+
175
+ # Increment session count
176
+ updated = increment_session(profile, project_path=project)
177
+
178
+ # Generate session ID and add to active_sessions
179
+ session_id: str | None = None
180
+ if project is not None:
181
+ personal_dir = Path(project) / ".raise" / "rai" / "personal"
182
+ sessions_index = personal_dir / "sessions" / "index.jsonl"
183
+ session_id = get_next_id(sessions_index, "SES")
184
+
185
+ # Migrate flat files if they exist (before creating dir)
186
+ migrate_flat_to_session(Path(project), session_id)
187
+
188
+ # Ensure per-session directory exists (migration may have created it)
189
+ session_dir = (
190
+ Path(project) / ".raise" / "rai" / "personal" / "sessions" / session_id
191
+ )
192
+ session_dir.mkdir(parents=True, exist_ok=True)
193
+
194
+ # Add to active_sessions (with stale warning)
195
+ agent_name = agent if agent else "unknown"
196
+ updated, stale_sessions = start_session(
197
+ updated, session_id=session_id, project_path=project, agent=agent_name
198
+ )
199
+
200
+ # Warn about stale sessions
201
+ if stale_sessions:
202
+ typer.echo("⚠ Warning: Stale sessions detected (started >24h ago):")
203
+ for stale in stale_sessions:
204
+ typer.echo(
205
+ f" - {stale.session_id} (started {stale.started_at.date()}, project: {stale.project})"
206
+ )
207
+ typer.echo(
208
+ "Consider closing these sessions with: rai session close --session <ID>\n"
209
+ )
210
+
211
+ save_developer_profile(updated)
212
+
213
+ # Emit session:start event
214
+ emitter = create_emitter()
215
+ emitter.emit(
216
+ SessionStartEvent(
217
+ session_id=session_id or "",
218
+ developer=updated.name,
219
+ )
220
+ )
221
+
222
+ # Format agent for output
223
+ agent_name = agent if agent else "unknown"
224
+
225
+ if context and project is not None:
226
+ project_path = Path(project)
227
+ # Load state from per-session dir (migration moved flat file there)
228
+ # Falls back to flat file if no session_id
229
+ state = load_session_state(project_path, session_id=session_id)
230
+ bundle = assemble_context_bundle(
231
+ updated, state, project_path, session_id=session_id
232
+ )
233
+ typer.echo(bundle)
234
+ else:
235
+ if session_id:
236
+ typer.echo(f"▶ Session {session_id} started ({agent_name})")
237
+ else:
238
+ typer.echo(f"▶ Session started ({agent_name})")
239
+ typer.echo(f"Session recorded. (last: {updated.last_session})")
240
+
241
+
242
+ @session_app.command()
243
+ def context(
244
+ sections: Annotated[
245
+ str,
246
+ typer.Option(
247
+ "--sections",
248
+ "-s",
249
+ help="Comma-separated section names to load (e.g., 'governance,behavioral')",
250
+ ),
251
+ ],
252
+ project: Annotated[
253
+ str,
254
+ typer.Option(
255
+ "--project",
256
+ "-p",
257
+ help="Project path",
258
+ ),
259
+ ],
260
+ ) -> None:
261
+ """Load specific context sections for AI consumption.
262
+
263
+ Returns formatted priming sections selected by name. Use after
264
+ `rai session start --context` to load task-relevant context.
265
+
266
+ Available sections: governance, behavioral, coaching, deadlines, progress.
267
+
268
+ Examples:
269
+ $ raise session context --sections governance,behavioral --project .
270
+ $ raise session context --sections coaching --project /my/proj
271
+ """
272
+ profile = load_developer_profile()
273
+ if profile is None:
274
+ cli_error("No developer profile found")
275
+ return
276
+
277
+ project_path = Path(project)
278
+ state = load_session_state(project_path)
279
+
280
+ section_list = [s.strip() for s in sections.split(",") if s.strip()]
281
+
282
+ try:
283
+ output = assemble_sections(section_list, project_path, profile, state)
284
+ except ValueError as e:
285
+ cli_error(str(e))
286
+ return
287
+
288
+ if output:
289
+ typer.echo(output)
290
+
291
+
292
+ @session_app.command()
293
+ def close(
294
+ summary: Annotated[
295
+ str | None,
296
+ typer.Option(
297
+ "--summary",
298
+ "-s",
299
+ help="Session summary",
300
+ ),
301
+ ] = None,
302
+ session_type: Annotated[
303
+ str | None,
304
+ typer.Option(
305
+ "--type",
306
+ "-t",
307
+ help="Session type (feature, research, maintenance, etc.)",
308
+ ),
309
+ ] = None,
310
+ pattern: Annotated[
311
+ str | None,
312
+ typer.Option(
313
+ "--pattern",
314
+ help="Pattern description to record (format: 'description')",
315
+ ),
316
+ ] = None,
317
+ correction: Annotated[
318
+ str | None,
319
+ typer.Option(
320
+ "--correction",
321
+ help="Coaching correction observed",
322
+ ),
323
+ ] = None,
324
+ correction_lesson: Annotated[
325
+ str | None,
326
+ typer.Option(
327
+ "--correction-lesson",
328
+ help="Lesson from the correction",
329
+ ),
330
+ ] = None,
331
+ state_file: Annotated[
332
+ str | None,
333
+ typer.Option(
334
+ "--state-file",
335
+ help="YAML file with full structured session output",
336
+ ),
337
+ ] = None,
338
+ session: Annotated[
339
+ str | None,
340
+ typer.Option(
341
+ "--session",
342
+ help="Session ID to close (e.g., SES-177). Falls back to RAI_SESSION_ID env var.",
343
+ ),
344
+ ] = None,
345
+ project: Annotated[
346
+ str | None,
347
+ typer.Option(
348
+ "--project",
349
+ "-p",
350
+ help="Project path",
351
+ ),
352
+ ] = None,
353
+ ) -> None:
354
+ """End the current working session.
355
+
356
+ With no flags: clears active session state (legacy behavior).
357
+ With --summary or --state-file: performs full structured close —
358
+ records session, patterns, corrections, and updates state.
359
+
360
+ All writes are performed atomically by the CLI — skills should
361
+ NOT call separate telemetry/memory commands.
362
+
363
+ Examples:
364
+ $ raise session close
365
+ $ raise session close --summary "Session protocol design" --type feature
366
+ $ raise session close --state-file /tmp/session-output.yaml --project .
367
+ """
368
+ profile = load_developer_profile()
369
+
370
+ if profile is None:
371
+ cli_error("No developer profile found")
372
+ return # cli_error raises, but this helps pyright
373
+
374
+ # Resolve session ID (from --session flag or RAI_SESSION_ID env var)
375
+ resolved_session_id: str | None = None
376
+ if session:
377
+ import os
378
+
379
+ try:
380
+ resolved_session_id = resolve_session_id(
381
+ session_flag=session, env_var=os.getenv("RAI_SESSION_ID")
382
+ )
383
+ except RaiSessionNotFoundError as e:
384
+ cli_error(str(e))
385
+ return
386
+
387
+ # Determine if this is a structured close
388
+ is_structured = summary is not None or state_file is not None
389
+
390
+ if not is_structured:
391
+ # Legacy behavior: just clear active session
392
+ legacy_project = Path(project) if project else Path.cwd()
393
+ if not resolved_session_id:
394
+ # No session specified — find active session for THIS project
395
+ resolved_project = legacy_project.resolve()
396
+ for active in profile.active_sessions:
397
+ if (
398
+ active.project
399
+ and Path(active.project).resolve() == resolved_project
400
+ ):
401
+ resolved_session_id = active.session_id
402
+ break
403
+ if not resolved_session_id:
404
+ if not profile.active_sessions:
405
+ typer.echo("No active session to close.")
406
+ else:
407
+ typer.echo("No active session for this project.")
408
+ return
409
+
410
+ # CWD poka-yoke (RAISE-139): reject if project mismatch
411
+ _check_cwd_guard(profile, resolved_session_id, legacy_project)
412
+
413
+ # Emit before:session:close — hooks can abort
414
+ emitter = create_emitter()
415
+ before_result = emitter.emit(
416
+ BeforeSessionCloseEvent(
417
+ session_id=resolved_session_id,
418
+ outcome="legacy",
419
+ )
420
+ )
421
+ if before_result.aborted:
422
+ typer.echo(f"Session close aborted: {before_result.abort_message}")
423
+ raise typer.Exit(1)
424
+
425
+ updated = end_session(profile, session_id=resolved_session_id)
426
+ save_developer_profile(updated)
427
+
428
+ emitter.emit(
429
+ SessionCloseEvent(
430
+ session_id=resolved_session_id,
431
+ outcome="legacy",
432
+ )
433
+ )
434
+ typer.echo(f"Session {resolved_session_id} closed.")
435
+ return
436
+
437
+ # Structured close: build CloseInput from flags or state file
438
+ if state_file is not None:
439
+ try:
440
+ close_input = load_state_file(Path(state_file))
441
+ except (FileNotFoundError, ValueError) as e:
442
+ cli_error(f"Failed to load state file: {e}")
443
+ return # cli_error raises
444
+ else:
445
+ close_input = CloseInput(
446
+ summary=summary or "",
447
+ session_type=session_type or "feature",
448
+ )
449
+
450
+ # Coherence validation (RAISE-201): reject if state file session_id
451
+ # doesn't match the target session. Prevents race condition where
452
+ # parallel sessions overwrite each other's state files.
453
+ if (
454
+ state_file is not None
455
+ and close_input.session_id
456
+ and resolved_session_id
457
+ and close_input.session_id != resolved_session_id
458
+ ):
459
+ cli_error(
460
+ f"State file session_id ({close_input.session_id}) does not match "
461
+ f"target session ({resolved_session_id}).\n"
462
+ f"The file may have been overwritten by a parallel session.\n"
463
+ f"Re-run /rai-session-close to regenerate the state file.",
464
+ )
465
+ return # cli_error raises
466
+
467
+ # Override with CLI flags if provided alongside state file
468
+ if pattern:
469
+ close_input.patterns.append({"description": pattern, "type": "process"})
470
+ if correction and correction_lesson:
471
+ close_input.corrections.append(
472
+ {"what": correction, "lesson": correction_lesson}
473
+ )
474
+
475
+ # Resolve project path
476
+ project_path = Path(project) if project else Path.cwd()
477
+
478
+ # CWD poka-yoke (RAISE-139): reject if project mismatch
479
+ # When no --session flag, find the active session for THIS project
480
+ # (not just the first one — that may belong to a different project)
481
+ guard_session_id = resolved_session_id
482
+ if not guard_session_id and profile.active_sessions:
483
+ resolved_project = project_path.resolve()
484
+ for active in profile.active_sessions:
485
+ if active.project and Path(active.project).resolve() == resolved_project:
486
+ guard_session_id = active.session_id
487
+ break
488
+ # If no session matches this project, skip guard (no session to protect)
489
+ if guard_session_id:
490
+ _check_cwd_guard(profile, guard_session_id, project_path)
491
+
492
+ # Emit before:session:close — hooks can abort
493
+ emitter = create_emitter()
494
+ close_sid = guard_session_id or resolved_session_id or ""
495
+ before_result = emitter.emit(
496
+ BeforeSessionCloseEvent(
497
+ session_id=close_sid,
498
+ outcome="structured",
499
+ )
500
+ )
501
+ if before_result.aborted:
502
+ typer.echo(f"Session close aborted: {before_result.abort_message}")
503
+ raise typer.Exit(1)
504
+
505
+ # Process close (pass session_id for per-session state writes)
506
+ close_result = process_session_close(
507
+ close_input, profile, project_path, session_id=resolved_session_id
508
+ )
509
+
510
+ # Cleanup per-session directory
511
+ cleanup_session_id = resolved_session_id or close_result.session_id
512
+ if cleanup_session_id:
513
+ cleanup_session_dir(project_path, cleanup_session_id)
514
+
515
+ # Emit session:close event
516
+ emitter.emit(
517
+ SessionCloseEvent(
518
+ session_id=close_result.session_id,
519
+ outcome="structured",
520
+ )
521
+ )
522
+
523
+ # Output summary
524
+ typer.echo(f"Session {close_result.session_id} closed.")
525
+ if close_result.patterns_added > 0:
526
+ typer.echo(f" Patterns added: {close_result.patterns_added}")
527
+ if close_result.corrections_added > 0:
528
+ typer.echo(f" Corrections recorded: {close_result.corrections_added}")