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,212 @@
|
|
|
1
|
+
"""Skill scaffolding for creating new skills.
|
|
2
|
+
|
|
3
|
+
Generates new skill directories with properly structured SKILL.md files
|
|
4
|
+
following the RaiSE skill template.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from textwrap import dedent
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from raise_cli.skills.locator import get_default_skill_dir
|
|
14
|
+
|
|
15
|
+
# Mapping from domain prefix to lifecycle
|
|
16
|
+
DOMAIN_TO_LIFECYCLE = {
|
|
17
|
+
"session": "session",
|
|
18
|
+
"epic": "epic",
|
|
19
|
+
"story": "story",
|
|
20
|
+
"discover": "discovery",
|
|
21
|
+
"skill": "meta",
|
|
22
|
+
"research": "utility",
|
|
23
|
+
"debug": "utility",
|
|
24
|
+
"framework": "meta",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ScaffoldResult(BaseModel):
|
|
29
|
+
"""Result of scaffolding a skill."""
|
|
30
|
+
|
|
31
|
+
created: bool = Field(description="Whether the skill was created")
|
|
32
|
+
path: str | None = Field(default=None, description="Path to created skill")
|
|
33
|
+
error: str | None = Field(default=None, description="Error message if failed")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _infer_lifecycle(name: str) -> str:
|
|
37
|
+
"""Infer lifecycle from skill name domain."""
|
|
38
|
+
parts = name.split("-")
|
|
39
|
+
if parts:
|
|
40
|
+
domain = parts[0]
|
|
41
|
+
return DOMAIN_TO_LIFECYCLE.get(domain, "utility")
|
|
42
|
+
return "utility"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _generate_skill_content(
|
|
46
|
+
name: str,
|
|
47
|
+
lifecycle: str,
|
|
48
|
+
prerequisites: str | None,
|
|
49
|
+
next_skill: str | None,
|
|
50
|
+
) -> str:
|
|
51
|
+
"""Generate SKILL.md content from template."""
|
|
52
|
+
# Extract title from name (e.g., story-validate -> Story Validate)
|
|
53
|
+
title = " ".join(word.capitalize() for word in name.split("-"))
|
|
54
|
+
|
|
55
|
+
# Build prerequisites and next strings
|
|
56
|
+
prereq_str = prerequisites or ""
|
|
57
|
+
next_str = next_skill or ""
|
|
58
|
+
|
|
59
|
+
# Determine frequency based on lifecycle
|
|
60
|
+
frequency_map = {
|
|
61
|
+
"session": "per-session",
|
|
62
|
+
"epic": "per-epic",
|
|
63
|
+
"story": "per-story",
|
|
64
|
+
"discovery": "per-project",
|
|
65
|
+
"utility": "on-demand",
|
|
66
|
+
"meta": "on-demand",
|
|
67
|
+
}
|
|
68
|
+
frequency = frequency_map.get(lifecycle, "on-demand")
|
|
69
|
+
|
|
70
|
+
content = dedent(f"""\
|
|
71
|
+
---
|
|
72
|
+
name: {name}
|
|
73
|
+
description: >
|
|
74
|
+
[TODO: Add description of what this skill does]
|
|
75
|
+
|
|
76
|
+
license: MIT
|
|
77
|
+
|
|
78
|
+
metadata:
|
|
79
|
+
raise.work_cycle: {lifecycle}
|
|
80
|
+
raise.frequency: {frequency}
|
|
81
|
+
raise.fase: ""
|
|
82
|
+
raise.prerequisites: "{prereq_str}"
|
|
83
|
+
raise.next: "{next_str}"
|
|
84
|
+
raise.gate: ""
|
|
85
|
+
raise.adaptable: "true"
|
|
86
|
+
raise.version: "1.0.0"
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
# {title}
|
|
90
|
+
|
|
91
|
+
## Purpose
|
|
92
|
+
|
|
93
|
+
[TODO: Describe the purpose of this skill]
|
|
94
|
+
|
|
95
|
+
## Mastery Levels (ShuHaRi)
|
|
96
|
+
|
|
97
|
+
- **Shu**: [TODO: Beginner behavior]
|
|
98
|
+
- **Ha**: [TODO: Intermediate behavior]
|
|
99
|
+
- **Ri**: [TODO: Expert behavior]
|
|
100
|
+
|
|
101
|
+
## Context
|
|
102
|
+
|
|
103
|
+
**When to use:** [TODO: Add trigger conditions]
|
|
104
|
+
|
|
105
|
+
**When to skip:** [TODO: Add skip conditions]
|
|
106
|
+
|
|
107
|
+
**Inputs:** [TODO: Add required inputs]
|
|
108
|
+
|
|
109
|
+
## Steps
|
|
110
|
+
|
|
111
|
+
### Step 1: [TODO: Step Name]
|
|
112
|
+
|
|
113
|
+
[TODO: Describe what to do in this step]
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Example command
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Verification:** [TODO: How to verify this step succeeded]
|
|
120
|
+
|
|
121
|
+
## Output
|
|
122
|
+
|
|
123
|
+
| Item | Destination |
|
|
124
|
+
|------|-------------|
|
|
125
|
+
| [TODO] | [TODO] |
|
|
126
|
+
|
|
127
|
+
## Quality Checklist
|
|
128
|
+
|
|
129
|
+
- [ ] [TODO: Add verification items]
|
|
130
|
+
|
|
131
|
+
## References
|
|
132
|
+
|
|
133
|
+
- Previous: `/{prereq_str if prereq_str else "[none]"}`
|
|
134
|
+
- Next: `/{next_str if next_str else "[none]"}`
|
|
135
|
+
""")
|
|
136
|
+
|
|
137
|
+
return content
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def scaffold_skill(
|
|
141
|
+
name: str,
|
|
142
|
+
lifecycle: str | None = None,
|
|
143
|
+
after: str | None = None,
|
|
144
|
+
before: str | None = None,
|
|
145
|
+
skill_set: str | None = None,
|
|
146
|
+
from_builtin: bool = False,
|
|
147
|
+
) -> ScaffoldResult:
|
|
148
|
+
"""Scaffold a new skill with proper structure.
|
|
149
|
+
|
|
150
|
+
When ``skill_set`` is provided, creates the skill in
|
|
151
|
+
``.raise/skills/{skill_set}/{name}/`` instead of the default
|
|
152
|
+
IDE skill directory. When ``from_builtin`` is also True, copies
|
|
153
|
+
the existing deployed skill as a starting point. (S340.2)
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
name: Skill name (e.g., 'story-validate').
|
|
157
|
+
lifecycle: Lifecycle category. If not specified, inferred from name.
|
|
158
|
+
after: Skill that should come before this one (prerequisites).
|
|
159
|
+
before: Skill that should come after this one (next).
|
|
160
|
+
skill_set: Skill set name (e.g., 'my-team'). Creates in .raise/skills/{set}/.
|
|
161
|
+
from_builtin: Copy from deployed .claude/skills/{name}/ as starting point.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
ScaffoldResult with creation status and path or error.
|
|
165
|
+
"""
|
|
166
|
+
from pathlib import Path
|
|
167
|
+
|
|
168
|
+
# Determine target directory
|
|
169
|
+
if skill_set is not None:
|
|
170
|
+
skill_dir = Path.cwd() / ".raise" / "skills" / skill_set
|
|
171
|
+
else:
|
|
172
|
+
skill_dir = get_default_skill_dir()
|
|
173
|
+
if not skill_dir.exists():
|
|
174
|
+
skill_dir.mkdir(parents=True)
|
|
175
|
+
|
|
176
|
+
# Check if skill already exists in target
|
|
177
|
+
skill_path = skill_dir / name
|
|
178
|
+
if skill_path.exists():
|
|
179
|
+
return ScaffoldResult(
|
|
180
|
+
created=False,
|
|
181
|
+
error=f"Skill '{name}' already exists at {skill_path}",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# "Customize builtin" mode: copy from deployed skill
|
|
185
|
+
if from_builtin and skill_set is not None:
|
|
186
|
+
deployed = get_default_skill_dir() / name / "SKILL.md"
|
|
187
|
+
if deployed.exists():
|
|
188
|
+
skill_path.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
skill_file = skill_path / "SKILL.md"
|
|
190
|
+
skill_file.write_text(deployed.read_text(encoding="utf-8"), encoding="utf-8")
|
|
191
|
+
return ScaffoldResult(created=True, path=str(skill_file))
|
|
192
|
+
return ScaffoldResult(
|
|
193
|
+
created=False,
|
|
194
|
+
error=f"Builtin '{name}' not found at {deployed}",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Infer lifecycle if not specified
|
|
198
|
+
if lifecycle is None:
|
|
199
|
+
lifecycle = _infer_lifecycle(name)
|
|
200
|
+
|
|
201
|
+
# Generate content
|
|
202
|
+
content = _generate_skill_content(name, lifecycle, after, before)
|
|
203
|
+
|
|
204
|
+
# Create skill directory and file
|
|
205
|
+
skill_path.mkdir(parents=True, exist_ok=True)
|
|
206
|
+
skill_file = skill_path / "SKILL.md"
|
|
207
|
+
skill_file.write_text(content, encoding="utf-8")
|
|
208
|
+
|
|
209
|
+
return ScaffoldResult(
|
|
210
|
+
created=True,
|
|
211
|
+
path=str(skill_file),
|
|
212
|
+
)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Pydantic models for SKILL.md frontmatter and structure.
|
|
2
|
+
|
|
3
|
+
These models define the schema for RaiSE skills, enabling:
|
|
4
|
+
- Parsing of SKILL.md YAML frontmatter
|
|
5
|
+
- Validation of skill structure
|
|
6
|
+
- Type-safe access to skill metadata
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SkillMetadata(BaseModel):
|
|
17
|
+
"""Metadata for a RaiSE skill.
|
|
18
|
+
|
|
19
|
+
Maps from YAML frontmatter with 'raise.' prefix to clean attributes.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
work_cycle: str = Field(
|
|
23
|
+
description="Lifecycle: session, epic, story, discovery, utility, meta"
|
|
24
|
+
)
|
|
25
|
+
version: str = Field(description="Semantic version of the skill")
|
|
26
|
+
frequency: str | None = Field(
|
|
27
|
+
default=None, description="How often invoked: per-session, per-epic, etc."
|
|
28
|
+
)
|
|
29
|
+
fase: str | None = Field(default=None, description="Phase number or 'meta'")
|
|
30
|
+
prerequisites: str | None = Field(
|
|
31
|
+
default=None, description="Skills that must run before this one"
|
|
32
|
+
)
|
|
33
|
+
next: str | None = Field(
|
|
34
|
+
default=None, description="Skill that typically follows this one"
|
|
35
|
+
)
|
|
36
|
+
gate: str | None = Field(default=None, description="Validation gate for this skill")
|
|
37
|
+
adaptable: bool = Field(
|
|
38
|
+
default=True, description="Whether skill can be adapted by mastery level"
|
|
39
|
+
)
|
|
40
|
+
output_type: str | None = Field(
|
|
41
|
+
default=None, description="Artifact type this skill produces (e.g., story-design)"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_raw(cls, raw: dict[str, Any]) -> SkillMetadata:
|
|
46
|
+
"""Parse metadata from raw YAML dict with 'raise.' prefix.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
raw: Dictionary with keys like 'raise.work_cycle', 'raise.version', etc.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
SkillMetadata instance with cleaned attributes.
|
|
53
|
+
"""
|
|
54
|
+
# Strip 'raise.' prefix and convert to clean dict
|
|
55
|
+
cleaned: dict[str, Any] = {}
|
|
56
|
+
for key, value in raw.items():
|
|
57
|
+
if key.startswith("raise."):
|
|
58
|
+
clean_key = key[6:] # Remove 'raise.' prefix
|
|
59
|
+
# Handle boolean conversion
|
|
60
|
+
if clean_key == "adaptable" and isinstance(value, str):
|
|
61
|
+
value = value.lower() == "true"
|
|
62
|
+
cleaned[clean_key] = value
|
|
63
|
+
|
|
64
|
+
return cls(**cleaned)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SkillHookCommand(BaseModel):
|
|
68
|
+
"""A single hook command in a skill."""
|
|
69
|
+
|
|
70
|
+
type: str = Field(description="Hook type: 'command'")
|
|
71
|
+
command: str = Field(description="Shell command to execute")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SkillHook(BaseModel):
|
|
75
|
+
"""A hook configuration with nested commands."""
|
|
76
|
+
|
|
77
|
+
hooks: list[SkillHookCommand] = Field(
|
|
78
|
+
default_factory=lambda: [], description="List of hook commands"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SkillFrontmatter(BaseModel):
|
|
83
|
+
"""YAML frontmatter for a SKILL.md file.
|
|
84
|
+
|
|
85
|
+
This is the structured data at the top of each skill file,
|
|
86
|
+
containing name, description, metadata, and hooks.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
name: str = Field(description="Skill name in {domain}-{action} format")
|
|
90
|
+
description: str = Field(description="Brief description of the skill")
|
|
91
|
+
license: str | None = Field(default=None, description="License (typically MIT)")
|
|
92
|
+
metadata: SkillMetadata | None = Field(
|
|
93
|
+
default=None, description="RaiSE-specific metadata"
|
|
94
|
+
)
|
|
95
|
+
hooks: dict[str, list[SkillHook]] | None = Field(
|
|
96
|
+
default=None, description="Claude Code hooks (e.g., Stop)"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Skill(BaseModel):
|
|
101
|
+
"""A complete RaiSE skill with frontmatter and markdown body.
|
|
102
|
+
|
|
103
|
+
Represents a parsed SKILL.md file with all its components.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
frontmatter: SkillFrontmatter = Field(description="Parsed YAML frontmatter")
|
|
107
|
+
body: str = Field(description="Markdown content after frontmatter")
|
|
108
|
+
path: str = Field(description="Path to the SKILL.md file")
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def name(self) -> str:
|
|
112
|
+
"""Shortcut to skill name."""
|
|
113
|
+
return self.frontmatter.name
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def version(self) -> str | None:
|
|
117
|
+
"""Skill version from metadata, or None if no metadata."""
|
|
118
|
+
if self.frontmatter.metadata:
|
|
119
|
+
return self.frontmatter.metadata.version
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def lifecycle(self) -> str | None:
|
|
124
|
+
"""Skill lifecycle from metadata work_cycle."""
|
|
125
|
+
if self.frontmatter.metadata:
|
|
126
|
+
return self.frontmatter.metadata.work_cycle
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def description(self) -> str:
|
|
131
|
+
"""Shortcut to skill description."""
|
|
132
|
+
return self.frontmatter.description
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Skill set management — create, list, and diff skill sets.
|
|
2
|
+
|
|
3
|
+
Skill sets live in ``.raise/skills/{name}/`` and contain skill directories
|
|
4
|
+
with SKILL.md files. They overlay builtins when deployed with
|
|
5
|
+
``rai init --skill-set``. (S340.4, RAISE-344)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from importlib.resources import files
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from raise_cli.config.paths import get_raise_dir
|
|
17
|
+
from raise_cli.onboarding.skill_manifest import compute_content_hash
|
|
18
|
+
from raise_cli.onboarding.skills import copy_skill_tree
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CreateResult(BaseModel):
|
|
24
|
+
"""Result of skill set creation."""
|
|
25
|
+
|
|
26
|
+
created: bool
|
|
27
|
+
name: str
|
|
28
|
+
path: str | None = None
|
|
29
|
+
skill_count: int = 0
|
|
30
|
+
error: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SkillSetInfo(BaseModel):
|
|
34
|
+
"""Info about an existing skill set."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
path: str
|
|
38
|
+
skill_count: int = 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SkillSetDiff(BaseModel):
|
|
42
|
+
"""Diff of a skill set against builtins."""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
added: list[str] = Field(default_factory=list)
|
|
46
|
+
modified: list[str] = Field(default_factory=list)
|
|
47
|
+
unchanged: list[str] = Field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _skills_dir(project_root: Path) -> Path:
|
|
51
|
+
"""Get the .raise/skills/ directory."""
|
|
52
|
+
return get_raise_dir(project_root) / "skills"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _count_skills(set_dir: Path) -> int:
|
|
56
|
+
"""Count skill directories containing SKILL.md."""
|
|
57
|
+
if not set_dir.is_dir():
|
|
58
|
+
return 0
|
|
59
|
+
return sum(
|
|
60
|
+
1 for d in set_dir.iterdir()
|
|
61
|
+
if d.is_dir() and (d / "SKILL.md").exists()
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def create_skill_set(
|
|
66
|
+
name: str,
|
|
67
|
+
project_root: Path,
|
|
68
|
+
*,
|
|
69
|
+
empty: bool = False,
|
|
70
|
+
) -> CreateResult:
|
|
71
|
+
"""Create a new skill set directory, optionally from builtins.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
name: Skill set name (e.g., "my-team").
|
|
75
|
+
project_root: Project root directory.
|
|
76
|
+
empty: If True, create empty directory. Otherwise copy all builtins.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
CreateResult with creation status.
|
|
80
|
+
"""
|
|
81
|
+
from raise_cli.onboarding.skills import SkillScaffoldResult
|
|
82
|
+
from raise_cli.skills_base import DISTRIBUTABLE_SKILLS
|
|
83
|
+
|
|
84
|
+
set_dir = _skills_dir(project_root) / name
|
|
85
|
+
|
|
86
|
+
if set_dir.exists():
|
|
87
|
+
return CreateResult(
|
|
88
|
+
created=False,
|
|
89
|
+
name=name,
|
|
90
|
+
error=f"Skill set '{name}' already exists at {set_dir}",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
set_dir.mkdir(parents=True)
|
|
94
|
+
|
|
95
|
+
if empty:
|
|
96
|
+
return CreateResult(created=True, name=name, path=str(set_dir), skill_count=0)
|
|
97
|
+
|
|
98
|
+
# Copy builtins
|
|
99
|
+
base = files("raise_cli.skills_base")
|
|
100
|
+
result = SkillScaffoldResult()
|
|
101
|
+
for skill_name in DISTRIBUTABLE_SKILLS:
|
|
102
|
+
source = base / skill_name
|
|
103
|
+
dest = set_dir / skill_name
|
|
104
|
+
copy_skill_tree(source, dest, result, overwrite=True)
|
|
105
|
+
|
|
106
|
+
return CreateResult(
|
|
107
|
+
created=True,
|
|
108
|
+
name=name,
|
|
109
|
+
path=str(set_dir),
|
|
110
|
+
skill_count=len(DISTRIBUTABLE_SKILLS),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def list_skill_sets(project_root: Path) -> list[SkillSetInfo]:
|
|
115
|
+
"""List all skill sets in .raise/skills/.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
project_root: Project root directory.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
List of SkillSetInfo, sorted by name.
|
|
122
|
+
"""
|
|
123
|
+
skills_root = _skills_dir(project_root)
|
|
124
|
+
if not skills_root.is_dir():
|
|
125
|
+
return []
|
|
126
|
+
|
|
127
|
+
sets: list[SkillSetInfo] = []
|
|
128
|
+
for item in sorted(skills_root.iterdir()):
|
|
129
|
+
if not item.is_dir():
|
|
130
|
+
continue
|
|
131
|
+
sets.append(SkillSetInfo(
|
|
132
|
+
name=item.name,
|
|
133
|
+
path=str(item),
|
|
134
|
+
skill_count=_count_skills(item),
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
return sets
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def diff_skill_set(
|
|
141
|
+
name: str,
|
|
142
|
+
project_root: Path,
|
|
143
|
+
) -> SkillSetDiff | None:
|
|
144
|
+
"""Compare a skill set against builtins.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
name: Skill set name.
|
|
148
|
+
project_root: Project root directory.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
SkillSetDiff with added/modified/unchanged, or None if set doesn't exist.
|
|
152
|
+
"""
|
|
153
|
+
from raise_cli.skills_base import DISTRIBUTABLE_SKILLS
|
|
154
|
+
|
|
155
|
+
set_dir = _skills_dir(project_root) / name
|
|
156
|
+
if not set_dir.is_dir():
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
base = files("raise_cli.skills_base")
|
|
160
|
+
builtin_names = set(DISTRIBUTABLE_SKILLS)
|
|
161
|
+
|
|
162
|
+
added: list[str] = []
|
|
163
|
+
modified: list[str] = []
|
|
164
|
+
unchanged: list[str] = []
|
|
165
|
+
|
|
166
|
+
# Check each skill in the set
|
|
167
|
+
for skill_dir in sorted(set_dir.iterdir()):
|
|
168
|
+
if not skill_dir.is_dir():
|
|
169
|
+
continue
|
|
170
|
+
skill_md = skill_dir / "SKILL.md"
|
|
171
|
+
if not skill_md.exists():
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
skill_name = skill_dir.name
|
|
175
|
+
|
|
176
|
+
if skill_name not in builtin_names:
|
|
177
|
+
added.append(skill_name)
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
# Compare against builtin
|
|
181
|
+
set_hash = compute_content_hash(skill_md.read_text(encoding="utf-8"))
|
|
182
|
+
builtin_content = (base / skill_name / "SKILL.md").read_text(encoding="utf-8")
|
|
183
|
+
builtin_hash = compute_content_hash(builtin_content)
|
|
184
|
+
|
|
185
|
+
if set_hash == builtin_hash:
|
|
186
|
+
unchanged.append(skill_name)
|
|
187
|
+
else:
|
|
188
|
+
modified.append(skill_name)
|
|
189
|
+
|
|
190
|
+
return SkillSetDiff(
|
|
191
|
+
name=name,
|
|
192
|
+
added=added,
|
|
193
|
+
modified=modified,
|
|
194
|
+
unchanged=unchanged,
|
|
195
|
+
)
|