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