grimoire-kit 3.0.0__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.
- grimoire/__init__.py +7 -0
- grimoire/__version__.py +1 -0
- grimoire/cli/__init__.py +1 -0
- grimoire/cli/app.py +641 -0
- grimoire/cli/cmd_merge.py +37 -0
- grimoire/cli/cmd_upgrade.py +175 -0
- grimoire/core/__init__.py +16 -0
- grimoire/core/config.py +237 -0
- grimoire/core/exceptions.py +87 -0
- grimoire/core/merge.py +230 -0
- grimoire/core/project.py +230 -0
- grimoire/core/resolver.py +68 -0
- grimoire/core/scanner.py +208 -0
- grimoire/core/validator.py +249 -0
- grimoire/mcp/__init__.py +1 -0
- grimoire/mcp/server.py +310 -0
- grimoire/memory/__init__.py +7 -0
- grimoire/memory/backends/__init__.py +0 -0
- grimoire/memory/backends/base.py +84 -0
- grimoire/memory/backends/local.py +148 -0
- grimoire/memory/backends/ollama.py +254 -0
- grimoire/memory/backends/qdrant.py +238 -0
- grimoire/memory/manager.py +162 -0
- grimoire/py.typed +0 -0
- grimoire/registry/__init__.py +1 -0
- grimoire/registry/agents.py +189 -0
- grimoire/registry/local.py +117 -0
- grimoire/tools/__init__.py +19 -0
- grimoire/tools/_common.py +109 -0
- grimoire/tools/agent_forge.py +228 -0
- grimoire/tools/context_guard.py +259 -0
- grimoire/tools/context_router.py +320 -0
- grimoire/tools/harmony_check.py +358 -0
- grimoire/tools/memory_lint.py +468 -0
- grimoire/tools/preflight_check.py +208 -0
- grimoire/tools/stigmergy.py +356 -0
- grimoire_kit-3.0.0.dist-info/METADATA +910 -0
- grimoire_kit-3.0.0.dist-info/RECORD +41 -0
- grimoire_kit-3.0.0.dist-info/WHEEL +4 -0
- grimoire_kit-3.0.0.dist-info/entry_points.txt +3 -0
- grimoire_kit-3.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""``grimoire upgrade`` — migrate a v2 project to v3 structure.
|
|
2
|
+
|
|
3
|
+
Detects a v2 project by the presence of ``project-context.yaml`` without
|
|
4
|
+
v3 markers. Generates the v3 config from the v2 config,
|
|
5
|
+
ensures the v3 directory layout, and preserves all memory files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from grimoire.tools._common import load_yaml, save_yaml
|
|
15
|
+
|
|
16
|
+
# ── Data Models ───────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class UpgradeAction:
|
|
20
|
+
"""A single planned migration action."""
|
|
21
|
+
|
|
22
|
+
kind: str # "create-dir", "generate-file", "move-dir", "skip"
|
|
23
|
+
description: str
|
|
24
|
+
target: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(slots=True)
|
|
28
|
+
class UpgradePlan:
|
|
29
|
+
"""All planned actions for the v2 → v3 migration."""
|
|
30
|
+
|
|
31
|
+
source_version: str = "v2"
|
|
32
|
+
target_version: str = "v3"
|
|
33
|
+
actions: list[UpgradeAction] = field(default_factory=list)
|
|
34
|
+
warnings: list[str] = field(default_factory=list)
|
|
35
|
+
already_v3: bool = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ── Detection ─────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
def detect_version(project_root: Path) -> str:
|
|
41
|
+
"""Return ``"v3"`` if ``project-context.yaml`` contains v3 markers,
|
|
42
|
+
``"v2"`` if it exists but is v2-style, or ``"unknown"``."""
|
|
43
|
+
pctx = project_root / "project-context.yaml"
|
|
44
|
+
if not pctx.exists():
|
|
45
|
+
return "unknown"
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
data = load_yaml(pctx)
|
|
49
|
+
except OSError:
|
|
50
|
+
return "unknown"
|
|
51
|
+
|
|
52
|
+
if not isinstance(data, dict):
|
|
53
|
+
return "unknown"
|
|
54
|
+
|
|
55
|
+
# v3 has top-level 'grimoire' key with 'version'
|
|
56
|
+
if "grimoire" in data and isinstance(data["grimoire"], dict):
|
|
57
|
+
return "v3"
|
|
58
|
+
|
|
59
|
+
# v2 has top-level keys like 'project', 'communication_language', etc.
|
|
60
|
+
if "project" in data or "communication_language" in data:
|
|
61
|
+
return "v2"
|
|
62
|
+
|
|
63
|
+
return "unknown"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ── Planning ──────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
def _extract_v2_config(project_root: Path) -> dict[str, Any]:
|
|
69
|
+
"""Extract usable fields from a v2 project-context.yaml."""
|
|
70
|
+
data = load_yaml(project_root / "project-context.yaml")
|
|
71
|
+
if not isinstance(data, dict):
|
|
72
|
+
return {}
|
|
73
|
+
return data
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def plan_upgrade(project_root: Path) -> UpgradePlan:
|
|
77
|
+
"""Analyze the project and plan the migration."""
|
|
78
|
+
plan = UpgradePlan()
|
|
79
|
+
version = detect_version(project_root)
|
|
80
|
+
|
|
81
|
+
if version == "v3":
|
|
82
|
+
plan.already_v3 = True
|
|
83
|
+
return plan
|
|
84
|
+
|
|
85
|
+
if version == "unknown":
|
|
86
|
+
plan.warnings.append(
|
|
87
|
+
"No project-context.yaml found or unrecognizable format."
|
|
88
|
+
)
|
|
89
|
+
return plan
|
|
90
|
+
|
|
91
|
+
# v2 → v3 migration plan
|
|
92
|
+
_extract_v2_config(project_root) # validate readable
|
|
93
|
+
|
|
94
|
+
# 1. Generate bmad section in project-context.yaml
|
|
95
|
+
plan.actions.append(UpgradeAction(
|
|
96
|
+
kind="generate-file",
|
|
97
|
+
description="Add 'grimoire' section to project-context.yaml (v3 config)",
|
|
98
|
+
target="project-context.yaml",
|
|
99
|
+
))
|
|
100
|
+
|
|
101
|
+
# 2. Ensure v3 directories
|
|
102
|
+
for d in ("_grimoire", "_grimoire/_memory", "_grimoire-output",
|
|
103
|
+
"_grimoire/_config", "_grimoire/_config/agents"):
|
|
104
|
+
dp = project_root / d
|
|
105
|
+
if not dp.is_dir():
|
|
106
|
+
plan.actions.append(UpgradeAction(
|
|
107
|
+
kind="create-dir",
|
|
108
|
+
description=f"Create directory: {d}/",
|
|
109
|
+
target=d,
|
|
110
|
+
))
|
|
111
|
+
|
|
112
|
+
# 3. Warn about orphan files
|
|
113
|
+
old_dirs = ["agents", "tasks", "workflows"]
|
|
114
|
+
for od in old_dirs:
|
|
115
|
+
odp = project_root / od
|
|
116
|
+
if odp.is_dir():
|
|
117
|
+
plan.warnings.append(
|
|
118
|
+
f"Top-level '{od}/' directory exists — may need manual review."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return plan
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ── Execution ─────────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
def _generate_v3_section(v2_data: dict[str, Any]) -> dict[str, Any]:
|
|
127
|
+
"""Build the 'grimoire' section from v2 config data."""
|
|
128
|
+
project_name = v2_data.get("project", "unnamed")
|
|
129
|
+
if isinstance(project_name, dict):
|
|
130
|
+
project_name = project_name.get("name", "unnamed")
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"grimoire": {
|
|
134
|
+
"version": "3.0",
|
|
135
|
+
"migrated_from": "v2",
|
|
136
|
+
},
|
|
137
|
+
"project": {
|
|
138
|
+
"name": project_name,
|
|
139
|
+
},
|
|
140
|
+
"agents": {
|
|
141
|
+
"archetype": "minimal",
|
|
142
|
+
},
|
|
143
|
+
"memory": {
|
|
144
|
+
"backend": "auto",
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def execute_upgrade(project_root: Path, plan: UpgradePlan,
|
|
150
|
+
dry_run: bool = False) -> list[str]:
|
|
151
|
+
"""Execute the upgrade plan. Returns list of completed action descriptions."""
|
|
152
|
+
completed: list[str] = []
|
|
153
|
+
|
|
154
|
+
for action in plan.actions:
|
|
155
|
+
if action.kind == "create-dir":
|
|
156
|
+
dp = project_root / action.target
|
|
157
|
+
if not dry_run:
|
|
158
|
+
dp.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
completed.append(action.description)
|
|
160
|
+
|
|
161
|
+
elif action.kind == "generate-file" and action.target == "project-context.yaml":
|
|
162
|
+
pctx = project_root / "project-context.yaml"
|
|
163
|
+
v2_data = _extract_v2_config(project_root)
|
|
164
|
+
v3_section = _generate_v3_section(v2_data)
|
|
165
|
+
|
|
166
|
+
if not dry_run:
|
|
167
|
+
# Merge v3 section into existing file
|
|
168
|
+
existing = load_yaml(pctx) if pctx.exists() else {}
|
|
169
|
+
if not isinstance(existing, dict):
|
|
170
|
+
existing = {}
|
|
171
|
+
existing.update(v3_section)
|
|
172
|
+
save_yaml(existing, pctx)
|
|
173
|
+
completed.append(action.description)
|
|
174
|
+
|
|
175
|
+
return completed
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Grimoire core — business logic and domain models."""
|
|
2
|
+
|
|
3
|
+
from grimoire.core.config import GrimoireConfig
|
|
4
|
+
from grimoire.core.exceptions import GrimoireError
|
|
5
|
+
from grimoire.core.project import GrimoireProject
|
|
6
|
+
from grimoire.core.resolver import PathResolver
|
|
7
|
+
from grimoire.core.scanner import StackScanner
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"GrimoireConfig",
|
|
11
|
+
"GrimoireError",
|
|
12
|
+
"GrimoireProject",
|
|
13
|
+
"PathResolver",
|
|
14
|
+
"StackScanner",
|
|
15
|
+
]
|
|
16
|
+
|
grimoire/core/config.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Typed configuration for Grimoire projects.
|
|
2
|
+
|
|
3
|
+
Loads and validates ``project-context.yaml`` into typed dataclasses.
|
|
4
|
+
Unknown sections are preserved in ``extra`` so downstream tools can
|
|
5
|
+
access them without requiring schema changes here.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
from grimoire.core.config import GrimoireConfig
|
|
10
|
+
|
|
11
|
+
cfg = GrimoireConfig.from_yaml(Path("project-context.yaml"))
|
|
12
|
+
print(cfg.project.name)
|
|
13
|
+
print(cfg.user.language)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from grimoire.core.exceptions import GrimoireConfigError
|
|
23
|
+
|
|
24
|
+
# ── Sub-sections ──────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class RepoConfig:
|
|
28
|
+
"""A single repository entry."""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
path: str = "."
|
|
32
|
+
default_branch: str = "main"
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_dict(cls, data: dict[str, Any]) -> RepoConfig:
|
|
36
|
+
return cls(
|
|
37
|
+
name=str(data.get("name", "")),
|
|
38
|
+
path=str(data.get("path", ".")),
|
|
39
|
+
default_branch=str(data.get("default_branch", "main")),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class ProjectConfig:
|
|
45
|
+
"""The ``project:`` section."""
|
|
46
|
+
|
|
47
|
+
name: str
|
|
48
|
+
description: str = ""
|
|
49
|
+
type: str = "webapp"
|
|
50
|
+
metaphor: str = ""
|
|
51
|
+
stack: tuple[str, ...] = ()
|
|
52
|
+
repos: tuple[RepoConfig, ...] = ()
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_dict(cls, data: dict[str, Any]) -> ProjectConfig:
|
|
56
|
+
raw_stack = data.get("stack") or []
|
|
57
|
+
raw_repos = data.get("repos") or []
|
|
58
|
+
return cls(
|
|
59
|
+
name=str(data.get("name", "")),
|
|
60
|
+
description=str(data.get("description", "")),
|
|
61
|
+
type=str(data.get("type", "webapp")),
|
|
62
|
+
metaphor=str(data.get("metaphor", "")),
|
|
63
|
+
stack=tuple(str(s) for s in raw_stack),
|
|
64
|
+
repos=tuple(RepoConfig.from_dict(r) for r in raw_repos),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_VALID_SKILL_LEVELS = frozenset({"beginner", "intermediate", "expert"})
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class UserConfig:
|
|
73
|
+
"""The ``user:`` section."""
|
|
74
|
+
|
|
75
|
+
name: str = ""
|
|
76
|
+
language: str = "Français"
|
|
77
|
+
document_language: str = "Français"
|
|
78
|
+
skill_level: str = "intermediate"
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_dict(cls, data: dict[str, Any]) -> UserConfig:
|
|
82
|
+
skill = str(data.get("skill_level", "intermediate"))
|
|
83
|
+
if skill not in _VALID_SKILL_LEVELS:
|
|
84
|
+
raise GrimoireConfigError(
|
|
85
|
+
f"Invalid skill_level '{skill}', expected one of: {sorted(_VALID_SKILL_LEVELS)}"
|
|
86
|
+
)
|
|
87
|
+
return cls(
|
|
88
|
+
name=str(data.get("name", "")),
|
|
89
|
+
language=str(data.get("language", "Français")),
|
|
90
|
+
document_language=str(data.get("document_language", "Français")),
|
|
91
|
+
skill_level=skill,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
_VALID_BACKENDS = frozenset({
|
|
96
|
+
"auto", "local", "qdrant-local", "qdrant-server", "ollama",
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True, slots=True)
|
|
101
|
+
class MemoryConfig:
|
|
102
|
+
"""The ``memory:`` section."""
|
|
103
|
+
|
|
104
|
+
backend: str = "auto"
|
|
105
|
+
collection_prefix: str = "grimoire"
|
|
106
|
+
embedding_model: str = ""
|
|
107
|
+
qdrant_url: str = ""
|
|
108
|
+
ollama_url: str = ""
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_dict(cls, data: dict[str, Any]) -> MemoryConfig:
|
|
112
|
+
backend = str(data.get("backend", "auto"))
|
|
113
|
+
if backend not in _VALID_BACKENDS:
|
|
114
|
+
raise GrimoireConfigError(
|
|
115
|
+
f"Invalid memory backend '{backend}', expected one of: {sorted(_VALID_BACKENDS)}"
|
|
116
|
+
)
|
|
117
|
+
return cls(
|
|
118
|
+
backend=backend,
|
|
119
|
+
collection_prefix=str(data.get("collection_prefix", "grimoire")),
|
|
120
|
+
embedding_model=str(data.get("embedding_model", "")),
|
|
121
|
+
qdrant_url=str(data.get("qdrant_url", "")),
|
|
122
|
+
ollama_url=str(data.get("ollama_url", "")),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True, slots=True)
|
|
127
|
+
class AgentsConfig:
|
|
128
|
+
"""The ``agents:`` section."""
|
|
129
|
+
|
|
130
|
+
archetype: str = "minimal"
|
|
131
|
+
custom_agents: tuple[str, ...] = ()
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def from_dict(cls, data: dict[str, Any]) -> AgentsConfig:
|
|
135
|
+
raw = data.get("custom_agents") or []
|
|
136
|
+
return cls(
|
|
137
|
+
archetype=str(data.get("archetype", "minimal")),
|
|
138
|
+
custom_agents=tuple(str(a) for a in raw),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ── Root Config ───────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
_KNOWN_TOP_KEYS = frozenset({
|
|
145
|
+
"project", "user", "memory", "agents", "installed_archetypes",
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True, slots=True)
|
|
150
|
+
class GrimoireConfig:
|
|
151
|
+
"""Root Grimoire project configuration.
|
|
152
|
+
|
|
153
|
+
Immutable after construction. Unrecognised top-level keys are stored
|
|
154
|
+
in ``extra`` so tools can access them without schema changes.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
project: ProjectConfig
|
|
158
|
+
user: UserConfig = field(default_factory=UserConfig)
|
|
159
|
+
memory: MemoryConfig = field(default_factory=MemoryConfig)
|
|
160
|
+
agents: AgentsConfig = field(default_factory=AgentsConfig)
|
|
161
|
+
installed_archetypes: tuple[str, ...] = ()
|
|
162
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
163
|
+
|
|
164
|
+
# ── Factory methods ───────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def from_dict(cls, data: dict[str, Any]) -> GrimoireConfig:
|
|
168
|
+
"""Build a :class:`GrimoireConfig` from a parsed YAML dict.
|
|
169
|
+
|
|
170
|
+
Raises :class:`GrimoireConfigError` on validation failures.
|
|
171
|
+
"""
|
|
172
|
+
if not isinstance(data, dict):
|
|
173
|
+
raise GrimoireConfigError("Config root must be a YAML mapping")
|
|
174
|
+
|
|
175
|
+
raw_project = data.get("project")
|
|
176
|
+
if not isinstance(raw_project, dict) or not raw_project.get("name"):
|
|
177
|
+
raise GrimoireConfigError("Config must contain a 'project' section with a 'name' field")
|
|
178
|
+
|
|
179
|
+
raw_archetypes = data.get("installed_archetypes") or []
|
|
180
|
+
extra = {k: v for k, v in data.items() if k not in _KNOWN_TOP_KEYS}
|
|
181
|
+
|
|
182
|
+
return cls(
|
|
183
|
+
project=ProjectConfig.from_dict(raw_project),
|
|
184
|
+
user=UserConfig.from_dict(data.get("user") or {}),
|
|
185
|
+
memory=MemoryConfig.from_dict(data.get("memory") or {}),
|
|
186
|
+
agents=AgentsConfig.from_dict(data.get("agents") or {}),
|
|
187
|
+
installed_archetypes=tuple(str(a) for a in raw_archetypes),
|
|
188
|
+
extra=extra,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def from_yaml(cls, path: Path) -> GrimoireConfig:
|
|
193
|
+
"""Load config from a YAML file.
|
|
194
|
+
|
|
195
|
+
Raises :class:`GrimoireConfigError` if the file is missing, unreadable,
|
|
196
|
+
or contains invalid YAML.
|
|
197
|
+
"""
|
|
198
|
+
if not path.is_file():
|
|
199
|
+
raise GrimoireConfigError(f"Config file not found: {path}")
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
from ruamel.yaml import YAML
|
|
203
|
+
|
|
204
|
+
yaml = YAML(typ="safe")
|
|
205
|
+
raw = yaml.load(path)
|
|
206
|
+
except ImportError:
|
|
207
|
+
# Fallback to PyYAML if ruamel not available
|
|
208
|
+
try:
|
|
209
|
+
import yaml as pyyaml # type: ignore[import-untyped]
|
|
210
|
+
|
|
211
|
+
with open(path) as fh:
|
|
212
|
+
raw = pyyaml.safe_load(fh)
|
|
213
|
+
except Exception as exc:
|
|
214
|
+
raise GrimoireConfigError(f"Cannot parse '{path}': {exc}") from exc
|
|
215
|
+
except Exception as exc:
|
|
216
|
+
raise GrimoireConfigError(f"Cannot parse '{path}': {exc}") from exc
|
|
217
|
+
|
|
218
|
+
if raw is None:
|
|
219
|
+
raise GrimoireConfigError(f"Config file is empty: {path}")
|
|
220
|
+
|
|
221
|
+
return cls.from_dict(raw)
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def find_and_load(cls, start: Path | None = None) -> GrimoireConfig:
|
|
225
|
+
"""Walk up the directory tree to find ``project-context.yaml``.
|
|
226
|
+
|
|
227
|
+
Starts from *start* (default: cwd) and searches upward.
|
|
228
|
+
Raises :class:`GrimoireConfigError` if no config file is found.
|
|
229
|
+
"""
|
|
230
|
+
current = (start or Path.cwd()).resolve()
|
|
231
|
+
for parent in [current, *current.parents]:
|
|
232
|
+
candidate = parent / "project-context.yaml"
|
|
233
|
+
if candidate.is_file():
|
|
234
|
+
return cls.from_yaml(candidate)
|
|
235
|
+
raise GrimoireConfigError(
|
|
236
|
+
f"No 'project-context.yaml' found in {current} or any parent directory"
|
|
237
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Typed exception hierarchy for Grimoire Kit.
|
|
2
|
+
|
|
3
|
+
Every exception inherits from :class:`GrimoireError` so callers can
|
|
4
|
+
``except GrimoireError`` to catch all Grimoire-specific failures.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GrimoireError(Exception):
|
|
11
|
+
"""Base exception for all Grimoire Kit errors."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GrimoireConfigError(GrimoireError):
|
|
15
|
+
"""Invalid or missing ``bmad.yaml`` configuration.
|
|
16
|
+
|
|
17
|
+
Raised when the config file cannot be parsed, contains unknown keys,
|
|
18
|
+
or fails schema validation.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GrimoireProjectError(GrimoireError):
|
|
23
|
+
"""Project not initialised or has an invalid structure.
|
|
24
|
+
|
|
25
|
+
Raised when ``grimoire up`` or other project-scoped commands are run
|
|
26
|
+
outside of a properly initialised Grimoire project.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GrimoireAgentError(GrimoireError):
|
|
31
|
+
"""Agent not found, persona invalid, or activation failure.
|
|
32
|
+
|
|
33
|
+
Raised when an agent referenced in ``bmad.yaml`` cannot be loaded
|
|
34
|
+
from built-in archetypes, the registry, or custom paths.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class GrimoireToolError(GrimoireError):
|
|
39
|
+
"""A Grimoire tool failed during execution.
|
|
40
|
+
|
|
41
|
+
Raised when a tool (context-router, harmony-check, etc.) encounters
|
|
42
|
+
a runtime error that prevents it from producing a result.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GrimoireMergeError(GrimoireError):
|
|
47
|
+
"""General merge failure.
|
|
48
|
+
|
|
49
|
+
Raised when ``grimoire merge`` or ``grimoire init`` on an existing project
|
|
50
|
+
encounters an unrecoverable error during file merging.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class GrimoireMergeConflict(GrimoireMergeError): # noqa: N818 — semantic: it's a conflict, not an error
|
|
55
|
+
"""Unresolved merge conflict that requires user intervention.
|
|
56
|
+
|
|
57
|
+
Carries the list of conflicting paths so the caller can present
|
|
58
|
+
them to the user.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, message: str, conflicts: list[str] | None = None) -> None:
|
|
62
|
+
super().__init__(message)
|
|
63
|
+
self.conflicts: list[str] = conflicts or []
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class GrimoireRegistryError(GrimoireError):
|
|
67
|
+
"""Registry operation failure (network, auth, package not found).
|
|
68
|
+
|
|
69
|
+
Raised by the registry client when a search, install, or publish
|
|
70
|
+
operation fails.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class GrimoireMemoryError(GrimoireError):
|
|
75
|
+
"""Memory backend error (connection, serialisation, query failure).
|
|
76
|
+
|
|
77
|
+
Raised when the memory system cannot read, write, or consolidate
|
|
78
|
+
memories due to a backend issue.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class GrimoireValidationError(GrimoireError):
|
|
83
|
+
"""Schema or data validation failure.
|
|
84
|
+
|
|
85
|
+
Raised when DNA files, agent definitions, or other structured data
|
|
86
|
+
fail validation against their expected schema.
|
|
87
|
+
"""
|