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,134 @@
1
+ ---
2
+ name: rai-welcome
3
+ description: >
4
+ Conversational developer onboarding for RaiSE. Detects scenario,
5
+ sets up profile and graph, offers optional personalization.
6
+
7
+ license: MIT
8
+
9
+ metadata:
10
+ raise.work_cycle: utility
11
+ raise.frequency: once-per-developer
12
+ raise.fase: "setup"
13
+ raise.prerequisites: ""
14
+ raise.next: "rai-session-start"
15
+ raise.gate: ""
16
+ raise.adaptable: "true"
17
+ raise.version: "2.0.0"
18
+ raise.visibility: public
19
+ ---
20
+
21
+ # Welcome
22
+
23
+ ## Purpose
24
+
25
+ Get a developer fully set up in a RaiSE project through a guided flow that detects their situation and only asks what's needed.
26
+
27
+ ## Mastery Levels (ShuHaRi)
28
+
29
+ - **Shu**: Follow all steps, explain what each governance doc is for
30
+ - **Ha**: Detect scenario and fast-path through known setups
31
+ - **Ri**: One-shot setup with minimal questions
32
+
33
+ ## Context
34
+
35
+ **When to use:** First time a developer works in a RaiSE project. Subsequent runs verify setup.
36
+
37
+ **When to skip:** Developer is already set up (profile exists, graph exists, CLAUDE.local.md exists).
38
+
39
+ **Inputs:** A project with `.raise/` directory (from `rai init`).
40
+
41
+ ## Steps
42
+
43
+ ### Step 1: Detect Scenario
44
+
45
+ ```bash
46
+ ls ~/.rai/developer.yaml 2>/dev/null && echo "PROFILE_EXISTS" || echo "NO_PROFILE"
47
+ ls .raise/ 2>/dev/null && echo "RAISE_EXISTS" || echo "NO_RAISE"
48
+ ```
49
+
50
+ | Profile? | `.raise/`? | Action |
51
+ |----------|------------|--------|
52
+ | No | Yes | Full setup (Steps 2-5) |
53
+ | Yes | Yes | Verify only (Step 4) |
54
+ | Any | No | Stop: "Run `rai init` first, then `/rai-welcome` again." |
55
+
56
+ <verification>
57
+ Scenario detected. `.raise/` exists.
58
+ </verification>
59
+
60
+ ### Step 2: Create Profile (if needed)
61
+
62
+ Ask developer's name (only mandatory question). Derive pattern prefix (first letter, uppercased), confirm.
63
+
64
+ ```bash
65
+ rai session start --name "{name}" --project .
66
+ ```
67
+
68
+ Edit `~/.rai/developer.yaml` to add confirmed `pattern_prefix`.
69
+
70
+ <verification>
71
+ `~/.rai/developer.yaml` exists with name and pattern_prefix.
72
+ </verification>
73
+
74
+ ### Step 3: Optional Personalization
75
+
76
+ Frame as skippable: "Want to customize? Or skip — defaults work well."
77
+
78
+ If customize, ask up to 3 questions:
79
+ 1. **Language:** English / Spanish / Other → `communication.language`
80
+ 2. **Style:** Detailed / Balanced / Direct → `communication.style`
81
+ 3. **Focus guidance:** Yes / No → `communication.redirect_when_dispersing`
82
+
83
+ Defaults: `shu`, `balanced`, `en`, `detailed_explanations: true`, `redirect_when_dispersing: false`.
84
+
85
+ <verification>
86
+ Preferences saved or defaults accepted.
87
+ </verification>
88
+
89
+ ### Step 4: Verify Setup
90
+
91
+ Build graph if missing (`rai graph build`). Scaffold `CLAUDE.local.md` if missing. Run context bundle:
92
+
93
+ ```bash
94
+ rai session start --project . --context
95
+ ```
96
+
97
+ Check: developer name appears, session count shown, no errors.
98
+
99
+ <verification>
100
+ Profile, graph, and local config all present and functional.
101
+ </verification>
102
+
103
+ ### Step 5: Welcome Message
104
+
105
+ ```
106
+ Welcome to RaiSE, {name}!
107
+ Setup: Profile ({prefix}), Graph ({N} concepts), CLAUDE.local.md
108
+ Next: /rai-session-start
109
+ ```
110
+
111
+ ## Output
112
+
113
+ | Item | Destination |
114
+ |------|-------------|
115
+ | Developer profile | `~/.rai/developer.yaml` |
116
+ | Knowledge graph | `.raise/rai/memory/index.json` |
117
+ | Local config | `CLAUDE.local.md` |
118
+ | Next | `/rai-session-start` |
119
+
120
+ ## Quality Checklist
121
+
122
+ - [ ] Scenario detected before asking any questions
123
+ - [ ] Name is the only mandatory question
124
+ - [ ] Personalization clearly framed as optional
125
+ - [ ] Graph built if missing (not assumed)
126
+ - [ ] Context bundle runs successfully after setup
127
+ - [ ] NEVER overwrite existing CLAUDE.local.md
128
+ - [ ] NEVER ask about experience level — learned implicitly through coaching
129
+
130
+ ## References
131
+
132
+ - Profile model: `src/raise_cli/onboarding/profile.py`
133
+ - Next: `/rai-session-start`
134
+ - One-time skill: subsequent runs verify, not recreate
@@ -0,0 +1,42 @@
1
+ """Telemetry module for local signal collection.
2
+
3
+ This module provides infrastructure for collecting telemetry signals
4
+ as specified in ADR-018 (Local Telemetry Architecture).
5
+
6
+ Signals are stored locally in `.raise/rai/personal/telemetry/signals.jsonl` and follow
7
+ OpenTelemetry semantic conventions for future OTLP export.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from raise_cli.telemetry.schemas import (
13
+ CalibrationEvent,
14
+ CommandUsage,
15
+ ErrorEvent,
16
+ SessionEvent,
17
+ Signal,
18
+ SkillEvent,
19
+ WorkLifecycle,
20
+ )
21
+ from raise_cli.telemetry.writer import (
22
+ EmitResult,
23
+ emit,
24
+ emit_command_usage,
25
+ emit_error_event,
26
+ emit_skill_event,
27
+ )
28
+
29
+ __all__ = [
30
+ "CalibrationEvent",
31
+ "CommandUsage",
32
+ "EmitResult",
33
+ "ErrorEvent",
34
+ "SessionEvent",
35
+ "Signal",
36
+ "SkillEvent",
37
+ "WorkLifecycle",
38
+ "emit",
39
+ "emit_command_usage",
40
+ "emit_error_event",
41
+ "emit_skill_event",
42
+ ]
@@ -0,0 +1,285 @@
1
+ """Pydantic models for telemetry signals.
2
+
3
+ This module defines the signal schemas for local telemetry collection
4
+ as specified in ADR-018. Signals follow OpenTelemetry semantic conventions
5
+ for future OTLP export compatibility.
6
+
7
+ Signal types:
8
+ - SkillEvent: Tracks skill invocations (start/complete/abandon)
9
+ - SessionEvent: Tracks session outcomes
10
+ - CalibrationEvent: Tracks estimate vs actual for velocity calibration
11
+ - ErrorEvent: Tracks tool failures
12
+ - CommandUsage: Tracks CLI command usage
13
+ - WorkLifecycle: Tracks work items (epic/story) through phases (Lean flow analysis)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import datetime
19
+ from typing import Annotated, Literal
20
+
21
+ from pydantic import BaseModel, Field
22
+
23
+
24
+ class SkillEvent(BaseModel):
25
+ """A skill invocation event.
26
+
27
+ Emitted when a skill starts, completes, or is abandoned.
28
+
29
+ Attributes:
30
+ type: Discriminator field, always "skill_event".
31
+ timestamp: When the event occurred (UTC).
32
+ skill: Name of the skill (e.g., "story-design").
33
+ event: Event type (start, complete, abandon).
34
+ duration_sec: Duration in seconds (only for complete/abandon).
35
+
36
+ Examples:
37
+ >>> from datetime import datetime, timezone
38
+ >>> event = SkillEvent(
39
+ ... timestamp=datetime.now(timezone.utc),
40
+ ... skill="story-design",
41
+ ... event="complete",
42
+ ... duration_sec=1800
43
+ ... )
44
+ >>> event.type
45
+ 'skill_event'
46
+ """
47
+
48
+ type: Literal["skill_event"] = "skill_event"
49
+ timestamp: datetime = Field(..., description="When the event occurred (UTC)")
50
+ skill: str = Field(..., description="Name of the skill (e.g., 'story-design')")
51
+ event: Literal["start", "complete", "abandon"] = Field(
52
+ ..., description="Event type"
53
+ )
54
+ duration_sec: int | None = Field(
55
+ default=None, description="Duration in seconds (for complete/abandon)"
56
+ )
57
+
58
+
59
+ class SessionEvent(BaseModel):
60
+ """A session lifecycle event.
61
+
62
+ Emitted when a session closes, capturing its outcome.
63
+
64
+ Attributes:
65
+ type: Discriminator field, always "session_event".
66
+ timestamp: When the event occurred (UTC).
67
+ session_type: Type of session (e.g., "story", "research").
68
+ outcome: How the session ended.
69
+ duration_min: Duration in minutes.
70
+ stories: Story IDs worked on during the session.
71
+
72
+ Examples:
73
+ >>> from datetime import datetime, timezone
74
+ >>> event = SessionEvent(
75
+ ... timestamp=datetime.now(timezone.utc),
76
+ ... session_type="story",
77
+ ... outcome="success",
78
+ ... duration_min=90,
79
+ ... stories=["F9.1", "F9.2"]
80
+ ... )
81
+ >>> event.type
82
+ 'session_event'
83
+ """
84
+
85
+ type: Literal["session_event"] = "session_event"
86
+ timestamp: datetime = Field(..., description="When the event occurred (UTC)")
87
+ session_type: str = Field(
88
+ ..., description="Type of session (e.g., 'story', 'research')"
89
+ )
90
+ outcome: Literal["success", "partial", "abandoned"] = Field(
91
+ ..., description="How the session ended"
92
+ )
93
+ duration_min: int = Field(..., description="Duration in minutes")
94
+ stories: list[str] = Field(default_factory=list, description="Story IDs worked on")
95
+
96
+
97
+ class CalibrationEvent(BaseModel):
98
+ """A calibration data point for velocity tracking.
99
+
100
+ Emitted when a story is completed, comparing estimate to actual.
101
+
102
+ Attributes:
103
+ type: Discriminator field, always "calibration".
104
+ timestamp: When the event occurred (UTC).
105
+ story_id: Story identifier (e.g., "F9.1").
106
+ story_size: T-shirt size (XS, S, M, L).
107
+ estimated_min: Estimated duration in minutes.
108
+ actual_min: Actual duration in minutes.
109
+ velocity: Ratio of estimated to actual (>1 means faster than expected).
110
+
111
+ Examples:
112
+ >>> from datetime import datetime, timezone
113
+ >>> event = CalibrationEvent(
114
+ ... timestamp=datetime.now(timezone.utc),
115
+ ... story_id="F9.1",
116
+ ... story_size="XS",
117
+ ... estimated_min=25,
118
+ ... actual_min=20,
119
+ ... velocity=1.25
120
+ ... )
121
+ >>> event.velocity
122
+ 1.25
123
+ """
124
+
125
+ type: Literal["calibration"] = "calibration"
126
+ timestamp: datetime = Field(..., description="When the event occurred (UTC)")
127
+ story_id: str = Field(..., description="Story identifier (e.g., 'F9.1')")
128
+ story_size: str = Field(..., description="T-shirt size (XS, S, M, L)")
129
+ estimated_min: int = Field(..., description="Estimated duration in minutes")
130
+ actual_min: int = Field(..., description="Actual duration in minutes")
131
+ velocity: float = Field(
132
+ ..., description="Ratio of estimated to actual (>1 = faster)"
133
+ )
134
+
135
+
136
+ class ErrorEvent(BaseModel):
137
+ """A tool error event.
138
+
139
+ Emitted when a tool fails, for pattern detection.
140
+
141
+ Attributes:
142
+ type: Discriminator field, always "error_event".
143
+ timestamp: When the event occurred (UTC).
144
+ tool: Name of the tool that failed (e.g., "Bash", "Read").
145
+ error_type: Type of error (e.g., "command_not_found").
146
+ context: Brief context (no sensitive data).
147
+ recoverable: Whether the error was recoverable.
148
+
149
+ Examples:
150
+ >>> from datetime import datetime, timezone
151
+ >>> event = ErrorEvent(
152
+ ... timestamp=datetime.now(timezone.utc),
153
+ ... tool="Bash",
154
+ ... error_type="command_not_found",
155
+ ... context="pytest",
156
+ ... recoverable=True
157
+ ... )
158
+ >>> event.recoverable
159
+ True
160
+ """
161
+
162
+ type: Literal["error_event"] = "error_event"
163
+ timestamp: datetime = Field(..., description="When the event occurred (UTC)")
164
+ tool: str = Field(..., description="Name of the tool that failed")
165
+ error_type: str = Field(..., description="Type of error")
166
+ context: str = Field(..., description="Brief context (no sensitive data)")
167
+ recoverable: bool = Field(..., description="Whether the error was recoverable")
168
+
169
+
170
+ class CommandUsage(BaseModel):
171
+ """A CLI command usage event.
172
+
173
+ Emitted when a raise CLI command is invoked.
174
+
175
+ Attributes:
176
+ type: Discriminator field, always "command_usage".
177
+ timestamp: When the event occurred (UTC).
178
+ command: Main command name (e.g., "memory").
179
+ subcommand: Subcommand name if any (e.g., "query").
180
+
181
+ Examples:
182
+ >>> from datetime import datetime, timezone
183
+ >>> event = CommandUsage(
184
+ ... timestamp=datetime.now(timezone.utc),
185
+ ... command="memory",
186
+ ... subcommand="query"
187
+ ... )
188
+ >>> event.command
189
+ 'memory'
190
+ """
191
+
192
+ type: Literal["command_usage"] = "command_usage"
193
+ timestamp: datetime = Field(..., description="When the event occurred (UTC)")
194
+ command: str = Field(..., description="Main command name (e.g., 'memory')")
195
+ subcommand: str | None = Field(default=None, description="Subcommand name if any")
196
+
197
+
198
+ class WorkLifecycle(BaseModel):
199
+ """A unified work lifecycle event for Lean flow analysis.
200
+
201
+ Tracks work items (epics, stories, etc.) through normalized phases to enable:
202
+ - Lead time calculation (start to complete)
203
+ - Wait time detection (gaps between phases)
204
+ - WIP tracking (started but not completed)
205
+ - Bottleneck identification (longest phase)
206
+ - Flow efficiency (active time / lead time)
207
+ - Cross-level analysis (compare epic vs story flow)
208
+
209
+ Phases (normalized across all work types):
210
+ - design: Scope definition and specification
211
+ - plan: Task/story decomposition and sequencing
212
+ - implement: Active development work
213
+ - review: Retrospective and learnings
214
+
215
+ Attributes:
216
+ type: Discriminator field, always "work_lifecycle".
217
+ timestamp: When the event occurred (UTC).
218
+ work_type: Type of work item (epic, story, etc.).
219
+ work_id: Work item identifier (e.g., "E9", "F9.4").
220
+ event: Lifecycle event type.
221
+ phase: Current phase in the workflow.
222
+ blocker: Description of blocker (only for blocked event).
223
+
224
+ Examples:
225
+ >>> from datetime import datetime, timezone
226
+ >>> event = WorkLifecycle(
227
+ ... timestamp=datetime.now(timezone.utc),
228
+ ... work_type="story",
229
+ ... work_id="F9.4",
230
+ ... event="start",
231
+ ... phase="design"
232
+ ... )
233
+ >>> event.type
234
+ 'work_lifecycle'
235
+
236
+ >>> epic = WorkLifecycle(
237
+ ... timestamp=datetime.now(timezone.utc),
238
+ ... work_type="epic",
239
+ ... work_id="E9",
240
+ ... event="complete",
241
+ ... phase="review"
242
+ ... )
243
+ >>> epic.work_type
244
+ 'epic'
245
+
246
+ >>> blocked = WorkLifecycle(
247
+ ... timestamp=datetime.now(timezone.utc),
248
+ ... work_type="story",
249
+ ... work_id="F9.4",
250
+ ... event="blocked",
251
+ ... phase="plan",
252
+ ... blocker="unclear requirements"
253
+ ... )
254
+ >>> blocked.blocker
255
+ 'unclear requirements'
256
+ """
257
+
258
+ type: Literal["work_lifecycle"] = "work_lifecycle"
259
+ timestamp: datetime = Field(..., description="When the event occurred (UTC)")
260
+ work_type: Literal["epic", "story"] = Field(
261
+ ..., description="Type of work item (epic, story)"
262
+ )
263
+ work_id: str = Field(..., description="Work item identifier (e.g., 'E9', 'F9.4')")
264
+ event: Literal["start", "complete", "blocked", "unblocked", "abandoned"] = Field(
265
+ ..., description="Lifecycle event type"
266
+ )
267
+ phase: Literal["init", "design", "plan", "implement", "review", "close"] = Field(
268
+ ..., description="Current phase in the workflow"
269
+ )
270
+ blocker: str | None = Field(
271
+ default=None, description="Description of blocker (for blocked event)"
272
+ )
273
+
274
+
275
+ # Union type for type-safe signal handling
276
+ Signal = Annotated[
277
+ SkillEvent
278
+ | SessionEvent
279
+ | CalibrationEvent
280
+ | ErrorEvent
281
+ | CommandUsage
282
+ | WorkLifecycle,
283
+ Field(discriminator="type"),
284
+ ]
285
+ """Union of all signal types with discriminator for type-safe parsing."""
@@ -0,0 +1,217 @@
1
+ """Writer module for appending telemetry signals to JSONL.
2
+
3
+ This module provides the `emit()` function to append signals to
4
+ `.raise/rai/personal/telemetry/signals.jsonl` (gitignored, per-developer).
5
+
6
+ Signals are written as JSON lines (one JSON object per line),
7
+ which is append-friendly and git-friendly.
8
+
9
+ Note: Telemetry is personal data (F14.15) and should not be committed.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+ from datetime import UTC, datetime
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING, Literal
18
+
19
+ from raise_cli.compat import file_lock, file_unlock
20
+ from raise_cli.config.paths import (
21
+ SIGNALS_FILE,
22
+ TELEMETRY_SUBDIR,
23
+ get_personal_dir,
24
+ get_session_dir,
25
+ )
26
+
27
+ if TYPE_CHECKING:
28
+ from raise_cli.telemetry.schemas import Signal
29
+
30
+ # Type alias for skill event types (matches SkillEvent.event)
31
+ SkillEventType = Literal["start", "complete", "abandon"]
32
+
33
+
34
+ @dataclass
35
+ class EmitResult:
36
+ """Result of emitting a signal.
37
+
38
+ Attributes:
39
+ success: Whether the signal was written successfully.
40
+ path: Path where the signal was written.
41
+ error: Error message if failed, None otherwise.
42
+ """
43
+
44
+ success: bool
45
+ path: Path | None = None
46
+ error: str | None = None
47
+
48
+
49
+ def _get_telemetry_path(base_path: Path | None = None) -> Path:
50
+ """Get the path to the signals.jsonl file in personal directory.
51
+
52
+ Telemetry is personal data (per-developer, gitignored) per F14.15.
53
+ Path: .raise/rai/personal/telemetry/signals.jsonl
54
+
55
+ Args:
56
+ base_path: Project root directory. Defaults to current directory.
57
+
58
+ Returns:
59
+ Path to signals.jsonl file in personal directory.
60
+ """
61
+ return get_personal_dir(base_path) / TELEMETRY_SUBDIR / SIGNALS_FILE
62
+
63
+
64
+ def _ensure_directory(path: Path) -> None:
65
+ """Ensure the parent directory exists.
66
+
67
+ Args:
68
+ path: Path to file whose parent directory should exist.
69
+ """
70
+ path.parent.mkdir(parents=True, exist_ok=True)
71
+
72
+
73
+ def emit(
74
+ signal: Signal,
75
+ *,
76
+ base_path: Path | None = None,
77
+ session_id: str | None = None,
78
+ ) -> EmitResult:
79
+ """Emit a telemetry signal to the signals.jsonl file.
80
+
81
+ When session_id is provided, writes to per-session directory:
82
+ .raise/rai/personal/sessions/{session_id}/signals.jsonl
83
+
84
+ When session_id is None, writes to shared telemetry directory:
85
+ .raise/rai/personal/telemetry/signals.jsonl
86
+
87
+ Creates the directory if it doesn't exist. Uses file locking for
88
+ thread-safe writes. Telemetry is personal data (gitignored).
89
+
90
+ Args:
91
+ signal: The signal to emit (any of the 5 signal types).
92
+ base_path: Base directory for telemetry. Defaults to current directory.
93
+ session_id: Optional session ID for per-session isolation.
94
+
95
+ Returns:
96
+ EmitResult with success status and path or error message.
97
+ """
98
+ if session_id is not None:
99
+ path = get_session_dir(session_id, base_path) / SIGNALS_FILE
100
+ else:
101
+ path = _get_telemetry_path(base_path)
102
+
103
+ try:
104
+ _ensure_directory(path)
105
+
106
+ # Serialize signal to JSON line
107
+ json_line = signal.model_dump_json() + "\n"
108
+
109
+ # Append with file locking for thread safety
110
+ with open(path, "a", encoding="utf-8") as f:
111
+ file_lock(f)
112
+ try:
113
+ f.write(json_line)
114
+ finally:
115
+ file_unlock(f)
116
+
117
+ return EmitResult(success=True, path=path)
118
+
119
+ except PermissionError as e:
120
+ return EmitResult(
121
+ success=False, error=f"Permission denied writing to {path}: {e}"
122
+ )
123
+ except OSError as e:
124
+ return EmitResult(success=False, error=f"OS error writing to {path}: {e}")
125
+
126
+
127
+ def emit_skill_event(
128
+ skill: str,
129
+ event: SkillEventType,
130
+ duration_sec: int | None = None,
131
+ *,
132
+ base_path: Path | None = None,
133
+ session_id: str | None = None,
134
+ ) -> EmitResult:
135
+ """Convenience function to emit a skill event.
136
+
137
+ Args:
138
+ skill: Name of the skill (e.g., "story-design").
139
+ event: Event type ("start", "complete", "abandon").
140
+ duration_sec: Duration in seconds (for complete/abandon).
141
+ base_path: Base directory for telemetry.
142
+ session_id: Optional session ID for per-session isolation.
143
+
144
+ Returns:
145
+ EmitResult with success status.
146
+ """
147
+ from raise_cli.telemetry.schemas import SkillEvent
148
+
149
+ signal = SkillEvent(
150
+ timestamp=datetime.now(UTC),
151
+ skill=skill,
152
+ event=event,
153
+ duration_sec=duration_sec,
154
+ )
155
+ return emit(signal, base_path=base_path, session_id=session_id)
156
+
157
+
158
+ def emit_command_usage(
159
+ command: str,
160
+ subcommand: str | None = None,
161
+ *,
162
+ base_path: Path | None = None,
163
+ session_id: str | None = None,
164
+ ) -> EmitResult:
165
+ """Convenience function to emit a command usage event.
166
+
167
+ Args:
168
+ command: Main command name (e.g., "memory").
169
+ subcommand: Subcommand name if any (e.g., "query").
170
+ base_path: Base directory for telemetry.
171
+ session_id: Optional session ID for per-session isolation.
172
+
173
+ Returns:
174
+ EmitResult with success status.
175
+ """
176
+ from raise_cli.telemetry.schemas import CommandUsage
177
+
178
+ signal = CommandUsage(
179
+ timestamp=datetime.now(UTC),
180
+ command=command,
181
+ subcommand=subcommand,
182
+ )
183
+ return emit(signal, base_path=base_path, session_id=session_id)
184
+
185
+
186
+ def emit_error_event(
187
+ tool: str,
188
+ error_type: str,
189
+ context: str,
190
+ recoverable: bool,
191
+ *,
192
+ base_path: Path | None = None,
193
+ session_id: str | None = None,
194
+ ) -> EmitResult:
195
+ """Convenience function to emit an error event.
196
+
197
+ Args:
198
+ tool: Name of the tool that failed (e.g., "Bash").
199
+ error_type: Type of error (e.g., "command_not_found").
200
+ context: Brief context (no sensitive data).
201
+ recoverable: Whether the error was recoverable.
202
+ base_path: Base directory for telemetry.
203
+ session_id: Optional session ID for per-session isolation.
204
+
205
+ Returns:
206
+ EmitResult with success status.
207
+ """
208
+ from raise_cli.telemetry.schemas import ErrorEvent
209
+
210
+ signal = ErrorEvent(
211
+ timestamp=datetime.now(UTC),
212
+ tool=tool,
213
+ error_type=error_type,
214
+ context=context,
215
+ recoverable=recoverable,
216
+ )
217
+ return emit(signal, base_path=base_path, session_id=session_id)
File without changes