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,83 @@
1
+ """Built-in GateBridgeHook — bridges gate system into before: events.
2
+
3
+ Subscribes to ``before:`` events and runs matching WorkflowGates via
4
+ GateRegistry discovery. Returns ``abort`` if any gate fails, preventing
5
+ the guarded operation from proceeding.
6
+
7
+ This is the bridge between the independent gate and hook systems (PAT-E-454).
8
+ Gates remain standalone (AD-5) — the bridge is the only coupling point.
9
+
10
+ Architecture: ADR-039 §5 (Built-in hooks), S248.6
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from typing import ClassVar
17
+
18
+ from raise_cli.gates.models import GateContext, GateResult
19
+ from raise_cli.gates.registry import GateRegistry
20
+ from raise_cli.hooks.events import HookEvent, HookResult
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class GateBridgeHook:
26
+ """Bridges WorkflowGates into the hook lifecycle.
27
+
28
+ Subscribes to ``before:`` events, discovers gates registered for
29
+ the matching workflow point, runs them all, and returns ``abort``
30
+ if any gate fails. Priority 100 ensures gates run before other hooks.
31
+
32
+ Registered via ``rai.hooks`` entry point in pyproject.toml.
33
+ """
34
+
35
+ events: ClassVar[list[str]] = [
36
+ "before:release:publish",
37
+ "before:session:close",
38
+ ]
39
+ priority: ClassVar[int] = 100
40
+
41
+ def handle(self, event: HookEvent) -> HookResult:
42
+ """Run matching gates and abort if any fail."""
43
+ workflow_point = event.event_name
44
+
45
+ registry = GateRegistry()
46
+ registry.discover()
47
+ gates = registry.get_gates_for_point(workflow_point)
48
+
49
+ if not gates:
50
+ logger.debug("GateBridgeHook: no gates for '%s'", workflow_point)
51
+ return HookResult(status="ok")
52
+
53
+ logger.debug(
54
+ "GateBridgeHook: running %d gate(s) for '%s'",
55
+ len(gates),
56
+ workflow_point,
57
+ )
58
+
59
+ failures: list[GateResult] = []
60
+ for gate in gates:
61
+ context = GateContext(gate_id=gate.gate_id)
62
+ try:
63
+ result = gate.evaluate(context)
64
+ except Exception as exc: # noqa: BLE001
65
+ result = GateResult(
66
+ passed=False,
67
+ gate_id=gate.gate_id,
68
+ message=f"{type(exc).__name__}: {exc}",
69
+ )
70
+
71
+ if not result.passed:
72
+ failures.append(result)
73
+ logger.debug(
74
+ "GateBridgeHook: gate '%s' failed: %s",
75
+ gate.gate_id,
76
+ result.message,
77
+ )
78
+
79
+ if failures:
80
+ summary = "; ".join(f"{f.gate_id}: {f.message}" for f in failures)
81
+ return HookResult(status="abort", message=f"Gates failed: {summary}")
82
+
83
+ return HookResult(status="ok")
@@ -0,0 +1,127 @@
1
+ """Built-in JiraSyncHook — auto-sync work lifecycle events to Jira transitions.
2
+
3
+ Subscribes to ``work:start`` and ``work:close`` events. Maps lifecycle events
4
+ to Jira transition IDs via ``lifecycle_mapping`` in ``.raise/jira.yaml``.
5
+
6
+ Graceful degradation:
7
+ - No issue_key on event → skip (debug log)
8
+ - No lifecycle_mapping configured → skip (debug log)
9
+ - No mapping entry for event type → skip (debug log)
10
+ - Adapter unavailable → return error (no crash)
11
+
12
+ Architecture: E301 S301.6 (Skill Auto-Sync Hooks)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from pathlib import Path
19
+ from typing import ClassVar
20
+
21
+ import yaml
22
+
23
+ from raise_cli.hooks.events import HookEvent, HookResult, WorkCloseEvent, WorkStartEvent
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ _JIRA_YAML_PATH: Path = Path(".raise/jira.yaml")
28
+
29
+
30
+ def _load_lifecycle_mapping() -> dict[str, int] | None:
31
+ """Read lifecycle_mapping from .raise/jira.yaml.
32
+
33
+ Returns None if file missing or section absent.
34
+ """
35
+ if not _JIRA_YAML_PATH.exists():
36
+ return None
37
+ data = yaml.safe_load(_JIRA_YAML_PATH.read_text())
38
+ mapping = data.get("workflow", {}).get("lifecycle_mapping")
39
+ if not mapping:
40
+ return None
41
+ return mapping
42
+
43
+
44
+ def _resolve_status_name(transition_id: int) -> str:
45
+ """Reverse-lookup: transition ID → status name from status_mapping.
46
+
47
+ Falls back to string of transition_id if not found.
48
+ """
49
+ if not _JIRA_YAML_PATH.exists():
50
+ return str(transition_id)
51
+ data = yaml.safe_load(_JIRA_YAML_PATH.read_text())
52
+ status_mapping: dict[str, int] = data.get("workflow", {}).get("status_mapping", {})
53
+ for name, tid in status_mapping.items():
54
+ if tid == transition_id:
55
+ return name
56
+ return str(transition_id)
57
+
58
+
59
+ class JiraSyncHook:
60
+ """Auto-sync work lifecycle events to Jira transitions.
61
+
62
+ Reads ``lifecycle_mapping`` from ``.raise/jira.yaml``::
63
+
64
+ workflow:
65
+ lifecycle_mapping:
66
+ story_start: 31 # → In Progress
67
+ story_close: 41 # → Done
68
+ epic_start: 31
69
+ epic_close: 41
70
+
71
+ Maps ``work:start`` + ``work_type=story`` → ``story_start`` → transition ID 31
72
+ → adapter.transition_issue(issue_key, "in-progress").
73
+
74
+ Registered via ``rai.hooks`` entry point in pyproject.toml.
75
+ """
76
+
77
+ events: ClassVar[list[str]] = ["work:start", "work:close"]
78
+ priority: ClassVar[int] = -10 # after telemetry (priority 0)
79
+
80
+ def handle(self, event: HookEvent) -> HookResult:
81
+ """Handle work lifecycle events by triggering Jira transitions.
82
+
83
+ Never raises — returns HookResult(status="error") on failure.
84
+ """
85
+ # 1. Type-narrow to work events
86
+ if not isinstance(event, (WorkStartEvent, WorkCloseEvent)):
87
+ return HookResult(status="ok")
88
+
89
+ # 2. Skip if no issue key
90
+ if not event.issue_key:
91
+ logger.debug("JiraSyncHook: no issue_key, skipping")
92
+ return HookResult(status="ok")
93
+
94
+ # 3. Load lifecycle_mapping
95
+ mapping = _load_lifecycle_mapping()
96
+ if not mapping:
97
+ logger.debug("JiraSyncHook: no lifecycle_mapping configured")
98
+ return HookResult(status="ok")
99
+
100
+ # 4. Resolve mapping key: "{work_type}_{action}"
101
+ action = "start" if isinstance(event, WorkStartEvent) else "close"
102
+ mapping_key = f"{event.work_type}_{action}"
103
+ transition_id = mapping.get(mapping_key)
104
+ if transition_id is None:
105
+ logger.debug("JiraSyncHook: no mapping for %s", mapping_key)
106
+ return HookResult(status="ok")
107
+
108
+ # 5. Resolve status name from transition ID
109
+ status_name = _resolve_status_name(transition_id)
110
+
111
+ # 6. Call adapter
112
+ try:
113
+ from raise_cli.cli.commands._resolve import resolve_adapter
114
+
115
+ adapter = resolve_adapter(None)
116
+ adapter.transition_issue(event.issue_key, status_name)
117
+ logger.info(
118
+ "JiraSyncHook: %s → %s (%s)",
119
+ event.issue_key,
120
+ status_name,
121
+ mapping_key,
122
+ )
123
+ return HookResult(status="ok")
124
+ except Exception as exc: # noqa: BLE001
125
+ msg = f"JiraSyncHook: {type(exc).__name__}: {exc}"
126
+ logger.warning(msg)
127
+ return HookResult(status="error", message=msg)
@@ -0,0 +1,117 @@
1
+ """Built-in MemoryMdSyncHook — regenerates MEMORY.md after graph build.
2
+
3
+ Subscribes to ``graph:build`` and writes MEMORY.md to both the canonical
4
+ location (``.raise/rai/memory/MEMORY.md``) and the Claude Code copy
5
+ (``~/.claude/projects/{encoded}/memory/MEMORY.md``).
6
+
7
+ Error isolation: ``handle()`` never raises. Failures are logged and
8
+ returned as ``HookResult(status="error")``.
9
+
10
+ Architecture: S350.3 (MEMORY.md sync hook)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from pathlib import Path
17
+ from typing import ClassVar
18
+
19
+ from raise_cli.config.paths import (
20
+ get_claude_memory_path,
21
+ get_framework_dir,
22
+ get_memory_dir,
23
+ )
24
+ from raise_cli.hooks.events import GraphBuildEvent, HookEvent, HookResult
25
+ from raise_cli.onboarding.memory_md import generate_memory_md
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class MemoryMdSyncHook:
31
+ """Regenerate MEMORY.md after graph build.
32
+
33
+ Writes to:
34
+ 1. Canonical: ``.raise/rai/memory/MEMORY.md``
35
+ 2. Claude Code: ``~/.claude/projects/{hash}/memory/MEMORY.md``
36
+
37
+ Reads project name and development branch from manifest.yaml.
38
+ Falls back to sensible defaults if manifest is unavailable.
39
+
40
+ Registered via ``rai.hooks`` entry point in pyproject.toml.
41
+ """
42
+
43
+ events: ClassVar[list[str]] = ["graph:build"]
44
+ priority: ClassVar[int] = 0
45
+
46
+ def handle(self, event: HookEvent) -> HookResult:
47
+ """Generate MEMORY.md and write to canonical + Claude Code paths.
48
+
49
+ Never raises — returns ``HookResult(status="error")`` on failure.
50
+ """
51
+ try:
52
+ return self._do_handle(event)
53
+ except Exception as exc: # noqa: BLE001
54
+ msg = f"{type(exc).__name__}: {exc}"
55
+ logger.warning("MemoryMdSyncHook failed: %s", msg)
56
+ return HookResult(status="error", message=msg)
57
+
58
+ def _do_handle(self, event: HookEvent) -> HookResult:
59
+ """Internal handler — may raise, caller catches."""
60
+ assert isinstance(event, (HookEvent, GraphBuildEvent))
61
+
62
+ project_path = getattr(event, "project_path", None)
63
+ if project_path is None:
64
+ return HookResult(status="error", message="No project_path in event")
65
+
66
+ project_path = project_path.resolve()
67
+
68
+ # Load manifest for project name and development branch
69
+ project_name, dev_branch = self._load_project_info(project_path)
70
+
71
+ # Generate content
72
+ methodology_path = get_framework_dir(project_path) / "methodology.yaml"
73
+ patterns_path = get_memory_dir(project_path) / "patterns.jsonl"
74
+
75
+ content = generate_memory_md(
76
+ methodology_path=methodology_path,
77
+ patterns_path=patterns_path,
78
+ project_name=project_name,
79
+ development_branch=dev_branch,
80
+ )
81
+
82
+ # Write canonical copy
83
+ canonical = get_memory_dir(project_path) / "MEMORY.md"
84
+ canonical.parent.mkdir(parents=True, exist_ok=True)
85
+ canonical.write_text(content, encoding="utf-8")
86
+ logger.debug("MemoryMdSyncHook wrote canonical MEMORY.md: %s", canonical)
87
+
88
+ # Write Claude Code copy
89
+ claude_memory = get_claude_memory_path(project_path)
90
+ claude_memory.parent.mkdir(parents=True, exist_ok=True)
91
+ claude_memory.write_text(content, encoding="utf-8")
92
+ logger.debug("MemoryMdSyncHook wrote Claude Code MEMORY.md: %s", claude_memory)
93
+
94
+ return HookResult(status="ok")
95
+
96
+ @staticmethod
97
+ def _load_project_info(project_path: Path) -> tuple[str, str]:
98
+ """Load project name and development branch from manifest.
99
+
100
+ Returns:
101
+ Tuple of (project_name, development_branch) with defaults.
102
+ """
103
+
104
+ project_name = "project"
105
+ dev_branch = "main"
106
+
107
+ try:
108
+ from raise_cli.onboarding.manifest import load_manifest
109
+
110
+ manifest = load_manifest(Path(str(project_path)))
111
+ if manifest is not None:
112
+ project_name = manifest.project.name
113
+ dev_branch = manifest.branches.development
114
+ except Exception: # noqa: BLE001
115
+ logger.debug("Could not load manifest, using defaults")
116
+
117
+ return project_name, dev_branch
@@ -0,0 +1,72 @@
1
+ """Built-in TelemetryHook — writes CommandUsage signals for lifecycle events.
2
+
3
+ First real LifecycleHook implementation. Subscribes to all 9 after-events
4
+ and maps each to a ``CommandUsage`` telemetry signal via ``emit_command_usage()``.
5
+
6
+ Error isolation: ``handle()`` never raises. Emit failures are logged and
7
+ returned as ``HookResult(status="error")``.
8
+
9
+ Architecture: ADR-039 §5 (Built-in hooks), S248.3
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from typing import ClassVar
16
+
17
+ from raise_cli.hooks.events import HookEvent, HookResult
18
+ from raise_cli.telemetry.writer import emit_command_usage
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class TelemetryHook:
24
+ """Writes CommandUsage signals for CLI lifecycle events.
25
+
26
+ Maps event names to command/subcommand pairs::
27
+
28
+ "session:start" → command="session", subcommand="start"
29
+ "graph:build" → command="graph", subcommand="build"
30
+
31
+ Registered via ``rai.hooks`` entry point in pyproject.toml.
32
+ """
33
+
34
+ events: ClassVar[list[str]] = [
35
+ "session:start",
36
+ "session:close",
37
+ "graph:build",
38
+ "pattern:added",
39
+ "discover:scan",
40
+ "init:complete",
41
+ "adapter:loaded",
42
+ "adapter:failed",
43
+ "release:publish",
44
+ ]
45
+ priority: ClassVar[int] = 0
46
+
47
+ def handle(self, event: HookEvent) -> HookResult:
48
+ """Map event to CommandUsage signal and emit.
49
+
50
+ Never raises — returns ``HookResult(status="error")`` on failure.
51
+ """
52
+ command, subcommand = event.event_name.split(":", 1)
53
+
54
+ try:
55
+ result = emit_command_usage(command, subcommand)
56
+ except Exception as exc: # noqa: BLE001
57
+ msg = f"{type(exc).__name__}: {exc}"
58
+ logger.warning(
59
+ "TelemetryHook emit failed for '%s': %s", event.event_name, msg
60
+ )
61
+ return HookResult(status="error", message=msg)
62
+
63
+ if not result.success:
64
+ logger.warning(
65
+ "TelemetryHook emit failed for '%s': %s",
66
+ event.event_name,
67
+ result.error,
68
+ )
69
+ return HookResult(status="error", message=result.error or "unknown error")
70
+
71
+ logger.debug("TelemetryHook emitted CommandUsage for '%s'", event.event_name)
72
+ return HookResult(status="ok")
@@ -0,0 +1,184 @@
1
+ """Event emitter for lifecycle hooks.
2
+
3
+ Dispatches typed events to registered handlers with error isolation.
4
+ Handlers that raise exceptions are logged and skipped — the CLI never
5
+ crashes from a handler failure.
6
+
7
+ When a ``HookRegistry`` is provided, discovered hooks are auto-registered
8
+ and dispatched in priority order with per-hook timeout enforcement.
9
+
10
+ Architecture: ADR-039 §4 (Synchronous execution with error isolation)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from collections import defaultdict
17
+ from collections.abc import Callable
18
+ from concurrent.futures import ThreadPoolExecutor
19
+ from concurrent.futures import TimeoutError as FuturesTimeoutError
20
+ from typing import TYPE_CHECKING
21
+
22
+ from raise_cli.hooks.events import EmitResult, HookEvent, HookResult
23
+ from raise_cli.hooks.protocol import LifecycleHook
24
+
25
+ if TYPE_CHECKING:
26
+ from raise_cli.hooks.registry import HookRegistry
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ HandlerFn = Callable[[HookEvent], HookResult]
31
+
32
+
33
+ class EventEmitter:
34
+ """Dispatches lifecycle events to registered handlers.
35
+
36
+ Handlers are called in registration order per event name.
37
+ For ``before:`` events, if any handler returns ``abort``, the emit
38
+ result signals that the operation should be aborted.
39
+
40
+ When ``registry`` is provided, discovered hooks are auto-registered
41
+ and dispatched in priority order with per-hook timeout.
42
+
43
+ Example::
44
+
45
+ emitter = EventEmitter()
46
+ emitter.register("session:start", my_handler)
47
+ result = emitter.emit(SessionStartEvent(session_id="SES-1"))
48
+
49
+ With registry::
50
+
51
+ registry = HookRegistry()
52
+ registry.discover()
53
+ emitter = EventEmitter(registry=registry)
54
+ result = emitter.emit(SessionStartEvent(session_id="SES-1"))
55
+ """
56
+
57
+ DEFAULT_TIMEOUT: float = 5.0
58
+
59
+ def __init__(self, registry: HookRegistry | None = None) -> None:
60
+ self._handlers: dict[str, list[HandlerFn]] = defaultdict(list)
61
+ self._registry = registry
62
+
63
+ def register(self, event_name: str, handler: HandlerFn) -> None:
64
+ """Register a handler for a specific event name."""
65
+ self._handlers[event_name].append(handler)
66
+
67
+ def emit(self, event: HookEvent) -> EmitResult:
68
+ """Dispatch event to all registered handlers.
69
+
70
+ For ``before:`` events, if any handler returns ``abort``,
71
+ returns ``EmitResult(aborted=True)``. Remaining handlers
72
+ are still called (all-notify semantics).
73
+
74
+ Handler exceptions and timeouts are caught, logged, and
75
+ reported in ``EmitResult.handler_errors``.
76
+ """
77
+ errors: list[str] = []
78
+ aborted = False
79
+ abort_message = ""
80
+
81
+ # Phase 1: Registry hooks (priority-sorted, with timeout)
82
+ if self._registry is not None:
83
+ hooks = self._registry.get_hooks_for_event(event.event_name)
84
+ for hook in hooks:
85
+ result = self._call_hook_with_timeout(hook, event, errors)
86
+ if result is not None:
87
+ aborted, abort_message = self._check_abort(
88
+ result, event.event_name, aborted, abort_message
89
+ )
90
+
91
+ # Phase 2: Manual handlers (registration order, no timeout)
92
+ handlers = self._handlers.get(event.event_name, [])
93
+ for handler in handlers:
94
+ try:
95
+ result = handler(event)
96
+ except Exception as exc: # noqa: BLE001
97
+ error_msg = f"{type(exc).__name__}: {exc}"
98
+ errors.append(error_msg)
99
+ logger.warning(
100
+ "Hook handler %s raised %s for event '%s'",
101
+ getattr(handler, "__name__", repr(handler)),
102
+ error_msg,
103
+ event.event_name,
104
+ )
105
+ continue
106
+
107
+ aborted, abort_message = self._check_abort(
108
+ result, event.event_name, aborted, abort_message
109
+ )
110
+
111
+ return EmitResult(
112
+ aborted=aborted,
113
+ abort_message=abort_message,
114
+ handler_errors=tuple(errors),
115
+ )
116
+
117
+ def _call_hook_with_timeout(
118
+ self,
119
+ hook: LifecycleHook,
120
+ event: HookEvent,
121
+ errors: list[str],
122
+ ) -> HookResult | None:
123
+ """Call a hook's handle() with timeout enforcement.
124
+
125
+ Returns the HookResult on success, or None on error/timeout.
126
+ """
127
+ timeout: float = getattr(hook, "timeout", self.DEFAULT_TIMEOUT)
128
+ hook_name = type(hook).__name__
129
+
130
+ executor = ThreadPoolExecutor(max_workers=1)
131
+ try:
132
+ future = executor.submit(hook.handle, event)
133
+ return future.result(timeout=timeout)
134
+ except FuturesTimeoutError:
135
+ error_msg = f"TimeoutError: {hook_name} exceeded {timeout}s timeout"
136
+ errors.append(error_msg)
137
+ logger.warning(
138
+ "Hook %s timed out after %ss for event '%s'",
139
+ hook_name,
140
+ timeout,
141
+ event.event_name,
142
+ )
143
+ return None
144
+ except Exception as exc: # noqa: BLE001
145
+ error_msg = f"{type(exc).__name__}: {exc}"
146
+ errors.append(error_msg)
147
+ logger.warning(
148
+ "Hook %s raised %s for event '%s'",
149
+ hook_name,
150
+ error_msg,
151
+ event.event_name,
152
+ )
153
+ return None
154
+ finally:
155
+ executor.shutdown(wait=False, cancel_futures=True)
156
+
157
+ @staticmethod
158
+ def _check_abort(
159
+ result: HookResult,
160
+ event_name: str,
161
+ aborted: bool,
162
+ abort_message: str,
163
+ ) -> tuple[bool, str]:
164
+ """Check if a handler result triggers abort (before: events only)."""
165
+ if (
166
+ result.status == "abort"
167
+ and event_name.startswith("before:")
168
+ and not aborted
169
+ ):
170
+ return True, result.message
171
+ return aborted, abort_message
172
+
173
+
174
+ def create_emitter() -> EventEmitter:
175
+ """Create an EventEmitter with a discovered HookRegistry.
176
+
177
+ Convenience factory that avoids repeating registry setup in every
178
+ CLI command. Discovers hooks from ``rai.hooks`` entry points.
179
+ """
180
+ from raise_cli.hooks.registry import HookRegistry
181
+
182
+ registry = HookRegistry()
183
+ registry.discover()
184
+ return EventEmitter(registry=registry)