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
6 changes: 4 additions & 2 deletions samples/bmad-agent-dream-weaver/assets/module-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Registers this standalone module into a project. Module identity (name, code, ve

Both config scripts use an anti-zombie pattern — existing entries for this module are removed before writing fresh ones, so stale values never persist.

`{project-root}` is a **literal token** in config values — never substitute it with an actual path. It signals to the consuming LLM that the value is relative to the project root, not the skill root.
`{project-root}` is a **literal token** in config _values_ (the data written into the files above) — never substitute it there. It signals to the consuming LLM that the value is relative to the project root, not the skill root. **This does not apply to the filesystem path _arguments_ passed to the scripts below** (the `--*-path`, `--*-dir`, and `--target` arguments): those are real paths, so you **must** resolve `{project-root}` to the actual project root before running, or the scripts will write to a literal `{project-root}/` directory under the skill folder. The scripts reject an unresolved token with an error.

## Check Existing Config

Expand Down Expand Up @@ -48,7 +48,9 @@ Ask using the prompt with its default value. Apply `result` templates when stori

## Write Files

Write a temp JSON file with the collected answers structured as `{"core": {...}, "module": {...}}` (omit `core` if it already exists). Then run both scripts — they can run in parallel since they write to different files:
Write a temp JSON file with the collected answers structured as `{"core": {...}, "module": {...}}` (omit `core` if it already exists). Values inside this JSON keep the literal `{project-root}` token. Then run both scripts — they can run in parallel since they write to different files.

In the commands below, replace `{project-root}` in every path argument with the actual project root (e.g. `/home/me/myapp`) before running — these are filesystem paths, not config values.

```bash
python3 ./scripts/merge-config.py --config-path "{project-root}/_bmad/config.yaml" --user-config-path "{project-root}/_bmad/config.user.yaml" --module-yaml ./assets/module.yaml --answers {temp-file}
Expand Down
33 changes: 33 additions & 0 deletions samples/bmad-agent-dream-weaver/scripts/merge-config.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,42 @@ def write_config(config: dict, config_path: str, verbose: bool = False) -> None:
)


def reject_unresolved_paths(named_paths: list[tuple[str, str]]) -> None:
"""Exit with a clear error if any path argument still contains the literal
``{project-root}`` token. That token is meaningful only inside config
values; filesystem path arguments must be resolved by the caller. Failing
loudly here prevents silently creating a junk ``{project-root}/`` directory.
"""
for name, value in named_paths:
if value and "{project-root}" in value:
print(
json.dumps(
{
"status": "error",
"error": (
f"Unresolved '{{project-root}}' token in {name} path: {value!r}. "
"Resolve '{project-root}' to the actual project root before running "
"this script — it is a filesystem path, not a config value."
),
},
indent=2,
),
file=sys.stderr,
)
sys.exit(1)


def main():
args = parse_args()

reject_unresolved_paths(
[
("--config-path", args.config_path),
("--user-config-path", args.user_config_path),
("--legacy-dir", args.legacy_dir),
]
)

# Load inputs
module_yaml = load_yaml_file(args.module_yaml)
if not module_yaml:
Expand Down
28 changes: 28 additions & 0 deletions samples/bmad-agent-dream-weaver/scripts/merge-help-csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,37 @@ def cleanup_legacy_csvs(
return deleted


def reject_unresolved_paths(named_paths: list[tuple[str, str]]) -> None:
"""Exit with a clear error if any path argument still contains the literal
``{project-root}`` token. That token is meaningful only inside config
values; filesystem path arguments must be resolved by the caller. Failing
loudly here prevents silently creating a junk ``{project-root}/`` directory.
"""
for name, value in named_paths:
if value and "{project-root}" in value:
print(
json.dumps(
{
"status": "error",
"error": (
f"Unresolved '{{project-root}}' token in {name} path: {value!r}. "
"Resolve '{project-root}' to the actual project root before running "
"this script — it is a filesystem path, not a config value."
),
},
indent=2,
)
)
sys.exit(1)


def main():
args = parse_args()

reject_unresolved_paths(
[("--target", args.target), ("--legacy-dir", args.legacy_dir)]
)

# Read source entries
source_header, source_rows = read_csv_rows(args.source)
if not source_rows:
Expand Down
8 changes: 6 additions & 2 deletions samples/sample-module-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Installs and configures a BMad module into a project. Module identity (name, cod

Both config scripts use an anti-zombie pattern — existing entries for this module are removed before writing fresh ones, so stale values never persist.

`{project-root}` is a **literal token** in config values — never substitute it with an actual path. It signals to the consuming LLM that the value is relative to the project root, not the skill root.
`{project-root}` is a **literal token** in config _values_ (the data written into the files above) — never substitute it there. It signals to the consuming LLM that the value is relative to the project root, not the skill root. **This does not apply to the filesystem path _arguments_ passed to the scripts below** (the `--*-path`, `--*-dir`, and `--target` arguments): those are real paths, so you **must** resolve `{project-root}` to the actual project root before running, or the scripts will write to a literal `{project-root}/` directory under the skill folder. The scripts reject an unresolved token with an error.

## On Activation

Expand All @@ -40,7 +40,9 @@ Ask the user for values. Show defaults in brackets. Present all values together

## Write Files

Write a temp JSON file with the collected answers structured as `{"core": {...}, "module": {...}}` (omit `core` if it already exists). Then run both scripts — they can run in parallel since they write to different files:
Write a temp JSON file with the collected answers structured as `{"core": {...}, "module": {...}}` (omit `core` if it already exists). Values inside this JSON keep the literal `{project-root}` token. Then run both scripts — they can run in parallel since they write to different files.

In the commands below, replace `{project-root}` in every path argument with the actual project root (e.g. `/home/me/myapp`) before running — these are filesystem paths, not config values.

```bash
python3 ./scripts/merge-config.py --config-path "{project-root}/_bmad/config.yaml" --user-config-path "{project-root}/_bmad/config.user.yaml" --module-yaml ./assets/module.yaml --answers {temp-file} --legacy-dir "{project-root}/_bmad"
Expand All @@ -59,6 +61,8 @@ After writing config, create any output directories that were configured. For fi

After both merge scripts complete successfully, remove the installer's package directories. Skills and agents in these directories are already installed at `.claude/skills/` — the `_bmad/` directory should only contain config files.

As with the merge scripts, replace `{project-root}` in the `--bmad-dir` and `--skills-dir` path arguments with the actual project root before running.

```bash
python3 ./scripts/cleanup-legacy.py --bmad-dir "{project-root}/_bmad" --module-code sam --also-remove _config --skills-dir "{project-root}/.claude/skills"
```
Expand Down
28 changes: 28 additions & 0 deletions samples/sample-module-setup/scripts/cleanup-legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,37 @@ def cleanup_directories(
return removed, not_found, total_files


def reject_unresolved_paths(named_paths: list[tuple[str, str]]) -> None:
"""Exit with a clear error if any path argument still contains the literal
``{project-root}`` token. That token is meaningful only inside config
values; filesystem path arguments must be resolved by the caller. Failing
loudly here prevents silently operating on a junk ``{project-root}/`` directory.
"""
for name, value in named_paths:
if value and "{project-root}" in value:
print(
json.dumps(
{
"status": "error",
"error": (
f"Unresolved '{{project-root}}' token in {name} path: {value!r}. "
"Resolve '{project-root}' to the actual project root before running "
"this script — it is a filesystem path, not a config value."
),
},
indent=2,
)
)
sys.exit(1)


def main():
args = parse_args()

reject_unresolved_paths(
[("--bmad-dir", args.bmad_dir), ("--skills-dir", args.skills_dir)]
)

bmad_dir = args.bmad_dir
module_code = args.module_code

Expand Down
33 changes: 33 additions & 0 deletions samples/sample-module-setup/scripts/merge-config.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,42 @@ def write_config(config: dict, config_path: str, verbose: bool = False) -> None:
)


def reject_unresolved_paths(named_paths: list[tuple[str, str]]) -> None:
"""Exit with a clear error if any path argument still contains the literal
``{project-root}`` token. That token is meaningful only inside config
values; filesystem path arguments must be resolved by the caller. Failing
loudly here prevents silently creating a junk ``{project-root}/`` directory.
"""
for name, value in named_paths:
if value and "{project-root}" in value:
print(
json.dumps(
{
"status": "error",
"error": (
f"Unresolved '{{project-root}}' token in {name} path: {value!r}. "
"Resolve '{project-root}' to the actual project root before running "
"this script — it is a filesystem path, not a config value."
),
},
indent=2,
),
file=sys.stderr,
)
sys.exit(1)


def main():
args = parse_args()

reject_unresolved_paths(
[
("--config-path", args.config_path),
("--user-config-path", args.user_config_path),
("--legacy-dir", args.legacy_dir),
]
)

# Load inputs
module_yaml = load_yaml_file(args.module_yaml)
if not module_yaml:
Expand Down
28 changes: 28 additions & 0 deletions samples/sample-module-setup/scripts/merge-help-csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,37 @@ def cleanup_legacy_csvs(
return deleted


def reject_unresolved_paths(named_paths: list[tuple[str, str]]) -> None:
"""Exit with a clear error if any path argument still contains the literal
``{project-root}`` token. That token is meaningful only inside config
values; filesystem path arguments must be resolved by the caller. Failing
loudly here prevents silently creating a junk ``{project-root}/`` directory.
"""
for name, value in named_paths:
if value and "{project-root}" in value:
print(
json.dumps(
{
"status": "error",
"error": (
f"Unresolved '{{project-root}}' token in {name} path: {value!r}. "
"Resolve '{project-root}' to the actual project root before running "
"this script — it is a filesystem path, not a config value."
),
},
indent=2,
)
)
sys.exit(1)


def main():
args = parse_args()

reject_unresolved_paths(
[("--target", args.target), ("--legacy-dir", args.legacy_dir)]
)

# Read source entries
source_header, source_rows = read_csv_rows(args.source)
if not source_rows:
Expand Down
8 changes: 6 additions & 2 deletions skills/bmad-bmb-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Installs and configures a BMad module into a project. Module identity (name, cod

Both config scripts use an anti-zombie pattern — existing entries for this module are removed before writing fresh ones, so stale values never persist.

`{project-root}` is a **literal token** in config values — never substitute it with an actual path. It signals to the consuming LLM that the value is relative to the project root, not the skill root.
`{project-root}` is a **literal token** in config _values_ (the data written into the files above) — never substitute it there. It signals to the consuming LLM that the value is relative to the project root, not the skill root. **This does not apply to filesystem path _arguments_ passed to the scripts below** (`--target`, `--config-path`, `--user-config-path`, `--legacy-dir`, `--bmad-dir`, `--skills-dir`): those are real paths, so you **must** resolve `{project-root}` to the actual project root before running, or the scripts will write to a literal `{project-root}/` directory under the skill folder. The scripts reject an unresolved token with an error.

## On Activation

Expand All @@ -40,7 +40,9 @@ Ask the user for values. Show defaults in brackets. Present all values together

## Write Files

Write a temp JSON file with the collected answers structured as `{"core": {...}, "module": {...}}` (omit `core` if it already exists). Then run both scripts — they can run in parallel since they write to different files:
Write a temp JSON file with the collected answers structured as `{"core": {...}, "module": {...}}` (omit `core` if it already exists). Values inside this JSON keep the literal `{project-root}` token. Then run both scripts — they can run in parallel since they write to different files.

In the commands below, replace `{project-root}` in every path argument with the actual project root (e.g. `/home/me/myapp`) before running — these are filesystem paths, not config values. Leave `{temp-file}` and `bmb` as-is.

```bash
python3 ./scripts/merge-config.py --config-path "{project-root}/_bmad/config.yaml" --user-config-path "{project-root}/_bmad/config.user.yaml" --module-yaml ./assets/module.yaml --answers {temp-file} --legacy-dir "{project-root}/_bmad"
Expand All @@ -59,6 +61,8 @@ After writing config, create any output directories that were configured. For fi

After both merge scripts complete successfully, remove the installer's package directories. Skills and agents in these directories are already installed at `.claude/skills/` — the `_bmad/` directory should only contain config files.

As with the merge scripts, replace `{project-root}` in the `--bmad-dir` and `--skills-dir` path arguments with the actual project root before running.

```bash
python3 ./scripts/cleanup-legacy.py --bmad-dir "{project-root}/_bmad" --module-code bmb --also-remove _config --skills-dir "{project-root}/.claude/skills"
```
Expand Down
28 changes: 28 additions & 0 deletions skills/bmad-bmb-setup/scripts/cleanup-legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,37 @@ def cleanup_directories(
return removed, not_found, total_files


def reject_unresolved_paths(named_paths: list[tuple[str, str]]) -> None:
"""Exit with a clear error if any path argument still contains the literal
``{project-root}`` token. That token is meaningful only inside config
values; filesystem path arguments must be resolved by the caller. Failing
loudly here prevents silently operating on a junk ``{project-root}/`` directory.
"""
for name, value in named_paths:
if value and "{project-root}" in value:
print(
json.dumps(
{
"status": "error",
"error": (
f"Unresolved '{{project-root}}' token in {name} path: {value!r}. "
"Resolve '{project-root}' to the actual project root before running "
"this script — it is a filesystem path, not a config value."
),
},
indent=2,
)
)
sys.exit(1)


def main():
args = parse_args()

reject_unresolved_paths(
[("--bmad-dir", args.bmad_dir), ("--skills-dir", args.skills_dir)]
)

bmad_dir = args.bmad_dir
module_code = args.module_code

Expand Down
33 changes: 33 additions & 0 deletions skills/bmad-bmb-setup/scripts/merge-config.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,42 @@ def write_config(config: dict, config_path: str, verbose: bool = False) -> None:
)


def reject_unresolved_paths(named_paths: list[tuple[str, str]]) -> None:
"""Exit with a clear error if any path argument still contains the literal
``{project-root}`` token. That token is meaningful only inside config
values; filesystem path arguments must be resolved by the caller. Failing
loudly here prevents silently creating a junk ``{project-root}/`` directory.
"""
for name, value in named_paths:
if value and "{project-root}" in value:
print(
json.dumps(
{
"status": "error",
"error": (
f"Unresolved '{{project-root}}' token in {name} path: {value!r}. "
"Resolve '{project-root}' to the actual project root before running "
"this script — it is a filesystem path, not a config value."
),
},
indent=2,
),
file=sys.stderr,
)
sys.exit(1)


def main():
args = parse_args()

reject_unresolved_paths(
[
("--config-path", args.config_path),
("--user-config-path", args.user_config_path),
("--legacy-dir", args.legacy_dir),
]
)

# Load inputs
module_yaml = load_yaml_file(args.module_yaml)
if not module_yaml:
Expand Down
28 changes: 28 additions & 0 deletions skills/bmad-bmb-setup/scripts/merge-help-csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,37 @@ def cleanup_legacy_csvs(
return deleted


def reject_unresolved_paths(named_paths: list[tuple[str, str]]) -> None:
"""Exit with a clear error if any path argument still contains the literal
``{project-root}`` token. That token is meaningful only inside config
values; filesystem path arguments must be resolved by the caller. Failing
loudly here prevents silently creating a junk ``{project-root}/`` directory.
"""
for name, value in named_paths:
if value and "{project-root}" in value:
print(
json.dumps(
{
"status": "error",
"error": (
f"Unresolved '{{project-root}}' token in {name} path: {value!r}. "
"Resolve '{project-root}' to the actual project root before running "
"this script — it is a filesystem path, not a config value."
),
},
indent=2,
)
)
sys.exit(1)


def main():
args = parse_args()

reject_unresolved_paths(
[("--target", args.target), ("--legacy-dir", args.legacy_dir)]
)

# Read source entries
source_header, source_rows = read_csv_rows(args.source)
if not source_rows:
Expand Down
Loading
Loading