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
raise_cli/cli/main.py ADDED
@@ -0,0 +1,163 @@
1
+ """Main CLI application entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import StrEnum
6
+ from typing import Annotated, Literal
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from raise_cli import __version__
12
+ from raise_cli.cli.commands.adapters import adapters_app
13
+ from raise_cli.cli.commands.artifact import artifact_app
14
+ from raise_cli.cli.commands.backlog import backlog_app
15
+ from raise_cli.cli.commands.base import base_app
16
+ from raise_cli.cli.commands.discover import discover_app
17
+ from raise_cli.cli.commands.docs import docs_app
18
+ from raise_cli.cli.commands.doctor import doctor_app
19
+ from raise_cli.cli.commands.gate import gate_app
20
+ from raise_cli.cli.commands.graph import graph_app
21
+ from raise_cli.cli.commands.info import info_command
22
+ from raise_cli.cli.commands.init import init_command
23
+ from raise_cli.cli.commands.mcp import mcp_app
24
+ from raise_cli.cli.commands.memory import memory_app
25
+ from raise_cli.cli.commands.pattern import pattern_app
26
+ from raise_cli.cli.commands.profile import profile_app
27
+ from raise_cli.cli.commands.publish import publish_app
28
+ from raise_cli.cli.commands.release import release_app
29
+ from raise_cli.cli.commands.session import session_app
30
+ from raise_cli.cli.commands.signal import signal_app
31
+ from raise_cli.cli.commands.skill import skill_app
32
+ from raise_cli.config import RaiSettings
33
+
34
+ # Module-level state for error handling
35
+ _current_output_format: Literal["human", "json", "table"] = "human"
36
+
37
+
38
+ def get_output_format() -> Literal["human", "json", "table"]:
39
+ """Get the current output format.
40
+
41
+ Returns:
42
+ The output format string ("human", "json", or "table").
43
+ """
44
+ return _current_output_format
45
+
46
+
47
+ app = typer.Typer(
48
+ name="rai",
49
+ help="RaiSE CLI - Reliable AI Software Engineering",
50
+ no_args_is_help=True,
51
+ add_completion=False,
52
+ )
53
+
54
+ # Register command groups
55
+ app.add_typer(adapters_app, name="adapter")
56
+ app.add_typer(artifact_app, name="artifact")
57
+ app.add_typer(backlog_app, name="backlog")
58
+ app.add_typer(base_app, name="base")
59
+ app.add_typer(docs_app, name="docs")
60
+ app.add_typer(discover_app, name="discover")
61
+ app.add_typer(doctor_app, name="doctor")
62
+ app.add_typer(gate_app, name="gate")
63
+ app.add_typer(graph_app, name="graph")
64
+ app.add_typer(mcp_app, name="mcp")
65
+ app.add_typer(memory_app, name="memory")
66
+ app.add_typer(pattern_app, name="pattern")
67
+ app.add_typer(profile_app, name="profile")
68
+ app.add_typer(publish_app, name="publish")
69
+ app.add_typer(release_app, name="release")
70
+ app.add_typer(session_app, name="session")
71
+ app.add_typer(signal_app, name="signal")
72
+ app.add_typer(skill_app, name="skill")
73
+
74
+ # Register standalone commands
75
+ app.command("info")(info_command)
76
+ app.command("init")(init_command)
77
+
78
+ console = Console()
79
+
80
+
81
+ class OutputFormat(StrEnum):
82
+ """Output format options."""
83
+
84
+ human = "human"
85
+ json = "json"
86
+ table = "table"
87
+
88
+
89
+ def version_callback(value: bool) -> None:
90
+ """Print version and exit."""
91
+ if value:
92
+ console.print(f"raise-cli version {__version__}")
93
+ raise typer.Exit(0)
94
+
95
+
96
+ @app.callback()
97
+ def main(
98
+ ctx: typer.Context,
99
+ version: Annotated[
100
+ bool,
101
+ typer.Option(
102
+ "--version",
103
+ "-V",
104
+ callback=version_callback,
105
+ is_eager=True,
106
+ help="Show version and exit",
107
+ ),
108
+ ] = False,
109
+ format: Annotated[
110
+ OutputFormat,
111
+ typer.Option(
112
+ "--format",
113
+ "-f",
114
+ help="Output format (human, json, table)",
115
+ ),
116
+ ] = OutputFormat.human,
117
+ verbose: Annotated[
118
+ int,
119
+ typer.Option(
120
+ "--verbose",
121
+ "-v",
122
+ count=True,
123
+ help="Increase verbosity (-v, -vv, -vvv)",
124
+ ),
125
+ ] = 0,
126
+ quiet: Annotated[
127
+ bool,
128
+ typer.Option(
129
+ "--quiet",
130
+ "-q",
131
+ help="Suppress non-error output",
132
+ ),
133
+ ] = False,
134
+ ) -> None:
135
+ """RaiSE CLI - Reliable AI Software Engineering governance framework.
136
+
137
+ Global options apply to all commands and control output format and verbosity.
138
+ """
139
+ global _current_output_format # noqa: PLW0603
140
+ _current_output_format = format.value # type: ignore[assignment]
141
+
142
+ # Calculate verbosity from flags
143
+ verbosity = -1 if quiet else min(verbose, 3)
144
+
145
+ # Create settings with CLI overrides (highest priority)
146
+ settings = RaiSettings(
147
+ output_format=format.value, # type: ignore[arg-type]
148
+ verbosity=verbosity,
149
+ )
150
+
151
+ # Store in context for subcommands
152
+ ctx.ensure_object(dict)
153
+ ctx.obj["settings"] = settings
154
+
155
+ # Backward compatibility: keep individual values in ctx.obj
156
+ # (Can be removed once all commands migrate to using settings)
157
+ ctx.obj["format"] = format.value
158
+ ctx.obj["verbosity"] = verbosity
159
+ ctx.obj["quiet"] = quiet
160
+
161
+
162
+ if __name__ == "__main__":
163
+ app()
raise_cli/compat.py ADDED
@@ -0,0 +1,66 @@
1
+ """Cross-platform compatibility layer.
2
+
3
+ Centralizes all platform-specific code so the rest of the codebase
4
+ never checks sys.platform directly. Pattern used by pip, poetry, virtualenv.
5
+
6
+ All platform guards live here. Import from compat, not from fcntl/msvcrt.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import IO
14
+
15
+ IS_WINDOWS = sys.platform == "win32"
16
+
17
+
18
+ def file_lock(f: IO[str], *, exclusive: bool = True) -> None:
19
+ """Acquire a file lock. Uses fcntl on Unix, msvcrt on Windows."""
20
+ if sys.platform == "win32":
21
+ import msvcrt
22
+
23
+ msvcrt.locking(f.fileno(), msvcrt.LK_LOCK, 1)
24
+ else:
25
+ import fcntl
26
+
27
+ fcntl.flock(f.fileno(), fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
28
+
29
+
30
+ def file_unlock(f: IO[str]) -> None:
31
+ """Release a file lock. Uses fcntl on Unix, msvcrt on Windows."""
32
+ if sys.platform == "win32":
33
+ import msvcrt
34
+
35
+ msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1)
36
+ else:
37
+ import fcntl
38
+
39
+ fcntl.flock(f.fileno(), fcntl.LOCK_UN)
40
+
41
+
42
+ def portable_path(path: Path, relative_to: Path) -> str:
43
+ """Return forward-slash relative path string for serialization.
44
+
45
+ Always uses forward slashes regardless of OS, ensuring consistent
46
+ path strings in JSON, graph data, and other serialized formats.
47
+ """
48
+ return path.relative_to(relative_to).as_posix()
49
+
50
+
51
+ def to_file_uri(path: Path) -> str:
52
+ """Return correct file:// URI on any platform.
53
+
54
+ Uses pathlib's as_uri() which handles Windows drive letters correctly.
55
+ """
56
+ return path.resolve().as_uri()
57
+
58
+
59
+ def secure_permissions(path: Path) -> None:
60
+ """Set restrictive file permissions (0o600). No-op on Windows.
61
+
62
+ On Windows, POSIX chmod has no effect. For true Windows ACL
63
+ restriction, icacls would be needed — deferred until required.
64
+ """
65
+ if not IS_WINDOWS:
66
+ path.chmod(0o600)
@@ -0,0 +1,41 @@
1
+ """Configuration module for raise-cli.
2
+
3
+ Provides configuration settings and XDG-compliant directory helpers.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from raise_cli.config.agents import (
9
+ BUILTIN_AGENTS,
10
+ AgentChoice,
11
+ AgentConfig,
12
+ BuiltinAgentType,
13
+ get_agent_config,
14
+ )
15
+ from raise_cli.config.paths import get_cache_dir, get_config_dir, get_data_dir
16
+ from raise_cli.config.settings import RaiSettings
17
+
18
+ # Backward-compat aliases
19
+ IDE_CONFIGS = BUILTIN_AGENTS
20
+ IdeChoice = AgentChoice
21
+ IdeConfig = AgentConfig
22
+ IdeType = BuiltinAgentType
23
+ get_ide_config = get_agent_config
24
+
25
+ __all__ = [
26
+ "BUILTIN_AGENTS",
27
+ "AgentChoice",
28
+ "AgentConfig",
29
+ "BuiltinAgentType",
30
+ "get_agent_config",
31
+ # Backward-compat
32
+ "IDE_CONFIGS",
33
+ "IdeChoice",
34
+ "IdeConfig",
35
+ "IdeType",
36
+ "get_ide_config",
37
+ "get_cache_dir",
38
+ "get_config_dir",
39
+ "get_data_dir",
40
+ "RaiSettings",
41
+ ]
@@ -0,0 +1,105 @@
1
+ """AgentPlugin protocol and default implementation.
2
+
3
+ Defines the extensibility interface for agent-specific transformations.
4
+ Plugins can transform instructions, skills, and run post-init hooks.
5
+
6
+ Architecture: ADR-032 (Multi-agent skill distribution).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+ from typing import Any, Protocol, runtime_checkable
13
+
14
+ from raise_cli.config.agents import AgentConfig
15
+
16
+
17
+ @runtime_checkable
18
+ class AgentPlugin(Protocol):
19
+ """Protocol for custom agent connectors.
20
+
21
+ Implementors add agent-specific transformation logic without modifying
22
+ the raise-cli engine. A plugin only needs to implement the methods it uses —
23
+ duck typing, no forced inheritance.
24
+
25
+ Example (minimal plugin):
26
+ class MyPlugin:
27
+ def transform_instructions(self, content, config):
28
+ return f"<!-- {config.name} -->\\n{content}"
29
+
30
+ def transform_skill(self, frontmatter, body, config):
31
+ return frontmatter, body
32
+
33
+ def post_init(self, project_root, config):
34
+ return []
35
+ """
36
+
37
+ def transform_instructions(self, content: str, config: AgentConfig) -> str:
38
+ """Transform generated instructions for this target.
39
+
40
+ Called after the instructions content is generated, before writing to disk.
41
+ Default: pass-through (return content unchanged).
42
+
43
+ Args:
44
+ content: Generated markdown instructions content.
45
+ config: Target agent configuration.
46
+
47
+ Returns:
48
+ Transformed content string.
49
+ """
50
+ ...
51
+
52
+ def transform_skill(
53
+ self, frontmatter: dict[str, Any], body: str, config: AgentConfig
54
+ ) -> tuple[dict[str, Any], str]:
55
+ """Transform a SKILL.md for this target.
56
+
57
+ Called for each skill file during scaffolding.
58
+ Default: pass-through (return unchanged).
59
+
60
+ Args:
61
+ frontmatter: Parsed YAML frontmatter dict.
62
+ body: Skill body markdown text.
63
+ config: Target agent configuration.
64
+
65
+ Returns:
66
+ Tuple of (transformed_frontmatter, transformed_body).
67
+ """
68
+ ...
69
+
70
+ def post_init(self, project_root: Path, config: AgentConfig) -> list[str]:
71
+ """Run after all files are generated.
72
+
73
+ Called once at the end of `rai init` for this agent.
74
+ Default: no-op (return empty list).
75
+
76
+ Args:
77
+ project_root: Project root directory.
78
+ config: Target agent configuration.
79
+
80
+ Returns:
81
+ List of file paths created by this hook.
82
+ """
83
+ ...
84
+
85
+
86
+ class DefaultAgentPlugin:
87
+ """Default no-op plugin used when no plugin is specified.
88
+
89
+ All methods are pass-through — skills and instructions are copied
90
+ as-is with no transformation. Four of five built-in agents use this.
91
+ """
92
+
93
+ def transform_instructions(self, content: str, config: AgentConfig) -> str:
94
+ """Return content unchanged."""
95
+ return content
96
+
97
+ def transform_skill(
98
+ self, frontmatter: dict[str, Any], body: str, config: AgentConfig
99
+ ) -> tuple[dict[str, Any], str]:
100
+ """Return frontmatter and body unchanged (copies, not references)."""
101
+ return dict(frontmatter), body
102
+
103
+ def post_init(self, project_root: Path, config: AgentConfig) -> list[str]:
104
+ """No-op — return empty list."""
105
+ return []
@@ -0,0 +1,233 @@
1
+ """Agent registry — 3-tier YAML loading with override precedence.
2
+
3
+ Loads agent configurations from:
4
+ 1. Built-in YAML files (bundled in raise_cli.agents package)
5
+ 2. Project-level .raise/agents/*.yaml
6
+ 3. User-level ~/.rai/agents/*.yaml
7
+
8
+ Last-wins precedence: user > project > built-in.
9
+
10
+ Architecture: ADR-032 (Multi-agent skill distribution).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import importlib
16
+ import logging
17
+ from importlib.resources import files
18
+ from pathlib import Path
19
+ from typing import Any, cast
20
+
21
+ import yaml
22
+
23
+ from raise_cli.config.agent_plugin import AgentPlugin, DefaultAgentPlugin
24
+ from raise_cli.config.agents import AgentConfig
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ _DEFAULT_PLUGIN = DefaultAgentPlugin()
29
+
30
+
31
+ def _parse_agent_config(data: dict[str, Any]) -> AgentConfig:
32
+ """Parse a YAML dict into an AgentConfig model."""
33
+ return AgentConfig(
34
+ name=data["name"],
35
+ agent_type=data["agent_type"],
36
+ instructions_file=data["instructions_file"],
37
+ skills_dir=data.get("skills_dir"),
38
+ workflows_dir=data.get("workflows_dir"),
39
+ detection_markers=data.get("detection_markers", []),
40
+ plugin=data.get("plugin"),
41
+ )
42
+
43
+
44
+ def _load_yaml_dir(directory: Path) -> dict[str, AgentConfig]:
45
+ """Load all *.yaml files from a directory into agent configs."""
46
+ configs: dict[str, AgentConfig] = {}
47
+ if not directory.exists():
48
+ return configs
49
+ for yaml_file in sorted(directory.glob("*.yaml")):
50
+ try:
51
+ raw = yaml.safe_load(yaml_file.read_text(encoding="utf-8"))
52
+ if not isinstance(raw, dict):
53
+ logger.warning("Skipping %s: not a YAML mapping", yaml_file)
54
+ continue
55
+ config = _parse_agent_config(cast(dict[str, Any], raw))
56
+ configs[config.agent_type] = config
57
+ except (yaml.YAMLError, KeyError, TypeError, ValueError) as e:
58
+ logger.warning("Skipping %s: %s", yaml_file, e)
59
+ return configs
60
+
61
+
62
+ def _load_builtin_agents() -> dict[str, AgentConfig]:
63
+ """Load agent configs from the bundled raise_cli.agents package."""
64
+ configs: dict[str, AgentConfig] = {}
65
+ agents_pkg = files("raise_cli.agents")
66
+ for entry in agents_pkg.iterdir():
67
+ if not entry.name.endswith(".yaml"):
68
+ continue
69
+ try:
70
+ raw = yaml.safe_load(entry.read_text(encoding="utf-8"))
71
+ if not isinstance(raw, dict):
72
+ logger.warning("Skipping built-in %s: not a YAML mapping", entry.name)
73
+ continue
74
+ config = _parse_agent_config(cast(dict[str, Any], raw))
75
+ configs[config.agent_type] = config
76
+ except (yaml.YAMLError, KeyError, TypeError, ValueError) as e:
77
+ logger.warning("Skipping built-in %s: %s", entry.name, e)
78
+ return configs
79
+
80
+
81
+ def _resolve_plugin(plugin_path: str | None) -> AgentPlugin:
82
+ """Resolve a plugin module path string to an AgentPlugin instance.
83
+
84
+ Expects the module to contain a class with the same name as the last
85
+ segment of the module path (PascalCase), or a class named 'Plugin'.
86
+
87
+ Args:
88
+ plugin_path: Module path like "raise_cli.agents.copilot_plugin",
89
+ or None for default pass-through.
90
+
91
+ Returns:
92
+ AgentPlugin instance.
93
+ """
94
+ if not plugin_path:
95
+ return _DEFAULT_PLUGIN
96
+
97
+ try:
98
+ module = importlib.import_module(plugin_path)
99
+ except ImportError as e:
100
+ logger.warning("Could not import plugin %r: %s — using default", plugin_path, e)
101
+ return _DEFAULT_PLUGIN
102
+
103
+ # Convention: look for class named Plugin, or derive from module name
104
+ # e.g. "copilot_plugin" → "CopilotPlugin"
105
+ segments = plugin_path.split(".")
106
+ module_name = segments[-1] # e.g. "copilot_plugin"
107
+ class_name = "".join(part.capitalize() for part in module_name.split("_"))
108
+ # e.g. "CopilotPlugin"
109
+
110
+ cls = getattr(module, class_name, None) or getattr(module, "Plugin", None)
111
+ if cls is None:
112
+ logger.warning(
113
+ "Plugin module %r has no class %r or 'Plugin' — using default",
114
+ plugin_path,
115
+ class_name,
116
+ )
117
+ return _DEFAULT_PLUGIN
118
+
119
+ try:
120
+ return cls()
121
+ except Exception as e:
122
+ logger.warning(
123
+ "Could not instantiate %r from %r: %s", class_name, plugin_path, e
124
+ )
125
+ return _DEFAULT_PLUGIN
126
+
127
+
128
+ class AgentRegistry:
129
+ """Registry of agent configurations loaded from 3-tier YAML sources.
130
+
131
+ Provides config and plugin lookup by agent_type key.
132
+ """
133
+
134
+ def __init__(self, configs: dict[str, AgentConfig]) -> None:
135
+ self._configs = configs
136
+ self._plugins: dict[str, AgentPlugin] = {}
137
+
138
+ # ------------------------------------------------------------------
139
+ # Public API
140
+ # ------------------------------------------------------------------
141
+
142
+ def get_config(self, agent_type: str) -> AgentConfig:
143
+ """Return AgentConfig for the given agent_type.
144
+
145
+ Raises:
146
+ KeyError: If agent_type is not registered.
147
+ """
148
+ return self._configs[agent_type]
149
+
150
+ def get_plugin(self, agent_type: str) -> AgentPlugin:
151
+ """Return AgentPlugin for the given agent_type (cached after first call).
152
+
153
+ Falls back to DefaultAgentPlugin if no plugin specified or load fails.
154
+
155
+ Raises:
156
+ KeyError: If agent_type is not registered.
157
+ """
158
+ if agent_type not in self._configs:
159
+ raise KeyError(agent_type)
160
+ if agent_type not in self._plugins:
161
+ config = self._configs[agent_type]
162
+ self._plugins[agent_type] = _resolve_plugin(config.plugin)
163
+ return self._plugins[agent_type]
164
+
165
+ def list_agents(self) -> list[str]:
166
+ """Return sorted list of all registered agent_type keys."""
167
+ return sorted(self._configs.keys())
168
+
169
+ def detect_agents(
170
+ self, project_root: Path, user_home: Path | None = None
171
+ ) -> list[str]:
172
+ """Detect which registered agents have marker files in project_root.
173
+
174
+ Checks each agent's detection_markers list; stops at first match per
175
+ agent. Markers starting with ``~/`` are resolved against user_home
176
+ (defaults to Path.home()) instead of project_root.
177
+
178
+ Args:
179
+ project_root: Directory to check for project-relative marker paths.
180
+ user_home: Home directory for ``~/`` markers (defaults to Path.home()).
181
+
182
+ Returns:
183
+ Sorted list of detected agent_type strings.
184
+ """
185
+ home = user_home if user_home is not None else Path.home()
186
+ detected: list[str] = []
187
+ for agent_type, config in self._configs.items():
188
+ for marker in config.detection_markers:
189
+ if marker.startswith("~/"):
190
+ check_path = home / marker[2:]
191
+ else:
192
+ check_path = project_root / marker
193
+ if check_path.exists():
194
+ detected.append(agent_type)
195
+ break
196
+ return sorted(detected)
197
+
198
+
199
+ def load_registry(
200
+ project_root: Path | None = None,
201
+ user_home: Path | None = None,
202
+ ) -> AgentRegistry:
203
+ """Load the 3-tier agent registry.
204
+
205
+ Loading order (last-wins):
206
+ 1. Built-in YAML files bundled in raise_cli.agents package
207
+ 2. Project-level .raise/agents/*.yaml (if project_root provided)
208
+ 3. User-level ~/.rai/agents/*.yaml (if user_home provided; defaults to Path.home())
209
+
210
+ Args:
211
+ project_root: Project root directory for .raise/agents/ lookup.
212
+ user_home: Home directory for ~/.rai/agents/ lookup.
213
+ Defaults to Path.home() if not provided.
214
+
215
+ Returns:
216
+ AgentRegistry populated with merged configs.
217
+ """
218
+ # Tier 1: built-in
219
+ configs: dict[str, AgentConfig] = _load_builtin_agents()
220
+
221
+ # Tier 2: project-level
222
+ if project_root is not None:
223
+ project_agents_dir = project_root / ".raise" / "agents"
224
+ project_configs = _load_yaml_dir(project_agents_dir)
225
+ configs.update(project_configs)
226
+
227
+ # Tier 3: user-level
228
+ home = user_home if user_home is not None else Path.home()
229
+ user_agents_dir = home / ".rai" / "agents"
230
+ user_configs = _load_yaml_dir(user_agents_dir)
231
+ configs.update(user_configs)
232
+
233
+ return AgentRegistry(configs)