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,437 @@
1
+ """Scaffold bundled skills into a project with version-aware sync.
2
+
3
+ Copies RaiSE skills from the raise_cli.skills_base package to the project's
4
+ IDE skill directory during `rai init`. Uses the dpkg three-hash algorithm
5
+ to safely update skills: auto-update untouched files, keep customized,
6
+ prompt on conflict.
7
+
8
+ Uses importlib.resources to read bundled skill files (Python 3.9+).
9
+ Handles reference subdirectories (e.g., references/, _references/).
10
+
11
+ Example:
12
+ from raise_cli.onboarding.skills import scaffold_skills
13
+
14
+ result = scaffold_skills(project_path)
15
+ if result.skills_updated:
16
+ print(f"Updated {len(result.skills_updated)} skills")
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ from importlib.resources import files
23
+ from importlib.resources.abc import Traversable
24
+ from pathlib import Path
25
+
26
+ from pydantic import BaseModel, Field
27
+
28
+ from raise_cli.config.agent_plugin import AgentPlugin
29
+ from raise_cli.config.agents import AgentConfig, get_agent_config
30
+ from raise_cli.onboarding.skill_manifest import (
31
+ SkillEntry,
32
+ SkillManifest,
33
+ SkillSyncAction,
34
+ classify_skill,
35
+ compute_content_hash,
36
+ load_skill_manifest,
37
+ save_skill_manifest,
38
+ )
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ class SkillScaffoldResult(BaseModel):
44
+ """Result of skill scaffolding operation."""
45
+
46
+ # New sync-aware fields
47
+ skills_installed: list[str] = Field(default_factory=list)
48
+ skills_updated: list[str] = Field(default_factory=list)
49
+ skills_conflicted: list[str] = Field(default_factory=list)
50
+ skills_kept: list[str] = Field(default_factory=list)
51
+ skills_overwritten: list[str] = Field(default_factory=list)
52
+ skills_current: list[str] = Field(default_factory=list)
53
+
54
+ # Backward-compat fields
55
+ skills_copied: int = 0
56
+ already_existed: bool = False
57
+ files_copied: list[str] = Field(default_factory=list)
58
+ files_skipped: list[str] = Field(default_factory=list)
59
+ skills_skipped_names: list[str] = Field(default_factory=list)
60
+
61
+
62
+ def _apply_plugin_transform(
63
+ content: str,
64
+ plugin: AgentPlugin,
65
+ agent_config: AgentConfig,
66
+ ) -> str:
67
+ """Apply plugin.transform_skill to a SKILL.md content string."""
68
+ import yaml
69
+
70
+ from raise_cli.skills.parser import parse_frontmatter
71
+
72
+ fm, body = parse_frontmatter(content)
73
+ fm_out, body_out = plugin.transform_skill(fm, body, agent_config)
74
+ # Re-serialize: frontmatter + body
75
+ if fm_out:
76
+ return f"---\n{yaml.dump(fm_out, default_flow_style=False, allow_unicode=True)}---\n{body_out}"
77
+ return body_out
78
+
79
+
80
+ def _read_bundled_content(
81
+ base: Traversable,
82
+ skill_name: str,
83
+ *,
84
+ plugin: AgentPlugin | None = None,
85
+ agent_config: AgentConfig | None = None,
86
+ ) -> str:
87
+ """Read bundled SKILL.md content, applying plugin transforms if needed.
88
+
89
+ Args:
90
+ base: The skills_base package traversable.
91
+ skill_name: Name of the skill directory.
92
+ plugin: Optional plugin for content transforms.
93
+ agent_config: Agent config for plugin.
94
+
95
+ Returns:
96
+ The SKILL.md content as it would be written to disk.
97
+ """
98
+ raw = (base / skill_name / "SKILL.md").read_text(encoding="utf-8")
99
+ if plugin is not None and agent_config is not None:
100
+ raw = _apply_plugin_transform(raw, plugin, agent_config)
101
+ return raw
102
+
103
+
104
+ def copy_skill_tree(
105
+ source_dir: Path | Traversable,
106
+ dest_dir: Path,
107
+ result: SkillScaffoldResult,
108
+ *,
109
+ plugin: AgentPlugin | None = None,
110
+ agent_config: AgentConfig | None = None,
111
+ overwrite: bool = False,
112
+ ) -> int:
113
+ """Recursively copy skill files from source to destination.
114
+
115
+ Accepts both importlib Traversable (for bundled skills) and Path
116
+ (for filesystem overlay skills). Both types support iterdir(),
117
+ is_file(), is_dir(), read_text(), and name. (AR R1, S340.1)
118
+
119
+ Args:
120
+ source_dir: Resource directory (Traversable or Path).
121
+ dest_dir: Target directory on filesystem.
122
+ result: Result object to track copied/skipped files.
123
+ plugin: Optional plugin to transform SKILL.md files.
124
+ agent_config: Agent config passed to plugin.
125
+ overwrite: If True, overwrite existing files.
126
+
127
+ Returns:
128
+ Number of files copied.
129
+ """
130
+ copied = 0
131
+ for item in source_dir.iterdir():
132
+ if item.name == "__init__.py" or item.name == "__pycache__":
133
+ continue
134
+ dest = dest_dir / item.name
135
+ if item.is_file():
136
+ if dest.exists() and not overwrite:
137
+ result.files_skipped.append(str(dest))
138
+ logger.debug("Skipped (exists): %s", dest)
139
+ continue
140
+ dest.parent.mkdir(parents=True, exist_ok=True)
141
+ raw_content = item.read_text(encoding="utf-8")
142
+ if (
143
+ plugin is not None
144
+ and agent_config is not None
145
+ and item.name == "SKILL.md"
146
+ ):
147
+ raw_content = _apply_plugin_transform(raw_content, plugin, agent_config)
148
+ dest.write_text(raw_content, encoding="utf-8")
149
+ result.files_copied.append(str(dest))
150
+ copied += 1
151
+ logger.debug("Copied: %s", dest)
152
+ elif item.is_dir():
153
+ copied += copy_skill_tree(
154
+ item,
155
+ dest,
156
+ result,
157
+ plugin=plugin,
158
+ agent_config=agent_config,
159
+ overwrite=overwrite,
160
+ )
161
+ return copied
162
+
163
+
164
+ def _get_cli_version() -> str:
165
+ """Get current skills_base version for manifest."""
166
+ try:
167
+ from raise_cli.skills_base import __version__
168
+
169
+ return __version__
170
+ except ImportError:
171
+ return "unknown"
172
+
173
+
174
+ def scaffold_skills(
175
+ project_root: Path,
176
+ *,
177
+ agent_config: AgentConfig | None = None,
178
+ plugin: AgentPlugin | None = None,
179
+ force: bool = False,
180
+ skip_updates: bool = False,
181
+ dry_run: bool = False,
182
+ skill_set: str | None = None,
183
+ ) -> SkillScaffoldResult:
184
+ """Copy bundled skills to project skill directory with version-aware sync.
185
+
186
+ Uses the dpkg three-hash algorithm to detect changes:
187
+ - Untouched files are auto-updated silently
188
+ - Customized files are preserved (user's version kept)
189
+ - Conflicts (both changed) default to keep in non-interactive mode
190
+
191
+ When ``skill_set`` is provided, overlay skills from
192
+ ``.raise/skills/{skill_set}/`` are copied on top of builtins
193
+ after the standard deployment. Same-name overlay wins. (S340.1)
194
+
195
+ Args:
196
+ project_root: Project root directory.
197
+ agent_config: Agent configuration. Defaults to Claude.
198
+ plugin: Optional plugin to transform SKILL.md files during copy.
199
+ force: If True, overwrite all files without prompting.
200
+ skip_updates: If True, only install new skills (legacy behavior).
201
+ dry_run: If True, compute actions but don't write files.
202
+ skill_set: Skill set name for overlay (e.g. "my-team").
203
+ None = builtins only.
204
+
205
+ Returns:
206
+ SkillScaffoldResult with details of what was done.
207
+ """
208
+ from raise_cli.skills_base import DISTRIBUTABLE_SKILLS
209
+
210
+ config = agent_config or get_agent_config()
211
+ if config.skills_dir is None:
212
+ return SkillScaffoldResult()
213
+
214
+ base = files("raise_cli.skills_base")
215
+ skills_dir = project_root / config.skills_dir
216
+ result = SkillScaffoldResult()
217
+ manifest = load_skill_manifest(project_root) or SkillManifest()
218
+ cli_version = _get_cli_version()
219
+ batch_keep = False
220
+ batch_overwrite = False
221
+
222
+ for skill_name in DISTRIBUTABLE_SKILLS:
223
+ skill_dest = skills_dir / skill_name
224
+ skill_md = skill_dest / "SKILL.md"
225
+ source = base / skill_name
226
+
227
+ bundled_content = _read_bundled_content(
228
+ base, skill_name, plugin=plugin, agent_config=config
229
+ )
230
+ hash_new = compute_content_hash(bundled_content)
231
+
232
+ # --- Case: skill doesn't exist on disk → install ---
233
+ if not skill_md.exists():
234
+ if not dry_run:
235
+ copied = copy_skill_tree(
236
+ source,
237
+ skill_dest,
238
+ result,
239
+ plugin=plugin,
240
+ agent_config=config,
241
+ overwrite=True,
242
+ )
243
+ manifest.skills[skill_name] = SkillEntry(
244
+ sha256=hash_new,
245
+ version=cli_version,
246
+ )
247
+ if copied > 0:
248
+ result.skills_copied += 1
249
+ result.skills_installed.append(skill_name)
250
+ continue
251
+
252
+ # --- Skill exists on disk → classify with three-hash ---
253
+ hash_on_disk = compute_content_hash(skill_md.read_text(encoding="utf-8"))
254
+ entry = manifest.skills.get(skill_name)
255
+ hash_distributed = entry.sha256 if entry else None
256
+
257
+ action = classify_skill(hash_distributed, hash_on_disk, hash_new)
258
+
259
+ if action == SkillSyncAction.CURRENT:
260
+ result.skills_current.append(skill_name)
261
+ # Ensure manifest entry exists (legacy fixup)
262
+ if skill_name not in manifest.skills:
263
+ manifest.skills[skill_name] = SkillEntry(
264
+ sha256=hash_on_disk,
265
+ version=cli_version,
266
+ )
267
+
268
+ elif action == SkillSyncAction.AUTO_UPDATE:
269
+ if skip_updates:
270
+ result.skills_current.append(skill_name)
271
+ result.skills_skipped_names.append(skill_name)
272
+ elif not dry_run:
273
+ # Safe to overwrite — user hasn't touched it
274
+ skill_md.write_text(bundled_content, encoding="utf-8")
275
+ result.files_copied.append(str(skill_md))
276
+ # Also update reference files
277
+ copy_skill_tree(
278
+ source,
279
+ skill_dest,
280
+ result,
281
+ plugin=plugin,
282
+ agent_config=config,
283
+ overwrite=True,
284
+ )
285
+ manifest.skills[skill_name] = SkillEntry(
286
+ sha256=hash_new,
287
+ version=cli_version,
288
+ )
289
+ result.skills_updated.append(skill_name)
290
+ else:
291
+ result.skills_updated.append(skill_name)
292
+
293
+ elif action == SkillSyncAction.KEEP_USER:
294
+ result.skills_current.append(skill_name)
295
+ result.skills_skipped_names.append(skill_name)
296
+
297
+ elif action == SkillSyncAction.CONFLICT:
298
+ if force or batch_overwrite:
299
+ if not dry_run:
300
+ skill_md.write_text(bundled_content, encoding="utf-8")
301
+ result.files_copied.append(str(skill_md))
302
+ copy_skill_tree(
303
+ source,
304
+ skill_dest,
305
+ result,
306
+ plugin=plugin,
307
+ agent_config=config,
308
+ overwrite=True,
309
+ )
310
+ manifest.skills[skill_name] = SkillEntry(
311
+ sha256=hash_new,
312
+ version=cli_version,
313
+ )
314
+ result.skills_overwritten.append(skill_name)
315
+ elif skip_updates or batch_keep:
316
+ result.skills_conflicted.append(skill_name)
317
+ result.skills_skipped_names.append(skill_name)
318
+ elif dry_run:
319
+ result.skills_conflicted.append(skill_name)
320
+ else:
321
+ # Interactive conflict resolution
322
+ from raise_cli.onboarding.skill_conflict import (
323
+ ConflictAction,
324
+ prompt_skill_conflict,
325
+ )
326
+
327
+ on_disk_content = skill_md.read_text(encoding="utf-8")
328
+ user_action = prompt_skill_conflict(
329
+ skill_name,
330
+ on_disk_content,
331
+ bundled_content,
332
+ )
333
+
334
+ if user_action == ConflictAction.KEEP:
335
+ result.skills_kept.append(skill_name)
336
+ elif user_action == ConflictAction.KEEP_ALL:
337
+ result.skills_kept.append(skill_name)
338
+ batch_keep = True
339
+ elif user_action in (
340
+ ConflictAction.OVERWRITE,
341
+ ConflictAction.OVERWRITE_ALL,
342
+ ):
343
+ skill_md.write_text(bundled_content, encoding="utf-8")
344
+ result.files_copied.append(str(skill_md))
345
+ copy_skill_tree(
346
+ source,
347
+ skill_dest,
348
+ result,
349
+ plugin=plugin,
350
+ agent_config=config,
351
+ overwrite=True,
352
+ )
353
+ manifest.skills[skill_name] = SkillEntry(
354
+ sha256=hash_new,
355
+ version=cli_version,
356
+ )
357
+ result.skills_overwritten.append(skill_name)
358
+ if user_action == ConflictAction.OVERWRITE_ALL:
359
+ batch_overwrite = True
360
+ elif user_action == ConflictAction.BACKUP_OVERWRITE:
361
+ # Save backup before overwriting
362
+ backup_path = skill_md.with_suffix(".md.bak")
363
+ backup_path.write_text(on_disk_content, encoding="utf-8")
364
+ skill_md.write_text(bundled_content, encoding="utf-8")
365
+ result.files_copied.append(str(skill_md))
366
+ copy_skill_tree(
367
+ source,
368
+ skill_dest,
369
+ result,
370
+ plugin=plugin,
371
+ agent_config=config,
372
+ overwrite=True,
373
+ )
374
+ manifest.skills[skill_name] = SkillEntry(
375
+ sha256=hash_new,
376
+ version=cli_version,
377
+ )
378
+ result.skills_overwritten.append(skill_name)
379
+
380
+ elif action == SkillSyncAction.LEGACY:
381
+ # No manifest entry — first encounter
382
+ if hash_on_disk == hash_new:
383
+ # File matches bundled — safe to record
384
+ manifest.skills[skill_name] = SkillEntry(
385
+ sha256=hash_new,
386
+ version=cli_version,
387
+ )
388
+ else:
389
+ # File differs — treat as customized, record on-disk hash
390
+ manifest.skills[skill_name] = SkillEntry(
391
+ sha256=hash_on_disk,
392
+ version=cli_version,
393
+ )
394
+ result.skills_current.append(skill_name)
395
+ result.files_skipped.append(str(skill_md))
396
+ result.skills_skipped_names.append(skill_name)
397
+
398
+ # --- Skill set overlay (S340.1) ---
399
+ if skill_set is not None and not dry_run:
400
+ from raise_cli.config.paths import get_raise_dir
401
+
402
+ overlay_dir = get_raise_dir(project_root) / "skills" / skill_set
403
+ if overlay_dir.is_dir():
404
+ for skill_dir in sorted(overlay_dir.iterdir()):
405
+ if not skill_dir.is_dir():
406
+ continue
407
+ if not (skill_dir / "SKILL.md").exists():
408
+ continue
409
+ copy_skill_tree(
410
+ skill_dir,
411
+ skills_dir / skill_dir.name,
412
+ result,
413
+ plugin=plugin,
414
+ agent_config=config,
415
+ overwrite=True,
416
+ )
417
+ overlay_content = (skill_dir / "SKILL.md").read_text(encoding="utf-8")
418
+ manifest.skills[skill_dir.name] = SkillEntry(
419
+ sha256=compute_content_hash(overlay_content),
420
+ version=cli_version,
421
+ origin="project",
422
+ )
423
+ manifest.skill_set = skill_set
424
+ else:
425
+ logger.warning("Skill set '%s' not found at %s", skill_set, overlay_dir)
426
+
427
+ # Update backward-compat flag
428
+ result.already_existed = (
429
+ len(result.skills_installed) == 0 and len(result.skills_updated) == 0
430
+ )
431
+
432
+ # Persist manifest
433
+ if not dry_run:
434
+ manifest.raise_cli_version = cli_version
435
+ save_skill_manifest(manifest, project_root)
436
+
437
+ return result
@@ -0,0 +1,101 @@
1
+ """Scaffold workflow shims for IDEs that support workflows.
2
+
3
+ Generates one workflow file per distributable skill.
4
+ Each workflow is a minimal .md with YAML frontmatter (name + description)
5
+ and a one-line body referencing the skill.
6
+
7
+ IDEs without workflows (e.g., Claude Code) get a no-op.
8
+ Per-file idempotency: existing files are never overwritten.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from importlib.resources import files
15
+ from pathlib import Path
16
+
17
+ from pydantic import BaseModel, Field
18
+
19
+ from raise_cli.config.agents import AgentConfig, get_agent_config
20
+ from raise_cli.skills.parser import parse_frontmatter
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class WorkflowScaffoldResult(BaseModel):
26
+ """Result of workflow scaffolding operation."""
27
+
28
+ workflows_created: int = 0
29
+ already_existed: bool = False
30
+ skipped_no_workflows_dir: bool = False
31
+ files_created: list[str] = Field(default_factory=list)
32
+ files_skipped: list[str] = Field(default_factory=list)
33
+
34
+
35
+ def scaffold_workflows(
36
+ project_root: Path,
37
+ *,
38
+ agent_config: AgentConfig | None = None,
39
+ ) -> WorkflowScaffoldResult:
40
+ """Generate workflow shim files for each distributable skill.
41
+
42
+ Reads each skill's SKILL.md frontmatter (name + description)
43
+ and writes a minimal workflow file that references the skill.
44
+ Skips when the IDE has no workflows_dir (e.g., Claude Code).
45
+
46
+ Args:
47
+ project_root: Project root directory.
48
+ ide_config: IDE configuration. Defaults to Claude.
49
+
50
+ Returns:
51
+ WorkflowScaffoldResult with details of what was created or skipped.
52
+ """
53
+ from raise_cli.skills_base import DISTRIBUTABLE_SKILLS
54
+
55
+ config = agent_config or get_agent_config()
56
+ result = WorkflowScaffoldResult()
57
+
58
+ if config.workflows_dir is None:
59
+ result.skipped_no_workflows_dir = True
60
+ return result
61
+
62
+ base = files("raise_cli.skills_base")
63
+ workflows_dir = project_root / config.workflows_dir
64
+ workflows_dir.mkdir(parents=True, exist_ok=True)
65
+
66
+ for skill_name in DISTRIBUTABLE_SKILLS:
67
+ workflow_file = workflows_dir / f"{skill_name}.md"
68
+
69
+ if workflow_file.exists():
70
+ result.files_skipped.append(skill_name)
71
+ logger.debug("Skipped (exists): %s", workflow_file)
72
+ continue
73
+
74
+ # Read skill frontmatter for name + description
75
+ skill_md = base / skill_name / "SKILL.md"
76
+ content = skill_md.read_text(encoding="utf-8")
77
+ frontmatter, _body = parse_frontmatter(content)
78
+
79
+ name = frontmatter.get("name", skill_name)
80
+ description = frontmatter.get("description", "")
81
+
82
+ # Write workflow shim
83
+ skills_path = f"{config.skills_dir}/{skill_name}/SKILL.md"
84
+ workflow_content = (
85
+ f"---\nname: {name}\n"
86
+ f"description: >\n"
87
+ f" {description.strip()}\n"
88
+ f"---\n\n"
89
+ f"Run the `{skill_name}` skill from `{skills_path}`.\n"
90
+ )
91
+
92
+ workflow_file.write_text(workflow_content, encoding="utf-8")
93
+ result.files_created.append(str(workflow_file))
94
+ result.workflows_created += 1
95
+ logger.debug("Created: %s", workflow_file)
96
+
97
+ result.already_existed = (
98
+ result.workflows_created == 0 and len(result.files_skipped) > 0
99
+ )
100
+
101
+ return result
@@ -0,0 +1,28 @@
1
+ """Output formatting module for raise-cli.
2
+
3
+ Provides format-aware output (human, JSON, table) for CLI commands.
4
+
5
+ Example:
6
+ >>> from raise_cli.output import get_console
7
+ >>>
8
+ >>> console = get_console()
9
+ >>> console.print_success("Done!", details={"duration": "2.3s"})
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from raise_cli.output.console import (
15
+ OutputConsole,
16
+ OutputFormat,
17
+ configure_console,
18
+ get_console,
19
+ set_console,
20
+ )
21
+
22
+ __all__ = [
23
+ "OutputConsole",
24
+ "OutputFormat",
25
+ "get_console",
26
+ "set_console",
27
+ "configure_console",
28
+ ]