| name | bmad-pulse-setup |
| description | Installs and configures the PULSE module in a BMAD project. Use when the user requests 'install PULSE', 'configure PULSE', or 'setup PULSE'. |
Module Setup
Overview
Installs and configures a BMad module into a project. Module identity (name, code, version) comes from ./assets/module.yaml. Collects user preferences and writes them to these files:
{project-root}/_bmad/custom/config.toml — the module section [modules.pulse] with the pulse_* values (issue #73, toml-first). This is the layer resolve_config.py reads with higher priority than the installer defaults in config.toml, so the team's answers win. Existing comments and other sections in this human-owned file are preserved (tomlkit round-trip).
{project-root}/_bmad/config.yaml — shared project config: core settings at root only (e.g. output_folder, document_output_language). The legacy pulse: module section is no longer written here; a stale one is stripped on run (that strip is the yaml→toml migration). User-only keys (user_name, communication_language) are never written here.
{project-root}/_bmad/config.user.yaml — personal settings intended to be gitignored: user_name, communication_language, and any module variable marked user_setting: true in ./assets/module.yaml. These values live exclusively here.
{project-root}/_bmad/module-help.csv — registers module capabilities for the help system.
The config scripts use an anti-zombie pattern — the module's answered keys are overwritten with fresh values on each run, and the stale legacy pulse: yaml section is removed, so stale values never persist.
Migrating an existing yaml install (issue #73): re-running this setup is the migration path. Before prompting, read the current effective values (via resolve_config.py --key modules.pulse and the legacy pulse: section of config.yaml) and offer them as the prompt defaults, so accepting the defaults carries the team's existing answers into custom/config.toml and strips the legacy yaml section.
{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.
On Activation
- Read
./assets/module.yaml for module metadata and variable definitions (the code field is the module identifier)
- Check if
{project-root}/_bmad/config.yaml exists — if a section matching the module's code is already present, inform the user this is an update
- Check for per-module configuration at
{project-root}/_bmad/pulse/config.yaml and {project-root}/_bmad/core/config.yaml. If either file exists:
- If
{project-root}/_bmad/config.yaml does not yet have a section for this module: this is a fresh install. Inform the user that installer config was detected and values will be consolidated into the new format.
- If
{project-root}/_bmad/config.yaml already has a section for this module: this is a legacy migration. Inform the user that legacy per-module config was found alongside existing config, and legacy values will be used as fallback defaults.
- In both cases, per-module config files and directories will be cleaned up after setup.
If the user provides arguments (e.g. accept all defaults, --headless, or inline values like user name is BMad, I speak Swahili), map any provided values to config keys, use defaults for the rest, and skip interactive prompting. Still display the full confirmation summary at the end.
Reconcile Installed Skills (Self-Heal)
Run this first, before any other step. The upstream BMAD installer
performs an additive deploy when PULSE is already installed: brand-new
files are copied, but pre-existing files (module.yaml, SKILL.md, …) are
never overwritten and renamed/removed skills are never pruned. Updating
over an existing install therefore leaves a mixed state stuck at the old
version (see issue #36). This step force-syncs every PULSE skill directory
from the authoritative source (the freshly-fetched BMAD custom-module
cache) and prunes orphaned renamed folders.
python3 ./scripts/reconcile-skills.py --project-root "{project-root}"
The script is idempotent — on an already-in-sync tree it writes nothing and
reports action: up_to_date. It is non-fatal on fresh installs: when no
source/cache is found it exits 0 with action: skipped_no_source, so
continue normally. Exit codes: 0=success, 1=validation error, 2=runtime
error. Surface any non-zero exit and stop.
Inspect the JSON action field:
up_to_date / skipped_no_source — continue silently.
reconciled — report from_version → to_version to the user. If the
payload includes a notice saying bmad-pulse-setup was updated in
place, tell the user to re-run /bmad-pulse-setup once more so the
current version of this skill executes, then stop.
Run ./scripts/reconcile-skills.py --help for full usage (including
--source and --dry-run).
Collect Configuration
Ask the user for values. Show defaults in brackets. Present all values together so the user can respond once with only the values they want to change (e.g. "change language to Swahili, rest are fine"). Never tell the user to "press enter" or "leave blank" — in a chat interface they must type something to respond.
Default priority (highest wins): existing new config values > legacy config values > ./assets/module.yaml defaults. When legacy configs exist, read them and use matching values as defaults instead of module.yaml defaults. Only keys that match the current schema are carried forward — changed or removed keys are ignored.
Core config (only if no core keys exist yet): user_name (default: BMad), communication_language and document_output_language (default: English — ask as a single language question, both keys get the same answer), output_folder (default: {project-root}/_bmad-output). Of these, user_name and communication_language are written exclusively to config.user.yaml. The rest go to config.yaml at root and are shared across all modules.
Module config: Read each variable in ./assets/module.yaml that has a prompt field. Ask using that prompt, pre-filling the default with the current effective value when re-running on an existing install: prefer the resolved modules.pulse value (resolve_config.py --key modules.pulse), then the legacy pulse: value in config.yaml, then any legacy per-module value, then the module.yaml default. This is what carries an existing team's answers into custom/config.toml on the migration re-run.
Validation rules:
- If
pulse_estimation_method = story_points and pulse_story_point_hours_factor has not been set, warn before continuing
- If
pulse_estimation_method = bcp: do not require pulse_story_point_hours_factor (no factor applies — estimated_hours is derived upstream by bmad-module-bcp, not converted by PULSE). The value is semantic-only: it tells PULSE the upstream hours came from BCP so the dashboard surfaces the BCP Productivity section. Inform the user that the companion bmad-module-bcp module must be installed for estimated_hours to be BCP-derived; PULSE itself stays passive if it is not.
- If
pulse_dev_categories = custom, request a comma-separated list
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:
uv run ./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"
python3 ./scripts/merge-help-csv.py --target "{project-root}/_bmad/module-help.csv" --source ./assets/module-help.csv --legacy-dir "{project-root}/_bmad" --module-code pulse
Note (PEP 723): only merge-config.py runs via uv run — it declares third-party dependencies (pyyaml, tomlkit) in its inline # /// script header. Plain python3 fails with Error: tomlkit is required (PEP 723 dependency). uv run reads the header and provisions the libraries in an ephemeral environment. The other scripts in this skill are stdlib-only (dependencies = []) and run under plain python3.
merge-config.py writes the [modules.pulse] section to {project-root}/_bmad/custom/config.toml (derived from the --config-path directory; override with --custom-config-path), writes core keys to config.yaml, strips any legacy pulse: yaml section, and writes user settings to config.user.yaml. Check custom_config_path and module_keys in the output.
Both scripts output JSON to stdout with results. If either exits non-zero, surface the error and stop. The scripts automatically read legacy config values as fallback defaults, then delete the legacy files after a successful merge. Check legacy_configs_deleted and legacy_csvs_deleted in the output to confirm cleanup.
Run uv run ./scripts/merge-config.py --help or ./scripts/merge-help-csv.py --help for full usage.
Register Agent in Party Mode Roster
After writing config and help CSV, register the Levi agent so it joins the Party Mode roster and other agent-aware features.
Party Mode builds its roster from the [agents] table resolved by resolve_config.py — a deep-merge of {project-root}/_bmad/config.toml (base) and {project-root}/_bmad/custom/config.toml (team). Official modules get their [agents.*] entries written into the base config.toml by the BMAD core installer, but a custom module like PULSE is never written there — so without this step Levi is installed as a skill yet stays invisible to Party Mode (/bmad-party-mode never lists him). Register him in the team-owned custom/config.toml layer, which survives re-install:
uv run ./scripts/register-party-agent.py --project-root "{project-root}" --fragment ./assets/agent-manifest-fragment.csv
The script upserts [agents.bmad-agent-pulse] (anti-zombie, idempotent) from the agent-manifest-fragment.csv values, preserving existing comments and sections (tomlkit round-trip). It runs via uv run for its PEP 723 tomlkit dependency. Check agent_key and custom_config_path in the JSON output.
If successful, inform the user: "Agent Levi registered in the Party Mode roster (_bmad/custom/config.toml -> [agents.bmad-agent-pulse]) — run /bmad-party-mode to see him. To spotlight him, add a curated party group (e.g. a delivery/retro room) to _bmad/custom/bmad-party-mode.toml."
Legacy agent-manifest.csv (optional, compat)
Older BMAD layouts also read {project-root}/_bmad/_config/agent-manifest.csv. If that file exists, additionally merge the fragment into it (harmless where unused). If it does not exist, skip — the [agents] registration above is what Party Mode actually reads.
python3 ./scripts/merge-help-csv.py --target "{project-root}/_bmad/_config/agent-manifest.csv" --source ./assets/agent-manifest-fragment.csv --module-code pulse
Create Output Directories
After writing config, create any output directories that were configured. For filesystem operations only (such as creating directories), resolve the {project-root} token to the actual project root and create each path-type value from config.yaml that does not yet exist — this includes output_folder and any module variable whose value starts with {project-root}/. The paths stored in the config files must continue to use the literal {project-root} token; only the directories on disk should use the resolved paths. Use mkdir -p or equivalent to create the full path.
Cleanup Legacy Directories
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.
python3 ./scripts/cleanup-legacy.py --bmad-dir "{project-root}/_bmad" --module-code pulse --skills-dir "{project-root}/.claude/skills"
The script verifies that every skill in the legacy directories exists at .claude/skills/ before removing anything. Directories without skills (like _config/) are removed directly. If the script exits non-zero, surface the error and stop. Missing directories (already cleaned by a prior run) are not errors — the script is idempotent.
Check directories_removed and files_removed_count in the JSON output for the confirmation step. Run ./scripts/cleanup-legacy.py --help for full usage.
Generate Customize Overrides (Auto-Tracking)
After cleanup, configure auto-tracking by emitting two customize.toml
overrides in the consumer project. Auto-tracking now uses BMAD v6.4.0's
TOML-based customization framework instead of injecting steps into
workflow.md. This survives BMAD core upgrades because the override files
live in _bmad/custom/, which BMAD never overwrites.
Capability Gate
Run the capability detector first:
python3 ./scripts/detect_bmad_capability.py --project-root "{project-root}"
The script exits 0 (BMAD ≥6.4.0), 1 (BMAD ≤6.3.x), or 2 (BMAD not installed)
and prints a JSON payload to stdout describing the detection.
- Exit 0 — proceed with override emission below.
- Exit 1 — abort with this message: "PULSE v0.4.0 requires BMAD ≥6.4.0.
Detected BMAD ≤6.3.x. Either upgrade BMAD (
npx bmad-method install) or
pin to PULSE v0.3.x via --version."
- Exit 2 — abort: "BMAD is not installed in this project root. Run
npx bmad-method install first, then re-run /bmad-pulse-setup."
Cleanup Legacy Markers
Even on a fresh BMAD ≥6.4.0 install, scan for stale <!-- PULSE:auto-inject -->
blocks in workflow.md left over from previous PULSE versions. The script
silently skips when workflow.md is absent (BMAD 6.4.0 removed it from
bmad-dev-story).
python3 ./scripts/cleanup-legacy.py \
--remove-pulse-markers \
--project-root "{project-root}"
The script exits 0 in all normal cases and prints {"removed": <int>, "path": "..."}
on stdout (or {"removed": 0, "skipped": "workflow.md not found", "path": "..."} when
the file is absent — this is the common case on fresh BMAD 6.4.0 installs and is NOT
a failure). A non-zero exit indicates --project-root is missing or the file system
is broken; surface the error and stop.
Cleanup Legacy Levi Agent Folder
Pre-v0.4.5 PULSE distributed its own bmad-pulse-agent-levi/ skill folder
in parallel with the canonical bmad-agent-pulse/ folder auto-provisioned
by the BMAD installer from agent-manifest-fragment.csv. On BMAD v6.6.0
projects this produced two divergent entry points for the same Levi agent.
This step removes the legacy folder when the canonical folder is present.
The script refuses to remove the legacy folder if the canonical replacement
is missing, so a partial-state install is never stranded without an agent.
python3 ./scripts/cleanup-legacy.py \
--remove-legacy-agent \
--project-root "{project-root}"
The script exits 0 in every non-fatal case and prints a JSON payload with
an action field: removed, skipped_already_absent, or
skipped_no_canonical. Surface the notice to the user when action
is removed so they understand which folder was deleted. A non-zero exit
indicates --project-root is missing or the file system is broken;
surface the error and stop.
Emit Override Files
Emit the two override files. The conflict policy is abort + --force:
if either destination already exists, the script exits 3 and the file is
left untouched (sha256-stable). The user can re-run with --force after
inspecting the conflict.
python3 ./scripts/inject_customize.py \
--project-root "{project-root}" \
--skill bmad-dev-story
python3 ./scripts/inject_customize.py \
--project-root "{project-root}" \
--skill bmad-code-review
If either invocation exits 3, surface the message to the user verbatim
(it includes the destination path and instructs how to re-run with
--force). Do NOT auto-retry with --force — the choice is the user's.
.gitignore Allowlist Snippet
Print a copy-paste-ready snippet for the consumer's .gitignore so that
_bmad/custom/*.toml is committed (team overrides) while *.user.toml
stays private. The script is read-only — never modifies the file.
python3 ./scripts/print_gitignore_snippet.py --project-root "{project-root}"
Surface the script's stdout to the user.
Post-Injection
Inform the user:
- "PULSE auto-tracking integrated via
_bmad/custom/bmad-dev-story.toml and
_bmad/custom/bmad-code-review.toml. Every story will now automatically
track start (during /bmad-dev-story) and completion (after
/bmad-code-review)."
- "To disable: delete the two
.toml files from _bmad/custom/."
- "To customize: edit the files manually. Re-running
/bmad-pulse-setup will
abort if you changed them — pass --force only if you want PULSE's
defaults restored."
If any step above failed, do NOT block the rest of the setup. Report the
failure clearly and continue — the user can rerun the failing piece later.
Confirm
Use the script JSON output to display what was written — config values set (written to config.yaml at root for core, module section for module values), user settings written to config.user.yaml (user_keys in result), help entries added, fresh install vs update. If legacy files were deleted, mention the migration. If legacy directories were removed, report the count and list (e.g. "Cleaned up 106 installer package files from pulse/, core/ — skills are installed at .claude/skills/"). Then display the module_greeting from ./assets/module.yaml to the user.
Outcome
Once the user's user_name and communication_language are known (from collected input, arguments, or existing config), use them consistently for the remainder of the session: address the user by their configured name and communicate in their configured communication_language.