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,124 @@
1
+ """CopilotPlugin — transforms RaiSE skills to GitHub Copilot agent format.
2
+
3
+ GitHub Copilot uses:
4
+ - .github/agents/*.SKILL.md (different frontmatter: tools, infer)
5
+ - .github/prompts/*.prompt.md (one per skill, for Copilot workflows)
6
+
7
+ This plugin handles both transformations via AgentPlugin protocol hooks.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from pathlib import Path
14
+ from typing import Any, cast
15
+
16
+ from raise_cli.config.agents import AgentConfig
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ _COPILOT_TOOLS = ["execute", "read", "edit", "search"]
21
+ _REMOVE_FIELDS = {"license", "compatibility"}
22
+
23
+
24
+ class CopilotPlugin:
25
+ """Transform RaiSE skills and instructions for GitHub Copilot.
26
+
27
+ Copilot uses a different frontmatter schema for .github/agents/ files
28
+ and supports .github/prompts/*.prompt.md workflow files.
29
+ """
30
+
31
+ def transform_instructions(self, content: str, config: AgentConfig) -> str:
32
+ """Return instructions unchanged — Copilot reads standard markdown."""
33
+ return content
34
+
35
+ def transform_skill(
36
+ self, frontmatter: dict[str, Any], body: str, config: AgentConfig
37
+ ) -> tuple[dict[str, Any], str]:
38
+ """Transform skill frontmatter to Copilot agent format.
39
+
40
+ Adds Copilot-specific fields (tools, infer).
41
+ Removes fields unsupported by Copilot (license, compatibility).
42
+ """
43
+ fm = {k: v for k, v in frontmatter.items() if k not in _REMOVE_FIELDS}
44
+ fm["tools"] = list(_COPILOT_TOOLS)
45
+ fm["infer"] = True
46
+ return fm, body
47
+
48
+ def post_init(self, project_root: Path, config: AgentConfig) -> list[str]:
49
+ """Generate .prompt.md files from skills in .github/agents/.
50
+
51
+ Reads each SKILL.md in the skills_dir, extracts name and description
52
+ from frontmatter, and writes a .prompt.md to .github/prompts/.
53
+
54
+ Args:
55
+ project_root: Project root directory.
56
+ config: Copilot agent configuration.
57
+
58
+ Returns:
59
+ List of created .prompt.md file paths.
60
+ """
61
+ if config.skills_dir is None:
62
+ return []
63
+
64
+ agents_dir = project_root / config.skills_dir
65
+ if not agents_dir.exists():
66
+ return []
67
+
68
+ prompts_dir = project_root / ".github" / "prompts"
69
+ prompts_dir.mkdir(parents=True, exist_ok=True)
70
+
71
+ created: list[str] = []
72
+ for skill_dir in sorted(agents_dir.iterdir()):
73
+ if not skill_dir.is_dir():
74
+ continue
75
+ skill_md = skill_dir / "SKILL.md"
76
+ if not skill_md.exists():
77
+ continue
78
+
79
+ skill_name = skill_dir.name
80
+ frontmatter = _read_frontmatter(skill_md)
81
+ name = frontmatter.get("name", skill_name)
82
+ description = frontmatter.get("description", "")
83
+
84
+ prompt_content = _build_prompt_md(skill_name, name, str(description))
85
+ prompt_file = prompts_dir / f"{skill_name}.prompt.md"
86
+ prompt_file.write_text(prompt_content, encoding="utf-8")
87
+ created.append(str(prompt_file))
88
+ logger.debug("Created Copilot prompt: %s", prompt_file)
89
+
90
+ return created
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Helpers
95
+ # ---------------------------------------------------------------------------
96
+
97
+
98
+ def _read_frontmatter(skill_md: Path) -> dict[str, Any]:
99
+ """Parse YAML frontmatter from a SKILL.md file (best-effort)."""
100
+ try:
101
+ import yaml
102
+
103
+ content = skill_md.read_text(encoding="utf-8")
104
+ if not content.startswith("---"):
105
+ return {}
106
+ parts = content.split("---", 2)
107
+ if len(parts) < 3:
108
+ return {}
109
+ raw: Any = yaml.safe_load(parts[1])
110
+ if not isinstance(raw, dict):
111
+ return {}
112
+ return cast(dict[str, Any], raw)
113
+ except Exception:
114
+ return {}
115
+
116
+
117
+ def _build_prompt_md(skill_name: str, name: str, description: str) -> str:
118
+ """Build .prompt.md content for a Copilot workflow prompt."""
119
+ return (
120
+ f"# {name}\n\n"
121
+ f"{description.strip()}\n\n"
122
+ f"---\n\n"
123
+ f"Run the `{skill_name}` skill from `.github/agents/{skill_name}/SKILL.md`.\n"
124
+ )
@@ -0,0 +1,7 @@
1
+ name: Cursor
2
+ agent_type: cursor
3
+ instructions_file: .cursor/rules/raise.mdc
4
+ skills_dir: .cursor/skills
5
+ detection_markers:
6
+ - .cursor/rules
7
+ - .cursor
@@ -0,0 +1,8 @@
1
+ name: Roo Code
2
+ agent_type: roo
3
+ instructions_file: .roo/rules/raise.md
4
+ skills_dir: .roo/skills
5
+ detection_markers:
6
+ - .roo/rules
7
+ - .roo
8
+ - .rooignore
@@ -0,0 +1,8 @@
1
+ name: Windsurf
2
+ agent_type: windsurf
3
+ instructions_file: .windsurf/rules/raise.md
4
+ skills_dir: .windsurf/skills
5
+ workflows_dir: .windsurf/workflows
6
+ detection_markers:
7
+ - .windsurf/rules
8
+ - .windsurf
@@ -0,0 +1,30 @@
1
+ """Typed skill artifacts: models, validation, and YAML storage."""
2
+
3
+ from raise_cli.artifacts.models import ArtifactRefs, ArtifactType, SkillArtifact
4
+ from raise_cli.artifacts.reader import read_all_artifacts, read_artifact
5
+ from raise_cli.artifacts.renderer import render_artifact
6
+ from raise_cli.artifacts.story_design import (
7
+ AcceptanceCriterion,
8
+ Complexity,
9
+ Decision,
10
+ IntegrationPoint,
11
+ StoryDesignArtifact,
12
+ StoryDesignContent,
13
+ )
14
+ from raise_cli.artifacts.writer import write_artifact
15
+
16
+ __all__ = [
17
+ "AcceptanceCriterion",
18
+ "ArtifactRefs",
19
+ "ArtifactType",
20
+ "Complexity",
21
+ "Decision",
22
+ "IntegrationPoint",
23
+ "SkillArtifact",
24
+ "StoryDesignArtifact",
25
+ "StoryDesignContent",
26
+ "read_all_artifacts",
27
+ "read_artifact",
28
+ "render_artifact",
29
+ "write_artifact",
30
+ ]
@@ -0,0 +1,43 @@
1
+ """Pydantic models for typed skill artifacts."""
2
+
3
+ from datetime import datetime
4
+ from enum import StrEnum
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class ArtifactType(StrEnum):
11
+ """Types of skill artifacts.
12
+
13
+ Each value corresponds to a skill's output type declared
14
+ in SKILL.md frontmatter as ``raise.output_type``.
15
+ """
16
+
17
+ STORY_DESIGN = "story-design"
18
+
19
+
20
+ class ArtifactRefs(BaseModel):
21
+ """References from an artifact to external items."""
22
+
23
+ backlog_item: str | None = None
24
+ epic_scope: str | None = None
25
+ related_artifacts: list[str] = Field(default_factory=list)
26
+
27
+
28
+ class SkillArtifact(BaseModel):
29
+ """Base artifact produced by a skill execution.
30
+
31
+ Subclasses replace ``content`` with a typed model for
32
+ specific artifact types (e.g., StoryDesignContent).
33
+ """
34
+
35
+ artifact_type: ArtifactType
36
+ version: int = Field(default=1, ge=1)
37
+ skill: str
38
+ created: datetime
39
+ story: str | None = None
40
+ epic: str | None = None
41
+ content: dict[str, Any] = Field(default_factory=dict)
42
+ refs: ArtifactRefs = Field(default_factory=ArtifactRefs)
43
+ metadata: dict[str, Any] = Field(default_factory=dict)
@@ -0,0 +1,55 @@
1
+ """YAML reader for skill artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import yaml
8
+
9
+ from raise_cli.artifacts.models import SkillArtifact
10
+
11
+ # Lazy import to avoid circular dependency
12
+ _artifact_registry: dict[str, type[SkillArtifact]] | None = None
13
+
14
+
15
+ def _get_registry() -> dict[str, type[SkillArtifact]]:
16
+ """Build the artifact type registry on first access."""
17
+ global _artifact_registry # noqa: PLW0603
18
+ if _artifact_registry is None:
19
+ from raise_cli.artifacts.story_design import StoryDesignArtifact
20
+
21
+ _artifact_registry = {
22
+ "story-design": StoryDesignArtifact,
23
+ }
24
+ return _artifact_registry
25
+
26
+
27
+ def read_artifact(path: Path) -> SkillArtifact:
28
+ """Load and validate a single artifact from a YAML file.
29
+
30
+ Dispatches to the correct model subclass based on ``artifact_type``.
31
+ Falls back to ``SkillArtifact`` (base) for unknown types.
32
+
33
+ Raises:
34
+ pydantic.ValidationError: If the YAML doesn't match the schema.
35
+ """
36
+ data = yaml.safe_load(path.read_text())
37
+ artifact_type = data.get("artifact_type", "")
38
+ registry = _get_registry()
39
+ model_class = registry.get(artifact_type, SkillArtifact)
40
+ return model_class.model_validate(data)
41
+
42
+
43
+ def read_all_artifacts(artifacts_dir: Path) -> list[SkillArtifact]:
44
+ """Load all artifacts from a directory.
45
+
46
+ Returns an empty list if the directory doesn't exist or is empty.
47
+ Raises ``ValidationError`` if any file fails validation.
48
+ """
49
+ if not artifacts_dir.is_dir():
50
+ return []
51
+
52
+ artifacts: list[SkillArtifact] = []
53
+ for yaml_file in sorted(artifacts_dir.glob("*.yaml")):
54
+ artifacts.append(read_artifact(yaml_file))
55
+ return artifacts
@@ -0,0 +1,104 @@
1
+ """Render typed artifacts to human-readable Markdown."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+
6
+ from raise_cli.artifacts.models import SkillArtifact
7
+ from raise_cli.artifacts.story_design import (
8
+ StoryDesignArtifact,
9
+ StoryDesignContent,
10
+ )
11
+
12
+
13
+ def _render_acceptance_criteria(content: StoryDesignContent) -> str:
14
+ """Render AC as a numbered list."""
15
+ lines: list[str] = []
16
+ for i, ac in enumerate(content.acceptance_criteria, 1):
17
+ lines.append(f"{i}. [{ac.id}] {ac.description}")
18
+ return "\n".join(lines)
19
+
20
+
21
+ def _render_integration_points(content: StoryDesignContent) -> str:
22
+ """Render integration points as bullet list."""
23
+ if not content.integration_points:
24
+ return ""
25
+ lines: list[str] = []
26
+ for ip in content.integration_points:
27
+ files = ", ".join(f"`{f}`" for f in ip.files) if ip.files else "" # pyright: ignore[reportUnknownArgumentType]
28
+ line = f"- `{ip.module}` — {ip.change_type}"
29
+ if files:
30
+ line += f": {files}"
31
+ lines.append(line)
32
+ return "\n".join(lines)
33
+
34
+
35
+ def _render_decisions(content: StoryDesignContent) -> str:
36
+ """Render decisions with rationale and alternatives."""
37
+ if not content.decisions:
38
+ return ""
39
+ blocks: list[str] = []
40
+ for d in content.decisions:
41
+ block = f"### {d.id}: {d.choice}\n"
42
+ block += f"**Rationale:** {d.rationale}\n"
43
+ if d.alternatives_considered:
44
+ alts = ", ".join(d.alternatives_considered) # pyright: ignore[reportUnknownArgumentType]
45
+ block += f"**Alternatives:** {alts}"
46
+ blocks.append(block)
47
+ return "\n\n".join(blocks)
48
+
49
+
50
+ def _remove_empty_sections(md: str) -> str:
51
+ """Remove sections whose placeholder was empty."""
52
+ # Match ## heading followed by only whitespace until next ## or end
53
+ return re.sub(r"\n## [^\n]+\n\n\s*(?=\n##|\Z)", "", md)
54
+
55
+
56
+ def _get_template(project_root: Path | None = None) -> str:
57
+ """Load the story-design template."""
58
+ if project_root:
59
+ template_path = (
60
+ project_root / ".raise" / "templates" / "artifacts" / "story-design.md"
61
+ )
62
+ if template_path.exists():
63
+ return template_path.read_text()
64
+
65
+ # Fallback: inline default
66
+ return (
67
+ "# {story} Design: {summary}\n\n"
68
+ "**Story:** {story} | **Epic:** {epic} | **Complexity:** {complexity}\n"
69
+ "**Skill:** {skill} | **Created:** {created}\n\n"
70
+ "## Summary\n\n{summary}\n\n"
71
+ "## Acceptance Criteria\n\n{acceptance_criteria}\n\n"
72
+ "## Integration Points\n\n{integration_points}\n\n"
73
+ "## Decisions\n\n{decisions}\n"
74
+ )
75
+
76
+
77
+ def render_artifact(
78
+ artifact: SkillArtifact, project_root: Path | None = None
79
+ ) -> str:
80
+ """Render a typed artifact to Markdown.
81
+
82
+ Uses the template from ``.raise/templates/artifacts/`` if available,
83
+ falls back to a built-in default.
84
+ """
85
+ if not isinstance(artifact, StoryDesignArtifact):
86
+ # Fallback for non-story-design artifacts
87
+ return f"# Artifact: {artifact.artifact_type}\n\n{artifact.content}\n"
88
+
89
+ content = artifact.content
90
+ template = _get_template(project_root)
91
+
92
+ md = template.format(
93
+ story=artifact.story or "—",
94
+ epic=artifact.epic or "—",
95
+ summary=content.summary,
96
+ complexity=content.complexity.value,
97
+ skill=artifact.skill,
98
+ created=artifact.created.strftime("%Y-%m-%d"),
99
+ acceptance_criteria=_render_acceptance_criteria(content),
100
+ integration_points=_render_integration_points(content),
101
+ decisions=_render_decisions(content),
102
+ )
103
+
104
+ return _remove_empty_sections(md)
@@ -0,0 +1,69 @@
1
+ """Story-design artifact schema and governance validators."""
2
+
3
+ from enum import StrEnum
4
+ from typing import Literal
5
+
6
+ from pydantic import BaseModel, Field, model_validator
7
+
8
+ from raise_cli.artifacts.models import ArtifactType, SkillArtifact
9
+
10
+
11
+ class Complexity(StrEnum):
12
+ """Story complexity assessment."""
13
+
14
+ SIMPLE = "simple"
15
+ MODERATE = "moderate"
16
+ COMPLEX = "complex"
17
+
18
+
19
+ class AcceptanceCriterion(BaseModel):
20
+ """A single acceptance criterion."""
21
+
22
+ id: str
23
+ description: str
24
+ verifiable: bool = True
25
+
26
+
27
+ class IntegrationPoint(BaseModel):
28
+ """A codebase integration point affected by the story."""
29
+
30
+ module: str
31
+ change_type: str
32
+ files: list[str] = Field(default_factory=list)
33
+
34
+
35
+ class Decision(BaseModel):
36
+ """A design decision with rationale."""
37
+
38
+ id: str
39
+ choice: str
40
+ rationale: str
41
+ alternatives_considered: list[str] = Field(default_factory=list)
42
+
43
+
44
+ class StoryDesignContent(BaseModel):
45
+ """Typed content for a story-design artifact.
46
+
47
+ Governance rules are encoded as validators.
48
+ """
49
+
50
+ summary: str
51
+ complexity: Complexity
52
+ acceptance_criteria: list[AcceptanceCriterion] = Field(min_length=1, max_length=10)
53
+ integration_points: list[IntegrationPoint] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
54
+ decisions: list[Decision] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
55
+
56
+ @model_validator(mode="after")
57
+ def _validate_decisions_have_rationale(self) -> "StoryDesignContent":
58
+ for decision in self.decisions:
59
+ if not decision.rationale.strip():
60
+ msg = f"Decision {decision.id}: rationale must not be empty"
61
+ raise ValueError(msg)
62
+ return self
63
+
64
+
65
+ class StoryDesignArtifact(SkillArtifact):
66
+ """A story-design artifact with typed content."""
67
+
68
+ artifact_type: Literal[ArtifactType.STORY_DESIGN] = ArtifactType.STORY_DESIGN # pyright: ignore[reportIncompatibleVariableOverride]
69
+ content: StoryDesignContent # pyright: ignore[reportIncompatibleVariableOverride,reportGeneralTypeIssues]
@@ -0,0 +1,45 @@
1
+ """YAML writer for skill artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import yaml
8
+
9
+ from raise_cli.artifacts.models import SkillArtifact
10
+
11
+
12
+ def _artifact_filename(artifact: SkillArtifact) -> str:
13
+ """Derive filename from artifact story/epic and type.
14
+
15
+ Convention: ``{id}-{type_suffix}.yaml``
16
+ where type_suffix strips the first segment (e.g., ``story-design`` → ``design``).
17
+ """
18
+ # Use story if available, else epic
19
+ work_id = artifact.story or artifact.epic or "unknown"
20
+ work_id = work_id.lower()
21
+
22
+ # Strip prefix from artifact type: "story-design" → "design"
23
+ type_suffix = artifact.artifact_type.value.split("-", 1)[-1]
24
+
25
+ return f"{work_id}-{type_suffix}.yaml"
26
+
27
+
28
+ def write_artifact(artifact: SkillArtifact, project_root: Path) -> Path:
29
+ """Serialize artifact to YAML in ``.raise/artifacts/``.
30
+
31
+ Creates the directory if it doesn't exist.
32
+
33
+ Returns:
34
+ Path to the written YAML file.
35
+ """
36
+ artifacts_dir = project_root / ".raise" / "artifacts"
37
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
38
+
39
+ filename = _artifact_filename(artifact)
40
+ path = artifacts_dir / filename
41
+
42
+ data = artifact.model_dump(mode="json")
43
+ path.write_text(yaml.dump(data, default_flow_style=False, sort_keys=False))
44
+
45
+ return path
@@ -0,0 +1 @@
1
+ """Backlog sync and management utilities."""
@@ -0,0 +1,115 @@
1
+ """Backlog sync: query remote adapter, format markdown, write atomically.
2
+
3
+ Generates ``governance/backlog.md`` from a remote PM adapter (e.g., Jira).
4
+ The filesystem adapter is detected and rejected (it IS the source of truth).
5
+
6
+ Architecture: S347.6 (E347 Backlog Automation)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from datetime import UTC, datetime
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING
15
+
16
+ from pydantic import BaseModel, Field
17
+
18
+ from raise_cli.adapters.models import IssueSummary
19
+
20
+ if TYPE_CHECKING:
21
+ from raise_cli.adapters.protocols import ProjectManagementAdapter
22
+
23
+
24
+ class SyncResult(BaseModel):
25
+ """Result of a backlog sync operation."""
26
+
27
+ adapter_name: str = Field(..., description="Name of source adapter")
28
+ epic_count: int = Field(..., description="Number of epics synced")
29
+ timestamp: str = Field(..., description="ISO 8601 sync timestamp")
30
+ output_path: str = Field(..., description="Path to generated file")
31
+
32
+
33
+ def sync_backlog(
34
+ adapter: ProjectManagementAdapter,
35
+ adapter_name: str,
36
+ project_filter: str | None,
37
+ output_path: Path,
38
+ ) -> SyncResult:
39
+ """Query adapter, format markdown, write atomically.
40
+
41
+ Raises:
42
+ ValueError: If adapter is a FilesystemPMAdapter (self-sync).
43
+ RuntimeError: If adapter query fails (file left untouched).
44
+ """
45
+ # Guard: filesystem adapter is source of truth
46
+ from raise_cli.adapters.filesystem import FilesystemPMAdapter
47
+
48
+ if isinstance(adapter, FilesystemPMAdapter):
49
+ raise ValueError("Filesystem adapter is source of truth — nothing to sync.")
50
+
51
+ # Build query
52
+ query = "issuetype = Epic ORDER BY key ASC"
53
+ if project_filter:
54
+ query = f'project = "{project_filter}" AND issuetype = Epic ORDER BY key ASC'
55
+
56
+ # Fetch from adapter — let exceptions propagate (file untouched)
57
+ try:
58
+ results = adapter.search(query, limit=200)
59
+ except Exception as exc:
60
+ raise RuntimeError(f"Adapter '{adapter_name}' failed: {exc}") from exc
61
+
62
+ # Format and write
63
+ timestamp = datetime.now(UTC).isoformat()
64
+ content = _format_markdown(results, adapter_name, timestamp)
65
+ _atomic_write(output_path, content)
66
+
67
+ return SyncResult(
68
+ adapter_name=adapter_name,
69
+ epic_count=len(results),
70
+ timestamp=timestamp,
71
+ output_path=str(output_path),
72
+ )
73
+
74
+
75
+ def _escape_pipe(value: str) -> str:
76
+ """Escape pipe characters in markdown table cell values."""
77
+ return value.replace("|", "\\|")
78
+
79
+
80
+ def _format_markdown(
81
+ epics: list[IssueSummary], adapter_name: str, timestamp: str
82
+ ) -> str:
83
+ """Generate markdown table from IssueSummary list."""
84
+ lines: list[str] = [
85
+ f"<!-- Generated by `rai backlog sync` at {timestamp} from adapter: {adapter_name} -->",
86
+ "<!-- Do not edit manually. Re-run `rai backlog sync` to refresh. -->",
87
+ "",
88
+ "# Backlog",
89
+ "",
90
+ "## Epics Overview",
91
+ "",
92
+ "| Key | Epic | Status | Type |",
93
+ "|-----|------|--------|------|",
94
+ ]
95
+ for epic in epics:
96
+ summary = _escape_pipe(epic.summary)
97
+ status = _escape_pipe(epic.status)
98
+ issue_type = _escape_pipe(epic.issue_type)
99
+ lines.append(f"| {epic.key} | {summary} | {status} | {issue_type} |")
100
+ lines.append("") # trailing newline
101
+ return "\n".join(lines)
102
+
103
+
104
+ def _atomic_write(path: Path, content: str) -> None:
105
+ """Write content to path atomically via temp file + rename.
106
+
107
+ Uses ``str(path) + ".tmp"`` to preserve the original suffix
108
+ (e.g., ``backlog.md.tmp`` not ``backlog.tmp``).
109
+ Uses :func:`os.replace` instead of :meth:`Path.rename` for
110
+ cross-platform atomicity (``rename`` fails on Windows when target exists).
111
+ """
112
+ path.parent.mkdir(parents=True, exist_ok=True)
113
+ tmp = Path(str(path) + ".tmp")
114
+ tmp.write_text(content, encoding="utf-8")
115
+ os.replace(tmp, path)
@@ -0,0 +1,3 @@
1
+ """CLI presentation layer."""
2
+
3
+ from __future__ import annotations
@@ -0,0 +1,3 @@
1
+ """Commands."""
2
+
3
+ from __future__ import annotations