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