Compare commits
17 Commits
77f6106a8e
...
697d92e355
| Author | SHA1 | Date |
|---|---|---|
|
|
697d92e355 | |
|
|
7b590b0a90 | |
|
|
380590aa8b | |
|
|
e36f219c81 | |
|
|
9debc165aa | |
|
|
65b810a11f | |
|
|
e6cdc93b79 | |
|
|
e174bebc60 | |
|
|
fcf20f1c7b | |
|
|
e011192525 | |
|
|
91a57499e9 | |
|
|
48a7ec8bff | |
|
|
3da984a491 | |
|
|
815600e4ca | |
|
|
7ee5fa313b | |
|
|
3e89b30b3c | |
|
|
b4d73b7daf |
|
|
@ -13,7 +13,7 @@
|
|||
"name": "bmad-pro-skills",
|
||||
"source": "./",
|
||||
"description": "Next level skills for power users — advanced prompting techniques, agent management, and more.",
|
||||
"version": "6.3.0",
|
||||
"version": "6.6.0",
|
||||
"author": {
|
||||
"name": "Brian (BMad) Madison"
|
||||
},
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
"name": "bmad-method-lifecycle",
|
||||
"source": "./",
|
||||
"description": "Full-lifecycle AI development framework — agents and workflows for product analysis, planning, architecture, and implementation.",
|
||||
"version": "6.3.0",
|
||||
"version": "6.6.0",
|
||||
"author": {
|
||||
"name": "Brian (BMad) Madison"
|
||||
},
|
||||
|
|
|
|||
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -1,5 +1,31 @@
|
|||
# Changelog
|
||||
|
||||
## v6.6.0 - 2026-04-28
|
||||
|
||||
### 💥 Breaking Changes
|
||||
|
||||
* `--tools none` is no longer accepted; fresh `--yes` installs now require an explicit `--tools <id>`. Existing-install flows are unchanged. Run `npx bmad-method --list-tools` to see supported IDs (#2346)
|
||||
* `project_name` has moved from `[modules.bmm]` to `[core]` in `config.toml`. Existing installs are auto-migrated on next install/update — no manual action required (#2348)
|
||||
|
||||
### 🎁 Features
|
||||
|
||||
* **Non-interactive config for CI/Docker** — new `--set <module>.<key>=<value>` (repeatable) and `--list-options [module]` flags allow installer configuration without prompts. Routes values to the correct config file with prototype-pollution defenses (#2354)
|
||||
* **Brownfield epic scoping** — Create Epics and Stories workflow now detects file-overlap between epics and applies an Implementation Efficiency principle plus a design completeness gate, reducing unnecessary file churn (#1826)
|
||||
|
||||
### 🐛 Fixes
|
||||
|
||||
* **Custom module installer** — Azure DevOps URLs now parse correctly with multi-segment paths and `_git` prefixes (#2269); HTTP (non-HTTPS) Git URLs are preserved for self-hosted servers (#2344); community installs route through `PluginResolver` so marketplace plugins with nested `module.yaml` install all skills (#2331); URL-source modules resolve from disk cache on re-install instead of warning (#2323); local `--custom-content` modules resolve correctly and `[modules.<code>]` TOML keys use the module code rather than display name (#2316); `--yes` with `--custom-source` now runs the full update path so version tags are respected (#2336)
|
||||
* **Installer safety** — `--list-tools` flag added; empty/typo'd tool IDs rejected with specific errors (#2346)
|
||||
* **Channel and dist-tag handling** — installer launched from a prerelease (e.g. `@next`) now defaults external module channels to `next` instead of silently downgrading to stable (#2321); stable publishes advance the `@next` dist-tag so prerelease users no longer leapfrog or miss update notifications (#2320)
|
||||
* **Architecture validation gate** — step-07 validation template no longer ships pre-checked; status field is now templated against actual checklist completion (#2347)
|
||||
* **bmad-help data integrity** — `bmad-help.csv` is no longer transformed at merge time and is emitted in its documented schema; 31 misaligned rows in core/bmm `module-help.csv` repaired (#2349)
|
||||
* **Config robustness** — malformed `module.yaml` (scalars, arrays) is now rejected before crash (#2348)
|
||||
* **Legacy cleanup** — pre-v6.2.0 wrapper skills (`bmad-bmm-*`, `bmad-agent-bmm-*`) are removed automatically on upgrade so they no longer error with missing-file warnings (#2315)
|
||||
|
||||
### 📚 Docs
|
||||
|
||||
* Complete Chinese (zh-CN) translations for `named-agents.md` and `expand-bmad-for-your-org.md`; localized BMad Ecosystem sidebar (CIS, BMB, TEA, WDS) across zh-cn, vi-vn, fr-fr, cs-cz (#2355)
|
||||
|
||||
## v6.5.0 - 2026-04-26
|
||||
|
||||
### 🎁 Features
|
||||
|
|
|
|||
|
|
@ -52,6 +52,15 @@ Follow the installer prompts, then open your AI IDE (Claude Code, Cursor, etc.)
|
|||
npx bmad-method install --directory /path/to/project --modules bmm --tools claude-code --yes
|
||||
```
|
||||
|
||||
Override any module config option with `--set <module>.<key>=<value>` (repeatable). Run `--list-options [module]` to see locally-known official keys (built-in modules plus any external officials cached on this machine):
|
||||
|
||||
```bash
|
||||
npx bmad-method install --yes \
|
||||
--modules bmm --tools claude-code \
|
||||
--set bmm.project_knowledge=research \
|
||||
--set bmm.user_skill_level=expert
|
||||
```
|
||||
|
||||
[See all installation options](https://docs.bmad-method.org/how-to/non-interactive-installation/)
|
||||
|
||||
> **Not sure what to do?** Ask `bmad-help` — it tells you exactly what's next and what's optional. You can also ask questions like `bmad-help I just finished the architecture, what do I do next?`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,137 @@
|
|||
---
|
||||
title: "Forensic Investigation"
|
||||
description: How bmad-investigate treats every issue like a crime scene, grades evidence, and produces a structured case file engineers can act on
|
||||
sidebar:
|
||||
order: 6
|
||||
---
|
||||
|
||||
You hand `bmad-investigate` a crash log, a stack trace, or just a "this used to work, now it doesn't". The skill takes
|
||||
over the investigator's discipline for the duration of the run. It does not start fixing. It opens a case file.
|
||||
|
||||
Every finding gets graded. Every hypothesis gets a status. Wrong turns are kept, not erased. The deliverable is a
|
||||
document another engineer can pick up cold.
|
||||
|
||||
This page explains why investigation is its own discipline, and what the skill buys you that a regular dev workflow
|
||||
doesn't.
|
||||
|
||||
## The Problem With "Just Debug It"
|
||||
|
||||
Normal debugging blends three things: looking at evidence, reasoning about cause, and changing code to test the theory.
|
||||
When they're blended, two failure modes show up.
|
||||
|
||||
The first is **narrative lock-in**. The first plausible story becomes the working theory, and every observation gets
|
||||
bent to fit it. The bug stays unfixed until someone gives up and starts over. Hours later.
|
||||
|
||||
The second is **evidence amnesia**. You traced something, ruled it out, but didn't write down why. Two days later, with
|
||||
fresh eyes, you trace it again. Or worse, a colleague picks up the bug and re-runs the same dead end you already
|
||||
eliminated.
|
||||
|
||||
The skill's design is a direct response to both.
|
||||
|
||||
## Evidence Grading
|
||||
|
||||
Every finding in an investigation is one of three things.
|
||||
|
||||
- **Confirmed.** Directly observed in logs, code, or dumps; cited with a specific reference (a `path:line`, a log
|
||||
timestamp, a commit hash). If someone asks "how do you know?", you point at the citation.
|
||||
- **Deduced.** Logically follows from confirmed evidence; the reasoning chain is shown. If a step in the chain is wrong,
|
||||
the deduction is wrong, and you can see exactly which step.
|
||||
- **Hypothesized.** Plausible but unconfirmed. States what evidence would confirm or refute, and declares upfront what
|
||||
would close it. Hypotheses are explicitly *not facts*.
|
||||
|
||||
The grading is not about being humble. It's about making the case file readable. A reader can scan the Confirmed section
|
||||
to know what is true, the Deduced section to know what follows, and the Hypothesized section to know what is still open.
|
||||
Confusion between the three is the most common reason investigations spiral.
|
||||
|
||||
## Stronghold First
|
||||
|
||||
Investigation never starts from a theory. It starts from one piece of confirmed evidence and expands outward. That
|
||||
evidence might be a specific error message, a stack frame, or a timestamped log entry.
|
||||
|
||||
This is the opposite of how investigations often go. Someone has a hunch, builds a theory, and then hunts for evidence
|
||||
that supports it. The hunch can be right; the *method* is fragile because it makes confirmation bias the default.
|
||||
|
||||
A stronghold is a fact you can return to when reasoning gets murky. If a deduction takes you somewhere strange, you can
|
||||
walk it back to the stronghold and try a different branch. Without one, you don't know which step to undo.
|
||||
|
||||
When evidence is sparse, the skill says so and switches to hypothesis-driven exploration: form hypotheses from what's
|
||||
available, identify what would test each, present a prioritized data-collection list. Missing evidence is itself a
|
||||
finding.
|
||||
|
||||
## Hypothesis Discipline
|
||||
|
||||
Hypotheses are never deleted from the case file. When evidence confirms or refutes one, its **Status** field updates
|
||||
from Open to Confirmed or Refuted, and a **Resolution** explains what evidence settled it.
|
||||
|
||||
This rule has a real cost. Case files grow. The benefit is real too. The full reasoning history becomes part of the
|
||||
deliverable. Six months later, when a similar bug surfaces, the next investigator can read the original case file and
|
||||
see which paths were already eliminated and why. Without that history, every new investigator re-runs the same dead
|
||||
ends.
|
||||
|
||||
It also disciplines the present-tense investigator. If you can't delete a wrong hypothesis, you have to disprove it
|
||||
with cited evidence. Quietly dropping it when it becomes inconvenient is no longer an option.
|
||||
|
||||
## Challenge the Premise
|
||||
|
||||
The user's description of the problem is a hypothesis, not a fact. "The cache is broken" is something a user *believes*.
|
||||
Before the skill builds an investigation around it, the technical claims are verified independently. If the evidence
|
||||
contradicts the premise, the report says so directly.
|
||||
|
||||
This is the forensic instinct: the witness's account is data, not truth. Sometimes the reported bug is real but
|
||||
mislabeled. Sometimes the described symptom is downstream of a different cause. Investigations that take the premise as
|
||||
gospel diagnose the wrong defect, and the bug returns in a slightly different form.
|
||||
|
||||
## A Calibrated Walk
|
||||
|
||||
The skill is one procedure, not two modes. It calibrates how much defect-chasing versus how much area-exploration the
|
||||
input demands, on a continuous scale.
|
||||
|
||||
A symptom-driven case (a ticket, a crash, an error message, a "this used to work") leans into hypothesis tracking,
|
||||
timeline reconstruction, and a fix direction. A no-symptom case (understanding a module before you touch it, evaluating
|
||||
reusability, building a mental model) leans into I/O mapping, control-flow filtering, and a verification plan. Most
|
||||
real cases sit somewhere between, and the case file reflects whichever balance the evidence required.
|
||||
|
||||
The discipline is the same regardless of where on the scale a case lands: stronghold first, evidence grading, hypothesis
|
||||
tracking, never erase. The output is always at `{implementation_artifacts}/investigations/{slug}-investigation.md`, with
|
||||
sections that don't apply to a given case left empty or omitted.
|
||||
|
||||
When a deep bug requires understanding a broader subsystem, the procedure folds in the I/O mapping, control-flow
|
||||
filtering, working-backward-from-outputs, and cross-component boundary tracing techniques inline. The area model lands
|
||||
in the same case file. There is no mode switch.
|
||||
|
||||
## Methodology Lives in the Skill
|
||||
|
||||
The investigator's discipline is a property of the skill itself. Whoever invokes `bmad-investigate` takes on the
|
||||
methodology and communication style for the run: clinical precision, evidence-first language, no hedging, case-file
|
||||
framing. When the skill ends, the caller returns to its prior voice. No persona swap, just a tone shift from the skill's
|
||||
principles.
|
||||
|
||||
This matters because investigation and implementation reward different instincts. Investigators are slow and precise.
|
||||
Implementers are fast and confident. The same brain doing both in one session tends to do neither well. The skill
|
||||
carves out the investigative posture inline, without a context switch to a separate identity.
|
||||
|
||||
## What You Get
|
||||
|
||||
A completed investigation file:
|
||||
|
||||
- Separates Confirmed findings (with citations) from Deductions and Hypotheses
|
||||
- Preserves all hypotheses ever formed, with their final Status and Resolution
|
||||
- Reconstructs a timeline of events from multiple evidence sources
|
||||
- Identifies data gaps and what they would resolve
|
||||
- Provides actionable conclusions grounded in evidence
|
||||
- Includes a reproduction plan when a root cause is identified
|
||||
- Maintains an investigation backlog of paths still to explore
|
||||
|
||||
Hand it to an engineer who was not present and they understand what happened, what is known, and what remains uncertain.
|
||||
That's the bar.
|
||||
|
||||
## The Bigger Idea
|
||||
|
||||
Most "AI debugging" today blends evidence, reasoning, and code changes into one stream of plausible-looking text. The
|
||||
signal is hard to find, the dead ends repeat, and the case file, if there is one, is a chat log nobody wants to read.
|
||||
|
||||
`bmad-investigate` treats investigation as a discipline with its own deliverable. Evidence has a grade. Hypotheses have
|
||||
a status. Wrong turns are documented, not erased. The case file outlives the session.
|
||||
|
||||
When the next bug shows up that looks like one you've seen before, you have somewhere to start that isn't a blank
|
||||
prompt.
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
---
|
||||
title: "Enquête de code"
|
||||
description: Comment bmad-investigate traite chaque problème comme une scène d'enquête, classe les preuves et produit un dossier structuré sur lequel les ingénieurs peuvent agir
|
||||
sidebar:
|
||||
order: 6
|
||||
---
|
||||
|
||||
Vous confiez à `bmad-investigate` un journal de plantage, une trace de pile, ou simplement un « ça marchait avant, plus
|
||||
maintenant ». Le skill prend le relais avec la discipline d'enquête le temps de l'exécution. Il ne se met pas à
|
||||
corriger. Il ouvre un dossier d'enquête.
|
||||
|
||||
Chaque constatation reçoit une note. Chaque hypothèse a un statut. Les fausses pistes sont conservées, pas effacées. Le
|
||||
livrable est un document qu'un autre ingénieur peut reprendre à froid.
|
||||
|
||||
Cette page explique pourquoi l'enquête est une discipline à part entière, et ce que le skill apporte qu'un workflow de
|
||||
développement classique n'apporte pas.
|
||||
|
||||
## Le problème du « débogue, c'est tout »
|
||||
|
||||
Le débogage classique mélange trois activités : examiner les preuves, raisonner sur la cause, et modifier le code pour
|
||||
tester la théorie. Quand elles sont mélangées, deux modes de défaillance apparaissent.
|
||||
|
||||
Le premier est le **verrouillage narratif**[^1]. La première histoire plausible devient la théorie de travail, et chaque
|
||||
observation est tordue pour la confirmer. Le bug reste non corrigé jusqu'à ce que quelqu'un abandonne et reparte de
|
||||
zéro. Des heures plus tard.
|
||||
|
||||
Le second est l'**amnésie probatoire**. Vous avez tracé quelque chose, l'avez écarté, mais n'avez pas écrit pourquoi.
|
||||
Deux jours plus tard, avec un regard frais, vous le retracez. Pire encore, un collègue reprend le bug et refait la même
|
||||
impasse que vous aviez déjà éliminée.
|
||||
|
||||
La conception du skill est une réponse directe à ces deux modes.
|
||||
|
||||
## Classement des preuves
|
||||
|
||||
Chaque constatation dans une enquête appartient à l'une de trois catégories.
|
||||
|
||||
- **Confirmé.** Directement observé dans les logs, le code ou les dumps ; cité avec une référence spécifique (un
|
||||
`chemin:ligne`, un horodatage de log, un hash de commit). Si quelqu'un demande « comment le sais-tu ? », vous pointez
|
||||
la citation.
|
||||
- **Déduit.** Découle logiquement de preuves confirmées ; la chaîne de raisonnement est explicite. Si une étape de la
|
||||
chaîne est fausse, la déduction est fausse, et on peut voir précisément quelle étape.
|
||||
- **Hypothétique.** Plausible mais non confirmé. Énonce quelle preuve confirmerait ou réfuterait, et déclare d'avance ce
|
||||
qui le clôturerait. Les hypothèses sont explicitement *non factuelles*.
|
||||
|
||||
Le classement n'est pas une posture d'humilité. Il rend le dossier lisible. Un lecteur peut parcourir la section
|
||||
Confirmé pour savoir ce qui est vrai, la section Déduit pour savoir ce qui en découle, et la section Hypothétique pour
|
||||
savoir ce qui reste ouvert. Confondre les trois est la première raison pour laquelle les enquêtes dérapent.
|
||||
|
||||
## Tête de pont d'abord
|
||||
|
||||
L'enquête ne part jamais d'une théorie. Elle part d'une seule preuve confirmée et étend la zone à partir de là. Cette
|
||||
preuve peut être un message d'erreur précis, une trame de pile, ou une entrée de log horodatée.
|
||||
|
||||
C'est l'inverse de la manière dont les enquêtes se déroulent souvent : quelqu'un a une intuition, construit une théorie,
|
||||
puis cherche les preuves qui la soutiennent. L'intuition peut être correcte ; la *méthode* est fragile parce qu'elle
|
||||
fait du biais de confirmation[^2] le comportement par défaut.
|
||||
|
||||
Une tête de pont est un fait sur lequel vous pouvez revenir quand le raisonnement devient flou. Si une déduction vous
|
||||
emmène quelque part d'étrange, vous pouvez remonter jusqu'à la tête de pont et essayer une autre branche. Sans elle,
|
||||
vous ne savez pas quelle étape annuler.
|
||||
|
||||
Quand les preuves sont rares, le skill le dit et bascule en exploration guidée par hypothèses : formuler des hypothèses
|
||||
à partir de ce qui est disponible, identifier ce qui testerait chacune, présenter une liste priorisée de données à
|
||||
collecter. L'absence de preuve est elle-même une constatation.
|
||||
|
||||
## Discipline des hypothèses
|
||||
|
||||
Les hypothèses ne sont jamais supprimées du dossier. Quand une preuve en confirme ou en réfute une, son champ **Statut**
|
||||
passe d'Ouvert à Confirmé ou Réfuté, et une **Résolution** explique quelle preuve a tranché.
|
||||
|
||||
Cette règle a un coût réel : les dossiers grossissent. Le bénéfice est réel aussi. L'historique complet du raisonnement
|
||||
fait partie du livrable. Six mois plus tard, quand un bug similaire surgit, le prochain enquêteur peut lire le dossier
|
||||
original et voir quelles pistes ont déjà été éliminées et pourquoi. Sans cet historique, chaque nouvel enquêteur refait
|
||||
les mêmes impasses.
|
||||
|
||||
Cela discipline aussi l'enquêteur du présent. Si vous ne pouvez pas supprimer une hypothèse fausse, vous devez la
|
||||
réfuter avec une preuve citée. L'abandonner discrètement quand elle devient gênante n'est plus une option.
|
||||
|
||||
## Remettre en question la prémisse
|
||||
|
||||
La description du problème par l'utilisateur est une hypothèse, pas un fait. « Le cache est cassé » est quelque chose
|
||||
que l'utilisateur *croit*. Avant que le skill ne construise une enquête autour, les affirmations techniques sont
|
||||
vérifiées de manière indépendante. Si la preuve contredit la prémisse, le rapport le dit directement.
|
||||
|
||||
C'est l'instinct de l'enquêteur : le récit du témoin est une donnée, pas la vérité. Parfois le bug rapporté est réel
|
||||
mais mal étiqueté. Parfois le symptôme décrit est en aval d'une cause différente. Les enquêtes qui prennent la prémisse
|
||||
pour argent comptant diagnostiquent le mauvais défaut, et le bug revient sous une forme légèrement différente.
|
||||
|
||||
## Une marche calibrée
|
||||
|
||||
Le skill est une seule procédure, pas deux modes. Il calibre la part d'investigation de défaut versus la part
|
||||
d'exploration de zone que l'entrée demande, sur une échelle continue.
|
||||
|
||||
Un cas piloté par symptôme (un ticket, un plantage, un message d'erreur, un « ça marchait avant ») penche vers le suivi
|
||||
d'hypothèses, la reconstruction de la chronologie et une direction de correction. Un cas sans symptôme (comprendre un
|
||||
module avant de le toucher, évaluer la réutilisabilité, bâtir un modèle mental) penche vers la cartographie
|
||||
entrées/sorties, le filtrage du flux de contrôle et un plan de vérification. La plupart des cas réels se situent quelque
|
||||
part entre les deux, et le dossier reflète l'équilibre que les preuves ont exigé.
|
||||
|
||||
La discipline est la même quel que soit l'endroit de l'échelle où se situe un cas : tête de pont d'abord, classement
|
||||
des preuves, suivi des hypothèses, jamais effacer. La sortie est toujours
|
||||
`{implementation_artifacts}/investigations/{slug}-investigation.md`, avec les sections qui ne s'appliquent pas à un cas
|
||||
laissées vides ou omises.
|
||||
|
||||
Quand un bug profond exige de comprendre un sous-système plus large, la procédure intègre en ligne les techniques de
|
||||
cartographie entrées/sorties, de filtrage du flux de contrôle, de raisonnement à rebours depuis les sorties et de
|
||||
traçage des frontières inter-composants[^3]. Le modèle de la zone atterrit dans le même dossier. Pas de changement de
|
||||
mode.
|
||||
|
||||
## La méthodologie vit dans le skill
|
||||
|
||||
La discipline d'enquête est une propriété du skill lui-même. Quiconque invoque `bmad-investigate` adopte la méthodologie
|
||||
et le style de communication pour l'exécution : précision clinique, langage centré sur la preuve, pas de prudence
|
||||
inutile, présentation en dossier de cas. Quand le skill se termine, l'appelant retrouve sa voix d'avant. Pas de
|
||||
changement de persona, juste un déplacement de ton issu des principes du skill.
|
||||
|
||||
Cela compte parce que l'enquête et l'implémentation récompensent des instincts différents. Les enquêteurs sont lents et
|
||||
précis. Les implémenteurs sont rapides et confiants. Le même cerveau faisant les deux dans une seule session finit par
|
||||
mal faire les deux. Le skill délimite la posture d'enquête en ligne, sans changement de contexte vers une identité
|
||||
séparée.
|
||||
|
||||
## Ce que vous obtenez
|
||||
|
||||
Un fichier d'enquête achevé :
|
||||
|
||||
- Sépare les constatations Confirmées (avec citations) des Déductions et des Hypothèses
|
||||
- Préserve toutes les hypothèses jamais formulées, avec leur Statut final et leur Résolution
|
||||
- Reconstruit une chronologie des événements à partir de plusieurs sources de preuves
|
||||
- Identifie les lacunes de données et ce qu'elles résoudraient
|
||||
- Fournit des conclusions actionnables ancrées dans les preuves
|
||||
- Inclut un plan de reproduction quand une cause racine est identifiée
|
||||
- Maintient un backlog d'enquête de pistes encore à explorer
|
||||
|
||||
Donnez-le à un ingénieur qui n'était pas là, et il comprend ce qui s'est passé, ce qui est connu, et ce qui reste
|
||||
incertain. C'est la barre.
|
||||
|
||||
## L'idée plus large
|
||||
|
||||
La plupart du « débogage par IA » d'aujourd'hui mélange preuves, raisonnement et changements de code en un seul flux de
|
||||
texte plausible. Le signal est difficile à trouver, les impasses se répètent, et le dossier, s'il en existe un, est un
|
||||
journal de chat que personne ne veut lire.
|
||||
|
||||
`bmad-investigate` traite l'enquête comme une discipline avec son propre livrable. La preuve a une note. Les hypothèses
|
||||
ont un statut. Les fausses pistes sont documentées, pas effacées. Le dossier survit à la session.
|
||||
|
||||
Quand le prochain bug ressemblant à un que vous avez déjà vu apparaîtra, vous aurez un point de départ qui ne sera pas
|
||||
une invite vide.
|
||||
|
||||
## Glossaire
|
||||
|
||||
[^1]: **Verrouillage narratif** : phénomène cognitif par lequel un raisonnement adopte la première explication plausible
|
||||
et l'enrichit progressivement, devenant de plus en plus difficile à abandonner même face à des preuves contraires.
|
||||
[^2]: **Biais de confirmation** : tendance cognitive à rechercher, interpréter et favoriser les informations qui
|
||||
confirment des croyances préexistantes, tout en ignorant ou minimisant celles qui les contredisent.
|
||||
[^3]: **Passage de frontière** : transition entre deux zones d'exécution distinctes (langage, processus, machine,
|
||||
client/serveur, code/configuration). Les frontières concentrent les bugs car chaque côté suppose que l'autre s'est
|
||||
comporté comme documenté.
|
||||
|
|
@ -5,13 +5,23 @@ sidebar:
|
|||
order: 1
|
||||
---
|
||||
|
||||
La méthode BMad (BMM) est un module de l'écosystème BMad, conçu pour suivre les meilleures pratiques de l'ingénierie du contexte et de la planification. Les agents IA fonctionnent de manière optimale avec un contexte clair et structuré. Le système BMM construit ce contexte progressivement à travers 4 phases distinctes — chaque phase, et plusieurs workflows optionnels au sein de chaque phase, produisent des documents qui alimentent la phase suivante, afin que les agents sachent toujours quoi construire et pourquoi.
|
||||
La méthode BMad (BMM) est un module de l'écosystème BMad, conçu pour suivre les meilleures pratiques de l'ingénierie du
|
||||
contexte et de la planification. Les agents IA fonctionnent de manière optimale avec un contexte clair et structuré. Le
|
||||
système BMM construit ce contexte progressivement à travers 4 phases distinctes — chaque phase, et plusieurs workflows
|
||||
optionnels au sein de chaque phase, produisent des documents qui alimentent la phase suivante, afin que les agents
|
||||
sachent toujours quoi construire et pourquoi.
|
||||
|
||||
La logique et les concepts proviennent des méthodologies agiles qui ont été utilisées avec succès dans l'industrie comme cadre mental de référence.
|
||||
La logique et les concepts proviennent des méthodologies agiles qui ont été utilisées avec succès dans l'industrie comme
|
||||
cadre mental de référence.
|
||||
|
||||
Si à tout moment vous ne savez pas quoi faire, le skill `bmad-help` vous aidera à rester sur la bonne voie ou à savoir quoi faire ensuite. Vous pouvez toujours vous référer à cette page également — mais `bmad-help` est entièrement interactif et beaucoup plus rapide si vous avez déjà installé la méthode BMad. De plus, si vous utilisez différents modules qui ont étendu la méthode BMad ou ajouté d'autres modules complémentaires non extensifs — `bmad-help` évolue pour connaître tout ce qui est disponible et vous donner les meilleurs conseils du moment.
|
||||
Si à tout moment vous ne savez pas quoi faire, le skill `bmad-help` vous aidera à rester sur la bonne voie ou à savoir
|
||||
quoi faire ensuite. Vous pouvez toujours vous référer à cette page également — mais `bmad-help` est entièrement
|
||||
interactif et beaucoup plus rapide si vous avez déjà installé la méthode BMad. De plus, si vous utilisez différents
|
||||
modules qui ont étendu la méthode BMad ou ajouté d'autres modules complémentaires non extensifs — `bmad-help` évolue
|
||||
pour connaître tout ce qui est disponible et vous donner les meilleurs conseils du moment.
|
||||
|
||||
Note finale importante : Chaque workflow ci-dessous peut être exécuté directement avec l'outil de votre choix via un skill ou en chargeant d'abord un agent et en utilisant l'entrée du menu des agents.
|
||||
Note finale importante : Chaque workflow ci-dessous peut être exécuté directement avec l'outil de votre choix via un
|
||||
skill ou en chargeant d'abord un agent et en utilisant l'entrée du menu des agents.
|
||||
|
||||
<iframe src="/workflow-map-diagram-fr.html" title="Diagramme de la carte des workflows de la méthode BMad" width="100%" height="100%" style="border-radius: 8px; border: 1px solid #334155; min-height: 900px;"></iframe>
|
||||
|
||||
|
|
@ -21,14 +31,15 @@ Note finale importante : Chaque workflow ci-dessous peut être exécuté directe
|
|||
|
||||
## Phase 1 : Analyse (Optionnelle)
|
||||
|
||||
Explorez l’espace problème et validez les idées avant de vous engager dans la planification. [**Découvrez ce que fait chaque outil et quand l’utiliser**](../explanation/analysis-phase.md).
|
||||
Explorez l’espace problème et validez les idées avant de vous engager dans la planification. [**Découvrez ce que fait
|
||||
chaque outil et quand l’utiliser**](../explanation/analysis-phase.md).
|
||||
|
||||
| Workflow | Objectif | Produit |
|
||||
|---------------------------------------------------------------------------|------------------------------------------------------------------------------------------|---------------------------|
|
||||
| `bmad-brainstorming` | Brainstormez des idées de projet avec l’accompagnement guidé d’un coach de brainstorming | `brainstorming-report.md` |
|
||||
| `bmad-domain-research`, `bmad-market-research`, `bmad-technical-research` | Validez les hypothèses de marché, techniques ou de domaine | Rapport de recherches |
|
||||
| `bmad-product-brief` | Capturez la vision stratégique — idéal lorsque votre concept est clair | `product-brief.md` |
|
||||
| `bmad-prfaq` | Working Backwards — éprouvez et forgez votre concept produit | `prfaq-{project}.md` |
|
||||
| `bmad-prfaq` | Working Backwards — éprouvez et forgez votre concept produit | `prfaq-{project}.md` |
|
||||
|
||||
## Phase 2 : Planification
|
||||
|
||||
|
|
@ -36,60 +47,75 @@ Définissez ce qu'il faut construire et pour qui.
|
|||
|
||||
| Workflow | Objectif | Produit |
|
||||
|-------------------------|---------------------------------------------------------|--------------|
|
||||
| `bmad-create-prd` | Définissez les exigences (FRs/NFRs)[^1] | `PRD.md`[^2] |
|
||||
| `bmad-create-prd` | Définissez les exigences (FRs/NFRs)[^1] | `PRD.md`[^2] |
|
||||
| `bmad-create-ux-design` | Concevez l'expérience utilisateur (lorsque l'UX compte) | `ux-spec.md` |
|
||||
|
||||
## Phase 3 : Solutioning
|
||||
|
||||
Décidez comment le construire et décomposez le travail en stories.
|
||||
|
||||
| Workflow | Objectif | Produit |
|
||||
|---------------------------------------|---------------------------------------------------|------------------------------|
|
||||
| `bmad-create-architecture` | Rendez les décisions techniques explicites | `architecture.md` avec ADRs[^3] |
|
||||
| `bmad-create-epics-and-stories` | Décomposez les exigences en travail implémentable | Fichiers d'epic avec stories |
|
||||
| `bmad-check-implementation-readiness` | Vérification avant implémentation | Décision Passe/Réserves/Échec |
|
||||
| Workflow | Objectif | Produit |
|
||||
|---------------------------------------|---------------------------------------------------|---------------------------------|
|
||||
| `bmad-create-architecture` | Rendez les décisions techniques explicites | `architecture.md` avec ADRs[^3] |
|
||||
| `bmad-create-epics-and-stories` | Décomposez les exigences en travail implémentable | Fichiers d'epic avec stories |
|
||||
| `bmad-check-implementation-readiness` | Vérification avant implémentation | Décision Passe/Réserves/Échec |
|
||||
|
||||
## Phase 4 : Implémentation
|
||||
|
||||
Construisez, une story à la fois. Bientôt disponible : automatisation complète de la phase 4 !
|
||||
|
||||
| Workflow | Objectif | Produit |
|
||||
|------------------------|-------------------------------------------------------------------------------------|----------------------------------|
|
||||
| `bmad-sprint-planning` | Initialisez le suivi (une fois par projet pour séquencer le cycle de développement) | `sprint-status.yaml` |
|
||||
| `bmad-create-story` | Préparez la story suivante pour implémentation | `story-[slug].md` |
|
||||
| `bmad-dev-story` | Implémentez la story | Code fonctionnel + tests |
|
||||
| `bmad-code-review` | Validez la qualité de l'implémentation | Approuvé ou changements demandés |
|
||||
| `bmad-correct-course` | Gérez les changements significatifs en cours de sprint | Plan mis à jour ou réorientation |
|
||||
| `bmad-sprint-status` | Suivez la progression du sprint et le statut des stories | Mise à jour du statut du sprint |
|
||||
| `bmad-retrospective` | Revue après complétion d'un epic | Leçons apprises |
|
||||
| Workflow | Objectif | Produit |
|
||||
|------------------------|-------------------------------------------------------------------------------------|------------------------------------------------------|
|
||||
| `bmad-sprint-planning` | Initialisez le suivi (une fois par projet pour séquencer le cycle de développement) | `sprint-status.yaml` |
|
||||
| `bmad-create-story` | Préparez la story suivante pour implémentation | `story-[slug].md` |
|
||||
| `bmad-dev-story` | Implémentez la story | Code fonctionnel + tests |
|
||||
| `bmad-code-review` | Validez la qualité de l'implémentation | Approuvé ou changements demandés |
|
||||
| `bmad-correct-course` | Gérez les changements significatifs en cours de sprint | Plan mis à jour ou réorientation |
|
||||
| `bmad-sprint-status` | Suivez la progression du sprint et le statut des stories | Mise à jour du statut du sprint |
|
||||
| `bmad-retrospective` | Revue après complétion d'un epic | Leçons apprises |
|
||||
| `bmad-investigate` | Enquête de cas avec conclusions à preuves graduées, calibrée selon l'entrée | `{slug}-investigation.md` |
|
||||
|
||||
## Quick Dev (Parcours Parallèle)
|
||||
|
||||
Sautez les phases 1-3 pour les travaux de faible envergure et bien compris.
|
||||
|
||||
| Workflow | Objectif | Produit |
|
||||
|------------------|-------------------------------------------------------------------------------------|-----------------------|
|
||||
| Workflow | Objectif | Produit |
|
||||
|------------------|-------------------------------------------------------------------------------------|--------------------|
|
||||
| `bmad-quick-dev` | Flux rapide unifié — clarifie l'intention, planifie, implémente, révise et présente | `spec-*.md` + code |
|
||||
|
||||
## Gestion du Contexte
|
||||
|
||||
Chaque document devient le contexte de la phase suivante. Le PRD[^2] indique à l'architecte quelles contraintes sont importantes. L'architecture indique à l'agent de développement quels modèles suivre. Les fichiers de story fournissent un contexte focalisé et complet pour l'implémentation. Sans cette structure, les agents prennent des décisions incohérentes.
|
||||
Chaque document devient le contexte de la phase suivante. Le PRD[^2] indique à l'architecte quelles contraintes sont
|
||||
importantes. L'architecture indique à l'agent de développement quels modèles suivre. Les fichiers de story fournissent
|
||||
un contexte focalisé et complet pour l'implémentation. Sans cette structure, les agents prennent des décisions
|
||||
incohérentes.
|
||||
|
||||
### Contexte du Projet
|
||||
|
||||
:::tip[Recommandé]
|
||||
Créez `project-context.md` pour vous assurer que les agents IA suivent les règles et préférences de votre projet. Ce fichier fonctionne comme une constitution pour votre projet — il guide les décisions d'implémentation à travers tous les workflows. Ce fichier optionnel peut être généré à la fin de la création de l'architecture, ou dans un projet existant il peut également être généré pour capturer ce qui est important de conserver aligné avec les conventions actuelles.
|
||||
Créez `project-context.md` pour vous assurer que les agents IA suivent les règles et préférences de votre projet. Ce
|
||||
fichier fonctionne comme une constitution pour votre projet — il guide les décisions d'implémentation à travers tous les
|
||||
workflows. Ce fichier optionnel peut être généré à la fin de la création de l'architecture, ou dans un projet existant
|
||||
il peut également être généré pour capturer ce qui est important de conserver aligné avec les conventions actuelles.
|
||||
:::
|
||||
|
||||
**Comment le créer :**
|
||||
|
||||
- **Manuellement** — Créez `_bmad-output/project-context.md` avec votre pile technologique et vos règles d'implémentation
|
||||
- **Générez-le** — Exécutez `bmad-generate-project-context` pour l'auto-générer à partir de votre architecture ou de votre codebase
|
||||
- **Manuellement** — Créez `_bmad-output/project-context.md` avec votre pile technologique et vos règles
|
||||
d'implémentation
|
||||
- **Générez-le** — Exécutez `bmad-generate-project-context` pour l'auto-générer à partir de votre architecture ou de
|
||||
votre codebase
|
||||
|
||||
[**En savoir plus sur project-context.md**](../explanation/project-context.md)
|
||||
|
||||
## Glossaire
|
||||
|
||||
[^1]: FR / NFR (Functional / Non-Functional Requirement) : exigences décrivant respectivement **ce que le système doit faire** (fonctionnalités, comportements attendus) et **comment il doit le faire** (contraintes de performance, sécurité, fiabilité, ergonomie, etc.).
|
||||
[^2]: PRD (Product Requirements Document) : document de référence qui décrit les objectifs du produit, les besoins utilisateurs, les fonctionnalités attendues, les contraintes et les critères de succès, afin d’aligner les équipes sur ce qui doit être construit et pourquoi.
|
||||
[^3]: ADR (Architecture Decision Record) : document qui consigne une décision d’architecture, son contexte, les options envisagées, le choix retenu et ses conséquences, afin d’assurer la traçabilité et la compréhension des décisions techniques dans le temps.
|
||||
[^1]: FR / NFR (Functional / Non-Functional Requirement) : exigences décrivant respectivement **ce que le système doit
|
||||
faire** (fonctionnalités, comportements attendus) et **comment il doit le faire** (contraintes de performance, sécurité,
|
||||
fiabilité, ergonomie, etc.).
|
||||
[^2]: PRD (Product Requirements Document) : document de référence qui décrit les objectifs du produit, les besoins
|
||||
utilisateurs, les fonctionnalités attendues, les contraintes et les critères de succès, afin d’aligner les équipes sur
|
||||
ce qui doit être construit et pourquoi.
|
||||
[^3]: ADR (Architecture Decision Record) : document qui consigne une décision d’architecture, son contexte, les options
|
||||
envisagées, le choix retenu et ses conséquences, afin d’assurer la traçabilité et la compréhension des décisions
|
||||
techniques dans le temps.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ Use `npx bmad-method install` to set up BMad in your project. One command handle
|
|||
|
||||
- **Node.js** 20+ (the installer requires it)
|
||||
- **Git** (for cloning external modules)
|
||||
- **An AI tool** such as Claude Code or Cursor — or install without one using `--tools none`
|
||||
- **An AI tool** such as Claude Code or Cursor (run `npx bmad-method install --list-tools` to see all supported tools)
|
||||
|
||||
:::
|
||||
|
||||
|
|
@ -117,20 +117,23 @@ Under `--yes`, patch and minor upgrades apply automatically. Majors stay frozen
|
|||
|
||||
### Flag reference
|
||||
|
||||
| Flag | Purpose |
|
||||
| ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
|
||||
| `--yes`, `-y` | Skip all prompts; accept flag values + defaults |
|
||||
| `--directory <path>` | Install into this directory (default: current working dir) |
|
||||
| `--modules <a,b,c>` | Exact module set. Core is auto-added. Not a delta — list everything you want kept. |
|
||||
| `--tools <a,b>` or `--tools none` | IDE/tool selection. `none` skips tool config entirely. |
|
||||
| `--action <type>` | `install`, `update`, or `quick-update`. Defaults based on existing install state. |
|
||||
| `--custom-source <urls>` | Install custom modules from Git URLs or local paths |
|
||||
| `--channel <stable\|next>` | Apply to all externals (aliased as `--all-stable` / `--all-next`) |
|
||||
| `--all-stable` | Alias for `--channel=stable` |
|
||||
| `--all-next` | Alias for `--channel=next` |
|
||||
| `--next=<code>` | Put one module on next. Repeatable. |
|
||||
| `--pin <code>=<tag>` | Pin one module to a specific tag. Repeatable. |
|
||||
| `--user-name`, `--communication-language`, `--document-output-language`, `--output-folder` | Override per-user config defaults |
|
||||
| Flag | Purpose |
|
||||
| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--yes`, `-y` | Skip all prompts; accept flag values + defaults |
|
||||
| `--directory <path>` | Install into this directory (default: current working dir) |
|
||||
| `--modules <a,b,c>` | Exact module set. Core is auto-added. Not a delta — list everything you want kept. |
|
||||
| `--tools <a,b>` | IDE/tool selection. Required for fresh `--yes` installs. Run `--list-tools` for valid IDs. |
|
||||
| `--list-tools` | Print all supported tool/IDE IDs (with target directories) and exit. |
|
||||
| `--action <type>` | `install`, `update`, or `quick-update`. Defaults based on existing install state. |
|
||||
| `--custom-source <urls>` | Install custom modules from Git URLs or local paths |
|
||||
| `--channel <stable\|next>` | Apply to all externals (aliased as `--all-stable` / `--all-next`) |
|
||||
| `--all-stable` | Alias for `--channel=stable` |
|
||||
| `--all-next` | Alias for `--channel=next` |
|
||||
| `--next=<code>` | Put one module on next. Repeatable. |
|
||||
| `--pin <code>=<tag>` | Pin one module to a specific tag. Repeatable. |
|
||||
| `--set <module>.<key>=<value>` | Set any module config option non-interactively (preferred — see [Module config overrides](#module-config-overrides)). Repeatable. |
|
||||
| `--list-options [module]` | Print every `--set` key for built-in and locally-cached official modules, then exit. Pass a module code to scope to one module. |
|
||||
| `--user-name`, `--communication-language`, `--document-output-language`, `--output-folder` | Legacy shortcuts equivalent to `--set core.<key>=<value>` (still supported) |
|
||||
|
||||
Precedence when flags overlap: `--pin` beats `--next=` beats `--channel` / `--all-*` beats the registry default (`stable`).
|
||||
|
||||
|
|
@ -165,19 +168,56 @@ npx bmad-method install --yes --modules bmm,bmb --all-next --tools claude-code
|
|||
|
||||
```bash
|
||||
npx bmad-method install --yes --action update \
|
||||
--modules bmm,bmb,gds \
|
||||
--tools none
|
||||
--modules bmm,bmb,gds
|
||||
```
|
||||
|
||||
`--tools` is omitted intentionally — `--action update` reuses the tools configured during the first install.
|
||||
|
||||
**Mix channels — bmb on next, gds on stable:**
|
||||
|
||||
```bash
|
||||
npx bmad-method install --yes --action update \
|
||||
--modules bmm,bmb,cis,gds \
|
||||
--next=bmb \
|
||||
--tools none
|
||||
--next=bmb
|
||||
```
|
||||
|
||||
### Module config overrides
|
||||
|
||||
`--set <module>.<key>=<value>` lets you set any module config option non-interactively. It's repeatable and scales to every module — present and future. The flag is applied as a post-install patch: the installer runs its normal flow first, then `--set` upserts each value into `_bmad/config.toml` (team scope) or `_bmad/config.user.toml` (user scope), and into `_bmad/<module>/config.yaml` so declared values carry forward to the next install.
|
||||
|
||||
**Example — install bmm with explicit project knowledge and skill level:**
|
||||
|
||||
```bash
|
||||
npx bmad-method install --yes \
|
||||
--modules bmm \
|
||||
--tools claude-code \
|
||||
--set bmm.project_knowledge=research \
|
||||
--set bmm.user_skill_level=expert
|
||||
```
|
||||
|
||||
**Discover available keys for a module:**
|
||||
|
||||
```bash
|
||||
npx bmad-method install --list-options bmm
|
||||
```
|
||||
|
||||
`--list-options` (no argument) lists every key the installer can find locally — built-in modules (`core`, `bmm`) plus any currently cached official modules. The cache is per-machine and can be cleared, so previously installed officials won't appear on a fresh checkout or an ephemeral CI worker until they're installed again. Community and custom modules aren't enumerated here; read the module's `module.yaml` directly to see what keys it declares.
|
||||
|
||||
**How it works:**
|
||||
|
||||
- **Routing.** The patch step looks for `[modules.<module>] <key>` (or `[core] <key>`) in `config.user.toml` first; if found there, it updates that file. Otherwise it writes to the team-scope `config.toml`. So user-scope keys (e.g. `core.user_name`, `bmm.user_skill_level`) end up in `config.user.toml` and team-scope keys end up in `config.toml`, matching the partition the installer uses.
|
||||
- **Verbatim values.** The value is written exactly as you provided it — no `result:` template rendering. To get the rendered form (e.g. `{project-root}/research`), pass it explicitly: `--set bmm.project_knowledge='{project-root}/research'`.
|
||||
- **Carry-forward, declared keys.** Values for keys declared in `module.yaml` survive subsequent installs because they're also written to `_bmad/<module>/config.yaml`, which the installer reads as the prompt default on the next run.
|
||||
- **Carry-forward, undeclared keys.** A value for a key the module's schema doesn't declare lands in `config.toml` for the current install but won't be re-emitted on the next install (the manifest writer's schema-strict partition drops unknown keys). Re-pass `--set` if you need it sticky, or edit `_bmad/config.toml` directly.
|
||||
- **No validation.** `single-select` values aren't checked against the allowed choices, and unknown keys aren't rejected — whatever you assert is written.
|
||||
- **Modules not in `--modules`.** Setting a value for a module you didn't include prints a warning and the value is dropped (no file gets created for an uninstalled module).
|
||||
|
||||
The legacy core shortcuts (`--user-name`, `--output-folder`, etc.) still work and remain documented for backward compatibility, but `--set core.user_name=...` is equivalent.
|
||||
|
||||
:::note[Works with quick-update]
|
||||
`--set` is a post-install patch, so it applies the same way regardless of action type. Under `bmad install --action quick-update` (or `--yes` against an existing install, where quick-update is the default), `--set` patches the central config files at the end just like a regular install.
|
||||
:::
|
||||
|
||||
:::caution[Rate limit on shared IPs]
|
||||
Anonymous GitHub API calls are capped at 60/hour per IP. A single install hits the API once per external module to resolve the stable tag. Offices behind NAT, CI runner pools, and VPNs can collectively exhaust this.
|
||||
|
||||
|
|
@ -204,7 +244,7 @@ For cross-machine reproducibility, don't rely on rerunning the same `--modules`
|
|||
|
||||
```bash
|
||||
npx bmad-method install --yes --modules bmb,cis \
|
||||
--pin bmb=v1.7.0 --pin cis=v0.4.2 --tools none
|
||||
--pin bmb=v1.7.0 --pin cis=v0.4.2 --tools claude-code
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ Select **Yes**, then provide a source:
|
|||
| Input Type | Example |
|
||||
| --------------------- | ------------------------------------------------- |
|
||||
| HTTPS URL (any host) | `https://github.com/org/repo` |
|
||||
| HTTP URL (any host) | `http://host/org/repo` |
|
||||
| HTTPS URL with subdir | `https://github.com/org/repo/tree/main/my-module` |
|
||||
| SSH URL | `git@github.com:org/repo.git` |
|
||||
| Local path | `/Users/me/projects/my-module` |
|
||||
|
|
|
|||
|
|
@ -5,13 +5,22 @@ sidebar:
|
|||
order: 1
|
||||
---
|
||||
|
||||
The BMad Method (BMM) is a module in the BMad Ecosystem, targeted at following the best practices of context engineering and planning. AI agents work best with clear, structured context. The BMM system builds that context progressively across 4 distinct phases - each phase, and multiple workflows optionally within each phase, produce documents that inform the next, so agents always know what to build and why.
|
||||
The BMad Method (BMM) is a module in the BMad Ecosystem, targeted at following the best practices of context engineering
|
||||
and planning. AI agents work best with clear, structured context. The BMM system builds that context progressively
|
||||
across 4 distinct phases - each phase, and multiple workflows optionally within each phase, produce documents that
|
||||
inform the next, so agents always know what to build and why.
|
||||
|
||||
The rationale and concepts come from agile methodologies that have been used across the industry with great success as a mental framework.
|
||||
The rationale and concepts come from agile methodologies that have been used across the industry with great success as a
|
||||
mental framework.
|
||||
|
||||
If at any time you are unsure what to do, the `bmad-help` skill will help you stay on track or know what to do next. You can always refer to this for reference also - but `bmad-help` is fully interactive and much quicker if you have already installed the BMad Method. Additionally, if you are using different modules that have extended the BMad Method or added other complementary non-extension modules - `bmad-help` evolves to know all that is available to give you the best in-the-moment advice.
|
||||
If at any time you are unsure what to do, the `bmad-help` skill will help you stay on track or know what to do next. You
|
||||
can always refer to this for reference also - but `bmad-help` is fully interactive and much quicker if you have already
|
||||
installed the BMad Method. Additionally, if you are using different modules that have extended the BMad Method or added
|
||||
other complementary non-extension modules - `bmad-help` evolves to know all that is available to give you the best
|
||||
in-the-moment advice.
|
||||
|
||||
Final important note: Every workflow below can be run directly with your tool of choice via skill or by loading an agent first and using the entry from the agents menu.
|
||||
Final important note: Every workflow below can be run directly with your tool of choice via skill or by loading an agent
|
||||
first and using the entry from the agents menu.
|
||||
|
||||
<iframe src="/workflow-map-diagram.html" title="BMad Method Workflow Map Diagram" width="100%" height="100%" style="border-radius: 8px; border: 1px solid #334155; min-height: 900px;"></iframe>
|
||||
|
||||
|
|
@ -21,30 +30,31 @@ Final important note: Every workflow below can be run directly with your tool of
|
|||
|
||||
## Phase 1: Analysis (Optional)
|
||||
|
||||
Explore the problem space and validate ideas before committing to planning. [**Learn what each tool does and when to use it**](../explanation/analysis-phase.md).
|
||||
Explore the problem space and validate ideas before committing to planning. [**Learn what each tool does and when to use
|
||||
it**](../explanation/analysis-phase.md).
|
||||
|
||||
| Workflow | Purpose | Produces |
|
||||
| ------------------------------- | -------------------------------------------------------------------------- | ------------------------- |
|
||||
| `bmad-brainstorming` | Brainstorm Project Ideas with guided facilitation of a brainstorming coach | `brainstorming-report.md` |
|
||||
| `bmad-domain-research`, `bmad-market-research`, `bmad-technical-research` | Validate market, technical, or domain assumptions | Research findings |
|
||||
| `bmad-product-brief` | Capture strategic vision — best when your concept is clear | `product-brief.md` |
|
||||
| `bmad-prfaq` | Working Backwards — stress-test and forge your product concept | `prfaq-{project}.md` |
|
||||
| Workflow | Purpose | Produces |
|
||||
|---------------------------------------------------------------------------|----------------------------------------------------------------------------|---------------------------|
|
||||
| `bmad-brainstorming` | Brainstorm Project Ideas with guided facilitation of a brainstorming coach | `brainstorming-report.md` |
|
||||
| `bmad-domain-research`, `bmad-market-research`, `bmad-technical-research` | Validate market, technical, or domain assumptions | Research findings |
|
||||
| `bmad-product-brief` | Capture strategic vision — best when your concept is clear | `product-brief.md` |
|
||||
| `bmad-prfaq` | Working Backwards — stress-test and forge your product concept | `prfaq-{project}.md` |
|
||||
|
||||
## Phase 2: Planning
|
||||
|
||||
Define what to build and for whom.
|
||||
|
||||
| Workflow | Purpose | Produces |
|
||||
| --------------------------- | ---------------------------------------- | ------------ |
|
||||
| `bmad-create-prd` | Define requirements (FRs/NFRs) | `PRD.md` |
|
||||
| `bmad-create-ux-design` | Design user experience (when UX matters) | `ux-spec.md` |
|
||||
| Workflow | Purpose | Produces |
|
||||
|-------------------------|------------------------------------------|--------------|
|
||||
| `bmad-create-prd` | Define requirements (FRs/NFRs) | `PRD.md` |
|
||||
| `bmad-create-ux-design` | Design user experience (when UX matters) | `ux-spec.md` |
|
||||
|
||||
## Phase 3: Solutioning
|
||||
|
||||
Decide how to build it and break work into stories.
|
||||
|
||||
| Workflow | Purpose | Produces |
|
||||
| ----------------------------------------- | ------------------------------------------ | --------------------------- |
|
||||
| Workflow | Purpose | Produces |
|
||||
|---------------------------------------|--------------------------------------------|-----------------------------|
|
||||
| `bmad-create-architecture` | Make technical decisions explicit | `architecture.md` with ADRs |
|
||||
| `bmad-create-epics-and-stories` | Break requirements into implementable work | Epic files with stories |
|
||||
| `bmad-check-implementation-readiness` | Gate check before implementation | PASS/CONCERNS/FAIL decision |
|
||||
|
|
@ -53,32 +63,38 @@ Decide how to build it and break work into stories.
|
|||
|
||||
Build it, one story at a time. Coming soon, full phase 4 automation!
|
||||
|
||||
| Workflow | Purpose | Produces |
|
||||
| -------------------------- | ------------------------------------------------------------------------ | -------------------------------- |
|
||||
| `bmad-sprint-planning` | Initialize tracking (once per project to sequence the dev cycle) | `sprint-status.yaml` |
|
||||
| `bmad-create-story` | Prepare next story for implementation | `story-[slug].md` |
|
||||
| `bmad-dev-story` | Implement the story | Working code + tests |
|
||||
| `bmad-code-review` | Validate implementation quality | Approved or changes requested |
|
||||
| `bmad-correct-course` | Handle significant mid-sprint changes | Updated plan or re-routing |
|
||||
| `bmad-sprint-status` | Track sprint progress and story status | Sprint status update |
|
||||
| `bmad-retrospective` | Review after epic completion | Lessons learned |
|
||||
| Workflow | Purpose | Produces |
|
||||
|------------------------|-------------------------------------------------------------------------------|------------------------------------------------------|
|
||||
| `bmad-sprint-planning` | Initialize tracking (once per project to sequence the dev cycle) | `sprint-status.yaml` |
|
||||
| `bmad-create-story` | Prepare next story for implementation | `story-[slug].md` |
|
||||
| `bmad-dev-story` | Implement the story | Working code + tests |
|
||||
| `bmad-code-review` | Validate implementation quality | Approved or changes requested |
|
||||
| `bmad-correct-course` | Handle significant mid-sprint changes | Updated plan or re-routing |
|
||||
| `bmad-sprint-status` | Track sprint progress and story status | Sprint status update |
|
||||
| `bmad-retrospective` | Review after epic completion | Lessons learned |
|
||||
| `bmad-investigate` | Forensic case investigation with evidence-graded findings, calibrated to the input | `{slug}-investigation.md` |
|
||||
|
||||
## Quick Flow (Parallel Track)
|
||||
|
||||
Skip phases 1-3 for small, well-understood work.
|
||||
|
||||
| Workflow | Purpose | Produces |
|
||||
| ------------------ | --------------------------------------------------------------------------- | ---------------------- |
|
||||
| `bmad-quick-dev` | Unified quick flow — clarify intent, plan, implement, review, and present | `spec-*.md` + code |
|
||||
| Workflow | Purpose | Produces |
|
||||
|------------------|---------------------------------------------------------------------------|--------------------|
|
||||
| `bmad-quick-dev` | Unified quick flow — clarify intent, plan, implement, review, and present | `spec-*.md` + code |
|
||||
|
||||
## Context Management
|
||||
|
||||
Each document becomes context for the next phase. The PRD tells the architect what constraints matter. The architecture tells the dev agent which patterns to follow. Story files give focused, complete context for implementation. Without this structure, agents make inconsistent decisions.
|
||||
Each document becomes context for the next phase. The PRD tells the architect what constraints matter. The architecture
|
||||
tells the dev agent which patterns to follow. Story files give focused, complete context for implementation. Without
|
||||
this structure, agents make inconsistent decisions.
|
||||
|
||||
### Project Context
|
||||
|
||||
:::tip[Recommended]
|
||||
Create `project-context.md` to ensure AI agents follow your project's rules and preferences. This file works like a constitution for your project — it guides implementation decisions across all workflows. This optional file can be generated at the end of Architecture Creation, or in an existing project it can be generated also to capture whats important to keep aligned with current conventions.
|
||||
Create `project-context.md` to ensure AI agents follow your project's rules and preferences. This file works like a
|
||||
constitution for your project — it guides implementation decisions across all workflows. This optional file can be
|
||||
generated at the end of Architecture Creation, or in an existing project it can be generated also to capture whats
|
||||
important to keep aligned with current conventions.
|
||||
:::
|
||||
|
||||
**How to create it:**
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ Chọn **Yes**, rồi nhập nguồn:
|
|||
| Loại đầu vào | Ví dụ |
|
||||
| --------------------- | ------------------------------------------------- |
|
||||
| HTTPS URL trên bất kỳ host nào | `https://github.com/org/repo` |
|
||||
| HTTP URL trên bất kỳ host nào | `http://host/org/repo` |
|
||||
| HTTPS URL trỏ vào một thư mục con | `https://github.com/org/repo/tree/main/my-module` |
|
||||
| SSH URL | `git@github.com:org/repo.git` |
|
||||
| Đường dẫn cục bộ | `/Users/me/projects/my-module` |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
---
|
||||
title: "命名智能体"
|
||||
description: 为什么 BMad 的智能体有名字、人设和自定义能力——相比菜单驱动或纯提示驱动的方案,这解锁了哪些可能性
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
你说"嘿 Mary,咱们来头脑风暴",Mary 就激活了。她用你配置的语言、以她独特的人设向你打招呼,并提醒你随时可以用 `bmad-help`。然后她跳过菜单,直接进入头脑风暴——因为你的意图已经足够明确。
|
||||
|
||||
这一页解释背后发生了什么,以及 BMad 为什么这样设计。
|
||||
|
||||
## 三足鼎立
|
||||
|
||||
BMad 的智能体模型建立在三个可组合的基本要素之上:
|
||||
|
||||
| 要素 | 提供什么 | 所在位置 |
|
||||
|---|---|---|
|
||||
| **技能(Skill)** | 能力——一项智能体能做的具体事(头脑风暴、撰写 PRD、实现 story) | `.claude/skills/{skill-name}/SKILL.md`(或你所用 IDE 的等价位置) |
|
||||
| **命名智能体(Named Agent)** | 人设连续性——一个可辨识的身份,把一组相关技能包装在统一的语气、原则和视觉标识下 | 目录名以 `bmad-agent-*` 开头的技能 |
|
||||
| **自定义(Customization)** | 让它成为你的——覆盖选项可以重塑智能体行为、添加 MCP 集成、替换模板、叠加组织规范 | `_bmad/custom/{skill-name}.toml`(团队提交的覆盖)和 `.user.toml`(个人,已 gitignore) |
|
||||
|
||||
抽掉任何一条腿,体验就会坍塌:
|
||||
|
||||
- 有技能没智能体 → 用户只能靠名称或编号在能力列表里自行查找
|
||||
- 有智能体没技能 → 空有人设,没有能力
|
||||
- 没有自定义 → 所有人用一模一样的开箱默认,任何组织特有需求都只能靠 fork
|
||||
|
||||
## 命名智能体带来了什么
|
||||
|
||||
BMad 内置六个命名智能体,各自对应 BMad Method 的一个阶段:
|
||||
|
||||
| 智能体 | 阶段 | 模块 |
|
||||
|---|---|---|
|
||||
| 📊 **Mary**,商业分析师 | 分析 | 市场调研、头脑风暴、产品摘要、PRFAQ |
|
||||
| 📚 **Paige**,技术文档工程师 | 分析 | 项目文档、流程图、文档校验 |
|
||||
| 📋 **John**,产品经理 | 规划 | PRD 创建、Epic/Story 拆分、实施就绪评审 |
|
||||
| 🎨 **Sally**,UX 设计师 | 规划 | UX 设计规范 |
|
||||
| 🏗️ **Winston**,系统架构师 | 方案设计 | 技术架构、一致性检查 |
|
||||
| 💻 **Amelia**,高级工程师 | 实现 | Story 执行、快速开发、代码评审、Sprint 规划 |
|
||||
|
||||
每位智能体都有硬编码的身份(名字、职衔、专业领域)和可自定义的层(角色、原则、沟通风格、图标、菜单)。你可以重写 Mary 的原则或添加菜单项,但无法改她的名字——这是刻意为之的。品牌辨识度经得起自定义,所以"嘿 Mary"永远激活分析师,无论团队怎样塑造她的行为。
|
||||
|
||||
## 激活流程
|
||||
|
||||
调用命名智能体时,八个步骤依次执行:
|
||||
|
||||
1. **解析智能体配置** — 通过 Python 解析器(使用 stdlib `tomllib`)将内置 `customize.toml` 与团队覆盖和个人覆盖合并
|
||||
2. **执行前置步骤** — 团队配置的任何预处理行为
|
||||
3. **采用人设** — 硬编码身份加上自定义的角色、沟通风格、原则
|
||||
4. **加载持久化事实** — 组织规则、合规说明,可通过 `file:` 前缀加载文件(如 `file:{project-root}/docs/project-context.md`)
|
||||
5. **加载配置** — 用户名、沟通语言、输出语言、产物路径
|
||||
6. **打招呼** — 个性化问候,使用配置的语言,带上智能体的 emoji 前缀让你一眼认出谁在说话
|
||||
7. **执行后置步骤** — 团队配置的任何问候后设置
|
||||
8. **分发或展示菜单** — 如果你的开场消息能匹配某个菜单项,直接执行;否则展示菜单等待输入
|
||||
|
||||
第 8 步是意图与能力的交汇点。"嘿 Mary,咱们来头脑风暴"之所以跳过菜单渲染,是因为 `bmad-brainstorming` 显然对应 Mary 菜单上的 `BP`。如果你说的比较模糊,她会简短问一句,而不是走确认仪式。如果完全不匹配,她会正常继续对话。
|
||||
|
||||
## 为什么不只用菜单?
|
||||
|
||||
菜单迫使用户迁就工具。你得记住头脑风暴在分析师智能体的 `BP` 编码下,而不是 PM 智能体上,还得知道哪个人设负责哪些功能。这些都是工具强加给你的认知负担。
|
||||
|
||||
命名智能体把这个关系反转了。你用任何自然的方式,对着某个人说你想做什么。智能体知道自己是谁、能做什么。当你的意图足够清晰,她就直接开始。
|
||||
|
||||
菜单仍然作为兜底存在——探索时展示,确定时跳过。
|
||||
|
||||
## 为什么不直接用空白提示?
|
||||
|
||||
空白提示假设你知道"魔法咒语"。"帮我头脑风暴"也许有用,但"帮我发散下我这个 SaaS 创意"可能就不灵了,而结果取决于你怎么措辞。你变成了提示工程师。
|
||||
|
||||
命名智能体在不牺牲自由度的前提下增加了结构。人设保持一致,能力随时可发现,`bmad-help` 永远只差一个命令。你不用猜智能体能做什么,也不需要翻手册才能用它。
|
||||
|
||||
## 自定义是一等公民
|
||||
|
||||
自定义模型让这套方案能从单个开发者扩展到整个组织。
|
||||
|
||||
每个智能体自带 `customize.toml` 及合理默认值。团队在 `_bmad/custom/bmad-agent-{role}.toml` 中提交覆盖。个人可以在 `.user.toml`(已 gitignore)中叠加偏好。解析器在激活时按可预测的结构化规则合并三层配置。
|
||||
|
||||
大多数用户从不需要手写这些文件。`bmad-customize` 技能会引导你选择目标、区分智能体/工作流作用域、撰写覆盖、验证合并结果——让自定义能力对任何理解自己意图的人开放,不限于精通 TOML 的人。
|
||||
|
||||
举个例子:团队提交一个文件,告诉 Amelia 查库文档时一律用 Context7 MCP 工具,本地 epics 列表找不到 story 时回退到 Linear。Amelia 分发的每个开发工作流(dev-story、quick-dev、create-story、code-review)都继承这些行为,无需改源码、无需逐工作流重复配置。
|
||||
|
||||
此外还有第二个自定义面,用于**跨领域关注点**:中央配置 `_bmad/config.toml` 和 `_bmad/config.user.toml`(由安装器维护,从每个模块的 `module.yaml` 重建)加上 `_bmad/custom/config.toml`(团队提交)和 `_bmad/custom/config.user.toml`(个人,已 gitignore)作为覆盖。这里存放着 **智能体花名册** ——轻量级描述符,`bmad-party-mode`、`bmad-retrospective` 和 `bmad-advanced-elicitation` 等花名册消费者读取它来了解有哪些智能体可用、如何扮演它们。用团队覆盖在全组织范围重新定义某个智能体;用 `.user.toml` 覆盖添加虚构角色(Kirk、Spock、领域专家)作为个人实验——无需碰任何技能目录。每个技能的配置文件塑造 Mary **激活时的行为**;中央配置塑造其他技能**查看花名册时看到的 Mary**。
|
||||
|
||||
完整自定义文档和实操示例请参见:
|
||||
|
||||
- [如何自定义 BMad](../how-to/customize-bmad.md) — 可自定义项和合并规则的参考
|
||||
- [如何为组织扩展 BMad](../how-to/expand-bmad-for-your-org.md) — 五个实操方案,覆盖智能体全局规则、工作流约定、外部发布、模板替换和花名册管理
|
||||
- `bmad-customize` 技能 — 引导式编写助手,将你的意图转换为正确放置并经过验证的覆盖文件
|
||||
|
||||
## 更大的理念
|
||||
|
||||
当今大多数 AI 助手要么是菜单,要么是提示框,两者都把认知负担推给了用户。命名智能体加上可自定义技能,让你可以和一个了解项目的队友对话,并且让你的组织能塑造这个队友而不必 fork。
|
||||
|
||||
下次你输入"嘿 Mary,咱们来头脑风暴",她直接上手干活时,留意一下哪些事情**没有**发生。没有斜杠命令,没有菜单要翻,没有尴尬的功能介绍。这种"无感",正是设计本身。
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
---
|
||||
title: "如何为组织扩展 BMad"
|
||||
description: 五个自定义方案,无需 fork 即可重塑 BMad——涵盖智能体全局规则、工作流约定、外部发布、模板替换和花名册变更
|
||||
sidebar:
|
||||
order: 9
|
||||
---
|
||||
|
||||
BMad 的自定义机制让组织无需编辑已安装文件或 fork 技能就能重塑行为。本指南介绍五个方案,覆盖大部分企业级需求。
|
||||
|
||||
:::note[前置条件]
|
||||
|
||||
- 已在项目中安装 BMad(参见[如何安装 BMad](./install-bmad.md))
|
||||
- 熟悉自定义模型(参见[如何自定义 BMad](./customize-bmad.md))
|
||||
- PATH 中有 Python 3.11+(解析器只用标准库,不需要 `pip install`)
|
||||
:::
|
||||
|
||||
:::tip[如何应用这些方案]
|
||||
下面的**逐技能方案**(方案 1–4)可以通过运行 `bmad-customize` 技能并描述意图来应用——它会选择正确的配置面、生成覆盖文件并验证合并结果。方案 5(中央配置的花名册覆盖)超出 v1 技能范围,仍需手动编写。本文档中的方案是覆盖**什么**的权威参考;`bmad-customize` 负责处理**怎么做**的部分(针对智能体/工作流层面)。
|
||||
:::
|
||||
|
||||
## 三层心智模型
|
||||
|
||||
在选择方案之前,先理解你的覆盖落在哪一层:
|
||||
|
||||
| 层 | 覆盖文件位置 | 作用范围 |
|
||||
|---|---|---|
|
||||
| **智能体**(如 Amelia、Mary、John) | `_bmad/custom/bmad-agent-{role}.toml` 中的 `[agent]` 段 | 跟随人设进入**该智能体分发的每个工作流** |
|
||||
| **工作流**(如 product-brief、create-prd) | `_bmad/custom/{workflow-name}.toml` 中的 `[workflow]` 段 | 仅作用于该工作流的单次运行 |
|
||||
| **中央配置** | `_bmad/custom/config.toml` 中的 `[agents.*]`、`[core]`、`[modules.*]` | 花名册(party-mode、retrospective、elicitation 可用的角色)、全组织统一的安装设置 |
|
||||
|
||||
经验法则:如果规则应当在工程师做任何开发工作时生效,就自定义**开发智能体**。如果只在撰写产品摘要时生效,就自定义 **product-brief 工作流**。如果要改变"谁在场"(重命名智能体、添加自定义角色、统一产物路径),就编辑**中央配置**。
|
||||
|
||||
## 方案 1:让智能体的规则贯穿其分发的所有工作流
|
||||
|
||||
**场景:** 统一工具使用和外部系统集成,让智能体分发的每个工作流都继承这些行为。这是影响面最大的模式。
|
||||
|
||||
**示例:Amelia(开发智能体)查库文档一律用 Context7,本地 epics 列表找不到 story 时回退到 Linear。**
|
||||
|
||||
```toml
|
||||
# _bmad/custom/bmad-agent-dev.toml
|
||||
|
||||
[agent]
|
||||
|
||||
# 每次激活时加载。传递到 dev-story、quick-dev、
|
||||
# create-story、code-review、qa-generate——Amelia 分发的每个技能。
|
||||
persistent_facts = [
|
||||
"For any library documentation lookup (React, TypeScript, Zod, Prisma, etc.), call the context7 MCP tool (`mcp__context7__resolve_library_id` then `mcp__context7__get_library_docs`) before relying on training-data knowledge. Up-to-date docs trump memorized APIs.",
|
||||
"When a story reference isn't found in {planning_artifacts}/epics-and-stories.md, search Linear via `mcp__linear__search_issues` using the story ID or title before asking the user to clarify. If Linear returns a match, treat it as the authoritative story source.",
|
||||
]
|
||||
```
|
||||
|
||||
**为什么有效:** 两句话就能重塑组织内所有开发工作流,无需逐工作流重复配置、无需改源码。每个新工程师拉下仓库就自动继承这些约定。
|
||||
|
||||
**团队文件 vs 个人文件:**
|
||||
- `bmad-agent-dev.toml`:提交到 git,对整个团队生效
|
||||
- `bmad-agent-dev.user.toml`:已 gitignore,个人偏好叠加在上面
|
||||
|
||||
## 方案 2:在特定工作流中强制执行组织规范
|
||||
|
||||
**场景:** 塑造工作流输出的*内容*,使其满足合规、审计或下游消费者的要求。
|
||||
|
||||
**示例:每份产品摘要都必须包含合规字段,智能体知晓组织的发布规范。**
|
||||
|
||||
```toml
|
||||
# _bmad/custom/bmad-product-brief.toml
|
||||
|
||||
[workflow]
|
||||
|
||||
persistent_facts = [
|
||||
"Every brief must include an 'Owner' field, a 'Target Release' field, and a 'Security Review Status' field.",
|
||||
"Non-commercial briefs (internal tools, research projects) must still include a user-value section, but can omit market differentiation.",
|
||||
"file:{project-root}/docs/enterprise/brief-publishing-conventions.md",
|
||||
]
|
||||
```
|
||||
|
||||
**效果:** 这些事实在工作流激活的第 3 步加载。当智能体起草摘要时,它已了解必填字段和企业规范文档。内置默认值(`file:{project-root}/**/project-context.md`)仍会加载,因为这是追加操作。
|
||||
|
||||
## 方案 3:将完成的产出发布到外部系统
|
||||
|
||||
**场景:** 工作流生成输出后,自动发布到企业级记录系统(Confluence、Notion、SharePoint)并创建后续工作项(Jira、Linear、Asana)。
|
||||
|
||||
**示例:摘要自动发布到 Confluence,并提供可选的 Jira Epic 创建。**
|
||||
|
||||
```toml
|
||||
# _bmad/custom/bmad-product-brief.toml
|
||||
|
||||
[workflow]
|
||||
|
||||
# 终端钩子。标量覆盖会整体替换空默认值。
|
||||
on_complete = """
|
||||
Publish and offer follow-up:
|
||||
|
||||
1. Read the finalized brief file path from the prior step.
|
||||
2. Call `mcp__atlassian__confluence_create_page` with:
|
||||
- space: "PRODUCT"
|
||||
- parent: "Product Briefs"
|
||||
- title: the brief's title
|
||||
- body: the brief's markdown contents
|
||||
Capture the returned page URL.
|
||||
3. Tell the user: "Brief published to Confluence: <url>".
|
||||
4. Ask: "Want me to open a Jira epic for this brief now?"
|
||||
5. If yes, call `mcp__atlassian__jira_create_issue` with:
|
||||
- type: "Epic"
|
||||
- project: "PROD"
|
||||
- summary: the brief's title
|
||||
- description: a short summary plus a link back to the Confluence page.
|
||||
Report the epic key and URL.
|
||||
6. If no, exit cleanly.
|
||||
|
||||
If either MCP tool fails, report the failure, print the brief path,
|
||||
and ask the user to publish manually.
|
||||
"""
|
||||
```
|
||||
|
||||
**为什么用 `on_complete` 而不是 `activation_steps_append`:** `on_complete` 只在终端阶段运行一次,在工作流主输出写入之后。这是发布产物的正确时机。`activation_steps_append` 在每次激活时运行,在工作流开始之前。
|
||||
|
||||
**权衡:**
|
||||
- **Confluence 发布是非破坏性的**,完成时始终运行
|
||||
- **Jira Epic 创建对全团队可见**,会触发 Sprint 规划信号,因此需用户确认
|
||||
- **优雅降级:** 如果 MCP 工具失败,交给用户手动处理,而不是静默丢弃输出
|
||||
|
||||
## 方案 4:替换为你自己的输出模板
|
||||
|
||||
**场景:** 默认输出结构不符合组织期望的格式,或同一仓库中不同团队需要不同模板。
|
||||
|
||||
**示例:将 product-brief 工作流指向企业自有模板。**
|
||||
|
||||
```toml
|
||||
# _bmad/custom/bmad-product-brief.toml
|
||||
|
||||
[workflow]
|
||||
brief_template = "{project-root}/docs/enterprise/brief-template.md"
|
||||
```
|
||||
|
||||
**原理:** 工作流自带的 `customize.toml` 中 `brief_template = "resources/brief-template.md"`(裸路径,从技能根目录解析)。你的覆盖指向 `{project-root}` 下的文件,智能体在第 4 步读取你的模板而非内置模板。
|
||||
|
||||
**模板编写建议:**
|
||||
- 将模板放在 `{project-root}/docs/` 或 `{project-root}/_bmad/custom/templates/` 下,使它们与覆盖文件一起版本管理
|
||||
- 沿用内置模板的结构约定(章节标题、frontmatter),智能体会适配实际内容
|
||||
- 对于多团队仓库,使用 `.user.toml` 让各团队指向自己的模板,无需改动已提交的团队文件
|
||||
|
||||
## 方案 5:自定义花名册
|
||||
|
||||
**场景:** 改变 `bmad-party-mode`、`bmad-retrospective` 和 `bmad-advanced-elicitation` 等花名册驱动技能中*谁在场*,无需编辑源码或 fork。以下是三种常见变体。
|
||||
|
||||
### 5a. 在全组织范围内重塑 BMad 智能体
|
||||
|
||||
每个真实智能体都有一段安装器从 `module.yaml` 合成的描述符。覆盖它可以在所有花名册消费者中改变语气和定位:
|
||||
|
||||
```toml
|
||||
# _bmad/custom/config.toml(提交到 git——对每个开发者生效)
|
||||
|
||||
[agents.bmad-agent-analyst]
|
||||
description = "Mary the Regulatory-Aware Business Analyst — channels Porter and Minto, but lives and breathes FDA audit trails. Speaks like a forensic investigator presenting a case file."
|
||||
```
|
||||
|
||||
Party-mode 会用新描述来生成 Mary。分析师激活流程本身不受影响,因为 Mary 的行为由她的每技能 `customize.toml` 控制。这个覆盖改变的是**外部技能如何感知和介绍她**,而不是她的内部工作方式。
|
||||
|
||||
### 5b. 添加虚构或自定义智能体
|
||||
|
||||
一段完整的描述符就足以让花名册功能识别,不需要技能目录。适合在 party mode 或头脑风暴中增加性格多样性:
|
||||
|
||||
```toml
|
||||
# _bmad/custom/config.user.toml(个人——已 gitignore)
|
||||
|
||||
[agents.spock]
|
||||
team = "startrek"
|
||||
name = "Commander Spock"
|
||||
title = "Science Officer"
|
||||
icon = "🖖"
|
||||
description = "Logic first, emotion suppressed. Begins observations with 'Fascinating.' Never rounds up. Counterpoint to any argument that relies on gut instinct."
|
||||
|
||||
[agents.mccoy]
|
||||
team = "startrek"
|
||||
name = "Dr. Leonard McCoy"
|
||||
title = "Chief Medical Officer"
|
||||
icon = "⚕️"
|
||||
description = "Country doctor's warmth, short fuse. 'Dammit Jim, I'm a doctor not a ___.' Ethics-driven counterweight to Spock."
|
||||
```
|
||||
|
||||
让 party-mode "邀请企业号船员",它会按 `team = "startrek"` 过滤并生成 Spock 和 McCoy。真实的 BMad 智能体(Mary、Amelia)也可以同桌。
|
||||
|
||||
### 5c. 锁定团队安装设置
|
||||
|
||||
安装器会向每个开发者提示 `planning_artifacts` 路径等值。当组织需要一个统一答案时,在中央配置中锁定——任何开发者本地的提示回答都会在解析时被覆盖:
|
||||
|
||||
```toml
|
||||
# _bmad/custom/config.toml
|
||||
|
||||
[modules.bmm]
|
||||
planning_artifacts = "{project-root}/shared/planning"
|
||||
implementation_artifacts = "{project-root}/shared/implementation"
|
||||
|
||||
[core]
|
||||
document_output_language = "English"
|
||||
```
|
||||
|
||||
个人设置如 `user_name`、`communication_language` 或 `user_skill_level` 留在各开发者自己的 `_bmad/config.user.toml` 中。团队文件不应触碰这些。
|
||||
|
||||
**为什么用中央配置而不是逐智能体的 customize.toml:** 逐智能体文件塑造*一个*智能体激活时的行为。中央配置塑造花名册消费者*查看全局时看到的内容:*有哪些智能体、叫什么、属于哪个团队,以及整个仓库共识的安装设置。两个层面,各司其职。
|
||||
|
||||
## 在 IDE 会话文件中强化全局规则
|
||||
|
||||
BMad 的自定义在技能激活时加载。许多 IDE 工具还会在**每次会话开始时**加载一个全局指令文件,在任何技能运行之前(`CLAUDE.md`、`AGENTS.md`、`.cursor/rules/`、`.github/copilot-instructions.md` 等)。对于即使在 BMad 技能之外也应生效的规则,请在全局指令中也声明一份。
|
||||
|
||||
**何时需要"双重声明":**
|
||||
- 规则足够重要,即使在普通对话(没有激活技能)中也应遵守
|
||||
- 你需要"双保险",因为模型的训练数据默认值可能会拉偏方向
|
||||
- 规则足够精简,重复一次不会让会话文件臃肿
|
||||
|
||||
**示例:在仓库的 `CLAUDE.md` 中强化方案 1 的开发智能体规则。**
|
||||
|
||||
```markdown
|
||||
<!-- Any file-read of library docs goes through the context7 MCP tool
|
||||
(`mcp__context7__resolve_library_id` then `mcp__context7__get_library_docs`)
|
||||
before relying on training-data knowledge. -->
|
||||
```
|
||||
|
||||
一句话,每次会话加载。它与 `bmad-agent-dev.toml` 自定义配合,使规则在 Amelia 的工作流内和与助手的临时对话中都生效。各层各管各的范围:
|
||||
|
||||
| 层 | 作用范围 | 用途 |
|
||||
|---|---|---|
|
||||
| IDE 会话文件(`CLAUDE.md` / `AGENTS.md`) | 每次会话,在任何技能激活之前 | 简短的、应在 BMad 之外也生效的通用规则 |
|
||||
| BMad 智能体自定义 | 该智能体分发的每个工作流 | 智能体人设相关的行为 |
|
||||
| BMad 工作流自定义 | 单次工作流运行 | 工作流特定的输出格式、发布钩子、模板 |
|
||||
| BMad 中央配置 | 花名册 + 共享安装设置 | 谁在场、团队使用的共享路径 |
|
||||
|
||||
IDE 会话文件要**精简**。十几行精挑细选的规则比长篇大论有效得多。模型每轮都会读取它,噪声会淹没信号。
|
||||
|
||||
## 组合使用
|
||||
|
||||
五个方案可以自由组合。一个典型的企业级 `bmad-product-brief` 覆盖可能同时设置 `persistent_facts`(方案 2)、`on_complete`(方案 3)和 `brief_template`(方案 4)。智能体级规则(方案 1)在另一个以智能体命名的文件中,中央配置(方案 5)锁定共享花名册和团队设置,四者并行生效。
|
||||
|
||||
```toml
|
||||
# _bmad/custom/bmad-product-brief.toml(工作流级)
|
||||
|
||||
[workflow]
|
||||
persistent_facts = ["..."]
|
||||
brief_template = "{project-root}/docs/enterprise/brief-template.md"
|
||||
on_complete = """ ... """
|
||||
```
|
||||
|
||||
```toml
|
||||
# _bmad/custom/bmad-agent-analyst.toml(智能体级——Mary 分发 product-brief)
|
||||
|
||||
[agent]
|
||||
persistent_facts = ["Always include a 'Regulatory Review' section when the domain involves healthcare, finance, or children's data."]
|
||||
```
|
||||
|
||||
效果:Mary 在人设激活时加载监管评审规则。当用户选择 product-brief 菜单项时,工作流加载自己的规范、写入企业模板,完成后发布到 Confluence。每一层各有贡献,且无一需要编辑 BMad 源码。
|
||||
|
||||
## 故障排查
|
||||
|
||||
**覆盖没有生效?** 检查文件是否在 `_bmad/custom/` 下且使用了准确的技能目录名(如 `bmad-agent-dev.toml`,而非 `bmad-dev.toml`)。参见[如何自定义 BMad](./customize-bmad.md)。
|
||||
|
||||
**MCP 工具名称不确定?** 使用 MCP 服务器在当前会话中暴露的准确名称。如果不确定,让 Claude Code 列出可用的 MCP 工具。在 `persistent_facts` 或 `on_complete` 中硬编码的名称,在 MCP 服务器未连接时不会生效。
|
||||
|
||||
**方案不适用于你的场景?** 以上方案是示例性的。底层机制(三层合并、结构化规则、智能体贯穿工作流)支持更多模式,按需组合即可。
|
||||
|
|
@ -68,6 +68,7 @@ Would you like to install from a custom source (Git URL or local path)?
|
|||
| 输入类型 | 示例 |
|
||||
| -------- | ---- |
|
||||
| HTTPS URL(任意主机) | `https://github.com/org/repo` |
|
||||
| HTTP URL(任意主机) | `http://host/org/repo` |
|
||||
| 带子目录的 HTTPS URL | `https://github.com/org/repo/tree/main/my-module` |
|
||||
| SSH URL | `git@github.com:org/repo.git` |
|
||||
| 本地路径 | `/Users/me/projects/my-module` |
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "bmad-method",
|
||||
"version": "6.5.0",
|
||||
"version": "6.6.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bmad-method",
|
||||
"version": "6.5.0",
|
||||
"version": "6.6.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@clack/core": "^1.0.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "bmad-method",
|
||||
"version": "6.5.0",
|
||||
"version": "6.6.0",
|
||||
"description": "Breakthrough Method of Agile AI-driven Development",
|
||||
"keywords": [
|
||||
"agile",
|
||||
|
|
@ -39,12 +39,13 @@
|
|||
"lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
|
||||
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
||||
"prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
|
||||
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run validate:refs && npm run validate:skills",
|
||||
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run test:urls && npm run validate:refs && npm run validate:skills",
|
||||
"rebundle": "node tools/installer/bundlers/bundle-web.js rebundle",
|
||||
"test": "npm run test:refs && npm run test:install && npm run test:channels && npm run lint && npm run lint:md && npm run format:check",
|
||||
"test": "npm run test:refs && npm run test:install && npm run test:urls && npm run test:channels && npm run lint && npm run lint:md && npm run format:check",
|
||||
"test:channels": "node test/test-installer-channels.js",
|
||||
"test:install": "node test/test-installation-components.js",
|
||||
"test:refs": "node test/test-file-refs-csv.js",
|
||||
"test:urls": "node test/test-parse-source-urls.js",
|
||||
"validate:refs": "node tools/validate-file-refs.js --strict",
|
||||
"validate:skills": "node tools/validate-skills.js --strict"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
"description": "Produces battle-tested PRFAQ document and optional LLM distillate for PRD input.",
|
||||
"supports-headless": true,
|
||||
"phase-name": "1-analysis",
|
||||
"after": ["brainstorming", "perform-research"],
|
||||
"before": ["create-prd"],
|
||||
"preceded-by": ["brainstorming", "perform-research"],
|
||||
"followed-by": ["create-prd"],
|
||||
"is-required": false,
|
||||
"output-location": "{planning_artifacts}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
"description": "Produces executive product brief and optional LLM distillate for PRD input.",
|
||||
"supports-headless": true,
|
||||
"phase-name": "1-analysis",
|
||||
"after": ["brainstorming", "perform-research"],
|
||||
"before": ["create-prd"],
|
||||
"preceded-by": ["brainstorming", "perform-research"],
|
||||
"followed-by": ["create-prd"],
|
||||
"is-required": true,
|
||||
"output-location": "{planning_artifacts}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ Discover and load context documents using smart discovery. Documents can be in t
|
|||
- {planning_artifacts}/**
|
||||
- {output_folder}/**
|
||||
- {project_knowledge}/**
|
||||
- {implementation_artifacts}/investigations/**
|
||||
- docs/**
|
||||
|
||||
Also - when searching - documents can be a single markdown file, or a folder with an index and multiple files. For Example, if searching for `*foo*.md` and not found, also search for a folder called *foo*/index.md (which indicates sharded content)
|
||||
|
|
@ -86,6 +87,8 @@ Try to discover the following:
|
|||
- Research Documents (`/*research*.md`)
|
||||
- Project Documentation (generally multiple documents might be found for this in the `{project_knowledge}` or `docs` folder.)
|
||||
- Project Context (`**/project-context.md`)
|
||||
- Investigation Files (`{implementation_artifacts}/investigations/*-investigation.md`) — `bmad-investigate` case files
|
||||
when the PRD is being driven by a forensic investigation rather than greenfield ideation.
|
||||
|
||||
<critical>Confirm what you have found with the user, along with asking if the user wants to provide anything else. Only after this confirmation will you proceed to follow the loading rules</critical>
|
||||
|
||||
|
|
@ -120,6 +123,7 @@ Try to discover the following:
|
|||
- Product briefs: {{briefCount}} files {if briefCount > 0}✓ loaded{else}(none found){/if}
|
||||
- Research: {{researchCount}} files {if researchCount > 0}✓ loaded{else}(none found){/if}
|
||||
- Brainstorming: {{brainstormingCount}} files {if brainstormingCount > 0}✓ loaded{else}(none found){/if}
|
||||
- Investigations: {{investigationCount}} files {if investigationCount > 0}✓ loaded{else}(none found){/if}
|
||||
- Project docs: {{projectDocsCount}} files {if projectDocsCount > 0}✓ loaded (brownfield project){else}(none found - greenfield project){/if}
|
||||
|
||||
**Files loaded:** {list of specific file names or "No additional documents found"}
|
||||
|
|
@ -128,6 +132,10 @@ Try to discover the following:
|
|||
📋 **Note:** This is a **brownfield project**. Your existing project documentation has been loaded. In the next step, I'll ask specifically about what new features or changes you want to add to your existing system.
|
||||
{/if}
|
||||
|
||||
{if investigationCount > 0}
|
||||
🔎 **Note:** Investigation files have been loaded. The evidence-graded findings (Confirmed / Deduced / Hypothesized), timeline, and fix direction are available as context while we scope requirements.
|
||||
{/if}
|
||||
|
||||
Do you have any other documents you'd like me to include, or shall we continue to the next step?"
|
||||
|
||||
### 4. Present MENU OPTIONS
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ Read the frontmatter from `{outputFile}` to get document counts:
|
|||
- `briefCount` - Product briefs available
|
||||
- `researchCount` - Research documents available
|
||||
- `brainstormingCount` - Brainstorming docs available
|
||||
- `investigationCount` - bmad-investigate case files available
|
||||
- `projectDocsCount` - Existing project documentation
|
||||
|
||||
**Announce your understanding:**
|
||||
|
|
@ -71,6 +72,7 @@ Read the frontmatter from `{outputFile}` to get document counts:
|
|||
- Product briefs: {{briefCount}}
|
||||
- Research: {{researchCount}}
|
||||
- Brainstorming: {{brainstormingCount}}
|
||||
- Investigations: {{investigationCount}}
|
||||
- Project docs: {{projectDocsCount}}
|
||||
|
||||
{{if projectDocsCount > 0}}This is a brownfield project - I'll focus on understanding what you want to add or change.{{else}}This is a greenfield project - I'll help you define the full product vision.{{/if}}"
|
||||
|
|
|
|||
|
|
@ -227,37 +227,39 @@ Prepare the content to append to the document:
|
|||
|
||||
### Architecture Completeness Checklist
|
||||
|
||||
**✅ Requirements Analysis**
|
||||
Mark each item `[x]` only if validation confirms it; leave `[ ]` if it is missing, partial, or unverified. Any unchecked item must be reflected in the Gap Analysis above and in the Overall Status below.
|
||||
|
||||
- [x] Project context thoroughly analyzed
|
||||
- [x] Scale and complexity assessed
|
||||
- [x] Technical constraints identified
|
||||
- [x] Cross-cutting concerns mapped
|
||||
**Requirements Analysis**
|
||||
|
||||
**✅ Architectural Decisions**
|
||||
- [ ] Project context thoroughly analyzed
|
||||
- [ ] Scale and complexity assessed
|
||||
- [ ] Technical constraints identified
|
||||
- [ ] Cross-cutting concerns mapped
|
||||
|
||||
- [x] Critical decisions documented with versions
|
||||
- [x] Technology stack fully specified
|
||||
- [x] Integration patterns defined
|
||||
- [x] Performance considerations addressed
|
||||
**Architectural Decisions**
|
||||
|
||||
**✅ Implementation Patterns**
|
||||
- [ ] Critical decisions documented with versions
|
||||
- [ ] Technology stack fully specified
|
||||
- [ ] Integration patterns defined
|
||||
- [ ] Performance considerations addressed
|
||||
|
||||
- [x] Naming conventions established
|
||||
- [x] Structure patterns defined
|
||||
- [x] Communication patterns specified
|
||||
- [x] Process patterns documented
|
||||
**Implementation Patterns**
|
||||
|
||||
**✅ Project Structure**
|
||||
- [ ] Naming conventions established
|
||||
- [ ] Structure patterns defined
|
||||
- [ ] Communication patterns specified
|
||||
- [ ] Process patterns documented
|
||||
|
||||
- [x] Complete directory structure defined
|
||||
- [x] Component boundaries established
|
||||
- [x] Integration points mapped
|
||||
- [x] Requirements to structure mapping complete
|
||||
**Project Structure**
|
||||
|
||||
- [ ] Complete directory structure defined
|
||||
- [ ] Component boundaries established
|
||||
- [ ] Integration points mapped
|
||||
- [ ] Requirements to structure mapping complete
|
||||
|
||||
### Architecture Readiness Assessment
|
||||
|
||||
**Overall Status:** READY FOR IMPLEMENTATION
|
||||
**Overall Status:** {{READY FOR IMPLEMENTATION | READY WITH MINOR GAPS | NOT READY}} (choose READY FOR IMPLEMENTATION only when all 16 checklist items are `[x]` and no Critical Gaps remain; choose NOT READY when any Critical Gap is open or any Requirements Analysis or Architectural Decisions item is unchecked; otherwise READY WITH MINOR GAPS)
|
||||
|
||||
**Confidence Level:** {{high/medium/low}} based on validation results
|
||||
|
||||
|
|
|
|||
|
|
@ -88,3 +88,8 @@ skill = "bmad-create-story"
|
|||
code = "ER"
|
||||
description = "Party mode review of all work completed across an epic"
|
||||
skill = "bmad-retrospective"
|
||||
|
||||
[[agent.menu]]
|
||||
code = "IN"
|
||||
description = "Forensic case investigation with evidence-graded findings, calibrated to the input"
|
||||
skill = "bmad-investigate"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
---
|
||||
name: bmad-investigate
|
||||
description: Forensic case investigation with evidence-graded findings, calibrated to the input. Use when the user asks
|
||||
to investigate a bug, trace what caused an incident, walk through unfamiliar code, or build a mental model
|
||||
of a code area before working on it.
|
||||
---
|
||||
|
||||
# Investigate
|
||||
|
||||
## Overview
|
||||
|
||||
Reconstruct what's happening, or what an unfamiliar area does, from the available evidence. Produce a structured case
|
||||
file another engineer can pick up cold. Calibrate continuously between defect-chasing (symptom-driven) and
|
||||
area-exploration (no symptom); the same discipline applies on both ends.
|
||||
|
||||
**Args:** A ticket ID, log file path, diagnostic archive, error message, code area name, problem description, or a path
|
||||
to an existing case file. The last form resumes a prior investigation; everything else opens a new case.
|
||||
|
||||
**Output:** `{implementation_artifacts}/{workflow.case_file_subdir}/{workflow.case_file_filename}`. Reference inputs
|
||||
are recorded; raw content is not read into the parent context until an outcome calls for it.
|
||||
|
||||
`{slug}` is the ticket ID when one is provided, otherwise a short descriptive name agreed with the user, sanitized to
|
||||
lowercase alphanumeric with hyphens. On collision with an existing case file at the resolved path, ask whether to
|
||||
rename to `slug-YYYY-MM-DD.md` or resume the existing file (resuming routes to Outcome 0).
|
||||
|
||||
After every outcome, present what was learned and pause for the user before continuing.
|
||||
|
||||
## Principles
|
||||
|
||||
- **Evidence grading.**
|
||||
- **Confirmed.** Directly observed; cite `path:line`, log timestamp, or commit hash.
|
||||
- **Deduced.** Logically follows from Confirmed evidence; show the chain.
|
||||
- **Hypothesized.** Plausible but unconfirmed; state what would confirm or refute it.
|
||||
- **Stronghold first.** Anchor in one Confirmed piece of evidence and expand outward. Never start from a theory and
|
||||
hunt for support. When evidence is sparse, switch to evidence-light mode (Outcome 1 branch).
|
||||
- **Challenge the premise.** The user's description is a hypothesis, not a fact. Verify independently; if evidence
|
||||
contradicts, say so.
|
||||
- **Follow the evidence, not the narrative.** When evidence contradicts the working theory, update the theory — never
|
||||
the other way around. Resist confirmation bias even when the user is convinced.
|
||||
- **Hypotheses are never deleted.** Update Status (Open / Confirmed / Refuted) and add a Resolution. Wrong turns are
|
||||
part of the deliverable.
|
||||
- **Missing evidence is itself a finding.** Document the gap, what it would resolve, and how to obtain it.
|
||||
- **Write it down early.** Initialize the case file as soon as the slug is agreed; it is the persistent state across
|
||||
interruptions.
|
||||
- **Path:line citations** use CWD-relative format, no leading `/`, so they're clickable in IDE-embedded terminals.
|
||||
- **Delegation discipline.** When a step requires reading 5+ files or any file >10K tokens, delegate to a subagent
|
||||
that returns structured JSON only. Cite `path:line` from the result; don't re-read in the parent.
|
||||
- **Issue independent operations in parallel** (multi-grep, multi-read, parallel inventories) — one message, multiple
|
||||
tool calls.
|
||||
- **Communication.** Evidence-first language ("the evidence shows", "unconfirmed, requires X to verify"). No hedging,
|
||||
no narrative.
|
||||
|
||||
## On Activation
|
||||
|
||||
### Step 1: Resolve the workflow block
|
||||
|
||||
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`
|
||||
|
||||
If the script fails, stop and surface the error.
|
||||
|
||||
### Step 2: Execute prepend steps
|
||||
|
||||
Run each entry in `{workflow.activation_steps_prepend}` in order.
|
||||
|
||||
### Step 3: Load persistent facts
|
||||
|
||||
Treat each entry in `{workflow.persistent_facts}` as foundational context. `file:` prefixes are paths or globs under
|
||||
`{project-root}` (load contents); other entries are facts verbatim.
|
||||
|
||||
### Step 4: Load config
|
||||
|
||||
Load `{project-root}/_bmad/bmm/config.yaml` and resolve `{user_name}`, `{communication_language}`,
|
||||
`{document_output_language}`, `{implementation_artifacts}`, `{project_knowledge}`. If `{implementation_artifacts}` is
|
||||
unresolved, fall back to `./investigations/` and surface the fallback before initializing.
|
||||
|
||||
### Step 5: Greet
|
||||
|
||||
Greet `{user_name}` in `{communication_language}`.
|
||||
|
||||
### Step 6: Execute append steps
|
||||
|
||||
Run each entry in `{workflow.activation_steps_append}` in order.
|
||||
|
||||
### Step 7: Acknowledge and route
|
||||
|
||||
Acknowledge the input as a reference (record paths and IDs; don't read raw content). Path to an existing case file →
|
||||
Outcome 0. Otherwise → Outcome 1.
|
||||
|
||||
## Procedure
|
||||
|
||||
### Outcome 0: Existing case is loaded and surfaced
|
||||
|
||||
Read the case file. Surface, in order: open hypotheses (Status = Open) with their confirm/refute criteria; open
|
||||
backlog (Status ≠ Done); missing-evidence rows; last Conclusion with confidence. Ask which thread to pull. New
|
||||
evidence opens a new `## Follow-up: {YYYY-MM-DD}` block (append `#2`, `#3` on same-day reentry). Pause for user with the recap above; wait for direction.
|
||||
|
||||
### Outcome 1: Scope and stronghold are established
|
||||
|
||||
Acknowledge each input shape — record location, scope, time window only; bulk reads happen in Outcome 2.
|
||||
|
||||
- **Issue tracker ticket.** Fetch full details via available MCP tools.
|
||||
- **Diagnostic archive.** Record path, file count, time window.
|
||||
- **Log file or stack trace.** Record path and time window; only the stack frame already in the user's message is in
|
||||
scope here.
|
||||
- **Free-text description.** Capture verbatim; treat as hypothesis.
|
||||
- **Code area name** (no symptom). Record entry point.
|
||||
- **Recent commit area.** Record commit range.
|
||||
|
||||
If the user arrived with a hypothesis, register it as Hypothesis #1. Find the stronghold *independently*; the user's
|
||||
hypothesis is one of the things the stronghold validates or refutes.
|
||||
|
||||
Find a stronghold: a Confirmed piece of evidence (error message, function name, HTTP route, config parameter, test
|
||||
case). Anchor here.
|
||||
|
||||
**Initialize `{case_file}` before branching.** The path is
|
||||
`{implementation_artifacts}/{workflow.case_file_subdir}/{workflow.case_file_filename}` with `{slug}` substituted (slug
|
||||
and collision rules in Overview). Create the file from `{workflow.case_file_template}` and fill Hand-off Brief
|
||||
(rough), Case Info, Problem Statement, initial Evidence Inventory.
|
||||
|
||||
**Evidence-light branch.** When no Confirmed evidence is reachable: mark the case evidence-light in the Hand-off
|
||||
Brief; populate the Investigation Backlog with prioritized data-collection items; record "to make progress, I need one
|
||||
of: …"; pause for the user to provide evidence or authorize Outcome 2 to scan more broadly.
|
||||
|
||||
Otherwise present scope, stronghold, file path, proposed approach. Pause for user with the recap above; wait for direction.
|
||||
|
||||
### Outcome 2: Evidence perimeter is mapped
|
||||
|
||||
Survey the scene: inventory available evidence in parallel across these independent categories: diagnostic archives;
|
||||
issue tracker; version control; test results; static analysis; source code. For any category exceeding ~10K tokens,
|
||||
delegate to a subagent that returns a JSON manifest (paths, sizes, time windows, key fragments cited as `path:line`).
|
||||
|
||||
Classify each Available, Partial, or Missing — Missing is itself a finding. Update Evidence Inventory and Investigation
|
||||
Backlog. Pause for user with the recap above; wait for direction.
|
||||
|
||||
### Outcome 3: Cause is reasoned about with discipline
|
||||
|
||||
- **Trace causality.** Symptom-driven: trace backward from the symptom to producing conditions and the state that
|
||||
emerged. Exploration: trace backward from outputs (returns, side effects, messages sent) to producing conditions.
|
||||
Same technique, different anchor.
|
||||
- **Reconstruct the timeline** by cross-referencing logs, system events, version control, user observations.
|
||||
- **Form and test hypotheses.** State, identify confirming/refuting evidence, search, grade
|
||||
(Confirmed / Refuted / Open). Update Status. Never delete.
|
||||
- **Refutation pass.** Each time a hypothesis transitions toward Confirmed, actively look for refuting evidence first.
|
||||
Record the attempt in Resolution.
|
||||
- **Verify the user's premise.** If evidence contradicts, say so explicitly.
|
||||
- **Add discovered paths to the backlog.** Stay focused on the current thread.
|
||||
|
||||
Update Confirmed Findings, Deduced Conclusions, Hypothesized Paths, Backlog, Timeline. Highlight contradictions to the
|
||||
original premise. Pause for user with the recap above; wait for direction.
|
||||
|
||||
### Outcome 4: Source has been traced where it matters
|
||||
|
||||
Issue these first-pass scans as parallel tool calls in one message: grep for exact error strings; glob the affected
|
||||
directory for parallel implementations; `git log` for recent changes.
|
||||
|
||||
Then sequentially: read the surrounding code; follow the caller chain; watch for language and process boundary
|
||||
crossings (compiled→scripts, IPC, host→device, configuration flow).
|
||||
|
||||
Lean by case type:
|
||||
|
||||
- **Exploration:** I/O mapping (triggers, outputs, dependencies); frequent-terms scan; control-flow filtering
|
||||
(branches, loops, error handling, state-machine transitions).
|
||||
- **Symptom-driven:** depth assessment — is the root cause reachable from local context, or is a broader area model
|
||||
required? Surface escalations; never silently expand scope. Trivial-fix assessment — off-by-one, missing null check,
|
||||
swapped argument → one-line code suggestion or draft diff in the report; non-trivial → stop at the root cause area.
|
||||
|
||||
Investigation stops at the diagnosis; implementation is out of scope. Update Source Code Trace (Error origin, Trigger,
|
||||
Condition, Related files; area model when broader). Pause for user with the recap above; wait for direction.
|
||||
|
||||
### Outcome 5: Report is finalized and the hand-off is clean
|
||||
|
||||
Update `{case_file}`:
|
||||
|
||||
- **Hand-off Brief** rewritten to final form (3 sentences, 15-second read).
|
||||
- **Final Conclusion** with confidence: **High** (Confirmed root cause, deterministic repro), **Medium** (Deduced;
|
||||
minor uncertainty), **Low** (Hypothesized; clear data gap).
|
||||
- **Fix direction** when applicable (categorize by mechanism if multiple combine).
|
||||
- **Diagnostic steps** if uncertainty remains.
|
||||
- **Reproduction Plan** when applicable, or a verification plan for exploration cases.
|
||||
- **Status:** Active / Concluded / Blocked on evidence.
|
||||
|
||||
Present the conclusion, then a concrete next-steps menu: trivial fix → `bmad-quick-dev`; scope/plan adjustment →
|
||||
`bmad-correct-course`; tracked story → `bmad-create-story`; fresh review → `bmad-code-review`. Recommend the
|
||||
highest-value action. Mitigations and workarounds are generated only on explicit request — investigation stops at the
|
||||
diagnosis. Execute `{workflow.on_complete}` if non-empty. Pause for user with the recap above; wait for direction.
|
||||
|
||||
## Follow-up Iterations
|
||||
|
||||
Continue work by appending to `{case_file}` under a new `## Follow-up: {YYYY-MM-DD}` block (`#2`, `#3` on same-day
|
||||
reentry). The investigation is complete when:
|
||||
|
||||
- Root cause is Confirmed.
|
||||
- Root cause is Hypothesized with a clear data gap.
|
||||
- The mental model is sufficient for the user's stated goal (exploration cases).
|
||||
- The backlog contains only items requiring unavailable evidence.
|
||||
- The user explicitly concludes.
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
# DO NOT EDIT -- overwritten on every update.
|
||||
#
|
||||
# Workflow customization surface for bmad-investigate. Mirrors the
|
||||
# agent customization shape under the [workflow] namespace.
|
||||
|
||||
[workflow]
|
||||
|
||||
# --- Configurable below. Overrides merge per BMad structural rules: ---
|
||||
# scalars: override wins • arrays (persistent_facts, activation_steps_*): append
|
||||
# arrays-of-tables with `code`/`id`: replace matching items, append new ones.
|
||||
|
||||
# Steps to run before the standard activation (config load, greet).
|
||||
# Overrides append. Use for pre-flight loads, compliance checks, etc.
|
||||
|
||||
activation_steps_prepend = []
|
||||
|
||||
# Steps to run after greet but before the workflow begins.
|
||||
# Overrides append. Use for context-heavy setup that should happen
|
||||
# once the user has been acknowledged.
|
||||
|
||||
activation_steps_append = []
|
||||
|
||||
# Persistent facts the workflow keeps in mind for the whole run.
|
||||
# Use for citation conventions (path:line vs path#L42), grading-scale
|
||||
# overrides (ITIL severity 1-5 instead of High/Medium/Low), tone
|
||||
# directives (engineering vs exec-facing), or compliance constraints
|
||||
# the case file must respect.
|
||||
# Distinct from the runtime memory sidecar — these are static context
|
||||
# loaded on activation. Overrides append.
|
||||
#
|
||||
# Each entry is either:
|
||||
# - a literal sentence, e.g. "Use ITIL severity 1-5 instead of High/Medium/Low for confidence."
|
||||
# - a file reference prefixed with `file:`, e.g. "file:{project-root}/docs/standards.md"
|
||||
# (glob patterns are supported; the file's contents are loaded and treated as facts).
|
||||
|
||||
persistent_facts = [
|
||||
"file:{project-root}/**/project-context.md",
|
||||
]
|
||||
|
||||
# Scalar: path to the case-file template, resolved from the skill root.
|
||||
# Override to point at an org-shaped template (compliance sections,
|
||||
# SLA fields, post-mortem hooks, ITIL fields).
|
||||
|
||||
case_file_template = "references/case-file-template.md"
|
||||
|
||||
# Scalar: subdirectory under {implementation_artifacts} where case files land.
|
||||
# Override for org taxonomies (forensics/, cases/, incidents/, bug-bash/).
|
||||
|
||||
case_file_subdir = "investigations"
|
||||
|
||||
# Scalar: filename pattern for new case files. {slug} expands to the
|
||||
# ticket ID or a short user-agreed name.
|
||||
|
||||
case_file_filename = "{slug}-investigation.md"
|
||||
|
||||
# Scalar: executed when the workflow finalizes the case file at Outcome 5,
|
||||
# after the conclusion is presented. Override wins. Use for post-case
|
||||
# automation: post the case to Slack/Teams, push fields back to ticketing,
|
||||
# link the case to a sprint, trigger a follow-up retro.
|
||||
# Leave empty for no custom post-completion behavior.
|
||||
|
||||
on_complete = ""
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
# Investigation: {title}
|
||||
|
||||
## Hand-off Brief
|
||||
|
||||
1. **What happened.** {one-sentence problem statement, evidence-graded}
|
||||
2. **Where the case stands.** {status, last finding, what would unblock progress}
|
||||
3. **What's needed next.** {single recommended action with rationale}
|
||||
|
||||
## Case Info
|
||||
|
||||
| Field | Value |
|
||||
| ---------------- | -------------------------------------------------------------------------- |
|
||||
| Ticket | {ticket-id or "N/A"} |
|
||||
| Date opened | {date} |
|
||||
| Status | Active |
|
||||
| System | {OS, version, relevant environment details} |
|
||||
| Evidence sources | {diagnostic archive, logs, crash dump, code, version control, etc.} |
|
||||
|
||||
## Problem Statement
|
||||
|
||||
{User-reported description; the initial claim. May be refined or contradicted by evidence.}
|
||||
|
||||
## Evidence Inventory
|
||||
|
||||
| Source | Status | Notes |
|
||||
| -------- | ------------------------------- | --------- |
|
||||
| {source} | {Available / Partial / Missing} | {details} |
|
||||
|
||||
## Investigation Backlog
|
||||
|
||||
| # | Path to Explore | Priority | Status | Notes |
|
||||
| - | --------------- | --------------------- | ------------------------------------- | --------- |
|
||||
| 1 | {description} | {High / Medium / Low} | {Open / In Progress / Done / Blocked} | {context} |
|
||||
|
||||
## Timeline of Events
|
||||
|
||||
| Time | Event | Source | Confidence |
|
||||
| ----------- | ------------------- | --------------------- | --------------------- |
|
||||
| {timestamp} | {event description} | {log file, commit, …} | {Confirmed / Deduced} |
|
||||
|
||||
## Confirmed Findings
|
||||
|
||||
### Finding 1: {title}
|
||||
|
||||
**Evidence:** {citation — `path:line`, log timestamp, or commit hash}
|
||||
|
||||
**Detail:** {description}
|
||||
|
||||
## Deduced Conclusions
|
||||
|
||||
### Deduction 1: {title}
|
||||
|
||||
**Based on:** {which Confirmed Findings}
|
||||
|
||||
**Reasoning:** {logical chain}
|
||||
|
||||
**Conclusion:** {what follows}
|
||||
|
||||
## Hypothesized Paths
|
||||
|
||||
### Hypothesis 1: {title}
|
||||
|
||||
**Status:** {Open / Confirmed / Refuted}
|
||||
|
||||
**Theory:** {description}
|
||||
|
||||
**Supporting indicators:** {what makes this plausible}
|
||||
|
||||
**Would confirm:** {specific evidence that would prove this}
|
||||
|
||||
**Would refute:** {specific evidence that would disprove this}
|
||||
|
||||
**Resolution:** {when Status changes from Open, what evidence settled it}
|
||||
|
||||
## Missing Evidence
|
||||
|
||||
| Gap | Impact | How to Obtain |
|
||||
| ---------------- | ------------------------------------ | --------------- |
|
||||
| {what's missing} | {what it would confirm or eliminate} | {how to get it} |
|
||||
|
||||
## Source Code Trace
|
||||
|
||||
| Element | Detail |
|
||||
| ------------- | ------------------------------------------- |
|
||||
| Error origin | {file:line, function name} |
|
||||
| Trigger | {what causes this code to execute} |
|
||||
| Condition | {what state produces the observed behavior} |
|
||||
| Related files | {other files in the same code path} |
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Confidence:** {High / Medium / Low}
|
||||
|
||||
{Summary stating what is Confirmed vs. what remains Hypothesized. If a root cause is identified, state it; otherwise
|
||||
name the most promising hypothesized paths and what would resolve the remaining uncertainty.}
|
||||
|
||||
## Recommended Next Steps
|
||||
|
||||
### Fix direction
|
||||
|
||||
{What needs to change and why. Categorize by mechanism when multiple issues combine.}
|
||||
|
||||
### Diagnostic
|
||||
|
||||
{Steps to confirm the root cause: additional logging, targeted tests, data to collect.}
|
||||
|
||||
## Reproduction Plan
|
||||
|
||||
{Setup, trigger, expected results. Scale from isolated proof to full system reproduction.}
|
||||
|
||||
## Side Findings
|
||||
|
||||
Tangential observations surfaced during the investigation, evidence-graded, with citation when applicable.
|
||||
|
||||
- {observation}
|
||||
|
||||
## Follow-up: {date}
|
||||
|
||||
### New Evidence
|
||||
|
||||
### Additional Findings
|
||||
|
||||
### Updated Hypotheses
|
||||
|
||||
### Backlog Changes
|
||||
|
||||
### Updated Conclusion
|
||||
|
|
@ -1,33 +1,34 @@
|
|||
module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs
|
||||
module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
|
||||
BMad Method,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt,
|
||||
BMad Method,bmad-document-project,Document Project,DP,Analyze an existing project to produce useful documentation.,,anytime,,,false,project-knowledge,*
|
||||
BMad Method,bmad-generate-project-context,Generate Project Context,GPC,Scan existing codebase to generate a lean LLM-optimized project-context.md. Essential for brownfield projects.,,anytime,,,false,output_folder,project context
|
||||
BMad Method,bmad-quick-dev,Quick Dev,QQ,Unified intent-in code-out workflow: clarify plan implement review and present.,,anytime,,,false,implementation_artifacts,spec and project implementation
|
||||
BMad Method,bmad-correct-course,Correct Course,CC,Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories.,,anytime,,,false,planning_artifacts,change proposal
|
||||
BMad Method,bmad-document-project,Document Project,DP,Analyze an existing project to produce useful documentation.,,,anytime,,,false,project-knowledge,*
|
||||
BMad Method,bmad-generate-project-context,Generate Project Context,GPC,Scan existing codebase to generate a lean LLM-optimized project-context.md. Essential for brownfield projects.,,,anytime,,,false,output_folder,project context
|
||||
BMad Method,bmad-quick-dev,Quick Dev,QQ,Unified intent-in code-out workflow: clarify plan implement review and present.,,,anytime,,,false,implementation_artifacts,spec and project implementation
|
||||
BMad Method,bmad-correct-course,Correct Course,CC,Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories.,,,anytime,,,false,planning_artifacts,change proposal
|
||||
BMad Method,bmad-agent-tech-writer,Write Document,WD,"Describe in detail what you want, and the agent will follow documentation best practices. Multi-turn conversation with subprocess for research/review.",write,,anytime,,,false,project-knowledge,document
|
||||
BMad Method,bmad-agent-tech-writer,Update Standards,US,Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions.,update-standards,,anytime,,,false,_bmad/_memory/tech-writer-sidecar,standards
|
||||
BMad Method,bmad-agent-tech-writer,Mermaid Generate,MG,Create a Mermaid diagram based on user description. Will suggest diagram types if not specified.,mermaid,,anytime,,,false,planning_artifacts,mermaid diagram
|
||||
BMad Method,bmad-agent-tech-writer,Validate Document,VD,Review the specified document against documentation standards and best practices. Returns specific actionable improvement suggestions organized by priority.,validate,[path],anytime,,,false,planning_artifacts,validation report
|
||||
BMad Method,bmad-agent-tech-writer,Explain Concept,EC,Create clear technical explanations with examples and diagrams for complex concepts.,explain,[topic],anytime,,,false,project_knowledge,explanation
|
||||
BMad Method,bmad-brainstorming,Brainstorm Project,BP,Expert guided facilitation through a single or multiple techniques.,,1-analysis,,,false,planning_artifacts,brainstorming session
|
||||
BMad Method,bmad-market-research,Market Research,MR,"Market analysis competitive landscape customer needs and trends.",,1-analysis,,,false,"planning_artifacts|project-knowledge",research documents
|
||||
BMad Method,bmad-domain-research,Domain Research,DR,Industry domain deep dive subject matter expertise and terminology.,,1-analysis,,,false,"planning_artifacts|project_knowledge",research documents
|
||||
BMad Method,bmad-technical-research,Technical Research,TR,Technical feasibility architecture options and implementation approaches.,,1-analysis,,,false,"planning_artifacts|project_knowledge",research documents
|
||||
BMad Method,bmad-brainstorming,Brainstorm Project,BP,Expert guided facilitation through a single or multiple techniques.,,,1-analysis,,,false,planning_artifacts,brainstorming session
|
||||
BMad Method,bmad-market-research,Market Research,MR,Market analysis competitive landscape customer needs and trends.,,,1-analysis,,,false,planning_artifacts|project-knowledge,research documents
|
||||
BMad Method,bmad-domain-research,Domain Research,DR,Industry domain deep dive subject matter expertise and terminology.,,,1-analysis,,,false,planning_artifacts|project_knowledge,research documents
|
||||
BMad Method,bmad-technical-research,Technical Research,TR,Technical feasibility architecture options and implementation approaches.,,,1-analysis,,,false,planning_artifacts|project_knowledge,research documents
|
||||
BMad Method,bmad-product-brief,Create Brief,CB,An expert guided experience to nail down your product idea in a brief. a gentler approach than PRFAQ when you are already sure of your concept and nothing will sway you.,,-A,1-analysis,,,false,planning_artifacts,product brief
|
||||
BMad Method,bmad-prfaq,PRFAQ Challenge,WB,Working Backwards guided experience to forge and stress-test your product concept to ensure you have a great product that users will love and need through the PRFAQ gauntlet to determine feasibility and alignment with user needs. alternative to product brief.,,-H,1-analysis,,,false,planning_artifacts,prfaq document
|
||||
BMad Method,bmad-create-prd,Create PRD,CP,Expert led facilitation to produce your Product Requirements Document.,,2-planning,,,true,planning_artifacts,prd
|
||||
BMad Method,bmad-create-prd,Create PRD,CP,Expert led facilitation to produce your Product Requirements Document.,,,2-planning,,,true,planning_artifacts,prd
|
||||
BMad Method,bmad-validate-prd,Validate PRD,VP,,,[path],2-planning,bmad-create-prd,,false,planning_artifacts,prd validation report
|
||||
BMad Method,bmad-edit-prd,Edit PRD,EP,,,[path],2-planning,bmad-validate-prd,,false,planning_artifacts,updated prd
|
||||
BMad Method,bmad-create-ux-design,Create UX,CU,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project.",,2-planning,bmad-create-prd,,false,planning_artifacts,ux design
|
||||
BMad Method,bmad-create-architecture,Create Architecture,CA,Guided workflow to document technical decisions.,,3-solutioning,,,true,planning_artifacts,architecture
|
||||
BMad Method,bmad-create-epics-and-stories,Create Epics and Stories,CE,,,3-solutioning,bmad-create-architecture,,true,planning_artifacts,epics and stories
|
||||
BMad Method,bmad-check-implementation-readiness,Check Implementation Readiness,IR,Ensure PRD UX Architecture and Epics Stories are aligned.,,3-solutioning,bmad-create-epics-and-stories,,true,planning_artifacts,readiness report
|
||||
BMad Method,bmad-sprint-planning,Sprint Planning,SP,Kicks off implementation by producing a plan the implementation agents will follow in sequence for every story.,,4-implementation,,,true,implementation_artifacts,sprint status
|
||||
BMad Method,bmad-sprint-status,Sprint Status,SS,Anytime: Summarize sprint status and route to next workflow.,,4-implementation,bmad-sprint-planning,,false,,
|
||||
BMad Method,bmad-create-story,Create Story,CS,"Story cycle start: Prepare first found story in the sprint plan that is next or a specific epic/story designation.",create,,4-implementation,bmad-sprint-planning,bmad-create-story:validate,true,implementation_artifacts,story
|
||||
BMad Method,bmad-create-ux-design,Create UX,CU,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project.",,,2-planning,bmad-create-prd,,false,planning_artifacts,ux design
|
||||
BMad Method,bmad-create-architecture,Create Architecture,CA,Guided workflow to document technical decisions.,,,3-solutioning,,,true,planning_artifacts,architecture
|
||||
BMad Method,bmad-create-epics-and-stories,Create Epics and Stories,CE,,,,3-solutioning,bmad-create-architecture,,true,planning_artifacts,epics and stories
|
||||
BMad Method,bmad-check-implementation-readiness,Check Implementation Readiness,IR,Ensure PRD UX Architecture and Epics Stories are aligned.,,,3-solutioning,bmad-create-epics-and-stories,,true,planning_artifacts,readiness report
|
||||
BMad Method,bmad-sprint-planning,Sprint Planning,SP,Kicks off implementation by producing a plan the implementation agents will follow in sequence for every story.,,,4-implementation,,,true,implementation_artifacts,sprint status
|
||||
BMad Method,bmad-sprint-status,Sprint Status,SS,Anytime: Summarize sprint status and route to next workflow.,,,4-implementation,bmad-sprint-planning,,false,,
|
||||
BMad Method,bmad-create-story,Create Story,CS,Story cycle start: Prepare first found story in the sprint plan that is next or a specific epic/story designation.,create,,4-implementation,bmad-sprint-planning,bmad-create-story:validate,true,implementation_artifacts,story
|
||||
BMad Method,bmad-create-story,Validate Story,VS,Validates story readiness and completeness before development work begins.,validate,,4-implementation,bmad-create-story:create,bmad-dev-story,false,implementation_artifacts,story validation report
|
||||
BMad Method,bmad-dev-story,Dev Story,DS,Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed.,,4-implementation,bmad-create-story:validate,,true,,
|
||||
BMad Method,bmad-code-review,Code Review,CR,Story cycle: If issues back to DS if approved then next CS or ER if epic complete.,,4-implementation,bmad-dev-story,,false,,
|
||||
BMad Method,bmad-checkpoint-preview,Checkpoint,CK,Guided walkthrough of a change from purpose and context into details. Use for human review of commits branches or PRs.,,4-implementation,,,false,,
|
||||
BMad Method,bmad-qa-generate-e2e-tests,QA Automation Test,QA,Generate automated API and E2E tests for implemented code. NOT for code review or story validation — use CR for that.,,4-implementation,bmad-dev-story,,false,implementation_artifacts,test suite
|
||||
BMad Method,bmad-retrospective,Retrospective,ER,Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC.,,4-implementation,bmad-code-review,,false,implementation_artifacts,retrospective
|
||||
BMad Method,bmad-dev-story,Dev Story,DS,Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed.,,,4-implementation,bmad-create-story:validate,,true,,
|
||||
BMad Method,bmad-code-review,Code Review,CR,Story cycle: If issues back to DS if approved then next CS or ER if epic complete.,,,4-implementation,bmad-dev-story,,false,,
|
||||
BMad Method,bmad-checkpoint-preview,Checkpoint,CK,Guided walkthrough of a change from purpose and context into details. Use for human review of commits branches or PRs.,,,4-implementation,,,false,,
|
||||
BMad Method,bmad-qa-generate-e2e-tests,QA Automation Test,QA,Generate automated API and E2E tests for implemented code. NOT for code review or story validation — use CR for that.,,,4-implementation,bmad-dev-story,,false,implementation_artifacts,test suite
|
||||
BMad Method,bmad-retrospective,Retrospective,ER,Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC.,,,4-implementation,bmad-code-review,,false,implementation_artifacts,retrospective
|
||||
BMad Method,bmad-investigate,Investigate,IN,Forensic case investigation calibrated to the input. Evidence-graded analysis with hypothesis tracking. Produces a structured case file.,,4-implementation,,,false,implementation_artifacts,investigation report
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 3.
|
|
|
@ -5,15 +5,11 @@ default_selected: true # This module will be selected by default for new install
|
|||
|
||||
# Variables from Core Config inserted:
|
||||
## user_name
|
||||
## project_name
|
||||
## communication_language
|
||||
## document_output_language
|
||||
## output_folder
|
||||
|
||||
project_name:
|
||||
prompt: "What is your project called?"
|
||||
default: "{directory_name}"
|
||||
result: "{value}"
|
||||
|
||||
user_skill_level:
|
||||
prompt:
|
||||
- "What is your development experience level?"
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ parts: 1
|
|||
|
||||
## Solution Architecture
|
||||
- Plugins: skill bundles with Anthropic plugin standard as base format + bmad-manifest.json extending for BMAD-specific metadata (installer options, capabilities, help integration, phase ordering, dependencies)
|
||||
- Existing manifest example: `{"module-code":"bmm","replaces-skill":"bmad-create-product-brief","capabilities":[{"name":"create-brief","menu-code":"CB","supports-headless":true,"phase-name":"1-analysis","after":["brainstorming"],"before":["create-prd"],"is-required":true}]}`
|
||||
- Existing manifest example: `{"module-code":"bmm","replaces-skill":"bmad-create-product-brief","capabilities":[{"name":"create-brief","menu-code":"CB","supports-headless":true,"phase-name":"1-analysis","preceded-by":["brainstorming"],"followed-by":["create-prd"],"is-required":true}]}`
|
||||
- Vercel skills CLI handles platform translation; integration pattern (wrap/fork/call) is PRD decision
|
||||
- bmad-setup: global skill scanning installed bmad-manifest.json files, registering capabilities, configuring project settings; always included as base skill in every bundle (solves bootstrapping)
|
||||
- bmad-update: plugin update path without full reinstall; technical approach (diff/replace/preserve customizations) is PRD decision
|
||||
|
|
|
|||
|
|
@ -33,16 +33,16 @@ When this skill completes, the user should:
|
|||
The catalog uses this format:
|
||||
|
||||
```
|
||||
module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs
|
||||
module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
|
||||
```
|
||||
|
||||
**Phases** determine the high-level flow:
|
||||
- `anytime` — available regardless of workflow state
|
||||
- Numbered phases (`1-analysis`, `2-planning`, etc.) flow in order; naming varies by module
|
||||
|
||||
**Dependencies** determine ordering within and across phases:
|
||||
- `after` — skills that should ideally complete before this one
|
||||
- `before` — skills that should run after this one
|
||||
**Sequencing** determines recommended ordering within and across phases (these are soft suggestions, not hard gates — see `required` for gating):
|
||||
- `preceded-by` — skills that should ideally complete before this one
|
||||
- `followed-by` — skills that should ideally run after this one
|
||||
- Format: `skill-name` for single-action skills, `skill-name:action` for multi-action skills
|
||||
|
||||
**Required gates**:
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs
|
||||
module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
|
||||
Core,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt,
|
||||
Core,bmad-brainstorming,Brainstorming,BSP,Use early in ideation or when stuck generating ideas.,,anytime,,,false,{output_folder}/brainstorming,brainstorming session
|
||||
Core,bmad-party-mode,Party Mode,PM,Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate.,,anytime,,,false,,
|
||||
Core,bmad-help,BMad Help,BH,,,anytime,,,false,,
|
||||
Core,bmad-index-docs,Index Docs,ID,Use when LLM needs to understand available docs without loading everything.,,anytime,,,false,,
|
||||
Core,bmad-shard-doc,Shard Document,SD,Use when doc becomes too large (>500 lines) to manage effectively.,[path],anytime,,,false,,
|
||||
Core,bmad-editorial-review-prose,Editorial Review - Prose,EP,Use after drafting to polish written content.,[path],anytime,,,false,report located with target document,three-column markdown table with suggested fixes
|
||||
Core,bmad-editorial-review-structure,Editorial Review - Structure,ES,Use when doc produced from multiple subprocesses or needs structural improvement.,[path],anytime,,,false,report located with target document,
|
||||
Core,bmad-review-adversarial-general,Adversarial Review,AR,"Use for quality assurance or before finalizing deliverables. Code Review in other modules runs this automatically, but also useful for document reviews.",[path],anytime,,,false,,
|
||||
Core,bmad-review-edge-case-hunter,Edge Case Hunter Review,ECH,Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven.,[path],anytime,,,false,,
|
||||
Core,bmad-distillator,Distillator,DG,Use when you need token-efficient distillates that preserve all information for downstream LLM consumption.,[path],anytime,,,false,adjacent to source document or specified output_path,distillate markdown file(s)
|
||||
Core,bmad-customize,BMad Customize,BC,"Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required.",,anytime,,,false,{project-root}/_bmad/custom,TOML override files
|
||||
Core,bmad-brainstorming,Brainstorming,BSP,Use early in ideation or when stuck generating ideas.,,,anytime,,,false,{output_folder}/brainstorming,brainstorming session
|
||||
Core,bmad-party-mode,Party Mode,PM,Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate.,,,anytime,,,false,,
|
||||
Core,bmad-help,BMad Help,BH,,,,anytime,,,false,,
|
||||
Core,bmad-index-docs,Index Docs,ID,Use when LLM needs to understand available docs without loading everything.,,,anytime,,,false,,
|
||||
Core,bmad-shard-doc,Shard Document,SD,Use when doc becomes too large (>500 lines) to manage effectively.,,[path],anytime,,,false,,
|
||||
Core,bmad-editorial-review-prose,Editorial Review - Prose,EP,Use after drafting to polish written content.,,[path],anytime,,,false,report located with target document,three-column markdown table with suggested fixes
|
||||
Core,bmad-editorial-review-structure,Editorial Review - Structure,ES,Use when doc produced from multiple subprocesses or needs structural improvement.,,[path],anytime,,,false,report located with target document,
|
||||
Core,bmad-review-adversarial-general,Adversarial Review,AR,"Use for quality assurance or before finalizing deliverables. Code Review in other modules runs this automatically, but also useful for document reviews.",,[path],anytime,,,false,,
|
||||
Core,bmad-review-edge-case-hunter,Edge Case Hunter Review,ECH,Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven.,,[path],anytime,,,false,,
|
||||
Core,bmad-distillator,Distillator,DG,Use when you need token-efficient distillates that preserve all information for downstream LLM consumption.,,[path],anytime,,,false,adjacent to source document or specified output_path,distillate markdown file(s)
|
||||
Core,bmad-customize,BMad Customize,BC,"Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required.",,,anytime,,,false,{project-root}/_bmad/custom,TOML override files
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 3.
|
|
|
@ -11,6 +11,11 @@ user_name:
|
|||
default: "BMad"
|
||||
result: "{value}"
|
||||
|
||||
project_name:
|
||||
prompt: "What is your project called?"
|
||||
default: "{directory_name}"
|
||||
result: "{value}"
|
||||
|
||||
communication_language:
|
||||
prompt: "What language should agents use when chatting with you?"
|
||||
scope: user
|
||||
|
|
|
|||
|
|
@ -285,6 +285,10 @@ async function runTests() {
|
|||
const opencodeInstaller = platformCodes.platforms.opencode?.installer;
|
||||
|
||||
assert(opencodeInstaller?.target_dir === '.agents/skills', 'OpenCode target_dir uses native skills path');
|
||||
assert(
|
||||
opencodeInstaller?.commands_target_dir === '.opencode/commands',
|
||||
'OpenCode commands_target_dir is configured for /<skill> slash commands',
|
||||
);
|
||||
|
||||
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-test-'));
|
||||
const installedBmadDir = await createTestBmadFixture();
|
||||
|
|
@ -301,6 +305,55 @@ async function runTests() {
|
|||
const skillFile = path.join(tempProjectDir, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
||||
assert(await fs.pathExists(skillFile), 'OpenCode install writes SKILL.md directory output');
|
||||
|
||||
// Command pointer assertions: a /<canonicalId> slash command should exist
|
||||
// for each installed skill so users can invoke skills directly without
|
||||
// going through the /skills menu.
|
||||
const commandFile = path.join(tempProjectDir, '.opencode', 'commands', 'bmad-master.md');
|
||||
assert(await fs.pathExists(commandFile), 'OpenCode install writes per-skill command pointer file');
|
||||
|
||||
const commandContent = await fs.readFile(commandFile, 'utf8');
|
||||
assert(commandContent.includes('@skills/bmad-master'), 'Command pointer body references the skill via @skills/<canonicalId>');
|
||||
assert(commandContent.includes('description:'), 'Command pointer carries a description in YAML frontmatter');
|
||||
|
||||
// Idempotency: re-running install must not duplicate or rewrite pointers.
|
||||
const result2 = await ideManager.setup('opencode', tempProjectDir, installedBmadDir, {
|
||||
silent: true,
|
||||
selectedModules: ['bmm'],
|
||||
});
|
||||
assert(result2.success === true, 'Second OpenCode install succeeds (idempotent)');
|
||||
assert(await fs.pathExists(commandFile), 'Command pointer survives a second install pass');
|
||||
|
||||
// Description-update propagation: when the manifest description changes
|
||||
// and the on-disk pointer still matches the generator pattern, refresh
|
||||
// the file so users see the updated description.
|
||||
const csvPath = path.join(installedBmadDir, '_config', 'skill-manifest.csv');
|
||||
const updatedCsv =
|
||||
'canonicalId,name,description,module,path\n' +
|
||||
'"bmad-master","bmad-master","UPDATED description for the test agent","core","_bmad/core/bmad-master/SKILL.md"\n';
|
||||
await fs.writeFile(csvPath, updatedCsv);
|
||||
const result3 = await ideManager.setup('opencode', tempProjectDir, installedBmadDir, {
|
||||
silent: true,
|
||||
selectedModules: ['bmm'],
|
||||
});
|
||||
assert(result3.success === true, 'Third OpenCode install succeeds after description update');
|
||||
const refreshed = await fs.readFile(commandFile, 'utf8');
|
||||
assert(refreshed.includes('UPDATED description'), 'Generator-shaped pointer is refreshed when manifest description changes');
|
||||
|
||||
// Hand-edit preservation across the production install flow. The
|
||||
// installer passes previousSkillIds — without the cleanup-side spare,
|
||||
// hand edits would be wiped here.
|
||||
const SENTINEL = 'HAND_EDITED_BY_USER_SHOULD_SURVIVE';
|
||||
const handEditedBody = `---\ndescription: my custom description\n---\n\n${SENTINEL}\n`;
|
||||
await fs.writeFile(commandFile, handEditedBody);
|
||||
const result4 = await ideManager.setup('opencode', tempProjectDir, installedBmadDir, {
|
||||
silent: true,
|
||||
selectedModules: ['bmm'],
|
||||
previousSkillIds: new Set(['bmad-master']),
|
||||
});
|
||||
assert(result4.success === true, 'Fourth OpenCode install succeeds with hand-edited pointer present');
|
||||
const afterReinstall = await fs.readFile(commandFile, 'utf8');
|
||||
assert(afterReinstall.includes(SENTINEL), 'Hand-edited pointer survives a routine reinstall (cleanup spares active-manifest IDs)');
|
||||
|
||||
await fs.remove(tempProjectDir);
|
||||
await fs.remove(path.dirname(installedBmadDir));
|
||||
} catch (error) {
|
||||
|
|
@ -504,10 +557,83 @@ async function runTests() {
|
|||
const copilotInstaller = platformCodes17.platforms['github-copilot']?.installer;
|
||||
|
||||
assert(copilotInstaller?.target_dir === '.agents/skills', 'GitHub Copilot target_dir uses native skills path');
|
||||
assert(
|
||||
copilotInstaller?.commands_target_dir === '.github/agents',
|
||||
'GitHub Copilot commands_target_dir is configured for the Custom Agents picker',
|
||||
);
|
||||
assert(copilotInstaller?.commands_extension === '.agent.md', 'GitHub Copilot uses .agent.md extension for Custom Agents files');
|
||||
assert(
|
||||
typeof copilotInstaller?.commands_body_template === 'string' && copilotInstaller.commands_body_template.includes('{canonicalId}'),
|
||||
'GitHub Copilot defines a commands_body_template with {canonicalId} placeholder',
|
||||
);
|
||||
assert(
|
||||
copilotInstaller?.commands_filter === 'agents-only',
|
||||
'GitHub Copilot filters Custom Agents picker to persona agents only (agents-only)',
|
||||
);
|
||||
|
||||
const tempProjectDir17 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-copilot-test-'));
|
||||
const installedBmadDir17 = await createTestBmadFixture();
|
||||
|
||||
// Extend the fixture to exercise the agents-only filter, which detects
|
||||
// persona agents by the `[agent]` section in each skill's source
|
||||
// customize.toml. Five skill types covered:
|
||||
//
|
||||
// 1. Persona agent — has customize.toml with [agent] → INCLUDED
|
||||
// 2. Persona with non-conventional id — also has [agent] → INCLUDED
|
||||
// (verifies the filter doesn't depend on `-agent-` naming)
|
||||
// 3. Meta-skill whose id contains `-agent-` but isn't a
|
||||
// persona — has customize.toml with [workflow] → EXCLUDED
|
||||
// (mirrors `bmad-agent-builder` in the real manifest)
|
||||
// 4. Workflow skill — no customize.toml at all → EXCLUDED
|
||||
// 5. `bmad-help` — meta-help skill with no customize.toml;
|
||||
// every persona agent's activation already advertises it,
|
||||
// so it's correctly excluded from the picker as redundant → EXCLUDED
|
||||
const fixtureCsvPath17 = path.join(installedBmadDir17, '_config', 'skill-manifest.csv');
|
||||
await fs.writeFile(
|
||||
fixtureCsvPath17,
|
||||
[
|
||||
'canonicalId,name,description,module,path',
|
||||
'"bmad-master","bmad-master","Workflow with no customize.toml — should NOT appear in Copilot agents picker","core","_bmad/core/bmad-master/SKILL.md"',
|
||||
'"bmad-agent-fixture","bmad-agent-fixture","Persona agent — customize.toml has [agent], SHOULD appear","core","_bmad/core/bmad-agent-fixture/SKILL.md"',
|
||||
'"bmad-tea","bmad-tea","Non-conventional id but [agent] in customize.toml — SHOULD appear","core","_bmad/core/bmad-tea/SKILL.md"',
|
||||
'"bmad-agent-builder","bmad-agent-builder","Skill-builder workflow — id contains -agent- but customize.toml has [workflow] — should NOT appear","core","_bmad/core/bmad-agent-builder/SKILL.md"',
|
||||
'"bmad-help","bmad-help","Meta-help skill — no customize.toml; SHOULD NOT appear in agents picker (toml-driven filter)","core","_bmad/core/bmad-help/SKILL.md"',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
// Materialise the source skill directories so the agents-only filter
|
||||
// can read their customize.toml. The bmad-master and bmad-agent-builder
|
||||
// SKILL.md files were already populated by createTestBmadFixture (they
|
||||
// share the bmad-master target_dir layout); only the customize.toml
|
||||
// and the new agent fixtures need to be created here.
|
||||
for (const id of ['bmad-agent-fixture', 'bmad-tea', 'bmad-agent-builder', 'bmad-help']) {
|
||||
const dir17 = path.join(installedBmadDir17, 'core', id);
|
||||
await fs.ensureDir(dir17);
|
||||
await fs.writeFile(
|
||||
path.join(dir17, 'SKILL.md'),
|
||||
['---', `name: ${id}`, `description: fixture for ${id}`, '---', '', `Body of ${id}.`].join('\n'),
|
||||
);
|
||||
}
|
||||
// Note: bmad-help intentionally has NO customize.toml — it exercises
|
||||
// the toml-driven filter's exclusion path (a skill with no
|
||||
// customize.toml is correctly kept out of the Copilot agents picker).
|
||||
// [agent] customize.toml for the two persona fixtures.
|
||||
await fs.writeFile(
|
||||
path.join(installedBmadDir17, 'core', 'bmad-agent-fixture', 'customize.toml'),
|
||||
['[agent]', 'name = "Fixture Agent"', 'title = "Test Persona"', ''].join('\n'),
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(installedBmadDir17, 'core', 'bmad-tea', 'customize.toml'),
|
||||
['[agent]', 'name = "Murat"', 'title = "Test Architect"', ''].join('\n'),
|
||||
);
|
||||
// [workflow] customize.toml for the meta-skill — its id contains `-agent-`
|
||||
// but it is NOT a persona (mirrors bmad-agent-builder in production).
|
||||
await fs.writeFile(
|
||||
path.join(installedBmadDir17, 'core', 'bmad-agent-builder', 'customize.toml'),
|
||||
['[workflow]', '', '# Meta-skill that builds agents but is not itself a persona.', ''].join('\n'),
|
||||
);
|
||||
|
||||
const copilotInstructionsPath17 = path.join(tempProjectDir17, '.github', 'copilot-instructions.md');
|
||||
await fs.ensureDir(path.dirname(copilotInstructionsPath17));
|
||||
await fs.writeFile(
|
||||
|
|
@ -543,6 +669,56 @@ async function runTests() {
|
|||
'GitHub Copilot setup preserves user content in copilot-instructions.md',
|
||||
);
|
||||
|
||||
// Custom Agents picker integration: persona agents (those with [agent]
|
||||
// in their source customize.toml) get .agent.md files in
|
||||
// .github/agents/. Workflows and meta-skills with [workflow] (or no
|
||||
// customize.toml at all) do NOT — the agents-only filter keeps the
|
||||
// picker uncluttered and the signal is naming-independent.
|
||||
const agentsDir17 = path.join(tempProjectDir17, '.github', 'agents');
|
||||
const agentFileForPersona17 = path.join(agentsDir17, 'bmad-agent-fixture.agent.md');
|
||||
const agentFileForTea17 = path.join(agentsDir17, 'bmad-tea.agent.md');
|
||||
const agentFileForWorkflow17 = path.join(agentsDir17, 'bmad-master.agent.md');
|
||||
const agentFileForMetaSkill17 = path.join(agentsDir17, 'bmad-agent-builder.agent.md');
|
||||
const agentFileForBmadHelp17 = path.join(agentsDir17, 'bmad-help.agent.md');
|
||||
|
||||
assert(
|
||||
await fs.pathExists(agentFileForPersona17),
|
||||
'Persona agent ([agent] in customize.toml) gets a .agent.md file in .github/agents/',
|
||||
);
|
||||
assert(await fs.pathExists(agentFileForTea17), 'Non-conventional id with [agent] in customize.toml is included (no allowlist needed)');
|
||||
assert(!(await fs.pathExists(agentFileForWorkflow17)), 'Workflow skill (no customize.toml) is FILTERED OUT of .github/agents/');
|
||||
assert(
|
||||
!(await fs.pathExists(agentFileForBmadHelp17)),
|
||||
'bmad-help is excluded from Copilot agents picker (no customize.toml; allowlist removed per maintainer feedback)',
|
||||
);
|
||||
assert(
|
||||
!(await fs.pathExists(agentFileForMetaSkill17)),
|
||||
'Meta-skill with -agent- in id but [workflow] in customize.toml is FILTERED OUT (signal is behavior, not naming)',
|
||||
);
|
||||
|
||||
// Body content of the persona agent file: frontmatter description +
|
||||
// LOAD pattern referencing the skill's SKILL.md path under target_dir.
|
||||
const personaAgentContent17 = await fs.readFile(agentFileForPersona17, 'utf8');
|
||||
assert(
|
||||
personaAgentContent17.includes('description:'),
|
||||
'Copilot agent pointer carries a description in YAML frontmatter (drives the agents picker label)',
|
||||
);
|
||||
assert(
|
||||
personaAgentContent17.includes('{project-root}/.agents/skills/bmad-agent-fixture/SKILL.md'),
|
||||
'Copilot agent pointer body resolves to the skill via LOAD {project-root}/<target_dir>/<id>/SKILL.md',
|
||||
);
|
||||
|
||||
// Idempotency: re-running setup must not duplicate or rewrite the agent
|
||||
// pointer when the source manifest is unchanged, AND must not start
|
||||
// emitting workflow-skill agent files.
|
||||
const result17b = await ideManager17.setup('github-copilot', tempProjectDir17, installedBmadDir17, {
|
||||
silent: true,
|
||||
selectedModules: ['bmm'],
|
||||
});
|
||||
assert(result17b.success === true, 'Second GitHub Copilot install succeeds (idempotent)');
|
||||
assert(await fs.pathExists(agentFileForPersona17), 'Persona agent pointer survives a second install pass');
|
||||
assert(!(await fs.pathExists(agentFileForWorkflow17)), 'Workflow skill remains filtered out of agents picker on second install');
|
||||
|
||||
await fs.remove(tempProjectDir17);
|
||||
await fs.remove(path.dirname(installedBmadDir17));
|
||||
} catch (error) {
|
||||
|
|
@ -1813,12 +1989,12 @@ async function runTests() {
|
|||
const moduleConfigs = {
|
||||
core: {
|
||||
user_name: 'TestUser',
|
||||
project_name: 'demo-project',
|
||||
communication_language: 'Spanish',
|
||||
document_output_language: 'English',
|
||||
output_folder: '_bmad-output',
|
||||
},
|
||||
bmm: {
|
||||
project_name: 'demo-project',
|
||||
user_skill_level: 'expert',
|
||||
planning_artifacts: '{project-root}/_bmad-output/planning-artifacts',
|
||||
implementation_artifacts: '{project-root}/_bmad-output/implementation-artifacts',
|
||||
|
|
@ -1826,7 +2002,10 @@ async function runTests() {
|
|||
// Spread-from-core pollution: legacy per-module config.yaml merges
|
||||
// core values into every module; writeCentralConfig must strip these
|
||||
// from [modules.bmm] so core values only live in [core].
|
||||
// project_name is now a core key (#2279), so it joins user_name etc.
|
||||
// as a spread-from-core key that must be stripped.
|
||||
user_name: 'TestUser',
|
||||
project_name: 'stale-bmm-copy',
|
||||
communication_language: 'Spanish',
|
||||
document_output_language: 'English',
|
||||
output_folder: '_bmad-output',
|
||||
|
|
@ -1874,6 +2053,7 @@ async function runTests() {
|
|||
assert(teamContent.includes('[core]'), 'config.toml has [core] section');
|
||||
assert(teamContent.includes('document_output_language = "English"'), 'Team-scope core key lands in config.toml');
|
||||
assert(teamContent.includes('output_folder = "_bmad-output"'), 'Team-scope output_folder lands in config.toml');
|
||||
assert(teamContent.includes('project_name = "demo-project"'), 'project_name lands in [core] (core key as of #2279)');
|
||||
assert(!teamContent.includes('user_name'), 'user_name (scope: user) is absent from config.toml');
|
||||
assert(!teamContent.includes('communication_language'), 'communication_language (scope: user) is absent from config.toml');
|
||||
|
||||
|
|
@ -1888,7 +2068,9 @@ async function runTests() {
|
|||
assert(bmmTeamMatch !== null, 'config.toml has [modules.bmm] section');
|
||||
if (bmmTeamMatch) {
|
||||
const bmmTeamBlock = bmmTeamMatch[0];
|
||||
assert(bmmTeamBlock.includes('project_name = "demo-project"'), 'bmm team-scope key lands under [modules.bmm]');
|
||||
assert(bmmTeamBlock.includes('planning_artifacts'), 'bmm-owned team-scope key (planning_artifacts) lands under [modules.bmm]');
|
||||
assert(!bmmTeamBlock.includes('project_name'), 'project_name stripped from [modules.bmm] (now a core key, #2279)');
|
||||
assert(!bmmTeamBlock.includes('stale-bmm-copy'), 'stale bmm-copy of project_name not leaked into config.toml');
|
||||
assert(!bmmTeamBlock.includes('user_name'), 'user_name stripped from [modules.bmm] (core-key pollution)');
|
||||
assert(!bmmTeamBlock.includes('communication_language'), 'communication_language stripped from [modules.bmm]');
|
||||
assert(!bmmTeamBlock.includes('user_skill_level'), 'user_skill_level (scope: user) absent from [modules.bmm] in config.toml');
|
||||
|
|
@ -2731,6 +2913,113 @@ async function runTests() {
|
|||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test Suite 40c: OpenCode command pointers in multi-IDE batches
|
||||
// ============================================================
|
||||
// Regression: when OpenCode is the *peer* in a setupBatch sharing
|
||||
// .agents/skills (e.g. with openhands), the skill write is dedup-skipped
|
||||
// but the per-IDE .opencode/commands/ pointers must still be generated.
|
||||
// Symmetrically, partial uninstall while a peer remains must still clean
|
||||
// up OpenCode's own command pointers.
|
||||
console.log(`${colors.yellow}Test Suite 40c: OpenCode command pointers in shared-target batches${colors.reset}\n`);
|
||||
|
||||
try {
|
||||
clearCache();
|
||||
const platformCodes40c = await loadPlatformCodes();
|
||||
const opencodeTarget40c = platformCodes40c.platforms.opencode?.installer?.target_dir;
|
||||
const openhandsTarget40c = platformCodes40c.platforms.openhands?.installer?.target_dir;
|
||||
assert(
|
||||
opencodeTarget40c === '.agents/skills' && openhandsTarget40c === '.agents/skills',
|
||||
'OpenCode and OpenHands share .agents/skills target_dir',
|
||||
);
|
||||
|
||||
// Order A: opencode first → opencode is the writer.
|
||||
const projA = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-batch-a-'));
|
||||
const bmadA = await createTestBmadFixture();
|
||||
const mgrA = new IdeManager();
|
||||
await mgrA.ensureInitialized();
|
||||
const resultsA = await mgrA.setupBatch(['opencode', 'openhands'], projA, bmadA, {
|
||||
silent: true,
|
||||
selectedModules: ['core'],
|
||||
});
|
||||
const cmdA = path.join(projA, '.opencode', 'commands', 'bmad-master.md');
|
||||
assert(
|
||||
resultsA.every((r) => r.success === true),
|
||||
'opencode-first batch: all platforms succeed',
|
||||
);
|
||||
assert(await fs.pathExists(cmdA), 'opencode-first batch: command pointer is created');
|
||||
|
||||
// Order B: openhands first → opencode is the peer (skipTarget=true).
|
||||
// Without the fix, the early-return would bypass installCommandPointers.
|
||||
const projB = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-batch-b-'));
|
||||
const bmadB = await createTestBmadFixture();
|
||||
const mgrB = new IdeManager();
|
||||
await mgrB.ensureInitialized();
|
||||
const resultsB = await mgrB.setupBatch(['openhands', 'opencode'], projB, bmadB, {
|
||||
silent: true,
|
||||
selectedModules: ['core'],
|
||||
});
|
||||
const cmdB = path.join(projB, '.opencode', 'commands', 'bmad-master.md');
|
||||
const opencodeResultB = resultsB.find((r) => r.ide === 'opencode');
|
||||
assert(
|
||||
resultsB.every((r) => r.success === true),
|
||||
'openhands-first batch: all platforms succeed',
|
||||
);
|
||||
assert(
|
||||
opencodeResultB?.handlerResult?.results?.sharedTargetHandledByPeer === true,
|
||||
'openhands-first batch: opencode is marked sharedTargetHandledByPeer (skill write deduped)',
|
||||
);
|
||||
assert(await fs.pathExists(cmdB), 'openhands-first batch: command pointer is generated even when skill write is deduped');
|
||||
|
||||
// Cleanup symmetry: uninstall opencode while openhands remains.
|
||||
// Uses an in-project bmadDir so the cleanup path can compute removalSet
|
||||
// from the manifest (the production layout). The cross-temp-dir fixture
|
||||
// above can't exercise this — same constraint Test Suite 40 documents.
|
||||
const projC = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-batch-c-'));
|
||||
const bmadC = path.join(projC, '_bmad');
|
||||
await fs.ensureDir(path.join(bmadC, '_config'));
|
||||
await fs.writeFile(
|
||||
path.join(bmadC, '_config', 'skill-manifest.csv'),
|
||||
'canonicalId,name,description,module,path\n' +
|
||||
'"bmad-master","bmad-master","Minimal test agent fixture","core","_bmad/core/bmad-master/SKILL.md"\n',
|
||||
);
|
||||
const skillC = path.join(bmadC, 'core', 'bmad-master');
|
||||
await fs.ensureDir(skillC);
|
||||
await fs.writeFile(
|
||||
path.join(skillC, 'SKILL.md'),
|
||||
['---', 'name: bmad-master', 'description: Minimal test agent fixture', '---', '', 'You are a test agent.'].join('\n'),
|
||||
);
|
||||
|
||||
const mgrC = new IdeManager();
|
||||
await mgrC.ensureInitialized();
|
||||
await mgrC.setupBatch(['openhands', 'opencode'], projC, bmadC, {
|
||||
silent: true,
|
||||
selectedModules: ['core'],
|
||||
});
|
||||
const cmdC = path.join(projC, '.opencode', 'commands', 'bmad-master.md');
|
||||
assert(await fs.pathExists(cmdC), 'in-project fixture: pointer is generated for opencode peer');
|
||||
|
||||
const cleanupResultsC = await mgrC.cleanupByList(projC, ['opencode'], {
|
||||
silent: true,
|
||||
remainingIdes: ['openhands'],
|
||||
});
|
||||
assert(cleanupResultsC[0].success !== false, 'opencode partial-uninstall reports success');
|
||||
const sharedSurvivesC = await fs.pathExists(path.join(projC, '.agents', 'skills', 'bmad-master', 'SKILL.md'));
|
||||
assert(sharedSurvivesC, 'shared .agents/skills/ survives partial uninstall (peer still uses it)');
|
||||
assert(!(await fs.pathExists(cmdC)), 'opencode command pointer is removed on partial uninstall even when peer remains');
|
||||
|
||||
await fs.remove(projA).catch(() => {});
|
||||
await fs.remove(path.dirname(bmadA)).catch(() => {});
|
||||
await fs.remove(projB).catch(() => {});
|
||||
await fs.remove(path.dirname(bmadB)).catch(() => {});
|
||||
await fs.remove(projC).catch(() => {});
|
||||
} catch (error) {
|
||||
console.log(`${colors.red}Test Suite 40c setup failed: ${error.message}${colors.reset}`);
|
||||
failed++;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test Suite 41: Custom-module skill ownership (non-bmad prefix)
|
||||
// ============================================================
|
||||
|
|
@ -2773,6 +3062,464 @@ async function runTests() {
|
|||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test Suite 42: --tools flag parsing & validation (#2326)
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 42: --tools flag parsing & validation${colors.reset}\n`);
|
||||
try {
|
||||
const { UI } = require('../tools/installer/ui');
|
||||
const ui = new UI();
|
||||
const known = new Set(['claude-code', 'cursor', 'windsurf']);
|
||||
|
||||
assert(
|
||||
JSON.stringify(ui._parseToolsFlag('claude-code', known)) === JSON.stringify(['claude-code']),
|
||||
'parseToolsFlag returns single ID',
|
||||
);
|
||||
|
||||
assert(
|
||||
JSON.stringify(ui._parseToolsFlag('claude-code,cursor', known)) === JSON.stringify(['claude-code', 'cursor']),
|
||||
'parseToolsFlag returns multiple IDs',
|
||||
);
|
||||
|
||||
assert(
|
||||
JSON.stringify(ui._parseToolsFlag(' claude-code , cursor ', known)) === JSON.stringify(['claude-code', 'cursor']),
|
||||
'parseToolsFlag trims whitespace',
|
||||
);
|
||||
|
||||
let emptyErr;
|
||||
try {
|
||||
ui._parseToolsFlag('', known);
|
||||
} catch (error) {
|
||||
emptyErr = error;
|
||||
}
|
||||
assert(
|
||||
emptyErr && emptyErr.expected === true && /empty/i.test(emptyErr.message),
|
||||
'parseToolsFlag rejects empty string with expected=true',
|
||||
);
|
||||
|
||||
let commasOnlyErr;
|
||||
try {
|
||||
ui._parseToolsFlag(' , , ', known);
|
||||
} catch (error) {
|
||||
commasOnlyErr = error;
|
||||
}
|
||||
assert(commasOnlyErr && commasOnlyErr.expected === true, 'parseToolsFlag rejects whitespace/comma-only input');
|
||||
|
||||
let noneErr;
|
||||
try {
|
||||
ui._parseToolsFlag('none', known);
|
||||
} catch (error) {
|
||||
noneErr = error;
|
||||
}
|
||||
assert(noneErr && noneErr.expected === true && /Unknown tool ID/.test(noneErr.message), 'parseToolsFlag rejects "none" as unknown ID');
|
||||
|
||||
let typoErr;
|
||||
try {
|
||||
ui._parseToolsFlag('claude-code,claude-cdoe', known);
|
||||
} catch (error) {
|
||||
typoErr = error;
|
||||
}
|
||||
const typoHeader = typoErr ? typoErr.message.split('\n')[0] : '';
|
||||
assert(
|
||||
typoErr && typoErr.expected === true && /claude-cdoe/.test(typoHeader) && !/claude-code/.test(typoHeader),
|
||||
'parseToolsFlag reports only the unknown ID in error header (valid ones not listed as unknown)',
|
||||
);
|
||||
|
||||
// --list-tools and --tools validation must agree on what counts as a valid ID.
|
||||
const { formatPlatformList } = require('../tools/installer/ide/platform-codes');
|
||||
const { IdeManager } = require('../tools/installer/ide/manager');
|
||||
const ideManager42 = new IdeManager();
|
||||
await ideManager42.ensureInitialized();
|
||||
const validIds = new Set(ideManager42.getAvailableIdes().map((i) => i.value));
|
||||
const listed = await formatPlatformList();
|
||||
// Each entry line starts with ' *' (preferred) or ' ' (other), followed by the ID, then padding.
|
||||
const entryLines = listed.split('\n').filter((l) => /^( \*| {2})[a-z]/.test(l));
|
||||
const listedIds = entryLines.map((l) => l.trim().replace(/^\*/, '').split(/\s+/)[0]);
|
||||
const missingFromList = [...validIds].filter((id) => !listedIds.includes(id));
|
||||
const extraInList = listedIds.filter((id) => !validIds.has(id));
|
||||
assert(
|
||||
missingFromList.length === 0 && extraInList.length === 0,
|
||||
'--list-tools output matches the IDs that --tools accepts',
|
||||
`Missing from list: ${missingFromList.join(',') || '(none)'}; Extra in list: ${extraInList.join(',') || '(none)'}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(`${colors.red}Test Suite 42 setup failed: ${error.message}${colors.reset}`);
|
||||
console.log(error.stack);
|
||||
failed++;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test Suite 43: project_name promoted to core + hoist migration (#2279)
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 43: project_name in core + hoist migration${colors.reset}\n`);
|
||||
try {
|
||||
const yamlLib = require('yaml');
|
||||
const coreSchemaPath = path.join(__dirname, '..', 'src', 'core-skills', 'module.yaml');
|
||||
const bmmSchemaPath = path.join(__dirname, '..', 'src', 'bmm-skills', 'module.yaml');
|
||||
const coreSchema = yamlLib.parse(await fs.readFile(coreSchemaPath, 'utf8'));
|
||||
const bmmSchema = yamlLib.parse(await fs.readFile(bmmSchemaPath, 'utf8'));
|
||||
|
||||
assert(
|
||||
coreSchema.project_name && coreSchema.project_name.prompt && coreSchema.project_name.default === '{directory_name}',
|
||||
'core/module.yaml declares project_name with {directory_name} default',
|
||||
);
|
||||
|
||||
assert(coreSchema.project_name.scope === undefined, 'project_name has no user scope (project-scoped, not user-scoped)');
|
||||
|
||||
assert(bmmSchema.project_name === undefined, 'bmm/module.yaml no longer declares project_name (now inherited from core)');
|
||||
|
||||
// Set up a mock existing install: bmm directory has project_name (legacy),
|
||||
// core has user_name but not project_name. After hoist, project_name should
|
||||
// move to core, leaving bmm with only its own keys.
|
||||
const fixtureRoot43 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-43-'));
|
||||
const bmadDir43 = path.join(fixtureRoot43, '_bmad');
|
||||
await fs.ensureDir(path.join(bmadDir43, '_config'));
|
||||
await fs.writeFile(path.join(bmadDir43, '_config', 'manifest.yaml'), 'modules: []\n', 'utf8');
|
||||
await fs.ensureDir(path.join(bmadDir43, 'core'));
|
||||
await fs.ensureDir(path.join(bmadDir43, 'bmm'));
|
||||
await fs.writeFile(path.join(bmadDir43, 'core', 'config.yaml'), 'user_name: alice\n', 'utf8');
|
||||
await fs.writeFile(
|
||||
path.join(bmadDir43, 'bmm', 'config.yaml'),
|
||||
'project_name: legacy-from-bmm\nuser_skill_level: intermediate\n',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const officialModules43 = new OfficialModules();
|
||||
await officialModules43.loadExistingConfig(fixtureRoot43);
|
||||
|
||||
assert(
|
||||
officialModules43.existingConfig.core?.project_name === 'legacy-from-bmm',
|
||||
'loadExistingConfig hoists bmm.project_name to core on existing-install upgrade',
|
||||
);
|
||||
|
||||
assert(
|
||||
!('project_name' in (officialModules43.existingConfig.bmm || {})),
|
||||
'loadExistingConfig removes project_name from bmm after hoisting',
|
||||
);
|
||||
|
||||
assert(
|
||||
officialModules43.existingConfig.bmm?.user_skill_level === 'intermediate',
|
||||
'loadExistingConfig leaves non-core bmm keys (user_skill_level) untouched',
|
||||
);
|
||||
|
||||
assert(officialModules43.existingConfig.core?.user_name === 'alice', 'loadExistingConfig preserves pre-existing core values');
|
||||
|
||||
// Precedence: if core already has the key, hoist must NOT overwrite it.
|
||||
const fixtureRoot43b = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-43b-'));
|
||||
const bmadDir43b = path.join(fixtureRoot43b, '_bmad');
|
||||
await fs.ensureDir(path.join(bmadDir43b, '_config'));
|
||||
await fs.writeFile(path.join(bmadDir43b, '_config', 'manifest.yaml'), 'modules: []\n', 'utf8');
|
||||
await fs.ensureDir(path.join(bmadDir43b, 'core'));
|
||||
await fs.ensureDir(path.join(bmadDir43b, 'bmm'));
|
||||
await fs.writeFile(path.join(bmadDir43b, 'core', 'config.yaml'), 'project_name: from-core\n', 'utf8');
|
||||
await fs.writeFile(path.join(bmadDir43b, 'bmm', 'config.yaml'), 'project_name: stale-from-bmm\n', 'utf8');
|
||||
|
||||
const officialModules43b = new OfficialModules();
|
||||
await officialModules43b.loadExistingConfig(fixtureRoot43b);
|
||||
|
||||
assert(officialModules43b.existingConfig.core?.project_name === 'from-core', 'hoist does not overwrite an existing core value');
|
||||
|
||||
assert(
|
||||
!('project_name' in (officialModules43b.existingConfig.bmm || {})),
|
||||
'hoist still strips the duplicate from bmm so writeCentralConfig partition stays clean',
|
||||
);
|
||||
|
||||
// Malformed config.yaml (parses to a scalar) must not crash loadExistingConfig
|
||||
// or the hoist pass — they should treat it as "no config for that module"
|
||||
// and continue. Regression for augment review on PR #2348.
|
||||
const fixtureRoot43c = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-43c-'));
|
||||
const bmadDir43c = path.join(fixtureRoot43c, '_bmad');
|
||||
await fs.ensureDir(path.join(bmadDir43c, '_config'));
|
||||
await fs.writeFile(path.join(bmadDir43c, '_config', 'manifest.yaml'), 'modules: []\n', 'utf8');
|
||||
await fs.ensureDir(path.join(bmadDir43c, 'core'));
|
||||
await fs.ensureDir(path.join(bmadDir43c, 'bmm'));
|
||||
// Scalar YAML — yaml.parse returns the literal 42 (truthy non-object).
|
||||
// Pre-fix this crashed _hoistCoreKeysFromLegacyModuleConfigs with
|
||||
// "Cannot use 'in' operator to search for 'project_name' in 42".
|
||||
await fs.writeFile(path.join(bmadDir43c, 'core', 'config.yaml'), '42\n', 'utf8');
|
||||
await fs.writeFile(path.join(bmadDir43c, 'bmm', 'config.yaml'), 'project_name: rescued\n', 'utf8');
|
||||
|
||||
const officialModules43c = new OfficialModules();
|
||||
let crashErr;
|
||||
try {
|
||||
await officialModules43c.loadExistingConfig(fixtureRoot43c);
|
||||
} catch (error) {
|
||||
crashErr = error;
|
||||
}
|
||||
assert(!crashErr, 'loadExistingConfig does not crash on a scalar core/config.yaml', crashErr?.stack);
|
||||
|
||||
assert(
|
||||
officialModules43c.existingConfig.core?.project_name === 'rescued',
|
||||
'scalar core gets replaced with {} and bmm.project_name still hoists in',
|
||||
);
|
||||
|
||||
await fs.remove(fixtureRoot43).catch(() => {});
|
||||
await fs.remove(fixtureRoot43b).catch(() => {});
|
||||
await fs.remove(fixtureRoot43c).catch(() => {});
|
||||
} catch (error) {
|
||||
console.log(`${colors.red}Test Suite 43 setup failed: ${error.message}${colors.reset}`);
|
||||
console.log(error.stack);
|
||||
failed++;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test Suite 44: --set <module>.<key>=<value> CLI overrides (#1663)
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 44: --set CLI overrides${colors.reset}\n`);
|
||||
try {
|
||||
const { parseSetEntry, parseSetEntries, applySetOverrides, upsertTomlKey, tomlString } = require('../tools/installer/set-overrides');
|
||||
const { discoverOfficialModuleYamls, formatOptionsList } = require('../tools/installer/list-options');
|
||||
|
||||
// ---- Parser ----------------------------------------------------------
|
||||
const ok = parseSetEntry('bmm.project_knowledge=research');
|
||||
assert(
|
||||
ok.module === 'bmm' && ok.key === 'project_knowledge' && ok.value === 'research',
|
||||
'parseSetEntry splits <module>.<key>=<value> correctly',
|
||||
);
|
||||
assert(parseSetEntry('bmm.weird=a=b=c').value === 'a=b=c', 'parseSetEntry preserves additional "=" inside the value');
|
||||
|
||||
const badInputs = ['no-equals', 'no-dot=value', '=value', '.=value', 'foo.=value', '.bar=value', ''];
|
||||
let allBadThrow = true;
|
||||
for (const bad of badInputs) {
|
||||
try {
|
||||
parseSetEntry(bad);
|
||||
allBadThrow = false;
|
||||
} catch {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
assert(allBadThrow, `parseSetEntry rejects malformed inputs (${badInputs.length} cases)`);
|
||||
|
||||
const multi = parseSetEntries(['bmm.project_knowledge=research', 'bmm.user_skill_level=expert', 'core.user_name=Brian']);
|
||||
assert(
|
||||
multi.bmm.project_knowledge === 'research' && multi.bmm.user_skill_level === 'expert' && multi.core.user_name === 'Brian',
|
||||
'parseSetEntries groups by module',
|
||||
);
|
||||
assert(parseSetEntries(['bmm.x=first', 'bmm.x=second']).bmm.x === 'second', 'parseSetEntries: later --set entry overrides earlier');
|
||||
const empty = parseSetEntries();
|
||||
assert(empty && Object.keys(empty).length === 0, 'parseSetEntries() returns empty object when called without args');
|
||||
|
||||
// Prototype-pollution guard. `--set __proto__.x=1` would otherwise reach
|
||||
// `overrides.__proto__[x] = 1` and pollute every plain object.
|
||||
const polluteProbe = {};
|
||||
let pollutionThrown = false;
|
||||
try {
|
||||
parseSetEntries(['__proto__.polluted=1']);
|
||||
} catch {
|
||||
pollutionThrown = true;
|
||||
}
|
||||
assert(pollutionThrown, 'parseSetEntries rejects __proto__ as a module name');
|
||||
assert(polluteProbe.polluted === undefined, 'Object.prototype is not polluted by __proto__ in --set entries');
|
||||
let constructorThrown = false;
|
||||
try {
|
||||
parseSetEntries(['bmm.constructor=evil']);
|
||||
} catch {
|
||||
constructorThrown = true;
|
||||
}
|
||||
assert(constructorThrown, 'parseSetEntries rejects "constructor" as a key name');
|
||||
|
||||
// ---- tomlString ------------------------------------------------------
|
||||
assert(tomlString('hello') === '"hello"', 'tomlString quotes a plain string');
|
||||
assert(tomlString('with "quotes"') === String.raw`"with \"quotes\""`, 'tomlString escapes embedded double-quotes');
|
||||
assert(tomlString(String.raw`back\slash`) === String.raw`"back\\slash"`, 'tomlString escapes backslashes');
|
||||
assert(tomlString('line1\nline2') === String.raw`"line1\nline2"`, 'tomlString escapes newlines');
|
||||
|
||||
// ---- upsertTomlKey: insert into existing section ---------------------
|
||||
{
|
||||
const before = `[core]\nuser_name = "Brian"\n\n[modules.bmm]\nproject_knowledge = "{project-root}/docs"\n`;
|
||||
const after = upsertTomlKey(before, '[modules.bmm]', 'future_thing', '"persists"');
|
||||
assert(after.includes('future_thing = "persists"'), 'upsertTomlKey inserts a new key into an existing section');
|
||||
assert(/project_knowledge = "{project-root}\/docs"/.test(after), 'upsertTomlKey preserves existing keys');
|
||||
}
|
||||
|
||||
// ---- upsertTomlKey: replace existing key, keep comment tail ----------
|
||||
{
|
||||
const before = `[core]\nuser_name = "old" # set on first install\n`;
|
||||
const after = upsertTomlKey(before, '[core]', 'user_name', '"Brian"');
|
||||
assert(/user_name = "Brian"\s+# set on first install/.test(after), 'upsertTomlKey preserves trailing comments');
|
||||
assert(!after.includes('"old"'), 'upsertTomlKey replaces the prior value');
|
||||
}
|
||||
|
||||
// ---- upsertTomlKey: section missing → append new section -------------
|
||||
{
|
||||
const before = `[core]\nuser_name = "Brian"\n`;
|
||||
const after = upsertTomlKey(before, '[modules.bmm]', 'project_knowledge', '"research"');
|
||||
assert(after.includes('[modules.bmm]'), 'upsertTomlKey appends a new section when missing');
|
||||
assert(after.includes('project_knowledge = "research"'), 'upsertTomlKey appends the key under the new section');
|
||||
// Existing section remains untouched
|
||||
assert(after.indexOf('[core]') < after.indexOf('[modules.bmm]'), 'upsertTomlKey adds the new section AFTER existing content');
|
||||
}
|
||||
|
||||
// ---- upsertTomlKey: empty file ---------------------------------------
|
||||
{
|
||||
const after = upsertTomlKey('', '[core]', 'user_name', '"Brian"');
|
||||
assert(after.startsWith('[core]'), 'upsertTomlKey on an empty string emits the section header');
|
||||
assert(after.includes('user_name = "Brian"'), 'upsertTomlKey on an empty string writes the key');
|
||||
}
|
||||
|
||||
// ---- upsertTomlKey: trailing newline preserved -----------------------
|
||||
{
|
||||
const withTrailing = upsertTomlKey('[core]\nuser_name = "old"\n', '[core]', 'user_name', '"new"');
|
||||
assert(withTrailing.endsWith('\n'), 'upsertTomlKey preserves trailing newline');
|
||||
const withoutTrailing = upsertTomlKey('[core]\nuser_name = "old"', '[core]', 'user_name', '"new"');
|
||||
assert(!withoutTrailing.endsWith('\n'), 'upsertTomlKey preserves absence of trailing newline');
|
||||
}
|
||||
|
||||
// ---- applySetOverrides happy path ------------------------------------
|
||||
{
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-applyset-'));
|
||||
const bmadDir = path.join(tmp, '_bmad');
|
||||
await fs.ensureDir(bmadDir);
|
||||
// Seed a realistic post-install state: team config has bmm.project_knowledge,
|
||||
// user config has core.user_name. The applySetOverrides router should
|
||||
// route bmm.user_skill_level → user.toml (already there), core.user_name
|
||||
// update → user.toml (already there), and a brand-new key → team.toml.
|
||||
await fs.writeFile(
|
||||
path.join(bmadDir, 'config.toml'),
|
||||
'[core]\nproject_name = "demo"\n\n[modules.bmm]\nproject_knowledge = "{project-root}/docs"\n',
|
||||
'utf8',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(bmadDir, 'config.user.toml'),
|
||||
'[core]\nuser_name = "OldName"\n\n[modules.bmm]\nuser_skill_level = "intermediate"\n',
|
||||
'utf8',
|
||||
);
|
||||
// Per-module config.yaml stubs are the "is this module installed?"
|
||||
// signal applySetOverrides uses to skip uninstalled-module overrides.
|
||||
await fs.ensureDir(path.join(bmadDir, 'core'));
|
||||
await fs.writeFile(path.join(bmadDir, 'core', 'config.yaml'), 'project_name: demo\n', 'utf8');
|
||||
await fs.ensureDir(path.join(bmadDir, 'bmm'));
|
||||
await fs.writeFile(
|
||||
path.join(bmadDir, 'bmm', 'config.yaml'),
|
||||
'project_knowledge: "{project-root}/docs"\nuser_skill_level: intermediate\n',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const overrides = {
|
||||
core: { user_name: 'Brian' },
|
||||
bmm: { user_skill_level: 'expert', future_thing: 'persists' },
|
||||
};
|
||||
const applied = await applySetOverrides(overrides, bmadDir);
|
||||
|
||||
const team = await fs.readFile(path.join(bmadDir, 'config.toml'), 'utf8');
|
||||
const user = await fs.readFile(path.join(bmadDir, 'config.user.toml'), 'utf8');
|
||||
|
||||
assert(user.includes('user_name = "Brian"'), 'applySetOverrides updates user-scope key in config.user.toml');
|
||||
assert(user.includes('user_skill_level = "expert"'), 'applySetOverrides updates pre-existing user-scope key in config.user.toml');
|
||||
assert(team.includes('future_thing = "persists"'), 'applySetOverrides routes brand-new key to team config.toml');
|
||||
assert(team.includes('project_knowledge = "{project-root}/docs"'), 'applySetOverrides leaves untouched team keys alone');
|
||||
assert(!team.includes('user_name = "Brian"'), 'applySetOverrides does NOT duplicate user-scope key into team file');
|
||||
|
||||
const summary = applied
|
||||
.map((a) => `${a.module}.${a.key}->${a.scope}`)
|
||||
.sort()
|
||||
.join(',');
|
||||
assert(
|
||||
summary === 'bmm.future_thing->team,bmm.user_skill_level->user,core.user_name->user',
|
||||
`applySetOverrides reports correct routing decisions (got: ${summary})`,
|
||||
);
|
||||
|
||||
await fs.remove(tmp).catch(() => {});
|
||||
}
|
||||
|
||||
// ---- applySetOverrides creates config.user.toml if missing -----------
|
||||
{
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-applyset-nouser-'));
|
||||
const bmadDir = path.join(tmp, '_bmad');
|
||||
await fs.ensureDir(bmadDir);
|
||||
await fs.writeFile(path.join(bmadDir, 'config.toml'), '[core]\nuser_name = "Brian"\n', 'utf8');
|
||||
await fs.ensureDir(path.join(bmadDir, 'core'));
|
||||
await fs.writeFile(path.join(bmadDir, 'core', 'config.yaml'), 'user_name: Brian\n', 'utf8');
|
||||
// Override targets a key only in team config; routes to team. user.toml
|
||||
// never gets created in this case (correct — no user-scope writes).
|
||||
await applySetOverrides({ core: { user_name: 'Updated' } }, bmadDir);
|
||||
const team = await fs.readFile(path.join(bmadDir, 'config.toml'), 'utf8');
|
||||
assert(team.includes('user_name = "Updated"'), 'applySetOverrides updates team key when user.toml is absent');
|
||||
assert(
|
||||
!(await fs.pathExists(path.join(bmadDir, 'config.user.toml'))),
|
||||
'applySetOverrides does not create config.user.toml unnecessarily',
|
||||
);
|
||||
await fs.remove(tmp).catch(() => {});
|
||||
}
|
||||
|
||||
// ---- applySetOverrides skips modules without per-module config.yaml --
|
||||
{
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-applyset-skip-'));
|
||||
const bmadDir = path.join(tmp, '_bmad');
|
||||
await fs.ensureDir(bmadDir);
|
||||
await fs.writeFile(path.join(bmadDir, 'config.toml'), '[core]\nuser_name = "Brian"\n', 'utf8');
|
||||
await fs.ensureDir(path.join(bmadDir, 'core'));
|
||||
await fs.writeFile(path.join(bmadDir, 'core', 'config.yaml'), 'user_name: Brian\n', 'utf8');
|
||||
// bmm is not installed (no `_bmad/bmm/config.yaml`). The override for
|
||||
// bmm should be silently skipped, no `[modules.bmm]` section created.
|
||||
const applied = await applySetOverrides({ bmm: { foo: 'bar' }, core: { user_name: 'Updated' } }, bmadDir);
|
||||
const team = await fs.readFile(path.join(bmadDir, 'config.toml'), 'utf8');
|
||||
assert(!team.includes('[modules.bmm]'), 'applySetOverrides does NOT create section for uninstalled module');
|
||||
assert(team.includes('user_name = "Updated"'), 'applySetOverrides still applies overrides for installed modules');
|
||||
assert(applied.length === 1 && applied[0].module === 'core', 'applySetOverrides reports only the installed-module entries');
|
||||
await fs.remove(tmp).catch(() => {});
|
||||
}
|
||||
|
||||
// ---- applySetOverrides: empty/missing input is a no-op ---------------
|
||||
{
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-applyset-empty-'));
|
||||
const bmadDir = path.join(tmp, '_bmad');
|
||||
await fs.ensureDir(bmadDir);
|
||||
const empty1 = await applySetOverrides({}, bmadDir);
|
||||
const empty2 = await applySetOverrides(null, bmadDir);
|
||||
const empty3 = await applySetOverrides(undefined, bmadDir);
|
||||
assert(
|
||||
empty1.length === 0 && empty2.length === 0 && empty3.length === 0,
|
||||
'applySetOverrides is a no-op for empty/null/undefined input',
|
||||
);
|
||||
await fs.remove(tmp).catch(() => {});
|
||||
}
|
||||
|
||||
// ---- discoverOfficialModuleYamls + formatOptionsList -----------------
|
||||
// These read the on-disk external-module cache. Point that env at a temp
|
||||
// dir so test results don't depend on whatever the developer / CI runner
|
||||
// has cached.
|
||||
const priorCacheEnv44 = process.env.BMAD_EXTERNAL_MODULES_CACHE;
|
||||
const tempCacheDir44 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-list-options-cache-'));
|
||||
process.env.BMAD_EXTERNAL_MODULES_CACHE = tempCacheDir44;
|
||||
try {
|
||||
const discovered = await discoverOfficialModuleYamls();
|
||||
const codes = new Set(discovered.map((d) => d.code));
|
||||
assert(codes.has('core') && codes.has('bmm'), 'discoverOfficialModuleYamls finds core and bmm built-ins');
|
||||
|
||||
const bmmListing = await formatOptionsList('bmm');
|
||||
assert(bmmListing.ok === true, '--list-options bmm reports ok: true');
|
||||
assert(bmmListing.text.includes('bmm.project_knowledge'), '--list-options bmm renders bmm.project_knowledge');
|
||||
assert(bmmListing.text.includes('bmm.user_skill_level'), '--list-options bmm renders bmm.user_skill_level');
|
||||
|
||||
// Case-insensitive filter.
|
||||
const bmmUpper = await formatOptionsList('BMM');
|
||||
assert(bmmUpper.ok === true && bmmUpper.text.includes('bmm.project_knowledge'), '--list-options is case-insensitive');
|
||||
|
||||
// Unknown module → non-zero exit signal.
|
||||
const unknown = await formatOptionsList('definitely-not-a-module');
|
||||
assert(unknown.ok === false, '--list-options <unknown> reports ok: false');
|
||||
assert(unknown.text.includes('No locally-known module.yaml'), '--list-options unknown explains the miss');
|
||||
} finally {
|
||||
if (priorCacheEnv44 === undefined) {
|
||||
delete process.env.BMAD_EXTERNAL_MODULES_CACHE;
|
||||
} else {
|
||||
process.env.BMAD_EXTERNAL_MODULES_CACHE = priorCacheEnv44;
|
||||
}
|
||||
await fs.remove(tempCacheDir44).catch(() => {});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`${colors.red}Test Suite 44 setup failed: ${error.message}${colors.reset}`);
|
||||
console.log(error.stack);
|
||||
failed++;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Summary
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -0,0 +1,294 @@
|
|||
/**
|
||||
* parseSource() URL parsing tests
|
||||
*
|
||||
* Verifies that CustomModuleManager.parseSource() correctly handles Git URLs
|
||||
* across arbitrary hosts and path shapes (deep paths, nested groups, browse
|
||||
* links, repo names containing dots, etc.) using host-agnostic rules.
|
||||
*
|
||||
* Usage: node test/test-parse-source-urls.js
|
||||
*/
|
||||
|
||||
const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager');
|
||||
|
||||
// ANSI colors
|
||||
const colors = {
|
||||
reset: '\u001B[0m',
|
||||
green: '\u001B[32m',
|
||||
red: '\u001B[31m',
|
||||
cyan: '\u001B[36m',
|
||||
dim: '\u001B[2m',
|
||||
};
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, testName, errorMessage = '') {
|
||||
if (condition) {
|
||||
console.log(`${colors.green}✓${colors.reset} ${testName}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(`${colors.red}✗${colors.reset} ${testName}`);
|
||||
if (errorMessage) {
|
||||
console.log(` ${colors.dim}${errorMessage}${colors.reset}`);
|
||||
}
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
const manager = new CustomModuleManager();
|
||||
|
||||
// ─── Deep path shapes (4+ segments) ─────────────────────────────────────────
|
||||
|
||||
console.log(`\n${colors.cyan}Deep path shapes${colors.reset}\n`);
|
||||
|
||||
{
|
||||
// Hosts that expose the repo at a nested path like /<org>/<project>/<marker>/<repo>.
|
||||
// The parser must preserve the full path (no stripping of intermediate segments).
|
||||
const result = manager.parseSource('https://git.example.com/myorg/MyProject/_git/my-module');
|
||||
assert(result.isValid === true, 'nested-path URL is valid');
|
||||
assert(result.type === 'url', 'nested-path type is url');
|
||||
assert(
|
||||
result.cloneUrl === 'https://git.example.com/myorg/MyProject/_git/my-module',
|
||||
'nested-path cloneUrl preserves full path',
|
||||
`Got: ${result.cloneUrl}`,
|
||||
);
|
||||
assert(result.subdir === null, 'nested-path URL has no subdir');
|
||||
assert(
|
||||
result.cacheKey === 'git.example.com/myorg/MyProject/_git/my-module',
|
||||
'nested-path cacheKey includes full repo path',
|
||||
`Got: ${result.cacheKey}`,
|
||||
);
|
||||
assert(result.displayName === '_git/my-module', 'nested-path displayName uses last two segments', `Got: ${result.displayName}`);
|
||||
}
|
||||
|
||||
{
|
||||
const result = manager.parseSource('https://git.example.com/myorg/MyProject/_git/my-module.git');
|
||||
assert(result.isValid === true, 'nested-path URL with .git suffix is valid');
|
||||
assert(
|
||||
result.cloneUrl === 'https://git.example.com/myorg/MyProject/_git/my-module',
|
||||
'nested-path .git suffix stripped from cloneUrl',
|
||||
`Got: ${result.cloneUrl}`,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// Browse links that use ?path=/... to point at a subdirectory.
|
||||
const result = manager.parseSource('https://git.example.com/myorg/MyProject/_git/my-module?path=/path/to/subdir');
|
||||
assert(result.isValid === true, 'URL with ?path= is valid');
|
||||
assert(
|
||||
result.cloneUrl === 'https://git.example.com/myorg/MyProject/_git/my-module',
|
||||
'?path= cloneUrl excludes subdir',
|
||||
`Got: ${result.cloneUrl}`,
|
||||
);
|
||||
assert(result.subdir === 'path/to/subdir', '?path= subdir correctly extracted', `Got: ${result.subdir}`);
|
||||
}
|
||||
|
||||
// ─── Azure DevOps URLs (Issue #2268) ────────────────────────────────────────
|
||||
|
||||
console.log(`\n${colors.cyan}Azure DevOps URLs (Issue #2268)${colors.reset}\n`);
|
||||
|
||||
{
|
||||
// Modern dev.azure.com format — the exact URL from the bug report.
|
||||
const result = manager.parseSource('https://dev.azure.com/myorg/MyProject/_git/my-module');
|
||||
assert(result.isValid === true, 'ADO modern URL is valid');
|
||||
assert(result.type === 'url', 'ADO modern type is url');
|
||||
assert(
|
||||
result.cloneUrl === 'https://dev.azure.com/myorg/MyProject/_git/my-module',
|
||||
'ADO modern cloneUrl preserves full _git path',
|
||||
`Got: ${result.cloneUrl}`,
|
||||
);
|
||||
assert(
|
||||
result.cacheKey === 'dev.azure.com/myorg/MyProject/_git/my-module',
|
||||
'ADO modern cacheKey includes full path',
|
||||
`Got: ${result.cacheKey}`,
|
||||
);
|
||||
assert(result.subdir === null, 'ADO modern URL has no subdir');
|
||||
}
|
||||
|
||||
{
|
||||
// Modern format with .git suffix
|
||||
const result = manager.parseSource('https://dev.azure.com/myorg/MyProject/_git/my-module.git');
|
||||
assert(result.isValid === true, 'ADO modern .git suffix is valid');
|
||||
assert(
|
||||
result.cloneUrl === 'https://dev.azure.com/myorg/MyProject/_git/my-module',
|
||||
'ADO modern .git suffix stripped from cloneUrl',
|
||||
`Got: ${result.cloneUrl}`,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// Modern format with ?path= subdir (browse link)
|
||||
const result = manager.parseSource('https://dev.azure.com/myorg/MyProject/_git/my-module?path=/src/skills');
|
||||
assert(result.isValid === true, 'ADO modern ?path= is valid');
|
||||
assert(
|
||||
result.cloneUrl === 'https://dev.azure.com/myorg/MyProject/_git/my-module',
|
||||
'ADO modern ?path= cloneUrl excludes subdir',
|
||||
`Got: ${result.cloneUrl}`,
|
||||
);
|
||||
assert(result.subdir === 'src/skills', 'ADO modern ?path= subdir extracted', `Got: ${result.subdir}`);
|
||||
}
|
||||
|
||||
{
|
||||
// Legacy visualstudio.com format
|
||||
const result = manager.parseSource('https://myorg.visualstudio.com/MyProject/_git/my-module');
|
||||
assert(result.isValid === true, 'ADO legacy URL is valid');
|
||||
assert(
|
||||
result.cloneUrl === 'https://myorg.visualstudio.com/MyProject/_git/my-module',
|
||||
'ADO legacy cloneUrl preserves full path',
|
||||
`Got: ${result.cloneUrl}`,
|
||||
);
|
||||
assert(
|
||||
result.cacheKey === 'myorg.visualstudio.com/MyProject/_git/my-module',
|
||||
'ADO legacy cacheKey includes full path',
|
||||
`Got: ${result.cacheKey}`,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// Legacy format with .git suffix
|
||||
const result = manager.parseSource('https://myorg.visualstudio.com/MyProject/_git/my-module.git');
|
||||
assert(result.isValid === true, 'ADO legacy .git suffix is valid');
|
||||
assert(
|
||||
result.cloneUrl === 'https://myorg.visualstudio.com/MyProject/_git/my-module',
|
||||
'ADO legacy .git suffix stripped from cloneUrl',
|
||||
`Got: ${result.cloneUrl}`,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// Legacy format with ?path= subdir
|
||||
const result = manager.parseSource('https://myorg.visualstudio.com/MyProject/_git/my-module?path=/src');
|
||||
assert(result.isValid === true, 'ADO legacy ?path= is valid');
|
||||
assert(
|
||||
result.cloneUrl === 'https://myorg.visualstudio.com/MyProject/_git/my-module',
|
||||
'ADO legacy ?path= cloneUrl excludes subdir',
|
||||
`Got: ${result.cloneUrl}`,
|
||||
);
|
||||
assert(result.subdir === 'src', 'ADO legacy ?path= subdir extracted', `Got: ${result.subdir}`);
|
||||
}
|
||||
|
||||
// ─── Subdomain hosts ────────────────────────────────────────────────────────
|
||||
|
||||
console.log(`\n${colors.cyan}Subdomain hosts${colors.reset}\n`);
|
||||
|
||||
{
|
||||
const result = manager.parseSource('https://myorg.example.com/MyProject/_git/my-module');
|
||||
assert(result.isValid === true, 'subdomain URL is valid');
|
||||
assert(result.type === 'url', 'subdomain type is url');
|
||||
assert(
|
||||
result.cloneUrl === 'https://myorg.example.com/MyProject/_git/my-module',
|
||||
'subdomain cloneUrl preserves full path',
|
||||
`Got: ${result.cloneUrl}`,
|
||||
);
|
||||
assert(result.subdir === null, 'subdomain URL has no subdir');
|
||||
assert(
|
||||
result.cacheKey === 'myorg.example.com/MyProject/_git/my-module',
|
||||
'subdomain cacheKey includes full repo path',
|
||||
`Got: ${result.cacheKey}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Simple owner/repo URLs (regression) ────────────────────────────────────
|
||||
|
||||
console.log(`\n${colors.cyan}Simple owner/repo URLs (regression check)${colors.reset}\n`);
|
||||
|
||||
{
|
||||
const result = manager.parseSource('https://github.com/owner/repo');
|
||||
assert(result.isValid === true, 'GitHub basic URL still valid');
|
||||
assert(result.cloneUrl === 'https://github.com/owner/repo', 'GitHub cloneUrl unchanged', `Got: ${result.cloneUrl}`);
|
||||
assert(result.cacheKey === 'github.com/owner/repo', 'GitHub cacheKey unchanged', `Got: ${result.cacheKey}`);
|
||||
}
|
||||
|
||||
{
|
||||
const result = manager.parseSource('https://github.com/owner/repo/tree/main/subdir');
|
||||
assert(result.isValid === true, 'GitHub URL with tree path still valid');
|
||||
assert(result.cloneUrl === 'https://github.com/owner/repo', 'GitHub tree URL cloneUrl correct', `Got: ${result.cloneUrl}`);
|
||||
assert(result.subdir === 'subdir', 'GitHub tree subdir still extracted', `Got: ${result.subdir}`);
|
||||
}
|
||||
|
||||
{
|
||||
const result = manager.parseSource('git@github.com:owner/repo.git');
|
||||
assert(result.isValid === true, 'SSH URL still valid');
|
||||
assert(result.cloneUrl === 'git@github.com:owner/repo.git', 'SSH cloneUrl unchanged', `Got: ${result.cloneUrl}`);
|
||||
}
|
||||
|
||||
// ─── Generic URL handling (any host, any path depth) ────────────────────────
|
||||
|
||||
console.log(`\n${colors.cyan}Generic URL handling${colors.reset}\n`);
|
||||
|
||||
{
|
||||
// GitLab nested groups — the old 2-segment regex would have failed this.
|
||||
const result = manager.parseSource('https://gitlab.com/group/subgroup/repo');
|
||||
assert(result.isValid === true, 'GitLab nested-group URL is valid');
|
||||
assert(
|
||||
result.cloneUrl === 'https://gitlab.com/group/subgroup/repo',
|
||||
'GitLab nested-group cloneUrl preserves full path',
|
||||
`Got: ${result.cloneUrl}`,
|
||||
);
|
||||
assert(
|
||||
result.cacheKey === 'gitlab.com/group/subgroup/repo',
|
||||
'GitLab nested-group cacheKey includes full path',
|
||||
`Got: ${result.cacheKey}`,
|
||||
);
|
||||
assert(result.displayName === 'subgroup/repo', 'GitLab nested-group displayName uses last two segments', `Got: ${result.displayName}`);
|
||||
}
|
||||
|
||||
{
|
||||
const result = manager.parseSource('https://gitlab.com/group/subgroup/repo/-/tree/main/src/module');
|
||||
assert(result.isValid === true, 'GitLab nested-group tree URL is valid');
|
||||
assert(
|
||||
result.cloneUrl === 'https://gitlab.com/group/subgroup/repo',
|
||||
'GitLab nested-group tree cloneUrl excludes subdir',
|
||||
`Got: ${result.cloneUrl}`,
|
||||
);
|
||||
assert(result.subdir === 'src/module', 'GitLab nested-group tree subdir extracted', `Got: ${result.subdir}`);
|
||||
}
|
||||
|
||||
{
|
||||
// Self-hosted host with a repo name containing dots — the old regex
|
||||
// explicitly excluded dots from the repo segment.
|
||||
const result = manager.parseSource('https://git.example.com/owner/my.repo.name');
|
||||
assert(result.isValid === true, 'repo name with dots is valid');
|
||||
assert(
|
||||
result.cloneUrl === 'https://git.example.com/owner/my.repo.name',
|
||||
'repo name with dots preserved in cloneUrl',
|
||||
`Got: ${result.cloneUrl}`,
|
||||
);
|
||||
assert(result.displayName === 'owner/my.repo.name', 'repo name with dots preserved in displayName', `Got: ${result.displayName}`);
|
||||
}
|
||||
|
||||
{
|
||||
// Browser URL pointing at a ref with NO trailing subdir must still strip
|
||||
// the /tree/<ref> segment from the clone URL.
|
||||
const result = manager.parseSource('https://github.com/owner/repo/tree/main');
|
||||
assert(result.isValid === true, 'tree URL without subdir is valid');
|
||||
assert(
|
||||
result.cloneUrl === 'https://github.com/owner/repo',
|
||||
'tree URL without subdir strips ref from cloneUrl',
|
||||
`Got: ${result.cloneUrl}`,
|
||||
);
|
||||
assert(result.subdir === null, 'tree URL without subdir yields null subdir', `Got: ${result.subdir}`);
|
||||
assert(result.displayName === 'owner/repo', 'tree URL without subdir displayName is owner/repo', `Got: ${result.displayName}`);
|
||||
}
|
||||
|
||||
{
|
||||
// Same shape for GitLab's /-/tree form and Gitea's /src/branch form.
|
||||
const gitlab = manager.parseSource('https://gitlab.com/group/repo/-/tree/main');
|
||||
assert(
|
||||
gitlab.cloneUrl === 'https://gitlab.com/group/repo' && gitlab.subdir === null,
|
||||
'GitLab /-/tree/<ref> without subdir strips ref',
|
||||
`Got: ${gitlab.cloneUrl} subdir=${gitlab.subdir}`,
|
||||
);
|
||||
|
||||
const gitea = manager.parseSource('https://gitea.example.com/owner/repo/src/branch/main');
|
||||
assert(
|
||||
gitea.cloneUrl === 'https://gitea.example.com/owner/repo' && gitea.subdir === null,
|
||||
'Gitea /src/branch/<ref> without subdir strips ref',
|
||||
`Got: ${gitea.cloneUrl} subdir=${gitea.subdir}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Summary ────────────────────────────────────────────────────────────────
|
||||
|
||||
console.log(`\n${colors.cyan}Results: ${passed} passed, ${failed} failed${colors.reset}\n`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
|
|
@ -15,7 +15,18 @@ module.exports = {
|
|||
['--modules <modules>', 'Comma-separated list of module IDs to install (e.g., "bmm,bmb")'],
|
||||
[
|
||||
'--tools <tools>',
|
||||
'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Use "none" to skip tool configuration.',
|
||||
'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Required for fresh non-interactive (--yes) installs. Run with --list-tools to see all valid IDs.',
|
||||
],
|
||||
['--list-tools', 'Print all supported tool/IDE IDs (with target directories) and exit.'],
|
||||
[
|
||||
'--set <spec>',
|
||||
'Set a module config option non-interactively. Spec format: <module>.<key>=<value> (e.g. bmm.project_knowledge=research). Repeatable. Run --list-options to see available keys.',
|
||||
(value, prev) => [...(prev || []), value],
|
||||
[],
|
||||
],
|
||||
[
|
||||
'--list-options [module]',
|
||||
'List available --set keys for all locally-known official modules, or for a single module by code, then exit.',
|
||||
],
|
||||
['--action <type>', 'Action type for existing installations: install, update, or quick-update'],
|
||||
['--user-name <name>', 'Name for agents to use (default: system username)'],
|
||||
|
|
@ -40,12 +51,49 @@ module.exports = {
|
|||
],
|
||||
action: async (options) => {
|
||||
try {
|
||||
if (options.listTools) {
|
||||
const { formatPlatformList } = require('../ide/platform-codes');
|
||||
process.stdout.write((await formatPlatformList()) + '\n');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (options.listOptions !== undefined) {
|
||||
const { formatOptionsList } = require('../list-options');
|
||||
const moduleArg = options.listOptions === true ? null : options.listOptions;
|
||||
const { text, ok } = await formatOptionsList(moduleArg);
|
||||
const stream = ok ? process.stdout : process.stderr;
|
||||
// process.exit() forces immediate termination and can truncate the
|
||||
// buffered write when stdout/stderr is piped or captured by CI. Wait
|
||||
// for the write to flush, then set process.exitCode and return so the
|
||||
// event loop drains naturally. Non-zero exit when a single-module
|
||||
// lookup misses so a CI typo like `--list-options bmn` doesn't look
|
||||
// successful in scripts.
|
||||
await new Promise((resolve, reject) => {
|
||||
stream.write(text + '\n', (error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
process.exitCode = ok ? 0 : 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set debug flag as environment variable for all components
|
||||
if (options.debug) {
|
||||
process.env.BMAD_DEBUG_MANIFEST = 'true';
|
||||
await prompts.log.info('Debug mode enabled');
|
||||
}
|
||||
|
||||
// Validate --set syntax up-front so malformed entries fail fast,
|
||||
// before we touch the network or filesystem. Parsed entries are
|
||||
// re-derived inside ui.js where overrides are seeded.
|
||||
if (options.set && options.set.length > 0) {
|
||||
const { parseSetEntries } = require('../set-overrides');
|
||||
try {
|
||||
parseSetEntries(options.set);
|
||||
} catch (error) {
|
||||
await prompts.log.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const config = await ui.promptInstall(options);
|
||||
|
||||
// Handle cancel
|
||||
|
|
@ -54,8 +102,13 @@ module.exports = {
|
|||
process.exit(0);
|
||||
}
|
||||
|
||||
// Handle quick update separately
|
||||
// Handle quick update separately. --set is a post-install TOML patch so
|
||||
// it works the same way for quick-update as for a regular install — the
|
||||
// installer runs, then `applySetOverrides` patches the central config
|
||||
// files. Pass the parsed overrides through.
|
||||
if (config.actionType === 'quick-update') {
|
||||
const { parseSetEntries } = require('../set-overrides');
|
||||
config.setOverrides = parseSetEntries(options.set || []);
|
||||
const result = await installer.quickUpdate(config);
|
||||
await prompts.log.success('Quick update complete!');
|
||||
await prompts.log.info(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`);
|
||||
|
|
@ -81,7 +134,7 @@ module.exports = {
|
|||
} else {
|
||||
await prompts.log.error(`Installation failed: ${error.message}`);
|
||||
}
|
||||
if (error.stack) {
|
||||
if (error.stack && !error.expected) {
|
||||
await prompts.log.message(error.stack);
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,19 @@
|
|||
* User input comes from either UI answers or headless CLI flags.
|
||||
*/
|
||||
class Config {
|
||||
constructor({ directory, modules, ides, skipPrompts, verbose, actionType, coreConfig, moduleConfigs, quickUpdate, channelOptions }) {
|
||||
constructor({
|
||||
directory,
|
||||
modules,
|
||||
ides,
|
||||
skipPrompts,
|
||||
verbose,
|
||||
actionType,
|
||||
coreConfig,
|
||||
moduleConfigs,
|
||||
quickUpdate,
|
||||
channelOptions,
|
||||
setOverrides,
|
||||
}) {
|
||||
this.directory = directory;
|
||||
this.modules = Object.freeze([...modules]);
|
||||
this.ides = Object.freeze([...ides]);
|
||||
|
|
@ -15,6 +27,11 @@ class Config {
|
|||
this._quickUpdate = quickUpdate;
|
||||
// channelOptions carry a Map + Set; don't deep-freeze.
|
||||
this.channelOptions = channelOptions || null;
|
||||
// Parsed `--set <module>.<key>=<value>` overrides, applied as a TOML
|
||||
// patch AFTER the install finishes. Shape: { moduleCode: { key: value } }.
|
||||
// Intentionally NOT integrated with the prompt/template/schema flow; see
|
||||
// `tools/installer/set-overrides.js` for the rationale and tradeoffs.
|
||||
this.setOverrides = setOverrides || {};
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
|
|
@ -40,6 +57,7 @@ class Config {
|
|||
moduleConfigs: userInput.moduleConfigs || null,
|
||||
quickUpdate: userInput._quickUpdate || false,
|
||||
channelOptions: userInput.channelOptions || null,
|
||||
setOverrides: userInput.setOverrides || {},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
|||
const { InstallPaths } = require('./install-paths');
|
||||
const { ExternalModuleManager } = require('../modules/external-manager');
|
||||
const { resolveModuleVersion } = require('../modules/version-resolver');
|
||||
const { MODULE_HELP_CSV_HEADER } = require('../modules/module-help-schema');
|
||||
|
||||
const { ExistingInstall } = require('./existing-install');
|
||||
const { warnPreNativeSkillsLegacy } = require('./legacy-warnings');
|
||||
|
|
@ -310,6 +311,19 @@ class Installer {
|
|||
moduleConfigs,
|
||||
});
|
||||
|
||||
// Apply post-install --set TOML patches. Runs after writeCentralConfig
|
||||
// (inside generateManifests above) so the patch operates on the
|
||||
// freshly written `_bmad/config.toml` / `_bmad/config.user.toml`.
|
||||
// See `tools/installer/set-overrides.js` for routing rules.
|
||||
if (config.setOverrides && Object.keys(config.setOverrides).length > 0) {
|
||||
const { applySetOverrides } = require('../set-overrides');
|
||||
const applied = await applySetOverrides(config.setOverrides, paths.bmadDir);
|
||||
if (applied.length > 0) {
|
||||
const summary = applied.map((a) => `${a.module}.${a.key} → ${a.file}`).join(', ');
|
||||
await prompts.log.info(`Applied --set overrides: ${summary}`);
|
||||
}
|
||||
}
|
||||
|
||||
message('Generating help catalog...');
|
||||
await this.mergeModuleHelpCatalogs(paths.bmadDir, manifestGen.agents);
|
||||
addResult('Help catalog', 'ok');
|
||||
|
|
@ -923,29 +937,15 @@ class Installer {
|
|||
/**
|
||||
* Merge all module-help.csv files into a single bmad-help.csv.
|
||||
* Scans all installed modules for module-help.csv and merges them.
|
||||
* Enriches agent info from the in-memory agent list produced by ManifestGenerator.
|
||||
* Output is written to _bmad/_config/bmad-help.csv.
|
||||
* Output preserves the source schema verbatim — see schema below.
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Array<Object>} agentEntries - Agents collected from module.yaml (code, name, title, icon, module, ...)
|
||||
* @param {Array<Object>} _agentEntries - Unused; retained for call-site compatibility
|
||||
*/
|
||||
async mergeModuleHelpCatalogs(bmadDir, agentEntries = []) {
|
||||
async mergeModuleHelpCatalogs(bmadDir, _agentEntries = []) {
|
||||
const allRows = [];
|
||||
const headerRow =
|
||||
'module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs';
|
||||
|
||||
// Build agent lookup from the in-memory list (agent code → command + display fields).
|
||||
const agentInfo = new Map();
|
||||
for (const agent of agentEntries) {
|
||||
if (!agent || !agent.code) continue;
|
||||
const agentCommand = agent.module ? `bmad:${agent.module}:agent:${agent.code}` : `bmad:agent:${agent.code}`;
|
||||
const displayName = agent.name || agent.code;
|
||||
const titleCombined = agent.icon && agent.title ? `${agent.icon} ${agent.title}` : agent.title || agent.code;
|
||||
agentInfo.set(agent.code, {
|
||||
command: agentCommand,
|
||||
displayName,
|
||||
title: titleCombined,
|
||||
});
|
||||
}
|
||||
const headerRow = MODULE_HELP_CSV_HEADER;
|
||||
const COLUMN_COUNT = 13;
|
||||
const PHASE_INDEX = 7;
|
||||
|
||||
// Get all installed module directories
|
||||
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
||||
|
|
@ -976,72 +976,37 @@ class Installer {
|
|||
const content = await fs.readFile(helpFilePath, 'utf8');
|
||||
const lines = content.split('\n').filter((line) => line.trim() && !line.startsWith('#'));
|
||||
|
||||
let headerWarned = false;
|
||||
for (const line of lines) {
|
||||
// Skip header row
|
||||
// Header row: warn on drift from canonical schema, then skip.
|
||||
// Data rows are loaded positionally regardless, so the warning
|
||||
// is advisory — the maintainer should rename their columns.
|
||||
if (line.startsWith('module,')) {
|
||||
if (!headerWarned && line.trim() !== headerRow) {
|
||||
await prompts.log.warn(
|
||||
` ${moduleName}/module-help.csv header does not match canonical schema. ` +
|
||||
`Expected: ${headerRow} | Found: ${line.trim()} | Data loaded positionally.`,
|
||||
);
|
||||
headerWarned = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse the line - handle quoted fields with commas
|
||||
const columns = this.parseCSVLine(line);
|
||||
if (columns.length >= 12) {
|
||||
// Map old schema to new schema
|
||||
// Old: module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs
|
||||
// New: module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs
|
||||
if (columns.length < COLUMN_COUNT - 1) continue;
|
||||
|
||||
const [
|
||||
module,
|
||||
phase,
|
||||
name,
|
||||
code,
|
||||
sequence,
|
||||
workflowFile,
|
||||
command,
|
||||
required,
|
||||
agentName,
|
||||
options,
|
||||
description,
|
||||
outputLocation,
|
||||
outputs,
|
||||
] = columns;
|
||||
// Pad short rows; truncate over-long rows
|
||||
const padded = columns.slice(0, COLUMN_COUNT);
|
||||
while (padded.length < COLUMN_COUNT) padded.push('');
|
||||
|
||||
// Pass through _meta rows as-is (module metadata, not a skill)
|
||||
if (phase === '_meta') {
|
||||
const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || '';
|
||||
const metaRow = [finalModule, '_meta', '', '', '', '', '', 'false', '', '', '', '', '', '', outputLocation || '', ''];
|
||||
allRows.push(metaRow.map((c) => this.escapeCSVField(c)).join(','));
|
||||
continue;
|
||||
}
|
||||
|
||||
// If module column is empty, set it to this module's name (except for core which stays empty for universal tools)
|
||||
const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || '';
|
||||
|
||||
// Lookup agent info
|
||||
const cleanAgentName = agentName ? agentName.trim() : '';
|
||||
const agentData = agentInfo.get(cleanAgentName) || { command: '', displayName: '', title: '' };
|
||||
|
||||
// Build new row with agent info
|
||||
const newRow = [
|
||||
finalModule,
|
||||
phase || '',
|
||||
name || '',
|
||||
code || '',
|
||||
sequence || '',
|
||||
workflowFile || '',
|
||||
command || '',
|
||||
required || 'false',
|
||||
cleanAgentName,
|
||||
agentData.command,
|
||||
agentData.displayName,
|
||||
agentData.title,
|
||||
options || '',
|
||||
description || '',
|
||||
outputLocation || '',
|
||||
outputs || '',
|
||||
];
|
||||
|
||||
allRows.push(newRow.map((c) => this.escapeCSVField(c)).join(','));
|
||||
// If module column is empty, fill with this module's name
|
||||
// (core stays empty so its rows render as universal tools)
|
||||
if ((!padded[0] || padded[0].trim() === '') && moduleName !== 'core') {
|
||||
padded[0] = moduleName;
|
||||
}
|
||||
|
||||
allRows.push(padded.map((c) => this.escapeCSVField(c)).join(','));
|
||||
}
|
||||
|
||||
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
|
||||
|
|
@ -1053,44 +1018,34 @@ class Installer {
|
|||
}
|
||||
}
|
||||
|
||||
// Sort by module, then phase, then sequence
|
||||
allRows.sort((a, b) => {
|
||||
const colsA = this.parseCSVLine(a);
|
||||
const colsB = this.parseCSVLine(b);
|
||||
// Sort by module, then phase. Stable sort preserves authored order within a phase.
|
||||
const decorated = allRows.map((row, index) => ({ row, index, cols: this.parseCSVLine(row) }));
|
||||
decorated.sort((a, b) => {
|
||||
const moduleA = (a.cols[0] || '').toLowerCase();
|
||||
const moduleB = (b.cols[0] || '').toLowerCase();
|
||||
if (moduleA !== moduleB) return moduleA.localeCompare(moduleB);
|
||||
|
||||
// Module comparison (empty module/universal tools come first)
|
||||
const moduleA = (colsA[0] || '').toLowerCase();
|
||||
const moduleB = (colsB[0] || '').toLowerCase();
|
||||
if (moduleA !== moduleB) {
|
||||
return moduleA.localeCompare(moduleB);
|
||||
}
|
||||
const phaseA = a.cols[PHASE_INDEX] || '';
|
||||
const phaseB = b.cols[PHASE_INDEX] || '';
|
||||
if (phaseA !== phaseB) return phaseA.localeCompare(phaseB);
|
||||
|
||||
// Phase comparison
|
||||
const phaseA = colsA[1] || '';
|
||||
const phaseB = colsB[1] || '';
|
||||
if (phaseA !== phaseB) {
|
||||
return phaseA.localeCompare(phaseB);
|
||||
}
|
||||
|
||||
// Sequence comparison
|
||||
const seqA = parseInt(colsA[4] || '0', 10);
|
||||
const seqB = parseInt(colsB[4] || '0', 10);
|
||||
return seqA - seqB;
|
||||
return a.index - b.index;
|
||||
});
|
||||
const sortedRows = decorated.map((d) => d.row);
|
||||
|
||||
// Write merged catalog
|
||||
const outputDir = path.join(bmadDir, '_config');
|
||||
await fs.ensureDir(outputDir);
|
||||
const outputPath = path.join(outputDir, 'bmad-help.csv');
|
||||
|
||||
const mergedContent = [headerRow, ...allRows].join('\n');
|
||||
const mergedContent = [headerRow, ...sortedRows].join('\n');
|
||||
await fs.writeFile(outputPath, mergedContent, 'utf8');
|
||||
|
||||
// Track the installed file
|
||||
this.installedFiles.add(outputPath);
|
||||
|
||||
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
|
||||
await prompts.log.message(` Generated bmad-help.csv: ${allRows.length} workflows`);
|
||||
await prompts.log.message(` Generated bmad-help.csv: ${sortedRows.length} workflows`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1352,6 +1307,10 @@ class Installer {
|
|||
ides: configuredIdes,
|
||||
coreConfig: quickModules.collectedConfig.core,
|
||||
moduleConfigs: quickModules.collectedConfig,
|
||||
// Forward `--set` overrides so the post-install patch step
|
||||
// (`applySetOverrides`) runs at the end of quick-update too. The
|
||||
// installer.install path applies them after writeCentralConfig.
|
||||
setOverrides: config.setOverrides || {},
|
||||
actionType: 'install',
|
||||
_quickUpdate: true,
|
||||
_preserveModules: skippedModules,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,125 @@ const csv = require('csv-parse/sync');
|
|||
const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
|
||||
const { getInstalledCanonicalIds, isBmadOwnedEntry } = require('./shared/installed-skills');
|
||||
|
||||
// Reserved OpenCode slash commands. A skill whose canonicalId collides with
|
||||
// one of these is skipped during command-pointer generation so it doesn't
|
||||
// shadow a built-in.
|
||||
const RESERVED_OPENCODE_COMMANDS = new Set([
|
||||
'review',
|
||||
'commit',
|
||||
'init',
|
||||
'help',
|
||||
'skills',
|
||||
'fast',
|
||||
'compact',
|
||||
'clear',
|
||||
'undo',
|
||||
'redo',
|
||||
'edit',
|
||||
'editor',
|
||||
'exit',
|
||||
'quit',
|
||||
'theme',
|
||||
'config',
|
||||
'model',
|
||||
'session',
|
||||
]);
|
||||
|
||||
// Wrap a description for safe insertion into single-line YAML frontmatter.
|
||||
// Leaves plain values untouched; double-quotes (and escapes) anything that
|
||||
// could break YAML parsing or span multiple lines.
|
||||
function yamlSafeSingleLine(value) {
|
||||
const collapsed = String(value)
|
||||
.replaceAll(/[\r\n]+/g, ' ')
|
||||
.trim();
|
||||
const needsQuoting = /[:#'"\\]/.test(collapsed) || /^[!&*?|>%@`[{]/.test(collapsed);
|
||||
if (!needsQuoting) return collapsed;
|
||||
const escaped = collapsed.replaceAll('\\', '\\\\').replaceAll('"', String.raw`\"`);
|
||||
return `"${escaped}"`;
|
||||
}
|
||||
|
||||
// Validate that a canonicalId is a safe basename — no path separators, no
|
||||
// parent-dir traversal, no leading dots, only the character set we expect.
|
||||
// Defense-in-depth: the manifest is trusted today, but the value flows
|
||||
// directly into a file path and a malformed entry should not write outside
|
||||
// the commands directory.
|
||||
function isSafeCanonicalId(value) {
|
||||
return typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(value) && !value.includes('..');
|
||||
}
|
||||
|
||||
// Default body template for command pointer files. Used when a platform's
|
||||
// installer config doesn't override `commands_body_template`. Matches
|
||||
// OpenCode's native `@skills/<id>` skill-reference syntax.
|
||||
const DEFAULT_COMMANDS_BODY_TEMPLATE = '@skills/{canonicalId}';
|
||||
|
||||
// Is this skill a persona agent (vs. a workflow/tool/standalone skill)?
|
||||
// Used by platforms that surface only persona agents (e.g. Copilot's Custom
|
||||
// Agents picker). Signal: the skill's source `customize.toml` has an
|
||||
// `[agent]` section. This is the actual configuration source of truth —
|
||||
// every BMAD persona is configured via [agent] in its customize.toml,
|
||||
// every workflow uses [workflow], every standalone skill has no
|
||||
// customize.toml at all. Verified against the full installed manifest:
|
||||
// catches exactly the 20 description-confirmed personas across BMM, CIS,
|
||||
// GDS, WDS, TEA, and correctly excludes meta-skills like
|
||||
// `bmad-agent-builder` (a skill-builder workflow whose canonical id
|
||||
// contains `-agent-` but which has no [agent] section because it isn't a
|
||||
// persona itself).
|
||||
//
|
||||
// Reading the source toml — at install time the source skill directory
|
||||
// (resolved from manifest record.path) still exists; cleanup runs later
|
||||
// in the install flow.
|
||||
async function isAgentSkill(record, bmadDir) {
|
||||
if (!record?.path || !bmadDir) return false;
|
||||
const bmadFolderName = path.basename(bmadDir);
|
||||
const bmadPrefix = bmadFolderName + '/';
|
||||
const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path;
|
||||
const tomlPath = path.join(bmadDir, path.dirname(relativePath), 'customize.toml');
|
||||
if (!(await fs.pathExists(tomlPath))) return false;
|
||||
try {
|
||||
const content = await fs.readFile(tomlPath, 'utf8');
|
||||
return /^\[agent\]/m.test(content);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve placeholders in a body template. Supported placeholders:
|
||||
// {canonicalId} — the skill's canonical id
|
||||
// {target_dir} — the platform's skill install directory (e.g. .agents/skills)
|
||||
// {project-root} — left as a literal placeholder for the model/tool to expand
|
||||
// at runtime; consistent with PR #1769's templates.
|
||||
function expandBodyTemplate(template, { canonicalId, targetDir }) {
|
||||
return template.replaceAll('{canonicalId}', canonicalId).replaceAll('{target_dir}', targetDir);
|
||||
}
|
||||
|
||||
// The exact body the installer would generate for a given description and
|
||||
// canonicalId, given the platform's body template. Centralised so both the
|
||||
// write and the freshness-check paths agree on the canonical form.
|
||||
function buildCommandPointerBody(description, canonicalId, { template, targetDir }) {
|
||||
const bodyText = expandBodyTemplate(template, { canonicalId, targetDir });
|
||||
return `---\ndescription: ${yamlSafeSingleLine(description)}\n---\n\n${bodyText}\n`;
|
||||
}
|
||||
|
||||
// Heuristic: does an existing pointer file look like our generator's output
|
||||
// (and therefore safe to refresh) versus a user-modified file (which we
|
||||
// preserve)? We check the body shape rather than full equality so that
|
||||
// description-only edits in the manifest can propagate without trampling
|
||||
// hand edits to the body.
|
||||
function looksLikeGeneratorOutput(content, canonicalId, { template, targetDir }) {
|
||||
if (typeof content !== 'string') return false;
|
||||
const trimmed = content.trim();
|
||||
const expectedTail = expandBodyTemplate(template, { canonicalId, targetDir }).trim();
|
||||
// Must end with the exact body our generator writes (post-expansion).
|
||||
if (!trimmed.endsWith(expectedTail)) return false;
|
||||
// Must start with frontmatter containing exactly one description: line.
|
||||
const fmMatch = trimmed.match(/^---\n([\S\s]*?)\n---\n/);
|
||||
if (!fmMatch) return false;
|
||||
const fmLines = fmMatch[1].split('\n').filter((l) => l.length > 0);
|
||||
if (fmLines.length !== 1) return false;
|
||||
if (!fmLines[0].startsWith('description:')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Config-driven IDE setup handler
|
||||
*
|
||||
|
|
@ -97,9 +216,15 @@ class ConfigDrivenIdeSetup {
|
|||
}
|
||||
|
||||
// When a peer platform in the same install batch owns this target_dir,
|
||||
// skip the skill write — the peer has already populated it.
|
||||
// skip the skill write — the peer has already populated it. Command
|
||||
// pointers, however, write to a separate per-IDE directory and must
|
||||
// still be generated for this IDE; they are not deduped across peers.
|
||||
if (options.skipTarget) {
|
||||
return { success: true, results: { skills: 0, sharedTargetHandledByPeer: true } };
|
||||
const results = { skills: 0, sharedTargetHandledByPeer: true };
|
||||
if (this.installerConfig.commands_target_dir) {
|
||||
results.commands = await this.installCommandPointers(projectDir, bmadDir, this.installerConfig, options);
|
||||
}
|
||||
return { success: true, results };
|
||||
}
|
||||
|
||||
if (this.installerConfig.target_dir) {
|
||||
|
|
@ -128,11 +253,157 @@ class ConfigDrivenIdeSetup {
|
|||
results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config);
|
||||
results.skillDirectories = this.skillWriteTracker.size;
|
||||
|
||||
if (config.commands_target_dir) {
|
||||
results.commands = await this.installCommandPointers(projectDir, bmadDir, config, options);
|
||||
}
|
||||
|
||||
await this.printSummary(results, target_dir, options);
|
||||
this.skillWriteTracker = null;
|
||||
return { success: true, results };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate per-skill command pointer files for IDEs that surface commands
|
||||
* separately from skills (e.g. OpenCode's `.opencode/commands/<name>.md`).
|
||||
*
|
||||
* Each pointer is a tiny markdown file whose body is `@skills/<canonicalId>`
|
||||
* so invoking `/<canonicalId>` routes the user straight to the skill instead
|
||||
* of forcing them through a `/skills` menu.
|
||||
*
|
||||
* Skips:
|
||||
* - Names that collide with reserved built-in slash commands.
|
||||
* - canonicalIds that aren't safe basename-only identifiers (defense
|
||||
* against path traversal even though the manifest is currently trusted).
|
||||
* - Existing files whose body looks user-modified (preserves hand edits);
|
||||
* pointer files matching the generator pattern get overwritten so that
|
||||
* description changes in skill-manifest.csv propagate on re-install.
|
||||
*
|
||||
* Per-file write failures are recorded and reported but do not abort the
|
||||
* rest of the install — pointer files are a non-essential adjunct to the
|
||||
* skill copy that already succeeded.
|
||||
*
|
||||
* @param {string} projectDir
|
||||
* @param {string} bmadDir
|
||||
* @param {Object} config - Installer config; reads commands_target_dir.
|
||||
* @param {Object} options - Setup options. forceCommands overwrites existing
|
||||
* files unconditionally (including hand-modified ones).
|
||||
* @returns {Promise<Object>} { created, updated, skippedExisting, skippedCollision, skippedInvalidId, writeFailures, fallbackDescription }
|
||||
*/
|
||||
async installCommandPointers(projectDir, bmadDir, config, options = {}) {
|
||||
const result = {
|
||||
created: 0,
|
||||
updated: 0,
|
||||
skippedExisting: 0,
|
||||
skippedCollision: 0,
|
||||
skippedInvalidId: 0,
|
||||
skippedFiltered: 0,
|
||||
writeFailures: 0,
|
||||
fallbackDescription: 0,
|
||||
};
|
||||
|
||||
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
|
||||
if (!(await fs.pathExists(csvPath))) return result;
|
||||
|
||||
const commandsPath = path.join(projectDir, config.commands_target_dir);
|
||||
await fs.ensureDir(commandsPath);
|
||||
|
||||
// Per-platform pointer-file shape, all overrideable in platform-codes.yaml.
|
||||
const extension = config.commands_extension || '.md';
|
||||
const template = config.commands_body_template || DEFAULT_COMMANDS_BODY_TEMPLATE;
|
||||
const targetDir = config.target_dir;
|
||||
const filter = config.commands_filter || null;
|
||||
|
||||
const csvContent = await fs.readFile(csvPath, 'utf8');
|
||||
const records = csv.parse(csvContent, { columns: true, skip_empty_lines: true });
|
||||
|
||||
for (const record of records) {
|
||||
const canonicalId = record.canonicalId;
|
||||
if (!canonicalId) continue;
|
||||
|
||||
// Defensive basename validation. canonicalId comes from a trusted
|
||||
// manifest today, but the value flows directly into a file path —
|
||||
// reject anything that could escape commands_target_dir.
|
||||
if (!isSafeCanonicalId(canonicalId)) {
|
||||
result.skippedInvalidId++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Optional per-platform filter: surfaces that should only show
|
||||
// persona agents (e.g. Copilot's Custom Agents picker) skip
|
||||
// workflow/tool skills here so the picker isn't cluttered with
|
||||
// 90+ unrelated entries.
|
||||
if (filter === 'agents-only' && !(await isAgentSkill(record, bmadDir))) {
|
||||
result.skippedFiltered++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reserved-name guard is OpenCode-specific. Other adapters that opt
|
||||
// into commands_target_dir later should declare their own reserved
|
||||
// set rather than inheriting OpenCode's.
|
||||
if (this.name === 'opencode' && RESERVED_OPENCODE_COMMANDS.has(canonicalId)) {
|
||||
result.skippedCollision++;
|
||||
continue;
|
||||
}
|
||||
|
||||
let description = (record.description || '').trim();
|
||||
if (!description) {
|
||||
description = `Run the ${canonicalId} skill`;
|
||||
result.fallbackDescription++;
|
||||
}
|
||||
|
||||
const body = buildCommandPointerBody(description, canonicalId, { template, targetDir });
|
||||
const commandFile = path.join(commandsPath, `${canonicalId}${extension}`);
|
||||
|
||||
// If a pointer file already exists, decide whether to overwrite based
|
||||
// on whether it looks like generator output (description-only diff) or
|
||||
// a user-modified file. forceCommands overrides this protection.
|
||||
if (!options.forceCommands && (await fs.pathExists(commandFile))) {
|
||||
let existing;
|
||||
try {
|
||||
existing = await fs.readFile(commandFile, 'utf8');
|
||||
} catch {
|
||||
// Treat unreadable as user-owned and skip — safer than overwriting.
|
||||
result.skippedExisting++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existing === body) {
|
||||
// No-op idempotent re-run.
|
||||
result.skippedExisting++;
|
||||
continue;
|
||||
}
|
||||
if (looksLikeGeneratorOutput(existing, canonicalId, { template, targetDir })) {
|
||||
// Description (or other generated bit) has changed; refresh in place.
|
||||
try {
|
||||
await fs.writeFile(commandFile, body, 'utf8');
|
||||
result.updated++;
|
||||
} catch (error) {
|
||||
result.writeFailures++;
|
||||
if (!options.silent) {
|
||||
await prompts.log.warn(`Failed to update command pointer ${canonicalId}${extension}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Hand-modified pointer — preserve it.
|
||||
result.skippedExisting++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.writeFile(commandFile, body, 'utf8');
|
||||
result.created++;
|
||||
} catch (error) {
|
||||
result.writeFailures++;
|
||||
if (!options.silent) {
|
||||
await prompts.log.warn(`Failed to write command pointer ${canonicalId}${extension}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install verbatim native SKILL.md directories from skill-manifest.csv.
|
||||
* Copies the entire source directory as-is into the IDE skill directory.
|
||||
|
|
@ -207,6 +478,18 @@ class ConfigDrivenIdeSetup {
|
|||
if (count > 0) {
|
||||
await prompts.log.success(`${this.name} configured: ${count} skills → ${targetDir}`);
|
||||
}
|
||||
const cmd = results.commands;
|
||||
if (cmd && (cmd.created > 0 || cmd.updated > 0) && this.installerConfig?.commands_target_dir) {
|
||||
const total = cmd.created + cmd.updated;
|
||||
const detail = cmd.updated > 0 ? `${cmd.created} new, ${cmd.updated} refreshed` : `${total}`;
|
||||
await prompts.log.success(`${this.name} commands: ${detail} → ${this.installerConfig.commands_target_dir}`);
|
||||
if (cmd.skippedCollision > 0) {
|
||||
await prompts.log.message(` (${cmd.skippedCollision} skipped — name collides with reserved slash command)`);
|
||||
}
|
||||
if (cmd.writeFailures > 0) {
|
||||
await prompts.log.warn(` (${cmd.writeFailures} pointer writes failed — see warnings above)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -247,6 +530,36 @@ class ConfigDrivenIdeSetup {
|
|||
await this.cleanupRovoDevPrompts(projectDir, options);
|
||||
}
|
||||
|
||||
// Clean generated command pointer files in commands_target_dir.
|
||||
// Mirrors target_dir cleanup so uninstalls and skill removals don't
|
||||
// leave dangling /<canonicalId> commands pointing at missing skills.
|
||||
// Runs regardless of skipTarget — command pointers live in a per-IDE
|
||||
// directory and are not deduped across peers, so a peer-owned shared
|
||||
// skills directory does not protect this IDE's command pointers from
|
||||
// cleanup. The "currently active" set is passed so install-flow cleanup
|
||||
// (where removalSet contains skills that will be re-added moments later)
|
||||
// doesn't trample hand-edited pointers; install-flow cleanup will only
|
||||
// delete pointers for skills that are not in the new manifest.
|
||||
if (this.installerConfig?.commands_target_dir) {
|
||||
// In the install/update flow (signal: previousSkillIds was passed),
|
||||
// spare pointers whose canonicalId is still in the manifest so hand
|
||||
// edits survive a routine reinstall. In the uninstall flow (no
|
||||
// previousSkillIds — full uninstall or per-IDE removal via
|
||||
// cleanupByList), don't spare anything; the IDE itself is going away,
|
||||
// so its pointers should go with it.
|
||||
const isInstallFlow = options.previousSkillIds && options.previousSkillIds.size > 0;
|
||||
const activeSkillIds = isInstallFlow ? await this._readActiveSkillIds(resolvedBmadDir) : new Set();
|
||||
const extension = this.installerConfig.commands_extension || '.md';
|
||||
await this.cleanupCommandPointers(
|
||||
projectDir,
|
||||
this.installerConfig.commands_target_dir,
|
||||
options,
|
||||
removalSet,
|
||||
activeSkillIds,
|
||||
extension,
|
||||
);
|
||||
}
|
||||
|
||||
// Skip target_dir cleanup when a peer platform owns this directory
|
||||
// (set during dedup'd install or when uninstalling one of several
|
||||
// platforms that share the same target_dir).
|
||||
|
|
@ -346,6 +659,97 @@ class ConfigDrivenIdeSetup {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup generated command pointer files for entries in removalSet.
|
||||
* Symmetric counterpart to installCommandPointers — removes
|
||||
* `<canonicalId><extension>` files whose canonicalId is in the set. Removes
|
||||
* the commands directory entirely if it ends up empty.
|
||||
* @param {string} projectDir
|
||||
* @param {string} commandsTargetDir - Relative dir (e.g. .opencode/commands)
|
||||
* @param {Object} options
|
||||
* @param {Set<string>} removalSet - canonicalIds whose pointer files to remove
|
||||
* @param {Set<string>} [activeSkillIds] - canonicalIds present in the
|
||||
* current manifest. Pointers for IDs in this set are spared so an
|
||||
* install-flow cleanup (where removalSet === previousSkillIds and the
|
||||
* same skills are about to be re-installed) doesn't wipe hand-edited
|
||||
* pointer files. Pass an empty set or omit to delete every match in
|
||||
* removalSet (uninstall flow).
|
||||
* @param {string} [extension] - Pointer file extension (default '.md');
|
||||
* matches the platform's commands_extension config value so cleanup
|
||||
* correctly identifies pointer files for IDEs whose convention isn't .md
|
||||
* (e.g. Copilot's `.agent.md`).
|
||||
*/
|
||||
async cleanupCommandPointers(
|
||||
projectDir,
|
||||
commandsTargetDir,
|
||||
options = {},
|
||||
removalSet = new Set(),
|
||||
activeSkillIds = new Set(),
|
||||
extension = '.md',
|
||||
) {
|
||||
if (!removalSet || removalSet.size === 0) return;
|
||||
|
||||
const commandsPath = path.join(projectDir, commandsTargetDir);
|
||||
if (!(await fs.pathExists(commandsPath))) return;
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(commandsPath);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.endsWith(extension)) continue;
|
||||
const canonicalId = entry.slice(0, -extension.length);
|
||||
if (!removalSet.has(canonicalId)) continue;
|
||||
// Spare pointers for skills that are still in the manifest; the
|
||||
// install pass will refresh them in place if their content has gone
|
||||
// stale, while preserving hand edits.
|
||||
if (activeSkillIds.has(canonicalId)) continue;
|
||||
try {
|
||||
await fs.remove(path.join(commandsPath, entry));
|
||||
} catch {
|
||||
// Skip files we can't remove.
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the commands directory if we emptied it.
|
||||
try {
|
||||
const remaining = await fs.readdir(commandsPath);
|
||||
if (remaining.length === 0) {
|
||||
await fs.remove(commandsPath);
|
||||
}
|
||||
} catch {
|
||||
// Directory may already be gone.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the canonicalIds currently present in the skill-manifest.csv.
|
||||
* Used by cleanup to distinguish "re-install of an existing skill"
|
||||
* (preserve pointer) from "skill truly being removed" (delete pointer).
|
||||
* @param {string|null} bmadDir
|
||||
* @returns {Promise<Set<string>>}
|
||||
*/
|
||||
async _readActiveSkillIds(bmadDir) {
|
||||
const ids = new Set();
|
||||
if (!bmadDir) return ids;
|
||||
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
|
||||
if (!(await fs.pathExists(csvPath))) return ids;
|
||||
try {
|
||||
const content = await fs.readFile(csvPath, 'utf8');
|
||||
const records = csv.parse(content, { columns: true, skip_empty_lines: true });
|
||||
for (const record of records) {
|
||||
if (record.canonicalId) ids.add(record.canonicalId);
|
||||
}
|
||||
} catch {
|
||||
// Manifest unreadable — return an empty set so cleanup falls back to
|
||||
// the conservative "delete what removalSet says" behavior.
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup a specific target directory.
|
||||
* When removalSet is provided, only removes entries in that set.
|
||||
|
|
|
|||
|
|
@ -31,7 +31,50 @@ function clearCache() {
|
|||
_cachedPlatformCodes = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the installable platform list for human-readable output (used by --list-tools).
|
||||
* Sourced from IdeManager so this view matches what --tools accepts at install time
|
||||
* (suspended platforms excluded).
|
||||
* @returns {Promise<string>} Formatted multi-line string with id, name, target_dir, preferred flag.
|
||||
*/
|
||||
async function formatPlatformList() {
|
||||
const { IdeManager } = require('./manager');
|
||||
const ideManager = new IdeManager();
|
||||
await ideManager.ensureInitialized();
|
||||
|
||||
const entries = ideManager.getAvailableIdes().map((ide) => {
|
||||
const handler = ideManager.handlers.get(ide.value);
|
||||
return {
|
||||
id: ide.value,
|
||||
name: ide.name,
|
||||
targetDir: handler?.installerConfig?.target_dir || '',
|
||||
preferred: ide.preferred,
|
||||
};
|
||||
});
|
||||
|
||||
const idWidth = Math.max(...entries.map((e) => e.id.length), 'ID'.length);
|
||||
const nameWidth = Math.max(...entries.map((e) => e.name.length), 'Name'.length);
|
||||
|
||||
const pad = (s, w) => s + ' '.repeat(Math.max(0, w - s.length));
|
||||
const lines = [
|
||||
`Supported tool IDs (pass via --tools <id>[,<id>...]):`,
|
||||
'',
|
||||
` ${pad('ID', idWidth)} ${pad('Name', nameWidth)} Target dir`,
|
||||
` ${pad('-'.repeat(idWidth), idWidth)} ${pad('-'.repeat(nameWidth), nameWidth)} ${'-'.repeat(10)}`,
|
||||
];
|
||||
|
||||
for (const e of entries) {
|
||||
const star = e.preferred ? ' *' : ' ';
|
||||
lines.push(`${star}${pad(e.id, idWidth)} ${pad(e.name, nameWidth)} ${e.targetDir}`);
|
||||
}
|
||||
|
||||
lines.push('', '* = recommended / preferred', '', 'Example: bmad-method install --modules bmm --tools claude-code');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadPlatformCodes,
|
||||
clearCache,
|
||||
formatPlatformList,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -132,6 +132,21 @@ platforms:
|
|||
installer:
|
||||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
commands_target_dir: .github/agents
|
||||
commands_extension: .agent.md
|
||||
commands_body_template: "LOAD the FULL {project-root}/{target_dir}/{canonicalId}/SKILL.md, READ its entire contents and follow its directions exactly!"
|
||||
# The Custom Agents picker should only show persona agents (not
|
||||
# workflows/tools). Detected by reading each skill's source
|
||||
# `customize.toml` and checking for an `[agent]` section — that's
|
||||
# the actual configuration source of truth: every BMAD persona is
|
||||
# configured under `[agent]`, every workflow under `[workflow]`,
|
||||
# every standalone skill has no customize.toml. This signal is
|
||||
# naming-independent, so personas like `bmad-tea` (which doesn't
|
||||
# follow the `-agent-` convention) are still included, and
|
||||
# meta-skills like `bmad-agent-builder` (which contains `-agent-`
|
||||
# but is a skill-builder workflow, not a persona) are correctly
|
||||
# excluded.
|
||||
commands_filter: agents-only
|
||||
|
||||
goose:
|
||||
name: "Block Goose"
|
||||
|
|
@ -222,6 +237,7 @@ platforms:
|
|||
installer:
|
||||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
commands_target_dir: .opencode/commands
|
||||
|
||||
openhands:
|
||||
name: "OpenHands"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
const path = require('node:path');
|
||||
const fs = require('./fs-native');
|
||||
const yaml = require('yaml');
|
||||
const { getProjectRoot, getModulePath, getExternalModuleCachePath } = require('./project-root');
|
||||
|
||||
/**
|
||||
* Read a module.yaml and return its declared `code:` field, or null if missing/unparseable.
|
||||
*/
|
||||
async function readModuleCode(yamlPath) {
|
||||
try {
|
||||
const parsed = yaml.parse(await fs.readFile(yamlPath, 'utf8'));
|
||||
if (parsed && typeof parsed === 'object' && typeof parsed.code === 'string') {
|
||||
return parsed.code;
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover module.yaml files for officials we can read locally:
|
||||
* - core, bmm: bundled in src/ (always present)
|
||||
* - external officials: only if previously cloned to ~/.bmad/cache/external-modules/
|
||||
*
|
||||
* Each result's `code` is the `code:` field from the module.yaml when present;
|
||||
* that's the value `--set <module>.<key>=<value>` matches against.
|
||||
*
|
||||
* Community/custom modules are not enumerated; users reference their own
|
||||
* module.yaml directly per the design (see issue #1663).
|
||||
*
|
||||
* @returns {Promise<Array<{code: string, yamlPath: string, source: string}>>}
|
||||
*/
|
||||
async function discoverOfficialModuleYamls() {
|
||||
const found = [];
|
||||
// Dedupe is case-insensitive because module caches occasionally retain a
|
||||
// legacy UPPERCASE-named directory alongside the canonical lowercase one
|
||||
// (same module, different cache key from an older schema). We pick whichever
|
||||
// entry we see first and skip the alternate-case duplicate. NOTE: `--set`
|
||||
// matching itself is case-sensitive (it keys on `moduleName` from the install
|
||||
// flow's selected list, which is always lowercase short codes), so the
|
||||
// surfaced `code` here is what users should type. Don't change to
|
||||
// case-sensitive dedupe without revisiting that contract.
|
||||
const seenCodes = new Set();
|
||||
|
||||
const addFound = async (yamlPath, source, fallbackCode) => {
|
||||
const declaredCode = await readModuleCode(yamlPath);
|
||||
const code = declaredCode || fallbackCode;
|
||||
if (!code) return;
|
||||
const lower = code.toLowerCase();
|
||||
if (seenCodes.has(lower)) return;
|
||||
seenCodes.add(lower);
|
||||
found.push({ code, yamlPath, source });
|
||||
};
|
||||
|
||||
// Built-ins.
|
||||
for (const code of ['core', 'bmm']) {
|
||||
const yamlPath = path.join(getModulePath(code), 'module.yaml');
|
||||
if (await fs.pathExists(yamlPath)) {
|
||||
// Built-ins use their well-known short codes regardless of what the
|
||||
// module.yaml `code:` says, since the install flow keys on these.
|
||||
seenCodes.add(code.toLowerCase());
|
||||
found.push({ code, yamlPath, source: 'built-in' });
|
||||
}
|
||||
}
|
||||
|
||||
// Bundled in src/modules/<code>/module.yaml (rare, but supported by getModulePath).
|
||||
const srcModulesDir = path.join(getProjectRoot(), 'src', 'modules');
|
||||
if (await fs.pathExists(srcModulesDir)) {
|
||||
const entries = await fs.readdir(srcModulesDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const yamlPath = path.join(srcModulesDir, entry.name, 'module.yaml');
|
||||
if (await fs.pathExists(yamlPath)) {
|
||||
await addFound(yamlPath, 'bundled', entry.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// External cache (~/.bmad/cache/external-modules/<code>/...).
|
||||
const cacheRoot = getExternalModuleCachePath('').replace(/\/$/, '');
|
||||
if (await fs.pathExists(cacheRoot)) {
|
||||
const rawEntries = await fs.readdir(cacheRoot, { withFileTypes: true });
|
||||
for (const entry of rawEntries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const candidates = [
|
||||
path.join(cacheRoot, entry.name, 'module.yaml'),
|
||||
path.join(cacheRoot, entry.name, 'src', 'module.yaml'),
|
||||
path.join(cacheRoot, entry.name, 'skills', 'module.yaml'),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (await fs.pathExists(candidate)) {
|
||||
await addFound(candidate, 'cached', entry.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
function formatPromptText(item) {
|
||||
if (Array.isArray(item.prompt)) return item.prompt.join(' ');
|
||||
return String(item.prompt || '').trim();
|
||||
}
|
||||
|
||||
function inferType(item) {
|
||||
if (item['single-select']) return 'single-select';
|
||||
if (item['multi-select']) return 'multi-select';
|
||||
if (typeof item.default === 'boolean') return 'boolean';
|
||||
if (typeof item.default === 'number') return 'number';
|
||||
return 'string';
|
||||
}
|
||||
|
||||
function formatModuleOptions(code, parsed, source) {
|
||||
const lines = [];
|
||||
const header = source === 'built-in' ? code : `${code} (${source})`;
|
||||
lines.push(header + ':');
|
||||
|
||||
let count = 0;
|
||||
for (const [key, item] of Object.entries(parsed)) {
|
||||
if (!item || typeof item !== 'object' || !('prompt' in item)) continue;
|
||||
count++;
|
||||
const type = inferType(item);
|
||||
const scope = item.scope === 'user' ? ' [user-scope]' : '';
|
||||
const defaultStr = item.default === undefined || item.default === null ? '(none)' : String(item.default);
|
||||
lines.push(` ${code}.${key} (${type}${scope}) default: ${defaultStr}`);
|
||||
const promptText = formatPromptText(item);
|
||||
if (promptText) lines.push(` ${promptText}`);
|
||||
if (Array.isArray(item['single-select'])) {
|
||||
const values = item['single-select'].map((v) => (typeof v === 'object' ? v.value : v)).filter((v) => v !== undefined);
|
||||
if (values.length > 0) lines.push(` values: ${values.join(' | ')}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (count === 0) {
|
||||
lines.push(' (no configurable options)', '');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render `--list-options` output.
|
||||
*
|
||||
* Returns `{ text, ok }` so callers can surface a non-zero exit code on
|
||||
* a typo'd module-code lookup. Discovery dedupes case-insensitively, so
|
||||
* the lookup is also case-insensitive — typing `--list-options BMM` and
|
||||
* `--list-options bmm` both find the bmm built-in.
|
||||
*
|
||||
* @param {string|null} moduleCode - if non-null, restrict to this module
|
||||
* @returns {Promise<{text: string, ok: boolean}>}
|
||||
*/
|
||||
async function formatOptionsList(moduleCode) {
|
||||
const discovered = await discoverOfficialModuleYamls();
|
||||
const needle = moduleCode ? moduleCode.toLowerCase() : null;
|
||||
const filtered = needle ? discovered.filter((d) => d.code.toLowerCase() === needle) : discovered;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
if (moduleCode) {
|
||||
const text = [
|
||||
`No locally-known module.yaml for '${moduleCode}'.`,
|
||||
'',
|
||||
'Built-in modules (core, bmm) are always available. External officials',
|
||||
'appear here after they have been installed at least once on this machine',
|
||||
'(they are cached under ~/.bmad/cache/external-modules/).',
|
||||
'',
|
||||
'For community or custom modules, read the module.yaml file in that',
|
||||
"module's source repository directly.",
|
||||
].join('\n');
|
||||
return { text, ok: false };
|
||||
}
|
||||
return { text: 'No modules found.', ok: false };
|
||||
}
|
||||
|
||||
const sections = [];
|
||||
// Track when a module-scoped lookup couldn't actually be rendered (yaml
|
||||
// unparseable or empty after parse). The full `--list-options` output is
|
||||
// tolerant of one bad entry, but `--list-options <module>` against a single
|
||||
// unreadable module should still fail tooling so a CI script catches it.
|
||||
let moduleScopedFailure = false;
|
||||
sections.push('Available --set keys', 'Format: --set <module>.<key>=<value> (repeatable)', '');
|
||||
for (const { code, yamlPath, source } of filtered) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = yaml.parse(await fs.readFile(yamlPath, 'utf8'));
|
||||
} catch {
|
||||
sections.push(`${code} (${source}): could not parse module.yaml`, '');
|
||||
if (moduleCode) moduleScopedFailure = true;
|
||||
continue;
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
sections.push(`${code} (${source}): module.yaml is not a valid object (got ${Array.isArray(parsed) ? 'array' : typeof parsed})`, '');
|
||||
if (moduleCode) moduleScopedFailure = true;
|
||||
continue;
|
||||
}
|
||||
sections.push(formatModuleOptions(code, parsed, source));
|
||||
}
|
||||
|
||||
if (!moduleCode) {
|
||||
sections.push(
|
||||
'Community and custom modules are not listed here — read their module.yaml directly. Unknown keys still persist with a warning.',
|
||||
);
|
||||
}
|
||||
|
||||
return { text: sections.join('\n'), ok: !moduleScopedFailure };
|
||||
}
|
||||
|
||||
module.exports = { formatOptionsList, discoverOfficialModuleYamls };
|
||||
|
|
@ -24,8 +24,9 @@ class CustomModuleManager {
|
|||
|
||||
/**
|
||||
* Parse a user-provided source input into a structured descriptor.
|
||||
* Accepts local file paths, HTTPS Git URLs, and SSH Git URLs.
|
||||
* For HTTPS URLs with deep paths (e.g., /tree/main/subdir), extracts the subdir.
|
||||
* Accepts local file paths, HTTPS Git URLs, HTTP Git URLs, and SSH Git URLs.
|
||||
* For HTTPS/HTTP URLs with deep paths (e.g., /tree/main/subdir), extracts the subdir.
|
||||
* The original protocol (http or https) is preserved in the returned cloneUrl.
|
||||
*
|
||||
* @param {string} input - URL or local file path
|
||||
* @returns {Object} Parsed source descriptor:
|
||||
|
|
@ -127,58 +128,102 @@ class CustomModuleManager {
|
|||
};
|
||||
}
|
||||
|
||||
// HTTPS URL: https://host/owner/repo[/tree/branch/subdir][.git]
|
||||
const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/.]+?)(?:\.git)?(\/.*)?$/);
|
||||
if (httpsMatch) {
|
||||
const [, host, owner, repo, remainder] = httpsMatch;
|
||||
const cloneUrl = `https://${host}/${owner}/${repo}`;
|
||||
let subdir = null;
|
||||
let urlRef = null; // branch/tag extracted from /tree/<ref>/subdir
|
||||
// HTTPS/HTTP URL: generic handling for any Git host.
|
||||
// We avoid host-specific parsing — `git clone` will accept whatever URL the
|
||||
// user provides. We only need to (a) separate an optional browser-style
|
||||
// subdir suffix from the clone URL, (b) extract any embedded ref
|
||||
// (branch/tag) from deep-path URLs, and (c) derive a cache key / display
|
||||
// name from the path. The original protocol (http or https) is preserved.
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(trimmed);
|
||||
} catch {
|
||||
url = null;
|
||||
}
|
||||
|
||||
if (remainder) {
|
||||
// Extract subdir from deep path patterns used by various Git hosts
|
||||
if (url && url.host) {
|
||||
const host = url.host;
|
||||
let repoPath = url.pathname.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
let subdir = null;
|
||||
let urlRef = null; // branch/tag/commit extracted from deep-path URLs
|
||||
|
||||
// Detect browser-style deep-path patterns that embed a ref
|
||||
// (branch/tag/commit) and optional subdirectory. These appear
|
||||
// across many hosts:
|
||||
// GitHub /<repo>/tree|blob/<ref>[/<subdir>]
|
||||
// GitLab /<repo>/-/tree|blob/<ref>[/<subdir>]
|
||||
// Gitea /<repo>/src/<ref>[/<subdir>]
|
||||
// Gitea /<repo>/src/(branch|commit|tag)/<ref>[/<subdir>]
|
||||
// Group 1 = repo path prefix, Group 2 = ref, Group 3 = subdir (optional).
|
||||
const deepPathPatterns = [
|
||||
{ regex: /^\/(?:-\/)?tree\/([^/]+)\/(.+)$/, refIdx: 1, pathIdx: 2 }, // GitHub, GitLab
|
||||
{ regex: /^\/(?:-\/)?blob\/([^/]+)\/(.+)$/, refIdx: 1, pathIdx: 2 },
|
||||
{ regex: /^\/src\/([^/]+)\/(.+)$/, refIdx: 1, pathIdx: 2 }, // Gitea/Forgejo
|
||||
/^(.+?)\/(?:-\/)?(?:tree|blob)\/([^/]+)(?:\/(.+))?$/,
|
||||
/^(.+?)\/src\/(?:branch\/|commit\/|tag\/)?([^/]+)(?:\/(.+))?$/,
|
||||
];
|
||||
// Also match `/tree/<ref>` with no subdir
|
||||
const refOnlyPatterns = [/^\/(?:-\/)?tree\/([^/]+?)\/?$/, /^\/(?:-\/)?blob\/([^/]+?)\/?$/, /^\/src\/([^/]+?)\/?$/];
|
||||
|
||||
for (const p of deepPathPatterns) {
|
||||
const match = remainder.match(p.regex);
|
||||
for (const pattern of deepPathPatterns) {
|
||||
const match = repoPath.match(pattern);
|
||||
if (match) {
|
||||
urlRef = match[p.refIdx];
|
||||
subdir = match[p.pathIdx].replace(/\/$/, '');
|
||||
repoPath = match[1];
|
||||
if (match[2]) urlRef = match[2];
|
||||
if (match[3]) {
|
||||
const cleaned = match[3].replace(/\/+$/, '');
|
||||
if (cleaned) subdir = cleaned;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Some hosts use ?path=/subdir on browse links to point at a file or
|
||||
// directory. Honor it when no deep-path marker matched above.
|
||||
if (!subdir) {
|
||||
for (const r of refOnlyPatterns) {
|
||||
const match = remainder.match(r);
|
||||
if (match) {
|
||||
urlRef = match[1];
|
||||
break;
|
||||
}
|
||||
const pathParam = url.searchParams.get('path');
|
||||
if (pathParam) {
|
||||
const cleaned = pathParam.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
if (cleaned) subdir = cleaned;
|
||||
}
|
||||
}
|
||||
|
||||
// Strip a single trailing .git for a stable cacheKey/displayName.
|
||||
const repoPathClean = repoPath.replace(/\.git$/i, '');
|
||||
if (!repoPathClean) {
|
||||
return {
|
||||
type: null,
|
||||
cloneUrl: null,
|
||||
subdir: null,
|
||||
localPath: null,
|
||||
cacheKey: null,
|
||||
displayName: null,
|
||||
isValid: false,
|
||||
error: 'Not a valid Git URL or local path',
|
||||
};
|
||||
}
|
||||
|
||||
const cloneUrl = `${url.protocol}//${host}/${repoPathClean}`;
|
||||
const cacheKey = `${host}/${repoPathClean}`;
|
||||
|
||||
// Display name: prefer "<owner>/<repo>" using the last two meaningful
|
||||
// path segments.
|
||||
const segments = repoPathClean.split('/').filter(Boolean);
|
||||
const repoSeg = segments.at(-1);
|
||||
const ownerSeg = segments.at(-2);
|
||||
const displayName = ownerSeg ? `${ownerSeg}/${repoSeg}` : repoSeg;
|
||||
|
||||
// Precedence: explicit @version suffix > URL /tree/<ref> path segment.
|
||||
const version = versionSuffix || urlRef || null;
|
||||
|
||||
return {
|
||||
type: 'url',
|
||||
cloneUrl,
|
||||
subdir,
|
||||
localPath: null,
|
||||
version,
|
||||
rawInput: trimmedRaw,
|
||||
cacheKey,
|
||||
displayName,
|
||||
isValid: true,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Precedence: explicit @version suffix > URL /tree/<ref> path segment.
|
||||
const version = versionSuffix || urlRef || null;
|
||||
|
||||
return {
|
||||
type: 'url',
|
||||
cloneUrl,
|
||||
subdir,
|
||||
localPath: null,
|
||||
version,
|
||||
rawInput: trimmedRaw,
|
||||
cacheKey: `${host}/${owner}/${repo}`,
|
||||
displayName: `${owner}/${repo}`,
|
||||
isValid: true,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -311,7 +356,7 @@ class CustomModuleManager {
|
|||
/**
|
||||
* Clone a custom module repository to cache.
|
||||
* Supports any Git host (GitHub, GitLab, Bitbucket, self-hosted, etc.).
|
||||
* @param {string} sourceInput - Git URL (HTTPS or SSH)
|
||||
* @param {string} sourceInput - Git URL (HTTPS, HTTP, or SSH)
|
||||
* @param {Object} [options] - Clone options
|
||||
* @param {boolean} [options.silent] - Suppress spinner output
|
||||
* @param {boolean} [options.skipInstall] - Skip npm install (for browsing before user confirms)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Canonical schema for per-module `module-help.csv` files.
|
||||
*
|
||||
* Both the merger (`Installer.mergeModuleHelpCatalogs`) and the synthesizer
|
||||
* (`PluginResolver._buildSynthesizedHelpCsv`) emit this exact header. The
|
||||
* merger compares each per-module file's header against this string and
|
||||
* warns on drift, so any rename here must be matched in external module
|
||||
* authors' CSVs (or accepted as a positional fall-through with a warning).
|
||||
*/
|
||||
const MODULE_HELP_CSV_HEADER =
|
||||
'module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs';
|
||||
|
||||
module.exports = { MODULE_HELP_CSV_HEADER };
|
||||
|
|
@ -903,7 +903,10 @@ class OfficialModules {
|
|||
try {
|
||||
const content = await fs.readFile(moduleConfigPath, 'utf8');
|
||||
const moduleConfig = yaml.parse(content);
|
||||
if (moduleConfig) {
|
||||
// Only keep plain object parses. A corrupt config.yaml that parses
|
||||
// to a scalar or array would crash later code that does `key in cfg`
|
||||
// / `Object.keys(cfg)`; treat it the same as a parse error.
|
||||
if (moduleConfig && typeof moduleConfig === 'object' && !Array.isArray(moduleConfig)) {
|
||||
this._existingConfig[entry.name] = moduleConfig;
|
||||
foundAny = true;
|
||||
}
|
||||
|
|
@ -914,9 +917,58 @@ class OfficialModules {
|
|||
}
|
||||
}
|
||||
|
||||
if (foundAny) {
|
||||
await this._hoistCoreKeysFromLegacyModuleConfigs();
|
||||
}
|
||||
|
||||
return foundAny;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate prior answers when a key has moved from a non-core module to core
|
||||
* (e.g. project_name moving from bmm to core in #2279). Without this, the
|
||||
* partition logic in writeCentralConfig drops the value from the bmm bucket
|
||||
* (because it's now a core key) without re-homing it under [core], so the
|
||||
* user's prior answer silently disappears on the next install/quick-update.
|
||||
*/
|
||||
async _hoistCoreKeysFromLegacyModuleConfigs() {
|
||||
const coreSchemaPath = path.join(getSourcePath(), 'core-skills', 'module.yaml');
|
||||
if (!(await fs.pathExists(coreSchemaPath))) return;
|
||||
|
||||
let coreSchema;
|
||||
try {
|
||||
coreSchema = yaml.parse(await fs.readFile(coreSchemaPath, 'utf8'));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!coreSchema || typeof coreSchema !== 'object') return;
|
||||
|
||||
const coreKeys = new Set(
|
||||
Object.entries(coreSchema)
|
||||
.filter(([, v]) => v && typeof v === 'object' && 'prompt' in v)
|
||||
.map(([k]) => k),
|
||||
);
|
||||
if (coreKeys.size === 0) return;
|
||||
|
||||
// Belt-and-suspenders: loadExistingConfig already filters non-object parses,
|
||||
// but anyone calling _hoistCoreKeysFromLegacyModuleConfigs in isolation (or
|
||||
// future code paths populating _existingConfig directly) shouldn't be able
|
||||
// to crash this with a scalar / array.
|
||||
const existingCore = this._existingConfig.core;
|
||||
this._existingConfig.core = existingCore && typeof existingCore === 'object' && !Array.isArray(existingCore) ? existingCore : {};
|
||||
|
||||
for (const [moduleName, cfg] of Object.entries(this._existingConfig)) {
|
||||
if (moduleName === 'core' || !cfg || typeof cfg !== 'object' || Array.isArray(cfg)) continue;
|
||||
for (const key of Object.keys(cfg)) {
|
||||
if (!coreKeys.has(key)) continue;
|
||||
if (!(key in this._existingConfig.core)) {
|
||||
this._existingConfig.core[key] = cfg[key];
|
||||
}
|
||||
delete cfg[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-scan module schemas to gather metadata for the configuration gateway prompt.
|
||||
* Returns info about which modules have configurable options.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const fs = require('../fs-native');
|
||||
const path = require('node:path');
|
||||
const yaml = require('yaml');
|
||||
const { MODULE_HELP_CSV_HEADER } = require('./module-help-schema');
|
||||
|
||||
/**
|
||||
* Resolves how to install a plugin from marketplace.json by analyzing
|
||||
|
|
@ -338,8 +339,7 @@ class PluginResolver {
|
|||
* @returns {string} CSV content
|
||||
*/
|
||||
_buildSynthesizedHelpCsv(moduleName, skillInfos) {
|
||||
const header = 'module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs';
|
||||
const rows = [header];
|
||||
const rows = [MODULE_HELP_CSV_HEADER];
|
||||
|
||||
for (const info of skillInfos) {
|
||||
const displayName = this._formatDisplayName(info.name || info.dirName);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,330 @@
|
|||
// `--set <module>.<key>=<value>` is a post-install patch. The installer runs
|
||||
// its normal flow and writes `_bmad/config.toml`, `_bmad/config.user.toml`,
|
||||
// and `_bmad/<module>/config.yaml`; afterwards `applySetOverrides` upserts
|
||||
// each override into those files.
|
||||
//
|
||||
// This is intentionally NOT integrated with the prompt/template/schema
|
||||
// system. Tradeoffs:
|
||||
// - No `result:` template rendering: `--set bmm.project_knowledge=research`
|
||||
// writes "research" verbatim. Pass `--set bmm.project_knowledge='{project-root}/research'`
|
||||
// if you want the rendered form.
|
||||
// - Carry-forward across installs is best-effort: declared schema keys
|
||||
// persist via the existingValue path on the next interactive run; values
|
||||
// for keys outside any module's schema may need to be re-passed on each
|
||||
// install (or edited directly in `_bmad/config.toml`).
|
||||
// - No "key not in schema" validation: whatever you assert, we write.
|
||||
//
|
||||
// Names that, when used as object keys, can mutate `Object.prototype` and
|
||||
// cascade into every plain-object lookup in the process. The `--set` pipeline
|
||||
// assigns into plain `{}` maps keyed by user input, so `--set __proto__.x=1`
|
||||
// would otherwise reach `overrides.__proto__[x] = 1` and pollute every plain
|
||||
// object. We reject the names at parse time and harden the maps in
|
||||
// `parseSetEntries` with `Object.create(null)` for defense-in-depth.
|
||||
const PROTOTYPE_POLLUTING_NAMES = new Set(['__proto__', 'prototype', 'constructor']);
|
||||
|
||||
const path = require('node:path');
|
||||
const fs = require('./fs-native');
|
||||
const yaml = require('yaml');
|
||||
|
||||
/**
|
||||
* Parse a single `--set <module>.<key>=<value>` entry.
|
||||
* @param {string} entry - raw flag value
|
||||
* @returns {{module: string, key: string, value: string}}
|
||||
* @throws {Error} on malformed input
|
||||
*/
|
||||
function parseSetEntry(entry) {
|
||||
if (typeof entry !== 'string' || entry.length === 0) {
|
||||
throw new Error('--set: empty entry. Expected <module>.<key>=<value>');
|
||||
}
|
||||
const eq = entry.indexOf('=');
|
||||
if (eq === -1) {
|
||||
throw new Error(`--set "${entry}": missing '='. Expected <module>.<key>=<value>`);
|
||||
}
|
||||
const lhs = entry.slice(0, eq);
|
||||
// Note: only the LHS is trimmed. Values may legitimately contain leading
|
||||
// or trailing whitespace (paths with spaces, quoted strings); module / key
|
||||
// names cannot, so it's safe to be strict on the left.
|
||||
const value = entry.slice(eq + 1);
|
||||
const dot = lhs.indexOf('.');
|
||||
if (dot === -1) {
|
||||
throw new Error(`--set "${entry}": missing '.'. Expected <module>.<key>=<value>`);
|
||||
}
|
||||
const moduleCode = lhs.slice(0, dot).trim();
|
||||
const key = lhs.slice(dot + 1).trim();
|
||||
if (!moduleCode || !key) {
|
||||
throw new Error(`--set "${entry}": empty module or key. Expected <module>.<key>=<value>`);
|
||||
}
|
||||
if (PROTOTYPE_POLLUTING_NAMES.has(moduleCode) || PROTOTYPE_POLLUTING_NAMES.has(key)) {
|
||||
throw new Error(
|
||||
`--set "${entry}": '__proto__', 'prototype', and 'constructor' are reserved and cannot be used as a module or key name.`,
|
||||
);
|
||||
}
|
||||
return { module: moduleCode, key, value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse repeated `--set` entries into a `{ module: { key: value } }` map.
|
||||
* Later entries overwrite earlier ones for the same key. Both the outer
|
||||
* map and the per-module inner maps are `Object.create(null)` so callers
|
||||
* that bypass `parseSetEntry`'s name check still can't pollute prototypes.
|
||||
*
|
||||
* @param {string[]} entries
|
||||
* @returns {Object<string, Object<string, string>>}
|
||||
*/
|
||||
function parseSetEntries(entries) {
|
||||
const overrides = Object.create(null);
|
||||
if (!Array.isArray(entries)) return overrides;
|
||||
for (const entry of entries) {
|
||||
const { module: moduleCode, key, value } = parseSetEntry(entry);
|
||||
if (!overrides[moduleCode]) overrides[moduleCode] = Object.create(null);
|
||||
overrides[moduleCode][key] = value;
|
||||
}
|
||||
return overrides;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a JS string as a TOML basic string (double-quoted with escapes).
|
||||
* @param {string} value
|
||||
*/
|
||||
function tomlString(value) {
|
||||
const s = String(value);
|
||||
// Per the TOML spec, basic strings escape `\`, `"`, and control characters.
|
||||
return (
|
||||
'"' +
|
||||
s
|
||||
.replaceAll('\\', '\\\\')
|
||||
.replaceAll('"', String.raw`\"`)
|
||||
.replaceAll('\b', String.raw`\b`)
|
||||
.replaceAll('\f', String.raw`\f`)
|
||||
.replaceAll('\n', String.raw`\n`)
|
||||
.replaceAll('\r', String.raw`\r`)
|
||||
.replaceAll('\t', String.raw`\t`) +
|
||||
'"'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Section header for a given module code.
|
||||
* - `core` → `[core]`
|
||||
* - `<other>` → `[modules.<other>]`
|
||||
*
|
||||
* Mirrors the layout `manifest-generator.writeCentralConfig` produces.
|
||||
*/
|
||||
function sectionHeader(moduleCode) {
|
||||
return moduleCode === 'core' ? '[core]' : `[modules.${moduleCode}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update `key = value` inside a TOML section, returning the new
|
||||
* file content. The format produced by the installer is regular and small
|
||||
* enough that a line scanner is more reliable than pulling in a TOML
|
||||
* round-tripper that would normalize the file's existing whitespace and
|
||||
* comment structure.
|
||||
*
|
||||
* - If `[section]` exists and contains `key`, replace the value on that
|
||||
* line (preserving any inline comment after the value).
|
||||
* - If `[section]` exists but `key` doesn't, append `key = value` at the
|
||||
* end of the section (before the next `[...]` header or EOF, skipping
|
||||
* trailing blank lines so the section stays tidy).
|
||||
* - If `[section]` doesn't exist, append a new section block at EOF.
|
||||
*
|
||||
* @param {string} content existing file content (may be empty)
|
||||
* @param {string} section exact `[section]` header to target
|
||||
* @param {string} key
|
||||
* @param {string} valueToml already TOML-encoded value (e.g. `"foo"`)
|
||||
* @returns {string} new content
|
||||
*/
|
||||
function upsertTomlKey(content, section, key, valueToml) {
|
||||
const lines = content.split('\n');
|
||||
// Track whether the file already ended with a newline so we can preserve
|
||||
// that. `split('\n')` on `"a\n"` yields `['a', '']`, which gives us the
|
||||
// marker we need.
|
||||
const hadTrailingNewline = lines.length > 0 && lines.at(-1) === '';
|
||||
if (hadTrailingNewline) lines.pop();
|
||||
|
||||
// Locate the target section.
|
||||
const sectionStart = lines.findIndex((line) => line.trim() === section);
|
||||
if (sectionStart === -1) {
|
||||
// Section doesn't exist — append a new block. Pad with a blank line if
|
||||
// the file is non-empty so sections stay visually separated.
|
||||
if (lines.length > 0 && lines.at(-1).trim() !== '') lines.push('');
|
||||
lines.push(section, `${key} = ${valueToml}`);
|
||||
return lines.join('\n') + (hadTrailingNewline ? '\n' : '');
|
||||
}
|
||||
|
||||
// Find the section's end (next `[...]` header or EOF).
|
||||
let sectionEnd = lines.length;
|
||||
for (let i = sectionStart + 1; i < lines.length; i++) {
|
||||
if (/^\s*\[/.test(lines[i])) {
|
||||
sectionEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Look for the key inside the section. Match `<key> = ...` allowing
|
||||
// optional leading whitespace; preserve the comment tail (`# ...`) if any.
|
||||
const keyPattern = new RegExp(`^(\\s*)${escapeRegExp(key)}\\s*=\\s*(.*)$`);
|
||||
for (let i = sectionStart + 1; i < sectionEnd; i++) {
|
||||
const match = lines[i].match(keyPattern);
|
||||
if (match) {
|
||||
const indent = match[1];
|
||||
// Preserve trailing comment if present. We split on the first `#` that
|
||||
// is preceded by whitespace — TOML strings can't contain unescaped `#`
|
||||
// in basic-string form so this is safe for the values we emit.
|
||||
const tail = match[2];
|
||||
const commentIdx = tail.search(/\s+#/);
|
||||
const commentSuffix = commentIdx === -1 ? '' : tail.slice(commentIdx);
|
||||
lines[i] = `${indent}${key} = ${valueToml}${commentSuffix}`;
|
||||
return lines.join('\n') + (hadTrailingNewline ? '\n' : '');
|
||||
}
|
||||
}
|
||||
|
||||
// Section exists but key doesn't. Insert before the next section header,
|
||||
// skipping trailing blank lines inside the current section so the new
|
||||
// entry sits with its siblings.
|
||||
let insertAt = sectionEnd;
|
||||
while (insertAt > sectionStart + 1 && lines[insertAt - 1].trim() === '') {
|
||||
insertAt--;
|
||||
}
|
||||
lines.splice(insertAt, 0, `${key} = ${valueToml}`);
|
||||
return lines.join('\n') + (hadTrailingNewline ? '\n' : '');
|
||||
}
|
||||
|
||||
function escapeRegExp(s) {
|
||||
return s.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up `[section] key` in a TOML file. Returns true if the file exists,
|
||||
* the section is present, and `key` is set within it. Used by
|
||||
* `applySetOverrides` to route an override to the file that already owns
|
||||
* the key (so user-scope keys land in `config.user.toml`, team-scope keys
|
||||
* land in `config.toml`).
|
||||
*/
|
||||
async function tomlHasKey(filePath, section, key) {
|
||||
if (!(await fs.pathExists(filePath))) return false;
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
const sectionStart = lines.findIndex((line) => line.trim() === section);
|
||||
if (sectionStart === -1) return false;
|
||||
const keyPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
|
||||
for (let i = sectionStart + 1; i < lines.length; i++) {
|
||||
if (/^\s*\[/.test(lines[i])) return false;
|
||||
if (keyPattern.test(lines[i])) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply parsed `--set` overrides to the central TOML files written by the
|
||||
* installer. Called at the end of an install / quick-update.
|
||||
*
|
||||
* Routing per (module, key):
|
||||
* 1. If `_bmad/config.user.toml` already has `[section] key`, update there
|
||||
* (user-scope key like `core.user_name`, `bmm.user_skill_level`).
|
||||
* 2. Otherwise update `_bmad/config.toml` (team scope, the default).
|
||||
*
|
||||
* The schema-correct user/team partition lives in `manifest-generator`. We
|
||||
* intentionally don't re-read module schemas here — the only goal is to
|
||||
* match the file the installer just wrote the key to. For brand-new keys
|
||||
* (not in either file yet), team scope is the safe default.
|
||||
*
|
||||
* @param {Object<string, Object<string, string>>} overrides
|
||||
* @param {string} bmadDir absolute path to `_bmad/`
|
||||
* @returns {Promise<Array<{module:string,key:string,scope:'team'|'user',file:string}>>}
|
||||
* a list of applied entries (for caller logging)
|
||||
*/
|
||||
async function applySetOverrides(overrides, bmadDir) {
|
||||
const applied = [];
|
||||
if (!overrides || typeof overrides !== 'object') return applied;
|
||||
|
||||
const teamPath = path.join(bmadDir, 'config.toml');
|
||||
const userPath = path.join(bmadDir, 'config.user.toml');
|
||||
|
||||
for (const moduleCode of Object.keys(overrides)) {
|
||||
// Skip overrides for modules not actually installed. The installer writes
|
||||
// `_bmad/<module>/config.yaml` for every installed module (including core),
|
||||
// so its presence is a reliable "is this module here?" signal that works
|
||||
// for both fresh installs and quick-updates without coupling to caller-
|
||||
// supplied module lists.
|
||||
const moduleConfigYaml = path.join(bmadDir, moduleCode, 'config.yaml');
|
||||
if (!(await fs.pathExists(moduleConfigYaml))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const section = sectionHeader(moduleCode);
|
||||
const moduleOverrides = overrides[moduleCode] || {};
|
||||
for (const key of Object.keys(moduleOverrides)) {
|
||||
const value = moduleOverrides[key];
|
||||
const valueToml = tomlString(value);
|
||||
|
||||
const userOwnsIt = await tomlHasKey(userPath, section, key);
|
||||
const targetPath = userOwnsIt ? userPath : teamPath;
|
||||
|
||||
// The team file always exists post-install; the user file only exists
|
||||
// if the install wrote at least one user-scope key. If we're routing to
|
||||
// it but it doesn't exist yet, create it with a minimal header so it
|
||||
// has the same shape as installer-written user toml.
|
||||
let content = '';
|
||||
if (await fs.pathExists(targetPath)) {
|
||||
content = await fs.readFile(targetPath, 'utf8');
|
||||
} else {
|
||||
content = '# Personal overrides for _bmad/config.toml.\n';
|
||||
}
|
||||
|
||||
const next = upsertTomlKey(content, section, key, valueToml);
|
||||
await fs.writeFile(targetPath, next, 'utf8');
|
||||
applied.push({
|
||||
module: moduleCode,
|
||||
key,
|
||||
scope: userOwnsIt ? 'user' : 'team',
|
||||
file: path.basename(targetPath),
|
||||
});
|
||||
}
|
||||
|
||||
// Also patch the per-module yaml (`_bmad/<module>/config.yaml`). The
|
||||
// installer reads this file as `_existingConfig` on subsequent runs and
|
||||
// surfaces declared values as prompt defaults — under `--yes` those
|
||||
// defaults are accepted, so patching here gives `--set` natural
|
||||
// carry-forward for declared keys without needing schema-strict
|
||||
// partition exemptions in the manifest writer. For undeclared keys the
|
||||
// value lives in the per-module yaml but won't be re-emitted into
|
||||
// config.toml on the next install (the schema-strict partition drops
|
||||
// it); re-pass `--set` if you need it sticky.
|
||||
const moduleYamlPath = path.join(bmadDir, moduleCode, 'config.yaml');
|
||||
if (await fs.pathExists(moduleYamlPath)) {
|
||||
try {
|
||||
const text = await fs.readFile(moduleYamlPath, 'utf8');
|
||||
const parsed = yaml.parse(text);
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
// Preserve the installer's banner header (everything up to the
|
||||
// first non-comment line) so `_bmad/<module>/config.yaml` keeps
|
||||
// its provenance comments after we round-trip it.
|
||||
const headerLines = [];
|
||||
for (const line of text.split('\n')) {
|
||||
if (line.startsWith('#') || line.trim() === '') {
|
||||
headerLines.push(line);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(moduleOverrides)) {
|
||||
parsed[key] = moduleOverrides[key];
|
||||
}
|
||||
const body = yaml.stringify(parsed, { indent: 2, lineWidth: 0, minContentWidth: 0 });
|
||||
const header = headerLines.length > 0 ? headerLines.join('\n') + '\n' : '';
|
||||
await fs.writeFile(moduleYamlPath, header + body, 'utf8');
|
||||
}
|
||||
} catch {
|
||||
// Per-module yaml unparseable — skip silently. The central toml was
|
||||
// already patched above, which is the user-visible state for the
|
||||
// current install. Carry-forward will fail next install but the
|
||||
// current install reflects the override.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return applied;
|
||||
}
|
||||
|
||||
module.exports = { parseSetEntry, parseSetEntries, applySetOverrides, upsertTomlKey, tomlString };
|
||||
|
|
@ -16,6 +16,7 @@ const {
|
|||
} = require('./modules/channel-plan');
|
||||
const channelResolver = require('./modules/channel-resolver');
|
||||
const prompts = require('./prompts');
|
||||
const { parseSetEntries } = require('./set-overrides');
|
||||
|
||||
const manifest = new Manifest();
|
||||
|
||||
|
|
@ -200,12 +201,15 @@ class UI {
|
|||
actionType = options.action;
|
||||
await prompts.log.info(`Using action from command-line: ${actionType}`);
|
||||
} else if (options.yes) {
|
||||
// Default to quick-update if available, otherwise first available choice
|
||||
// Default to quick-update if available, unless flags that require the
|
||||
// full update path are present (e.g. --custom-source which re-clones
|
||||
// modules at a new version — quick-update skips that entirely).
|
||||
if (choices.length === 0) {
|
||||
throw new Error('No valid actions available for this installation');
|
||||
}
|
||||
const hasQuickUpdate = choices.some((c) => c.value === 'quick-update');
|
||||
actionType = hasQuickUpdate ? 'quick-update' : choices[0].value;
|
||||
const needsFullUpdate = !!options.customSource;
|
||||
actionType = hasQuickUpdate && !needsFullUpdate ? 'quick-update' : (choices.find((c) => c.value === 'update') || choices[0]).value;
|
||||
await prompts.log.info(`Non-interactive mode (--yes): defaulting to ${actionType}`);
|
||||
} else {
|
||||
actionType = await prompts.select({
|
||||
|
|
@ -241,8 +245,11 @@ class UI {
|
|||
.map((m) => m.trim())
|
||||
.filter(Boolean);
|
||||
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
|
||||
} else if (options.customSource) {
|
||||
// Custom source without --modules: start with empty list (core added below)
|
||||
} else if (options.customSource && !options.yes) {
|
||||
// Custom source without --modules or --yes: start with empty list
|
||||
// (only custom source modules + core will be installed).
|
||||
// When --yes is also set, fall through to the --yes branch so all
|
||||
// installed modules are included alongside the custom source modules.
|
||||
selectedModules = [];
|
||||
} else if (options.yes) {
|
||||
selectedModules = await this.getDefaultModules(installedModuleIds);
|
||||
|
|
@ -281,7 +288,7 @@ class UI {
|
|||
// Get tool selection
|
||||
const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
|
||||
|
||||
const moduleConfigs = await this.collectModuleConfigs(confirmedDirectory, selectedModules, {
|
||||
const { moduleConfigs, setOverrides } = await this.collectModuleConfigs(confirmedDirectory, selectedModules, {
|
||||
...options,
|
||||
channelOptions,
|
||||
});
|
||||
|
|
@ -307,6 +314,7 @@ class UI {
|
|||
skipIde: toolSelection.skipIde,
|
||||
coreConfig: moduleConfigs.core || {},
|
||||
moduleConfigs: moduleConfigs,
|
||||
setOverrides,
|
||||
skipPrompts: options.yes || false,
|
||||
channelOptions,
|
||||
};
|
||||
|
|
@ -358,7 +366,7 @@ class UI {
|
|||
await this._interactiveChannelGate({ options, channelOptions, selectedModules });
|
||||
|
||||
let toolSelection = await this.promptToolSelection(confirmedDirectory, options);
|
||||
const moduleConfigs = await this.collectModuleConfigs(confirmedDirectory, selectedModules, {
|
||||
const { moduleConfigs, setOverrides } = await this.collectModuleConfigs(confirmedDirectory, selectedModules, {
|
||||
...options,
|
||||
channelOptions,
|
||||
});
|
||||
|
|
@ -384,6 +392,7 @@ class UI {
|
|||
skipIde: toolSelection.skipIde,
|
||||
coreConfig: moduleConfigs.core || {},
|
||||
moduleConfigs: moduleConfigs,
|
||||
setOverrides,
|
||||
skipPrompts: options.yes || false,
|
||||
channelOptions,
|
||||
};
|
||||
|
|
@ -398,6 +407,37 @@ class UI {
|
|||
* @param {Object} options - Command-line options
|
||||
* @returns {Object} Tool configuration
|
||||
*/
|
||||
_parseToolsFlag(toolsArg, allKnownValues) {
|
||||
const selectedIdes = toolsArg
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (selectedIdes.length === 0) {
|
||||
const err = new Error(
|
||||
'--tools was passed empty. Provide at least one tool ID (e.g. --tools claude-code) or run with --list-tools to see valid IDs.',
|
||||
);
|
||||
err.expected = true;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const unknown = selectedIdes.filter((id) => !allKnownValues.has(id));
|
||||
if (unknown.length > 0) {
|
||||
const err = new Error(
|
||||
[
|
||||
`Unknown tool ID${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`,
|
||||
'',
|
||||
'Run with --list-tools to see all valid IDs.',
|
||||
'Common: claude-code, cursor, copilot, windsurf, cline',
|
||||
].join('\n'),
|
||||
);
|
||||
err.expected = true;
|
||||
throw err;
|
||||
}
|
||||
|
||||
return selectedIdes;
|
||||
}
|
||||
|
||||
async promptToolSelection(projectDir, options = {}) {
|
||||
const { ExistingInstall } = require('./core/existing-install');
|
||||
const { Installer } = require('./core/installer');
|
||||
|
|
@ -432,15 +472,10 @@ class UI {
|
|||
const allTools = [...preferredIdes, ...otherIdes];
|
||||
|
||||
// Non-interactive: handle --tools and --yes flags before interactive prompt
|
||||
if (options.tools) {
|
||||
if (options.tools.toLowerCase() === 'none') {
|
||||
await prompts.log.info('Skipping tool configuration (--tools none)');
|
||||
return { ides: [], skipIde: true };
|
||||
}
|
||||
const selectedIdes = options.tools
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean);
|
||||
// Use !== undefined so an explicit --tools "" falls through to _parseToolsFlag and
|
||||
// gets a specific "passed empty" error instead of being silently ignored.
|
||||
if (options.tools !== undefined) {
|
||||
const selectedIdes = this._parseToolsFlag(options.tools, allKnownValues);
|
||||
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
|
||||
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
||||
return { ides: selectedIdes, skipIde: false };
|
||||
|
|
@ -516,21 +551,13 @@ class UI {
|
|||
|
||||
let selectedIdes = [];
|
||||
|
||||
// Check if tools are provided via command-line
|
||||
if (options.tools) {
|
||||
// Check for explicit "none" value to skip tool installation
|
||||
if (options.tools.toLowerCase() === 'none') {
|
||||
await prompts.log.info('Skipping tool configuration (--tools none)');
|
||||
return { ides: [], skipIde: true };
|
||||
} else {
|
||||
selectedIdes = options.tools
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean);
|
||||
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
|
||||
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
||||
return { ides: selectedIdes, skipIde: false };
|
||||
}
|
||||
// Check if tools are provided via command-line.
|
||||
// Use !== undefined so an explicit --tools "" still hits _parseToolsFlag's empty-value error.
|
||||
if (options.tools !== undefined) {
|
||||
selectedIdes = this._parseToolsFlag(options.tools, allKnownValues);
|
||||
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
|
||||
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
||||
return { ides: selectedIdes, skipIde: false };
|
||||
} else if (options.yes) {
|
||||
// If --yes flag is set, skip tool prompt and use previously configured tools or empty
|
||||
if (configuredIdes.length > 0) {
|
||||
|
|
@ -538,8 +565,18 @@ class UI {
|
|||
await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
|
||||
return { ides: configuredIdes, skipIde: false };
|
||||
} else {
|
||||
await prompts.log.info('Skipping tool configuration (--yes flag, no previous tools)');
|
||||
return { ides: [], skipIde: true };
|
||||
const err = new Error(
|
||||
[
|
||||
'--tools is required for non-interactive install (--yes / -y) when no tools are previously configured.',
|
||||
'',
|
||||
'Common: claude-code, cursor, copilot, windsurf, cline',
|
||||
'See all supported tools: bmad-method install --list-tools',
|
||||
'',
|
||||
'Example: bmad-method install --modules bmm --tools claude-code -y',
|
||||
].join('\n'),
|
||||
);
|
||||
err.expected = true;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -675,6 +712,33 @@ class UI {
|
|||
*/
|
||||
async collectModuleConfigs(directory, modules, options = {}) {
|
||||
const { OfficialModules } = require('./modules/official-modules');
|
||||
|
||||
// Parse --set up front purely to surface user-error before the install
|
||||
// burns time on the network / filesystem. The actual application happens
|
||||
// in installer.install() as a post-write TOML patch — see
|
||||
// `tools/installer/set-overrides.js`. We also warn about overrides
|
||||
// targeting modules the user didn't include, since those will silently
|
||||
// miss the file the patch step looks for.
|
||||
let setOverrides = {};
|
||||
try {
|
||||
setOverrides = parseSetEntries(options.set || []);
|
||||
} catch (error) {
|
||||
// install.js validated already; rethrow as-is for the user.
|
||||
throw error;
|
||||
}
|
||||
// Drop overrides for modules that aren't in the install set so the
|
||||
// post-install patch step doesn't create orphan sections in config.toml
|
||||
// for modules that were never installed.
|
||||
const selectedModuleSet = new Set(['core', ...modules]);
|
||||
for (const moduleCode of Object.keys(setOverrides)) {
|
||||
if (!selectedModuleSet.has(moduleCode)) {
|
||||
await prompts.log.warn(
|
||||
`--set ${moduleCode}.* — module '${moduleCode}' is not in the install set; values will be ignored. Add it to --modules to apply.`,
|
||||
);
|
||||
delete setOverrides[moduleCode];
|
||||
}
|
||||
}
|
||||
|
||||
const configCollector = new OfficialModules({ channelOptions: options.channelOptions });
|
||||
|
||||
// Seed core config from CLI options if provided
|
||||
|
|
@ -724,6 +788,9 @@ class UI {
|
|||
const defaultUsername = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1);
|
||||
configCollector.collectedConfig.core = {
|
||||
user_name: defaultUsername,
|
||||
// {directory_name} default per src/core-skills/module.yaml — matches what the
|
||||
// interactive flow resolves via buildQuestion()'s {directory_name} placeholder.
|
||||
project_name: path.basename(directory),
|
||||
communication_language: 'English',
|
||||
document_output_language: 'English',
|
||||
output_folder: '_bmad-output',
|
||||
|
|
@ -737,7 +804,7 @@ class UI {
|
|||
skipPrompts: options.yes || false,
|
||||
});
|
||||
|
||||
return configCollector.collectedConfig;
|
||||
return { moduleConfigs: configCollector.collectedConfig, setOverrides };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -129,13 +129,45 @@ export default defineConfig({
|
|||
// TEA docs moved to standalone module site; keep BMM sidebar focused.
|
||||
{
|
||||
label: 'BMad Ecosystem',
|
||||
translations: { 'vi-VN': 'Hệ sinh thái BMad', 'zh-CN': 'BMad 生态系统', 'fr-FR': 'Écosystème BMad', 'cs-CZ': 'Ekosystém BMad' },
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ label: 'BMad Builder', link: 'https://bmad-builder-docs.bmad-method.org/', attrs: { target: '_blank' } },
|
||||
{ label: 'Creative Intelligence Suite', link: 'https://cis-docs.bmad-method.org/', attrs: { target: '_blank' } },
|
||||
{ label: 'Game Dev Studio', link: 'https://game-dev-studio-docs.bmad-method.org/', attrs: { target: '_blank' } },
|
||||
{
|
||||
label: 'BMad Builder',
|
||||
translations: { 'vi-VN': 'BMad Builder', 'zh-CN': 'BMad 构建器', 'fr-FR': 'BMad Builder', 'cs-CZ': 'BMad Builder' },
|
||||
link: 'https://bmad-builder-docs.bmad-method.org/',
|
||||
attrs: { target: '_blank' },
|
||||
},
|
||||
{
|
||||
label: 'Creative Intelligence Suite',
|
||||
translations: {
|
||||
'vi-VN': 'Bộ công cụ Trí tuệ Sáng tạo',
|
||||
'zh-CN': '创意智能套件',
|
||||
'fr-FR': "Suite d'Intelligence Créative",
|
||||
'cs-CZ': 'Sada kreativní inteligence',
|
||||
},
|
||||
link: 'https://cis-docs.bmad-method.org/',
|
||||
attrs: { target: '_blank' },
|
||||
},
|
||||
{
|
||||
label: 'Game Dev Studio',
|
||||
translations: {
|
||||
'vi-VN': 'Xưởng phát triển Game',
|
||||
'zh-CN': '游戏开发工作室',
|
||||
'fr-FR': 'Studio de Développement de Jeux',
|
||||
'cs-CZ': 'Herní vývojové studio',
|
||||
},
|
||||
link: 'https://game-dev-studio-docs.bmad-method.org/',
|
||||
attrs: { target: '_blank' },
|
||||
},
|
||||
{
|
||||
label: 'Test Architect (TEA)',
|
||||
translations: {
|
||||
'vi-VN': 'Kiến trúc sư Kiểm thử (TEA)',
|
||||
'zh-CN': '测试架构师 (TEA)',
|
||||
'fr-FR': 'Architecte de Tests (TEA)',
|
||||
'cs-CZ': 'Testovací architekt (TEA)',
|
||||
},
|
||||
link: 'https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/',
|
||||
attrs: { target: '_blank' },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -311,6 +311,16 @@
|
|||
<span class="output">leçons</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow">
|
||||
<div class="workflow-header">
|
||||
<span class="workflow-name">investigate</span>
|
||||
<span class="badge adhoc">à tout moment</span>
|
||||
</div>
|
||||
<div class="workflow-meta">
|
||||
<div class="agent"><div class="agent-icon amelia">A</div><span class="agent-name">Amelia</span></div>
|
||||
<span class="output">dossier de cas</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -322,6 +322,16 @@
|
|||
<span class="output">lessons</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow">
|
||||
<div class="workflow-header">
|
||||
<span class="workflow-name">investigate</span>
|
||||
<span class="badge adhoc">anytime</span>
|
||||
</div>
|
||||
<div class="workflow-meta">
|
||||
<div class="agent"><div class="agent-icon amelia">A</div><span class="agent-name">Amelia</span></div>
|
||||
<span class="output">case file</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue