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,410 @@
1
+ """CLI commands for Rai's telemetry signals: work lifecycle, sessions, calibration.
2
+
3
+ The signal group owns commands that emit telemetry events to JSONL files.
4
+ These were extracted from the `memory` God Object in RAISE-247 (ADR-038).
5
+
6
+ Commands:
7
+ - emit-work: Emit a work lifecycle event (epic/story phases)
8
+ - emit-session: Emit a session completion event
9
+ - emit-calibration: Emit an estimation calibration event
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from datetime import UTC, datetime
16
+ from typing import Annotated, Literal
17
+
18
+ import typer
19
+ from rich.console import Console
20
+
21
+ from raise_cli.cli.error_handler import cli_error
22
+ from raise_cli.session.resolver import resolve_session_id_optional
23
+ from raise_cli.telemetry.schemas import (
24
+ CalibrationEvent,
25
+ SessionEvent,
26
+ WorkLifecycle,
27
+ )
28
+ from raise_cli.telemetry.writer import emit
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ signal_app = typer.Typer(
33
+ name="signal",
34
+ help="Emit lifecycle and telemetry signals",
35
+ no_args_is_help=True,
36
+ )
37
+
38
+ console = Console()
39
+
40
+
41
+ @signal_app.command("emit-work")
42
+ def emit_work(
43
+ work_type: Annotated[
44
+ str,
45
+ typer.Argument(help="Work type (epic, story)"),
46
+ ],
47
+ work_id: Annotated[
48
+ str,
49
+ typer.Argument(help="Work ID (e.g., E9, F9.4)"),
50
+ ],
51
+ event_type: Annotated[
52
+ str,
53
+ typer.Option(
54
+ "--event",
55
+ "-e",
56
+ help="Event type (start, complete, blocked, unblocked, abandoned)",
57
+ ),
58
+ ] = "start",
59
+ phase: Annotated[
60
+ str,
61
+ typer.Option("--phase", "-p", help="Phase (design, plan, implement, review)"),
62
+ ] = "design",
63
+ blocker: Annotated[
64
+ str,
65
+ typer.Option(
66
+ "--blocker", "-b", help="Blocker description (for blocked events)"
67
+ ),
68
+ ] = "",
69
+ session: Annotated[
70
+ str | None,
71
+ typer.Option(
72
+ "--session",
73
+ help="Session ID (e.g., SES-177). Falls back to RAI_SESSION_ID env var.",
74
+ ),
75
+ ] = None,
76
+ ) -> None:
77
+ """Emit a work lifecycle event for Lean flow analysis.
78
+
79
+ Tracks work items (epics, stories) through normalized phases to enable:
80
+ - Lead time: total time from start to complete
81
+ - Wait time: gaps between phases
82
+ - WIP: work started but not completed
83
+ - Bottlenecks: which phase takes longest
84
+ - Cross-level analysis: compare epic vs story flow
85
+
86
+ Phases (normalized across all work types):
87
+ - design: Scope definition and specification
88
+ - plan: Task/story decomposition and sequencing
89
+ - implement: Active development work
90
+ - review: Retrospective and learnings
91
+
92
+ Examples:
93
+ # Epic lifecycle
94
+ $ rai signal emit-work epic E9 --event start --phase design
95
+ $ rai signal emit-work epic E9 -e complete -p design
96
+ $ rai signal emit-work epic E9 -e start -p plan
97
+
98
+ # Story lifecycle
99
+ $ rai signal emit-work story S9.4 --event start --phase design
100
+ $ rai signal emit-work story S9.4 -e complete -p implement
101
+ $ rai signal emit-work story S9.4 -e start -p review
102
+
103
+ # Work blocked
104
+ $ rai signal emit-work story S9.4 -e blocked -p plan -b "unclear requirements"
105
+
106
+ # Work unblocked
107
+ $ rai signal emit-work story S9.4 -e unblocked -p plan
108
+ """
109
+ # Validate work type
110
+ valid_work_types: list[Literal["epic", "story"]] = ["epic", "story"]
111
+ work_type_lower = work_type.lower()
112
+ if work_type_lower not in valid_work_types:
113
+ cli_error(
114
+ f"Invalid work type: {work_type}",
115
+ hint=f"Valid types: {', '.join(valid_work_types)}",
116
+ exit_code=7,
117
+ )
118
+
119
+ # Validate event type
120
+ valid_events: list[
121
+ Literal["start", "complete", "blocked", "unblocked", "abandoned"]
122
+ ] = [
123
+ "start",
124
+ "complete",
125
+ "blocked",
126
+ "unblocked",
127
+ "abandoned",
128
+ ]
129
+ if event_type not in valid_events:
130
+ cli_error(
131
+ f"Invalid event: {event_type}",
132
+ hint=f"Valid events: {', '.join(valid_events)}",
133
+ exit_code=7,
134
+ )
135
+
136
+ # Validate phase
137
+ valid_phases: list[
138
+ Literal["init", "design", "plan", "implement", "review", "close"]
139
+ ] = [
140
+ "init",
141
+ "design",
142
+ "plan",
143
+ "implement",
144
+ "review",
145
+ "close",
146
+ ]
147
+ if phase not in valid_phases:
148
+ cli_error(
149
+ f"Invalid phase: {phase}",
150
+ hint=f"Valid phases: {', '.join(valid_phases)}",
151
+ exit_code=7,
152
+ )
153
+
154
+ # Blocker is required for blocked events
155
+ blocker_value = blocker if blocker else None
156
+ if event_type == "blocked" and not blocker_value:
157
+ console.print(
158
+ "[yellow]Warning:[/yellow] No blocker description provided for blocked event"
159
+ )
160
+
161
+ # Create event
162
+ lifecycle_event = WorkLifecycle(
163
+ timestamp=datetime.now(UTC),
164
+ work_type=work_type_lower, # type: ignore[arg-type]
165
+ work_id=work_id,
166
+ event=event_type, # type: ignore[arg-type]
167
+ phase=phase, # type: ignore[arg-type]
168
+ blocker=blocker_value,
169
+ )
170
+
171
+ # Resolve optional session ID
172
+ import os
173
+
174
+ session_id = resolve_session_id_optional(session, os.environ.get("RAI_SESSION_ID"))
175
+
176
+ # Emit signal
177
+ result = emit(lifecycle_event, session_id=session_id)
178
+
179
+ # Bridge: fire WorkLifecycleEvent to hook system (non-fatal)
180
+ if result.success:
181
+ try:
182
+ from raise_cli.hooks.emitter import create_emitter
183
+ from raise_cli.hooks.events import WorkLifecycleEvent
184
+
185
+ emitter = create_emitter()
186
+ emitter.emit(
187
+ WorkLifecycleEvent(
188
+ work_type=work_type_lower,
189
+ work_id=work_id,
190
+ event=event_type,
191
+ phase=phase,
192
+ )
193
+ )
194
+ except Exception: # noqa: BLE001
195
+ # Hook failure is non-fatal — telemetry was already written
196
+ logger.warning("Hook dispatch failed for work:lifecycle event")
197
+
198
+ if result.success:
199
+ # Format label based on work type
200
+ label = f"{work_type_lower.capitalize()} {work_id}"
201
+
202
+ # Format output based on event type
203
+ if event_type == "start":
204
+ console.print(f"\n[green]▶[/green] {label} → {phase} started")
205
+ elif event_type == "complete":
206
+ console.print(f"\n[green]✓[/green] {label} → {phase} complete")
207
+ elif event_type == "blocked":
208
+ console.print(f"\n[red]⏸[/red] {label} → {phase} blocked")
209
+ if blocker_value:
210
+ console.print(f" Blocker: {blocker_value}")
211
+ elif event_type == "unblocked":
212
+ console.print(f"\n[green]▶[/green] {label} → {phase} unblocked")
213
+ elif event_type == "abandoned":
214
+ console.print(f"\n[yellow]✗[/yellow] {label} → {phase} abandoned")
215
+
216
+ console.print(f"\n[dim]Saved to: {result.path}[/dim]\n")
217
+ else:
218
+ cli_error(result.error or "Failed to emit work lifecycle event")
219
+
220
+
221
+ @signal_app.command("emit-session")
222
+ def emit_session(
223
+ session_type: Annotated[
224
+ str,
225
+ typer.Option(
226
+ "--type", "-t", help="Session type (e.g., story, research, maintenance)"
227
+ ),
228
+ ] = "story",
229
+ outcome: Annotated[
230
+ str,
231
+ typer.Option(
232
+ "--outcome",
233
+ "-o",
234
+ help="Session outcome (success, partial, abandoned)",
235
+ ),
236
+ ] = "success",
237
+ duration: Annotated[
238
+ int,
239
+ typer.Option("--duration", "-d", help="Session duration in minutes"),
240
+ ] = 0,
241
+ stories: Annotated[
242
+ str,
243
+ typer.Option("--stories", "-f", help="Stories worked on (comma-separated)"),
244
+ ] = "",
245
+ session: Annotated[
246
+ str | None,
247
+ typer.Option(
248
+ "--session",
249
+ help="Session ID (e.g., SES-177). Falls back to RAI_SESSION_ID env var.",
250
+ ),
251
+ ] = None,
252
+ ) -> None:
253
+ """Emit a session event to telemetry.
254
+
255
+ Records a session completion signal for local learning and insights.
256
+ Called at the end of /rai-session-close to capture session metadata.
257
+
258
+ Examples:
259
+ # Basic session complete
260
+ $ rai signal emit-session --type story --outcome success
261
+
262
+ # With duration and stories
263
+ $ rai signal emit-session -t story -o success -d 45 -f S9.1,S9.2,S9.3
264
+
265
+ # Research session
266
+ $ rai signal emit-session --type research --outcome partial --duration 90
267
+ """
268
+ # Validate outcome
269
+ valid_outcomes: list[Literal["success", "partial", "abandoned"]] = [
270
+ "success",
271
+ "partial",
272
+ "abandoned",
273
+ ]
274
+ if outcome not in valid_outcomes:
275
+ cli_error(
276
+ f"Invalid outcome: {outcome}",
277
+ hint=f"Valid outcomes: {', '.join(valid_outcomes)}",
278
+ exit_code=7,
279
+ )
280
+
281
+ # Parse stories
282
+ stories_list = [f.strip() for f in stories.split(",") if f.strip()]
283
+
284
+ # Create event
285
+ event = SessionEvent(
286
+ timestamp=datetime.now(UTC),
287
+ session_type=session_type,
288
+ outcome=outcome, # type: ignore[arg-type]
289
+ duration_min=duration,
290
+ stories=stories_list,
291
+ )
292
+
293
+ # Resolve optional session ID
294
+ import os
295
+
296
+ session_id = resolve_session_id_optional(session, os.environ.get("RAI_SESSION_ID"))
297
+
298
+ # Emit signal
299
+ result = emit(event, session_id=session_id)
300
+
301
+ if result.success:
302
+ console.print("\n[green]✓[/green] Session event recorded")
303
+ console.print(f" Type: {session_type}")
304
+ console.print(f" Outcome: {outcome}")
305
+ console.print(f" Duration: {duration} min")
306
+ if stories_list:
307
+ console.print(f" Stories: {', '.join(stories_list)}")
308
+ console.print(f"\n[dim]Saved to: {result.path}[/dim]\n")
309
+ else:
310
+ cli_error(result.error or "Failed to emit session event")
311
+
312
+
313
+ @signal_app.command("emit-calibration")
314
+ def emit_calibration(
315
+ story: Annotated[
316
+ str,
317
+ typer.Argument(help="Story ID (e.g., S9.4)"),
318
+ ],
319
+ size: Annotated[
320
+ str,
321
+ typer.Option("--size", "-s", help="T-shirt size (XS, S, M, L)"),
322
+ ] = "S",
323
+ estimated: Annotated[
324
+ int,
325
+ typer.Option("--estimated", "-e", help="Estimated duration in minutes"),
326
+ ] = 0,
327
+ actual: Annotated[
328
+ int,
329
+ typer.Option("--actual", "-a", help="Actual duration in minutes"),
330
+ ] = 0,
331
+ session: Annotated[
332
+ str | None,
333
+ typer.Option(
334
+ "--session",
335
+ help="Session ID (e.g., SES-177). Falls back to RAI_SESSION_ID env var.",
336
+ ),
337
+ ] = None,
338
+ ) -> None:
339
+ """Emit a calibration event to telemetry.
340
+
341
+ Records estimate vs actual for velocity tracking and pattern detection.
342
+ Called at the end of /rai-story-review to capture calibration data.
343
+
344
+ Velocity is calculated automatically: estimated / actual.
345
+ - velocity > 1.0 means faster than estimated
346
+ - velocity < 1.0 means slower than estimated
347
+
348
+ Examples:
349
+ # Story completed faster than estimated
350
+ $ rai signal emit-calibration S9.4 --size S --estimated 30 --actual 15
351
+
352
+ # Story took longer
353
+ $ rai signal emit-calibration S9.4 -s M -e 60 -a 90
354
+
355
+ # Short form
356
+ $ rai signal emit-calibration S9.4 -s S -e 30 -a 15
357
+ """
358
+ # Validate size
359
+ valid_sizes = ["XS", "S", "M", "L", "XL"]
360
+ size_upper = size.upper()
361
+ if size_upper not in valid_sizes:
362
+ cli_error(
363
+ f"Invalid size: {size}",
364
+ hint=f"Valid sizes: {', '.join(valid_sizes)}",
365
+ exit_code=7,
366
+ )
367
+
368
+ # Validate durations
369
+ if estimated <= 0:
370
+ cli_error("Estimated duration must be > 0", exit_code=7)
371
+ if actual <= 0:
372
+ cli_error("Actual duration must be > 0", exit_code=7)
373
+
374
+ # Calculate velocity
375
+ velocity = round(estimated / actual, 2)
376
+
377
+ # Create event
378
+ event = CalibrationEvent(
379
+ timestamp=datetime.now(UTC),
380
+ story_id=story,
381
+ story_size=size_upper,
382
+ estimated_min=estimated,
383
+ actual_min=actual,
384
+ velocity=velocity,
385
+ )
386
+
387
+ # Resolve optional session ID
388
+ import os
389
+
390
+ session_id = resolve_session_id_optional(session, os.environ.get("RAI_SESSION_ID"))
391
+
392
+ # Emit signal
393
+ result = emit(event, session_id=session_id)
394
+
395
+ if result.success:
396
+ console.print("\n[green]✓[/green] Calibration event recorded")
397
+ console.print(f" Story: {story}")
398
+ console.print(f" Size: {size_upper}")
399
+ console.print(f" Estimated: {estimated} min")
400
+ console.print(f" Actual: {actual} min")
401
+ console.print(f" Velocity: {velocity}x", end="")
402
+ if velocity > 1.0:
403
+ console.print(" [green](faster than estimated)[/green]")
404
+ elif velocity < 1.0:
405
+ console.print(" [yellow](slower than estimated)[/yellow]")
406
+ else:
407
+ console.print(" (on target)")
408
+ console.print(f"\n[dim]Saved to: {result.path}[/dim]\n")
409
+ else:
410
+ cli_error(result.error or "Failed to emit calibration event")