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,100 @@
1
+ """Interactive conflict resolution for skill file updates.
2
+
3
+ When both upstream and user have modified a skill file, this module
4
+ provides an interactive prompt (TTY) or safe default (non-TTY) to
5
+ resolve the conflict.
6
+
7
+ Inspired by Rails Thor [Ynaqdhm] pattern, adapted for our use case.
8
+ Default action is KEEP (protect user work).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import difflib
14
+ import sys
15
+ from enum import StrEnum
16
+
17
+
18
+ class ConflictAction(StrEnum):
19
+ """User's chosen action for a conflicting skill file."""
20
+
21
+ KEEP = "keep"
22
+ OVERWRITE = "overwrite"
23
+ DIFF = "diff"
24
+ BACKUP_OVERWRITE = "backup_overwrite"
25
+ KEEP_ALL = "keep_all"
26
+ OVERWRITE_ALL = "overwrite_all"
27
+
28
+
29
+ _INPUT_MAP: dict[str, ConflictAction] = {
30
+ "k": ConflictAction.KEEP,
31
+ "o": ConflictAction.OVERWRITE,
32
+ "d": ConflictAction.DIFF,
33
+ "b": ConflictAction.BACKUP_OVERWRITE,
34
+ "K": ConflictAction.KEEP_ALL,
35
+ "O": ConflictAction.OVERWRITE_ALL,
36
+ "": ConflictAction.KEEP, # Enter = keep (safe default)
37
+ }
38
+
39
+
40
+ def format_skill_diff(skill_name: str, old_content: str, new_content: str) -> str:
41
+ """Format a unified diff between old and new skill content.
42
+
43
+ Args:
44
+ skill_name: Name of the skill for display.
45
+ old_content: Current on-disk content.
46
+ new_content: New upstream content.
47
+
48
+ Returns:
49
+ Formatted unified diff string.
50
+ """
51
+ old_lines = old_content.splitlines(keepends=True)
52
+ new_lines = new_content.splitlines(keepends=True)
53
+ diff = difflib.unified_diff(
54
+ old_lines,
55
+ new_lines,
56
+ fromfile=f"{skill_name}/SKILL.md (yours)",
57
+ tofile=f"{skill_name}/SKILL.md (upstream)",
58
+ )
59
+ return "".join(diff)
60
+
61
+
62
+ def prompt_skill_conflict(
63
+ skill_name: str,
64
+ old_content: str,
65
+ new_content: str,
66
+ ) -> ConflictAction:
67
+ """Prompt user to resolve a skill file conflict.
68
+
69
+ In non-TTY environments, returns KEEP without prompting.
70
+
71
+ Args:
72
+ skill_name: Name of the conflicting skill.
73
+ old_content: Current on-disk content (user's version).
74
+ new_content: New upstream content.
75
+
76
+ Returns:
77
+ The user's chosen ConflictAction.
78
+ """
79
+ if not sys.stdin.isatty():
80
+ return ConflictAction.KEEP
81
+
82
+ print(f"\n {skill_name}/SKILL.md — both upstream and local changes")
83
+
84
+ while True:
85
+ choice = input(
86
+ " [d]iff [o]verwrite [k]eep (default) "
87
+ "[b]ackup+overwrite [O]verwrite-all [K]eep-all: "
88
+ )
89
+
90
+ action = _INPUT_MAP.get(choice)
91
+ if action is None:
92
+ print(f" Invalid choice: '{choice}'. Try again.")
93
+ continue
94
+
95
+ if action == ConflictAction.DIFF:
96
+ diff = format_skill_diff(skill_name, old_content, new_content)
97
+ print(diff if diff else " (no differences)")
98
+ continue # Re-prompt after showing diff
99
+
100
+ return action
@@ -0,0 +1,176 @@
1
+ """Skill manifest for tracking distributed skills and detecting upgrades.
2
+
3
+ The manifest (.raise/manifests/skills.json) stores per-file SHA256 hashes
4
+ of distributed skill files, enabling the dpkg three-hash algorithm for
5
+ safe upgrades: auto-update untouched files, keep customized, prompt on conflict.
6
+
7
+ The manifest also serves as the authoritative registry of RaiSE-managed skills
8
+ for the memory graph — skills not in the manifest are unmanaged.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import json
15
+ import logging
16
+ from datetime import UTC, datetime
17
+ from enum import StrEnum
18
+ from pathlib import Path
19
+
20
+ from pydantic import BaseModel, Field, ValidationError
21
+
22
+ from raise_cli.config.paths import MANIFESTS_SUBDIR, SKILLS_MANIFEST_FILE, get_raise_dir
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class SkillSyncAction(StrEnum):
28
+ """Classification of a skill's sync state using dpkg three-hash model."""
29
+
30
+ CURRENT = "current"
31
+ AUTO_UPDATE = "auto_update"
32
+ KEEP_USER = "keep_user"
33
+ CONFLICT = "conflict"
34
+ NEW = "new"
35
+ LEGACY = "legacy"
36
+
37
+
38
+ class SkillEntry(BaseModel):
39
+ """Metadata for a single distributed skill file.
40
+
41
+ Attributes:
42
+ sha256: SHA256 hex digest of the SKILL.md content as written to disk.
43
+ version: raise-cli version that distributed this skill.
44
+ origin: Who distributed this skill ('framework' or 'org').
45
+ distributed_at: When this skill was last written.
46
+ """
47
+
48
+ sha256: str
49
+ version: str
50
+ origin: str = "framework"
51
+ distributed_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
52
+
53
+
54
+ class SkillManifest(BaseModel):
55
+ """Manifest tracking all distributed skills for upgrade detection.
56
+
57
+ Attributes:
58
+ schema_version: Manifest format version for forward compat.
59
+ raise_cli_version: Which CLI version last wrote this manifest.
60
+ distributed_at: When the manifest was last written.
61
+ skills: Per-skill tracking entries keyed by skill name.
62
+ """
63
+
64
+ schema_version: str = "1.0"
65
+ raise_cli_version: str = Field(default_factory=lambda: _get_cli_version())
66
+ distributed_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
67
+ skill_set: str | None = Field(
68
+ default=None, description="Skill set name last deployed (None = builtins only)"
69
+ )
70
+ skills: dict[str, SkillEntry] = Field(default_factory=dict)
71
+
72
+
73
+ def _get_cli_version() -> str:
74
+ """Get the current raise-cli version."""
75
+ try:
76
+ from raise_cli.skills_base import __version__
77
+
78
+ return __version__
79
+ except ImportError:
80
+ return "unknown"
81
+
82
+
83
+ def compute_content_hash(content: str) -> str:
84
+ """Compute SHA256 hex digest of a string.
85
+
86
+ Args:
87
+ content: The text content to hash.
88
+
89
+ Returns:
90
+ 64-character hex digest string.
91
+ """
92
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()
93
+
94
+
95
+ def classify_skill(
96
+ hash_distributed: str | None,
97
+ hash_on_disk: str,
98
+ hash_new: str,
99
+ ) -> SkillSyncAction:
100
+ """Classify a skill's sync state using the dpkg three-hash algorithm.
101
+
102
+ Args:
103
+ hash_distributed: Hash of what we shipped last time (None if no manifest).
104
+ hash_on_disk: Hash of the file currently on disk.
105
+ hash_new: Hash of the new bundled version.
106
+
107
+ Returns:
108
+ SkillSyncAction indicating what to do with this skill.
109
+ """
110
+ if hash_distributed is None:
111
+ return SkillSyncAction.LEGACY
112
+
113
+ user_changed = hash_distributed != hash_on_disk
114
+ upstream_changed = hash_distributed != hash_new
115
+
116
+ if not user_changed and not upstream_changed:
117
+ return SkillSyncAction.CURRENT
118
+
119
+ if not user_changed and upstream_changed:
120
+ return SkillSyncAction.AUTO_UPDATE
121
+
122
+ if user_changed and not upstream_changed:
123
+ return SkillSyncAction.KEEP_USER
124
+
125
+ # Both changed — but if user's version matches new, they converged
126
+ if hash_on_disk == hash_new:
127
+ return SkillSyncAction.CURRENT
128
+
129
+ return SkillSyncAction.CONFLICT
130
+
131
+
132
+ def save_skill_manifest(manifest: SkillManifest, project_root: Path) -> None:
133
+ """Save skill manifest to .raise/manifests/skills.json.
134
+
135
+ Creates directories if they don't exist.
136
+
137
+ Args:
138
+ manifest: The manifest to save.
139
+ project_root: Root directory of the project.
140
+ """
141
+ manifest_dir = get_raise_dir(project_root) / MANIFESTS_SUBDIR
142
+ manifest_dir.mkdir(parents=True, exist_ok=True)
143
+
144
+ manifest_path = manifest_dir / SKILLS_MANIFEST_FILE
145
+ data = manifest.model_dump(mode="json")
146
+ manifest_path.write_text(
147
+ json.dumps(data, indent=2, default=str),
148
+ encoding="utf-8",
149
+ )
150
+ logger.debug("Saved skill manifest: %s", manifest_path)
151
+
152
+
153
+ def load_skill_manifest(project_root: Path) -> SkillManifest | None:
154
+ """Load skill manifest from .raise/manifests/skills.json.
155
+
156
+ Args:
157
+ project_root: Root directory of the project.
158
+
159
+ Returns:
160
+ SkillManifest if file exists and is valid, None otherwise.
161
+ """
162
+ manifest_path = (
163
+ get_raise_dir(project_root) / MANIFESTS_SUBDIR / SKILLS_MANIFEST_FILE
164
+ )
165
+
166
+ if not manifest_path.exists():
167
+ logger.debug("Skill manifest not found: %s", manifest_path)
168
+ return None
169
+
170
+ try:
171
+ content = manifest_path.read_text(encoding="utf-8")
172
+ data = json.loads(content)
173
+ return SkillManifest.model_validate(data)
174
+ except (json.JSONDecodeError, ValidationError) as e:
175
+ logger.warning("Invalid skill manifest: %s", e)
176
+ return None