diff --git a/samples/bmad-agent-dream-weaver/assets/module-setup.md b/samples/bmad-agent-dream-weaver/assets/module-setup.md index b1687b1..67d9a15 100644 --- a/samples/bmad-agent-dream-weaver/assets/module-setup.md +++ b/samples/bmad-agent-dream-weaver/assets/module-setup.md @@ -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 @@ -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} diff --git a/samples/bmad-agent-dream-weaver/scripts/merge-config.py b/samples/bmad-agent-dream-weaver/scripts/merge-config.py index 6ee0ac7..2ac9671 100755 --- a/samples/bmad-agent-dream-weaver/scripts/merge-config.py +++ b/samples/bmad-agent-dream-weaver/scripts/merge-config.py @@ -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: diff --git a/samples/bmad-agent-dream-weaver/scripts/merge-help-csv.py b/samples/bmad-agent-dream-weaver/scripts/merge-help-csv.py index 6ba1afe..f8f02f1 100755 --- a/samples/bmad-agent-dream-weaver/scripts/merge-help-csv.py +++ b/samples/bmad-agent-dream-weaver/scripts/merge-help-csv.py @@ -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: diff --git a/samples/sample-module-setup/SKILL.md b/samples/sample-module-setup/SKILL.md index c2d8fbd..147375d 100644 --- a/samples/sample-module-setup/SKILL.md +++ b/samples/sample-module-setup/SKILL.md @@ -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 @@ -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" @@ -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" ``` diff --git a/samples/sample-module-setup/scripts/cleanup-legacy.py b/samples/sample-module-setup/scripts/cleanup-legacy.py index fc12f40..b81ce41 100755 --- a/samples/sample-module-setup/scripts/cleanup-legacy.py +++ b/samples/sample-module-setup/scripts/cleanup-legacy.py @@ -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 diff --git a/samples/sample-module-setup/scripts/merge-config.py b/samples/sample-module-setup/scripts/merge-config.py index 6ee0ac7..2ac9671 100755 --- a/samples/sample-module-setup/scripts/merge-config.py +++ b/samples/sample-module-setup/scripts/merge-config.py @@ -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: diff --git a/samples/sample-module-setup/scripts/merge-help-csv.py b/samples/sample-module-setup/scripts/merge-help-csv.py index 6ba1afe..f8f02f1 100755 --- a/samples/sample-module-setup/scripts/merge-help-csv.py +++ b/samples/sample-module-setup/scripts/merge-help-csv.py @@ -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: diff --git a/skills/bmad-bmb-setup/SKILL.md b/skills/bmad-bmb-setup/SKILL.md index 80f6cdf..50fb6bf 100644 --- a/skills/bmad-bmb-setup/SKILL.md +++ b/skills/bmad-bmb-setup/SKILL.md @@ -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 @@ -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" @@ -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" ``` diff --git a/skills/bmad-bmb-setup/scripts/cleanup-legacy.py b/skills/bmad-bmb-setup/scripts/cleanup-legacy.py index fc12f40..b81ce41 100755 --- a/skills/bmad-bmb-setup/scripts/cleanup-legacy.py +++ b/skills/bmad-bmb-setup/scripts/cleanup-legacy.py @@ -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 diff --git a/skills/bmad-bmb-setup/scripts/merge-config.py b/skills/bmad-bmb-setup/scripts/merge-config.py index 6ee0ac7..2ac9671 100755 --- a/skills/bmad-bmb-setup/scripts/merge-config.py +++ b/skills/bmad-bmb-setup/scripts/merge-config.py @@ -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: diff --git a/skills/bmad-bmb-setup/scripts/merge-help-csv.py b/skills/bmad-bmb-setup/scripts/merge-help-csv.py index 6ba1afe..f8f02f1 100755 --- a/skills/bmad-bmb-setup/scripts/merge-help-csv.py +++ b/skills/bmad-bmb-setup/scripts/merge-help-csv.py @@ -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: diff --git a/skills/bmad-module-builder/assets/setup-skill-template/SKILL.md b/skills/bmad-module-builder/assets/setup-skill-template/SKILL.md index 7a94c76..1712675 100644 --- a/skills/bmad-module-builder/assets/setup-skill-template/SKILL.md +++ b/skills/bmad-module-builder/assets/setup-skill-template/SKILL.md @@ -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 @@ -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" @@ -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 {module-code} --also-remove _config --skills-dir "{project-root}/.claude/skills" ``` diff --git a/skills/bmad-module-builder/assets/setup-skill-template/scripts/cleanup-legacy.py b/skills/bmad-module-builder/assets/setup-skill-template/scripts/cleanup-legacy.py index fc12f40..b81ce41 100755 --- a/skills/bmad-module-builder/assets/setup-skill-template/scripts/cleanup-legacy.py +++ b/skills/bmad-module-builder/assets/setup-skill-template/scripts/cleanup-legacy.py @@ -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 diff --git a/skills/bmad-module-builder/assets/setup-skill-template/scripts/merge-config.py b/skills/bmad-module-builder/assets/setup-skill-template/scripts/merge-config.py index 6ee0ac7..2ac9671 100755 --- a/skills/bmad-module-builder/assets/setup-skill-template/scripts/merge-config.py +++ b/skills/bmad-module-builder/assets/setup-skill-template/scripts/merge-config.py @@ -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: diff --git a/skills/bmad-module-builder/assets/setup-skill-template/scripts/merge-help-csv.py b/skills/bmad-module-builder/assets/setup-skill-template/scripts/merge-help-csv.py index 6ba1afe..f8f02f1 100755 --- a/skills/bmad-module-builder/assets/setup-skill-template/scripts/merge-help-csv.py +++ b/skills/bmad-module-builder/assets/setup-skill-template/scripts/merge-help-csv.py @@ -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: diff --git a/skills/bmad-module-builder/assets/standalone-module-template/merge-config.py b/skills/bmad-module-builder/assets/standalone-module-template/merge-config.py index 6ee0ac7..2ac9671 100755 --- a/skills/bmad-module-builder/assets/standalone-module-template/merge-config.py +++ b/skills/bmad-module-builder/assets/standalone-module-template/merge-config.py @@ -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: diff --git a/skills/bmad-module-builder/assets/standalone-module-template/merge-help-csv.py b/skills/bmad-module-builder/assets/standalone-module-template/merge-help-csv.py index 6ba1afe..f8f02f1 100755 --- a/skills/bmad-module-builder/assets/standalone-module-template/merge-help-csv.py +++ b/skills/bmad-module-builder/assets/standalone-module-template/merge-help-csv.py @@ -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: diff --git a/skills/bmad-module-builder/assets/standalone-module-template/module-setup.md b/skills/bmad-module-builder/assets/standalone-module-template/module-setup.md index 34ec6db..f6f8508 100644 --- a/skills/bmad-module-builder/assets/standalone-module-template/module-setup.md +++ b/skills/bmad-module-builder/assets/standalone-module-template/module-setup.md @@ -15,7 +15,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 @@ -51,7 +51,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}