Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ chrome/extension-src/store-assets/

# Local working artifacts (not for upstream)
/PR-DESCRIPTION.md
/_bmad/render/

# Rust build
odp-rs/target/
Expand Down
17 changes: 17 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@

## 环境准备

### BMad 技能渲染运行时

`_bmad/scripts/render_skill.py`、`config_utils.py` 及许可证恢复自官方
[`bmad-code-org/BMAD-METHOD@beb368e5fc9b95bcec5e1de5bc7870dc15bece72`](https://github.com/bmad-code-org/BMAD-METHOD/tree/beb368e5fc9b95bcec5e1de5bc7870dc15bece72);
脚本保持上游原样,仅恢复这两个脚本和许可证,不代表恢复了完整 BMad 安装。
`_bmad/config.toml` 是最小项目配置,不是找回的个人设置;不要提交私人技能覆盖文件。
在仓库根目录渲染已安装的 build 技能:

```bash
uv run python _bmad/scripts/render_skill.py --project-root . --skill .agents/skills/bmad-build
```

生成的机器相关快照位于 `_bmad/render/`,已忽略,不应提交。此命令只验证技能渲染,
不验证 Docker 镜像构建或容器运行。

### 服务准备

```bash
# 启动 Redis(Celery 模式需要;local 模式可跳过)
docker compose up -d redis
Expand Down
14 changes: 14 additions & 0 deletions _bmad/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Runtime scripts restored unmodified from bmad-code-org/BMAD-METHOD
# revision beb368e5fc9b95bcec5e1de5bc7870dc15bece72, skills/bmad/scripts/.
# This is minimal project configuration, not recovered personal settings.

[core]
project_name = "opencli-Razormind"
output_folder = "{project-root}/_bmad-output"
communication_language = "Chinese"
document_output_language = "Chinese"

[modules.bmm]
planning_artifacts = "{project-root}/_bmad-output/planning-artifacts"
implementation_artifacts = "{project-root}/_bmad-output/implementation-artifacts"
project_knowledge = "{project-root}/docs"
30 changes: 30 additions & 0 deletions _bmad/scripts/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
MIT License

Copyright (c) 2025 BMad Code, LLC

This project incorporates contributions from the open source community.
See [CONTRIBUTORS.md](CONTRIBUTORS.md) for contributor attribution.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

TRADEMARK NOTICE:
BMad™, BMad Method™, and BMad Core™ are trademarks of BMad Code, LLC, covering all
casings and variations (including BMAD, bmad, BMadMethod, BMAD-METHOD, etc.). The use of
these trademarks in this software does not grant any rights to use the trademarks
for any other purpose. See [TRADEMARK.md](TRADEMARK.md) for detailed guidelines.
118 changes: 118 additions & 0 deletions _bmad/scripts/config_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Shared strict TOML loading and structural merge support."""

from __future__ import annotations

import tomllib
from pathlib import Path
from typing import Any, Iterable


class ConfigError(ValueError):
"""Raised when a present configuration layer cannot be used safely."""


_KEYED_MERGE_FIELDS = ("code", "id")


def load_toml(path: Path, *, required: bool = False) -> dict[str, Any]:
"""Load a TOML table, allowing absence only for optional layers."""
if not path.exists():
if required:
raise ConfigError(f"required TOML file not found: {path}")
return {}
if not path.is_file():
raise ConfigError(f"TOML layer is not a file: {path}")
try:
with path.open("rb") as stream:
parsed = tomllib.load(stream)
except tomllib.TOMLDecodeError as error:
raise ConfigError(f"failed to parse {path}: {error}") from error
except OSError as error:
raise ConfigError(f"failed to read {path}: {error}") from error
if not isinstance(parsed, dict):
raise ConfigError(f"TOML layer did not parse to a table: {path}")
return parsed


def _detect_keyed_merge_field(items: list[Any]) -> str | None:
if not items or not all(isinstance(item, dict) for item in items):
return None
for candidate in _KEYED_MERGE_FIELDS:
if all(candidate in item for item in items):
for item in items:
value = item[candidate]
if not isinstance(value, str):
raise ConfigError(
f"keyed array identifier `{candidate}` must be a string, "
f"got {type(value).__name__}"
)
if not value:
raise ConfigError(
f"keyed array identifier `{candidate}` must not be empty"
)
return candidate
return None


def _merge_arrays(base: list[Any], override: list[Any]) -> list[Any]:
keyed_field = _detect_keyed_merge_field(base + override)
if keyed_field is None:
return list(base) + list(override)

result: list[Any] = []
index_by_key: dict[str, int] = {}
for item in base:
copied = dict(item)
index_by_key[copied[keyed_field]] = len(result)
result.append(copied)
for item in override:
copied = dict(item)
key = copied[keyed_field]
if key in index_by_key:
result[index_by_key[key]] = copied
else:
index_by_key[key] = len(result)
result.append(copied)
return result


def structural_merge(base: Any, override: Any) -> Any:
"""Merge tables recursively, keyed table arrays by identity, and append other arrays."""
if isinstance(base, dict) and isinstance(override, dict):
result = dict(base)
for key, value in override.items():
result[key] = structural_merge(result[key], value) if key in result else value
return result
if isinstance(base, list) and isinstance(override, list):
return _merge_arrays(base, override)
return override


def merge_layers(layers: Iterable[dict[str, Any]]) -> dict[str, Any]:
merged: dict[str, Any] = {}
for layer in layers:
merged = structural_merge(merged, layer)
return merged


def load_central_config(project_root: Path) -> dict[str, Any]:
bmad_dir = project_root / "_bmad"
return merge_layers(
(
load_toml(bmad_dir / "config.toml", required=True),
load_toml(bmad_dir / "custom" / "config.toml"),
load_toml(bmad_dir / "custom" / "config.user.toml"),
)
)


def load_customization(project_root: Path | None, skill_dir: Path) -> dict[str, Any]:
skill_name = skill_dir.name
custom_dir = project_root / "_bmad" / "custom" if project_root else None
return merge_layers(
(
load_toml(skill_dir / "customize.toml", required=True),
load_toml(custom_dir / f"{skill_name}.toml") if custom_dir else {},
load_toml(custom_dir / f"{skill_name}.user.toml") if custom_dir else {},
)
)
Loading
Loading