Compare commits
11 Commits
ec1e1ac6cb
...
0a962d01eb
| Author | SHA1 | Date |
|---|---|---|
|
|
0a962d01eb | |
|
|
ea99b7ece5 | |
|
|
eabcd03f65 | |
|
|
17da5ca8ca | |
|
|
a0705af9be | |
|
|
f5030c7084 | |
|
|
daa7137623 | |
|
|
14fc7b2517 | |
|
|
edfb405e27 | |
|
|
36f9df69bf | |
|
|
4655bb1482 |
|
|
@ -0,0 +1,70 @@
|
||||||
|
---
|
||||||
|
title: "Fáze analýzy: od nápadu k základům"
|
||||||
|
description: Co je brainstorming, výzkum, product brief a PRFAQ — a kdy který nástroj použít
|
||||||
|
sidebar:
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
Fáze analýzy (fáze 1) vám pomůže jasně si promyslet váš produkt, než se pustíte do jeho tvorby. Každý nástroj v této fázi je volitelný, ale úplné vynechání analýzy znamená, že váš PRD je postaven na předpokladech namísto vhledu.
|
||||||
|
|
||||||
|
## Proč analýza před plánováním?
|
||||||
|
|
||||||
|
PRD odpovídá na otázku „Co bychom měli postavit a proč?“. Pokud jej nakrmíte vágním myšlením, získáte vágní PRD — a každý navazující dokument tuto vágnost zdědí. Architektura postavená na slabém PRD sází na špatnou techniku. Příběhy odvozené ze slabé architektury opomíjejí okrajové případy. Náklady se zvyšují.
|
||||||
|
|
||||||
|
Existují analytické nástroje, které vám PRD zostří. Napadají problém z různých úhlů — kreativní průzkum, realita trhu, jasnost zákazníka, proveditelnost — takže v době, kdy sedíte s agentem PM, víte, co a pro koho stavíte.
|
||||||
|
|
||||||
|
## Nástroje
|
||||||
|
|
||||||
|
### Brainstorming
|
||||||
|
|
||||||
|
**Co to je.** Zprostředkované tvůrčí sezení s využitím osvědčených technik generování nápadů. AI funguje jako kouč, který z vás tahá nápady prostřednictvím strukturovaných cvičení — negeneruje nápady za vás.
|
||||||
|
|
||||||
|
**Proč je to tady.** Neotřelé nápady potřebují prostor pro rozvoj, než se zakotví v požadavcích. Brainstorming tento prostor vytváří. Je cenný zejména tehdy, když máte problémovou oblast, ale nemáte jasné řešení, nebo když chcete prozkoumat více směrů, než se k něčemu zavážete.
|
||||||
|
|
||||||
|
**Kdy jej použít.** Máte nejasnou představu o tom, co chcete vytvořit, ale nemáte vykrystalizovaný koncept. Nebo máte koncept, ale chcete ho otestovat pod tlakem oproti alternativám.
|
||||||
|
|
||||||
|
Viz [Brainstorming](./brainstorming.md), kde se dozvíte, jak relace fungují.
|
||||||
|
|
||||||
|
### Výzkum (trhu, domény, technický)
|
||||||
|
|
||||||
|
**Co to je.** Tři cílené pracovní postupy výzkumu, které zkoumají různé rozměry vašeho nápadu. Výzkum trhu zkoumá konkurenci, trendy a nálady uživatelů. Doménový výzkum vytváří odborné znalosti v daném oboru a terminologii. Technický výzkum hodnotí proveditelnost, možnosti architektury a přístupy k implementaci.
|
||||||
|
|
||||||
|
**Proč je to tady.** Stavět na předpokladech je nejrychlejší způsob, jak vytvořit něco, co nikdo nepotřebuje. Výzkum zakládá váš koncept na realitě — co již existuje u konkurence, s čím uživatelé skutečně bojují, co je technicky proveditelné a jakým omezením specifickým pro dané odvětví budete čelit.
|
||||||
|
|
||||||
|
**Kdy ho použít.** Vstupujete do neznámé oblasti, tušíte, že konkurence existuje, ale nemáte ji zmapovanou, nebo váš koncept závisí na technických možnostech, které nemáte ověřené. Proveďte jeden, dva nebo všechny tři — každý z nich je samostatný.
|
||||||
|
|
||||||
|
### Product Brief
|
||||||
|
|
||||||
|
**Co to je.** Řízené zjišťovací sezení, jehož výsledkem je 1–2stránkové shrnutí vašeho konceptu produktu. AI funguje jako spolupracující obchodní analytik, který vám pomůže formulovat vizi, cílovou skupinu, nabídku hodnoty a rozsah.
|
||||||
|
|
||||||
|
**Proč tu je.** Produktový brief je jemnější cestou k plánování. Zachycuje vaši strategickou vizi ve strukturovaném formátu, který se přímo promítá do tvorby PRD. Nejlépe funguje, když jste již o svém konceptu přesvědčeni — znáte zákazníka, problém a zhruba víte, co chcete vytvořit. Brief tyto úvahy uspořádá a vyostří.
|
||||||
|
|
||||||
|
**Kdy jej použít.** Váš koncept je relativně jasný a chcete jej efektivně zdokumentovat ještě před vytvořením PRD. Jste si jisti svým směřováním a nepotřebujete své předpoklady agresivně zpochybňovat.
|
||||||
|
|
||||||
|
### PRFAQ (Working Backwards)
|
||||||
|
|
||||||
|
**Co to je.** Metodika Working Backwards společnosti Amazon upravená jako interaktivní výzva. Napíšete tiskovou zprávu oznamující váš hotový produkt dříve, než existuje jediný řádek kódu, a pak odpovíte na nejtěžší otázky, které by vám zákazníci a zainteresované strany položili. Umělá inteligence funguje jako neúprosný, ale konstruktivní produktový kouč.
|
||||||
|
|
||||||
|
**Proč je to tady.** PRFAQ je přísná cesta k plánování. Vynucuje si jasnost v zájmu zákazníka tím, že vás nutí obhájit každé tvrzení. Pokud nedokážete napsat přesvědčivou tiskovou zprávu, produkt není připraven. Pokud odpovědi na časté dotazy zákazníků odhalí nedostatky, jsou to nedostatky, které byste objevili mnohem později — a nákladněji — při implementaci. Hozená rukavice odhalí slabé myšlení v rané fázi, kdy je nejlevnější ho opravit.
|
||||||
|
|
||||||
|
**Kdy ji použít.** Před vyčleněním zdrojů chcete, aby váš koncept prošel zátěžovým testem. Nejste si jisti, zda to uživatele bude skutečně zajímat. Chcete si ověřit, že dokážete formulovat jasnou a obhajitelnou nabídku hodnoty. Nebo si prostě chcete disciplínou Working Backwards zpřesnit své myšlení.
|
||||||
|
|
||||||
|
## Který nástroj bych měl použít?
|
||||||
|
|
||||||
|
| Situace | Doporučený nástroj |
|
||||||
|
| --------- | ---------------- |
|
||||||
|
| „Mám nejasný nápad, ale nevím, kde začít“ | Brainstorming |
|
||||||
|
| „Než se rozhodnu, potřebuji pochopit trh“ | Výzkum |
|
||||||
|
| „Vím, co chci vytvořit, jen to potřebuji zdokumentovat“ | Product Brief |
|
||||||
|
| „Chci se ujistit, že tento nápad skutečně stojí za vybudování“ | PRFAQ |
|
||||||
|
| „Chci prozkoumat, pak ověřit a pak zdokumentovat“ | Brainstorming → Výzkum → PRFAQ nebo Brief |
|
||||||
|
|
||||||
|
Product Brief i PRFAQ jsou vstupem pro PRD — vyberte si jeden z nich podle toho, jak moc chcete být nároční. Brief je společným objevováním. PRFAQ je hozená rukavice. Obojí vás dovede ke stejnému cíli; PRFAQ testuje, zda si váš koncept zaslouží se tam dostat.
|
||||||
|
|
||||||
|
:::tip[Nejste si jisti?]
|
||||||
|
Spusťte `bmad-help` a popište svou situaci. Doporučí vám správný výchozí bod na základě toho, co jste již udělali a čeho se snažíte dosáhnout.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Co se stane po analýze?
|
||||||
|
|
||||||
|
Výstupy analýzy se přímo promítají do fáze 2 (plánování). Pracovní postup PRD přijímá jako vstupy produktové briefy, dokumenty PRFAQ, výsledky výzkumu a zprávy z brainstormingu — syntetizuje vše, co jste vytvořili, do strukturovaných požadavků. Čím více analýz provedete, tím ostřejší bude vaše PRD.
|
||||||
|
|
@ -17,7 +17,7 @@ Tato stránka uvádí výchozí BMM (Agile suite) agenty, kteří se instalují
|
||||||
|
|
||||||
| Agent | Skill ID | Spouštěče | Primární workflow |
|
| Agent | Skill ID | Spouštěče | Primární workflow |
|
||||||
| --------------------------- | -------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
| --------------------------- | -------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||||
| Analyst (Mary) | `bmad-analyst` | `BP`, `RS`, `CB`, `WB`, `DP` | Brainstorm projektu, výzkum, tvorba briefu, PRFAQ výzva, dokumentace projektu |
|
| Analyst (Mary) | `bmad-analyst` | `BP`, `MR`, `DR`, `TR`, `CB`, `WB`, `DP` | Brainstorm, průzkum trhu, doménový výzkum, technický výzkum, tvorba briefu, PRFAQ výzva, dokumentace projektu |
|
||||||
| Product Manager (John) | `bmad-pm` | `CP`, `VP`, `EP`, `CE`, `IR`, `CC` | Tvorba/validace/editace PRD, tvorba epiců a stories, připravenost implementace, korekce kurzu |
|
| Product Manager (John) | `bmad-pm` | `CP`, `VP`, `EP`, `CE`, `IR`, `CC` | Tvorba/validace/editace PRD, tvorba epiců a stories, připravenost implementace, korekce kurzu |
|
||||||
| Architect (Winston) | `bmad-architect` | `CA`, `IR` | Tvorba architektury, připravenost implementace |
|
| Architect (Winston) | `bmad-architect` | `CA`, `IR` | Tvorba architektury, připravenost implementace |
|
||||||
| Developer (Amelia) | `bmad-agent-dev` | `DS`, `QD`, `QA`, `CR`, `SP`, `CS`, `ER` | Dev story, Quick Dev, generování QA testů, revize kódu, plánování sprintu, tvorba story, retrospektiva epicu |
|
| Developer (Amelia) | `bmad-agent-dev` | `DS`, `QD`, `QA`, `CR`, `SP`, `CS`, `ER` | Dev story, Quick Dev, generování QA testů, revize kódu, plánování sprintu, tvorba story, retrospektiva epicu |
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ Workflow skills spouštějí strukturovaný, vícekrokový proces bez předchoz
|
||||||
| Příklad skillu | Účel |
|
| Příklad skillu | Účel |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `bmad-product-brief` | Vytvoření product briefu — řízené discovery, když je váš koncept jasný |
|
| `bmad-product-brief` | Vytvoření product briefu — řízené discovery, když je váš koncept jasný |
|
||||||
| `bmad-prfaq` | Working Backwards PRFAQ výzva pro zátěžový test vašeho produktového konceptu |
|
| `bmad-prfaq` | [Working Backwards PRFAQ](../explanation/analysis-phase.md#prfaq-working-backwards) výzva pro zátěžový test vašeho produktového konceptu |
|
||||||
| `bmad-create-prd` | Vytvoření dokumentu požadavků (PRD) |
|
| `bmad-create-prd` | Vytvoření dokumentu požadavků (PRD) |
|
||||||
| `bmad-create-architecture` | Návrh systémové architektury |
|
| `bmad-create-architecture` | Návrh systémové architektury |
|
||||||
| `bmad-create-epics-and-stories` | Vytvoření epiců a stories |
|
| `bmad-create-epics-and-stories` | Vytvoření epiců a stories |
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ This page lists the default BMM (Agile suite) agents that install with BMad Meth
|
||||||
|
|
||||||
| Agent | Skill ID | Triggers | Primary workflows |
|
| Agent | Skill ID | Triggers | Primary workflows |
|
||||||
| --------------------------- | -------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------- |
|
| --------------------------- | -------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||||
| Analyst (Mary) | `bmad-analyst` | `BP`, `RS`, `CB`, `WB`, `DP` | Brainstorm Project, Research, Create Brief, PRFAQ Challenge, Document Project |
|
| Analyst (Mary) | `bmad-analyst` | `BP`, `MR`, `DR`, `TR`, `CB`, `WB`, `DP` | Brainstorm, Market Research, Domain Research, Technical Research, Create Brief, PRFAQ Challenge, Document Project |
|
||||||
| Product Manager (John) | `bmad-pm` | `CP`, `VP`, `EP`, `CE`, `IR`, `CC` | Create/Validate/Edit PRD, Create Epics and Stories, Implementation Readiness, Correct Course |
|
| Product Manager (John) | `bmad-pm` | `CP`, `VP`, `EP`, `CE`, `IR`, `CC` | Create/Validate/Edit PRD, Create Epics and Stories, Implementation Readiness, Correct Course |
|
||||||
| Architect (Winston) | `bmad-architect` | `CA`, `IR` | Create Architecture, Implementation Readiness |
|
| Architect (Winston) | `bmad-architect` | `CA`, `IR` | Create Architecture, Implementation Readiness |
|
||||||
| Developer (Amelia) | `bmad-agent-dev` | `DS`, `QD`, `QA`, `CR`, `SP`, `CS`, `ER` | Dev Story, Quick Dev, QA Test Generation, Code Review, Sprint Planning, Create Story, Epic Retrospective |
|
| Developer (Amelia) | `bmad-agent-dev` | `DS`, `QD`, `QA`, `CR`, `SP`, `CS`, `ER` | Dev Story, Quick Dev, QA Test Generation, Code Review, Sprint Planning, Create Story, Epic Retrospective |
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ Workflow skills run a structured, multi-step process without loading an agent pe
|
||||||
| Example skill | Purpose |
|
| Example skill | Purpose |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `bmad-product-brief` | Create a product brief — guided discovery when your concept is clear |
|
| `bmad-product-brief` | Create a product brief — guided discovery when your concept is clear |
|
||||||
| `bmad-prfaq` | Working Backwards PRFAQ challenge to stress-test your product concept |
|
| `bmad-prfaq` | [Working Backwards PRFAQ](../explanation/analysis-phase.md#prfaq-working-backwards) challenge to stress-test your product concept |
|
||||||
| `bmad-create-prd` | Create a Product Requirements Document |
|
| `bmad-create-prd` | Create a Product Requirements Document |
|
||||||
| `bmad-create-architecture` | Design system architecture |
|
| `bmad-create-architecture` | Design system architecture |
|
||||||
| `bmad-create-epics-and-stories` | Create epics and stories |
|
| `bmad-create-epics-and-stories` | Create epics and stories |
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ Trang này liệt kê các agent mặc định của BMM (bộ Agile suite) đư
|
||||||
|
|
||||||
| Agent | Skill ID | Trigger | Workflow chính |
|
| Agent | Skill ID | Trigger | Workflow chính |
|
||||||
| --------------------------- | -------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------- |
|
| --------------------------- | -------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||||
| Analyst (Mary) | `bmad-analyst` | `BP`, `RS`, `CB`, `WB`, `DP` | Brainstorm Project, Research, Create Brief, PRFAQ Challenge, Document Project |
|
| Analyst (Mary) | `bmad-analyst` | `BP`, `MR`, `DR`, `TR`, `CB`, `WB`, `DP` | Brainstorm, Market Research, Domain Research, Technical Research, Create Brief, PRFAQ Challenge, Document Project |
|
||||||
| Product Manager (John) | `bmad-pm` | `CP`, `VP`, `EP`, `CE`, `IR`, `CC` | Create/Validate/Edit PRD, Create Epics and Stories, Implementation Readiness, Correct Course |
|
| Product Manager (John) | `bmad-pm` | `CP`, `VP`, `EP`, `CE`, `IR`, `CC` | Create/Validate/Edit PRD, Create Epics and Stories, Implementation Readiness, Correct Course |
|
||||||
| Architect (Winston) | `bmad-architect` | `CA`, `IR` | Create Architecture, Implementation Readiness |
|
| Architect (Winston) | `bmad-architect` | `CA`, `IR` | Create Architecture, Implementation Readiness |
|
||||||
| Developer (Amelia) | `bmad-agent-dev` | `DS`, `QD`, `QA`, `CR`, `SP`, `CS`, `ER` | Dev Story, Quick Dev, QA Test Generation, Code Review, Sprint Planning, Create Story, Epic Retrospective |
|
| Developer (Amelia) | `bmad-agent-dev` | `DS`, `QD`, `QA`, `CR`, `SP`, `CS`, `ER` | Dev Story, Quick Dev, QA Test Generation, Code Review, Sprint Planning, Create Story, Epic Retrospective |
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ Workflow skills chạy một quy trình có cấu trúc, nhiều bước mà kh
|
||||||
| Ví dụ skill | Mục đích |
|
| Ví dụ skill | Mục đích |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `bmad-product-brief` | Tạo product brief — phiên discovery có hướng dẫn khi concept của bạn đã rõ |
|
| `bmad-product-brief` | Tạo product brief — phiên discovery có hướng dẫn khi concept của bạn đã rõ |
|
||||||
| `bmad-prfaq` | Bài kiểm tra Working Backwards PRFAQ để stress-test concept sản phẩm |
|
| `bmad-prfaq` | Bài kiểm tra [Working Backwards PRFAQ](../explanation/analysis-phase.md#prfaq-working-backwards) để stress-test concept sản phẩm |
|
||||||
| `bmad-create-prd` | Tạo Product Requirements Document |
|
| `bmad-create-prd` | Tạo Product Requirements Document |
|
||||||
| `bmad-create-architecture` | Thiết kế kiến trúc hệ thống |
|
| `bmad-create-architecture` | Thiết kế kiến trúc hệ thống |
|
||||||
| `bmad-create-epics-and-stories` | Tạo epics và stories |
|
| `bmad-create-epics-and-stories` | Tạo epics và stories |
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ sidebar:
|
||||||
|
|
||||||
| 智能体 | Skill ID | 触发器 | 主要 workflow |
|
| 智能体 | Skill ID | 触发器 | 主要 workflow |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Analyst (Mary) | `bmad-analyst` | `BP`、`RS`、`CB`、`DP` | Brainstorm、Research、Create Brief、Document Project |
|
| Analyst (Mary) | `bmad-analyst` | `BP`、`MR`、`DR`、`TR`、`CB`、`WB`、`DP` | Brainstorm、Market Research、Domain Research、Technical Research、Create Brief、PRFAQ Challenge、Document Project |
|
||||||
| Product Manager (John) | `bmad-pm` | `CP`、`VP`、`EP`、`CE`、`IR`、`CC` | Create/Validate/Edit PRD、Create Epics and Stories、Implementation Readiness、Correct Course |
|
| Product Manager (John) | `bmad-pm` | `CP`、`VP`、`EP`、`CE`、`IR`、`CC` | Create/Validate/Edit PRD、Create Epics and Stories、Implementation Readiness、Correct Course |
|
||||||
| Architect (Winston) | `bmad-architect` | `CA`、`IR` | Create Architecture、Implementation Readiness |
|
| Architect (Winston) | `bmad-architect` | `CA`、`IR` | Create Architecture、Implementation Readiness |
|
||||||
| Developer (Amelia) | `bmad-agent-dev` | `DS`、`QD`、`QA`、`CR`、`SP`、`CS`、`ER` | Dev Story、Quick Dev、QA Test Generation、Code Review、Sprint Planning、Create Story、Epic Retrospective |
|
| Developer (Amelia) | `bmad-agent-dev` | `DS`、`QD`、`QA`、`CR`、`SP`、`CS`、`ER` | Dev Story、Quick Dev、QA Test Generation、Code Review、Sprint Planning、Create Story、Epic Retrospective |
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Step 8: Scoping Exercise - MVP & Future Features
|
# Step 8: Scoping Exercise - Scope Definition (Phased or Single-Release)
|
||||||
|
|
||||||
**Progress: Step 8 of 11** - Next: Functional Requirements
|
**Progress: Step 8 of 11** - Next: Functional Requirements
|
||||||
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
- 📋 YOU ARE A FACILITATOR, not a content generator
|
- 📋 YOU ARE A FACILITATOR, not a content generator
|
||||||
- 💬 FOCUS on strategic scope decisions that keep projects viable
|
- 💬 FOCUS on strategic scope decisions that keep projects viable
|
||||||
- 🎯 EMPHASIZE lean MVP thinking while preserving long-term vision
|
- 🎯 EMPHASIZE lean MVP thinking while preserving long-term vision
|
||||||
|
- ⚠️ NEVER de-scope, defer, or phase out requirements that the user explicitly included in their input documents without asking first
|
||||||
|
- ⚠️ NEVER invent phasing (MVP/Growth/Vision) unless the user requests phased delivery — if input documents define all components as core requirements, they are ALL in scope
|
||||||
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||||
- ✅ YOU MUST ALWAYS WRITE all artifact and document content in `{document_output_language}`
|
- ✅ YOU MUST ALWAYS WRITE all artifact and document content in `{document_output_language}`
|
||||||
|
|
||||||
|
|
@ -34,7 +36,7 @@
|
||||||
|
|
||||||
## YOUR TASK:
|
## YOUR TASK:
|
||||||
|
|
||||||
Conduct comprehensive scoping exercise to define MVP boundaries and prioritize features across development phases.
|
Conduct comprehensive scoping exercise to define release boundaries and prioritize features based on the user's chosen delivery mode (phased or single-release).
|
||||||
|
|
||||||
## SCOPING SEQUENCE:
|
## SCOPING SEQUENCE:
|
||||||
|
|
||||||
|
|
@ -75,30 +77,41 @@ Use structured decision-making for scope:
|
||||||
- Advanced functionality that builds on MVP
|
- Advanced functionality that builds on MVP
|
||||||
- Ask what features could be added in versions 2, 3, etc.
|
- Ask what features could be added in versions 2, 3, etc.
|
||||||
|
|
||||||
|
**⚠️ SCOPE CHANGE CONFIRMATION GATE:**
|
||||||
|
- If you believe any user-specified requirement should be deferred or de-scoped, you MUST present this to the user and get explicit confirmation BEFORE removing it from scope
|
||||||
|
- Frame it as a recommendation, not a decision: "I'd recommend deferring X because [reason]. Do you agree, or should it stay in scope?"
|
||||||
|
- NEVER silently move user requirements to a later phase or exclude them from MVP
|
||||||
|
- Before creating any consequential phase-based artifacts (e.g., phase tags, labels, or follow-on prompts), present artifact creation as a recommendation and proceed only after explicit user approval
|
||||||
|
|
||||||
### 4. Progressive Feature Roadmap
|
### 4. Progressive Feature Roadmap
|
||||||
|
|
||||||
Create phased development approach:
|
**CRITICAL: Phasing is NOT automatic. Check the user's input first.**
|
||||||
- Guide mapping of features across development phases
|
|
||||||
- Structure as Phase 1 (MVP), Phase 2 (Growth), Phase 3 (Vision)
|
|
||||||
- Ensure clear progression and dependencies
|
|
||||||
|
|
||||||
- Core user value delivery
|
Before proposing any phased approach, review the user's input documents:
|
||||||
- Essential user journeys
|
|
||||||
- Basic functionality that works reliably
|
|
||||||
|
|
||||||
**Phase 2: Growth**
|
- **If the input documents define all components as core requirements with no mention of phases:** Present all requirements as a single release scope. Do NOT invent phases or move requirements to fabricated future phases.
|
||||||
|
- **If the input documents explicitly request phased delivery:** Guide mapping of features across the phases the user defined.
|
||||||
|
- **If scope is unclear:** ASK the user whether they want phased delivery or a single release before proceeding.
|
||||||
|
|
||||||
- Additional user types
|
**When the user requests phased delivery**, guide mapping of features across the phases the user defines:
|
||||||
- Enhanced features
|
|
||||||
- Scale improvements
|
|
||||||
|
|
||||||
**Phase 3: Expansion**
|
- Use user-provided phase labels and count; if none are provided, propose a default (e.g., MVP/Growth/Vision) and ask for confirmation
|
||||||
|
- Ensure clear progression and dependencies between phases
|
||||||
|
|
||||||
- Advanced capabilities
|
**Each phase should address:**
|
||||||
- Platform features
|
|
||||||
- New markets or use cases
|
|
||||||
|
|
||||||
**Where does your current vision fit in this development sequence?**"
|
- Core user value delivery and essential journeys for that phase
|
||||||
|
- Clear boundaries on what ships in each phase
|
||||||
|
- Dependencies on prior phases
|
||||||
|
|
||||||
|
**When the user chooses a single release**, define the complete scope:
|
||||||
|
|
||||||
|
- All user-specified requirements are in scope
|
||||||
|
- Focus must-have vs nice-to-have analysis on what ships in this release
|
||||||
|
- Do NOT create phases — use must-have/nice-to-have priority within the single release
|
||||||
|
|
||||||
|
**If phased delivery:** "Where does your current vision fit in this development sequence?"
|
||||||
|
**If single release:** "How does your current vision map to this upcoming release?"
|
||||||
|
|
||||||
### 5. Risk-Based Scoping
|
### 5. Risk-Based Scoping
|
||||||
|
|
||||||
|
|
@ -129,6 +142,8 @@ Prepare comprehensive scoping section:
|
||||||
|
|
||||||
#### Content Structure:
|
#### Content Structure:
|
||||||
|
|
||||||
|
**If user chose phased delivery:**
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
## Project Scoping & Phased Development
|
## Project Scoping & Phased Development
|
||||||
|
|
||||||
|
|
@ -160,11 +175,39 @@ Prepare comprehensive scoping section:
|
||||||
**Resource Risks:** {{contingency_approach}}
|
**Resource Risks:** {{contingency_approach}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**If user chose single release (no phasing):**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Project Scoping
|
||||||
|
|
||||||
|
### Strategy & Philosophy
|
||||||
|
|
||||||
|
**Approach:** {{chosen_approach}}
|
||||||
|
**Resource Requirements:** {{team_size_and_skills}}
|
||||||
|
|
||||||
|
### Complete Feature Set
|
||||||
|
|
||||||
|
**Core User Journeys Supported:**
|
||||||
|
{{all_journeys}}
|
||||||
|
|
||||||
|
**Must-Have Capabilities:**
|
||||||
|
{{list_of_must_have_features}}
|
||||||
|
|
||||||
|
**Nice-to-Have Capabilities:**
|
||||||
|
{{list_of_nice_to_have_features}}
|
||||||
|
|
||||||
|
### Risk Mitigation Strategy
|
||||||
|
|
||||||
|
**Technical Risks:** {{mitigation_approach}}
|
||||||
|
**Market Risks:** {{validation_approach}}
|
||||||
|
**Resource Risks:** {{contingency_approach}}
|
||||||
|
```
|
||||||
|
|
||||||
### 7. Present MENU OPTIONS
|
### 7. Present MENU OPTIONS
|
||||||
|
|
||||||
Present the scoping decisions for review, then display menu:
|
Present the scoping decisions for review, then display menu:
|
||||||
- Show strategic scoping plan (using structure from step 6)
|
- Show strategic scoping plan (using structure from step 6)
|
||||||
- Highlight MVP boundaries and phased roadmap
|
- Highlight release boundaries and prioritization (phased roadmap only if phased delivery was selected)
|
||||||
- Ask if they'd like to refine further, get other perspectives, or proceed
|
- Ask if they'd like to refine further, get other perspectives, or proceed
|
||||||
- Present menu options naturally as part of conversation
|
- Present menu options naturally as part of conversation
|
||||||
|
|
||||||
|
|
@ -173,7 +216,7 @@ Display: "**Select:** [A] Advanced Elicitation [P] Party Mode [C] Continue to Fu
|
||||||
#### Menu Handling Logic:
|
#### Menu Handling Logic:
|
||||||
- IF A: Invoke the `bmad-advanced-elicitation` skill with the current scoping analysis, process the enhanced insights that come back, ask user if they accept the improvements, if yes update content then redisplay menu, if no keep original content then redisplay menu
|
- IF A: Invoke the `bmad-advanced-elicitation` skill with the current scoping analysis, process the enhanced insights that come back, ask user if they accept the improvements, if yes update content then redisplay menu, if no keep original content then redisplay menu
|
||||||
- IF P: Invoke the `bmad-party-mode` skill with the scoping context, process the collaborative insights on MVP and roadmap decisions, ask user if they accept the changes, if yes update content then redisplay menu, if no keep original content then redisplay menu
|
- IF P: Invoke the `bmad-party-mode` skill with the scoping context, process the collaborative insights on MVP and roadmap decisions, ask user if they accept the changes, if yes update content then redisplay menu, if no keep original content then redisplay menu
|
||||||
- IF C: Append the final content to {outputFile}, update frontmatter by adding this step name to the end of the stepsCompleted array, then read fully and follow: ./step-09-functional.md
|
- IF C: Append the final content to {outputFile}, update frontmatter by adding this step name to the end of the stepsCompleted array (also add `releaseMode: phased` or `releaseMode: single-release` to frontmatter based on user's choice), then read fully and follow: ./step-09-functional.md
|
||||||
- IF Any other: help user respond, then redisplay menu
|
- IF Any other: help user respond, then redisplay menu
|
||||||
|
|
||||||
#### EXECUTION RULES:
|
#### EXECUTION RULES:
|
||||||
|
|
@ -189,8 +232,9 @@ When user selects 'C', append the content directly to the document using the str
|
||||||
|
|
||||||
✅ Complete PRD document analyzed for scope implications
|
✅ Complete PRD document analyzed for scope implications
|
||||||
✅ Strategic MVP approach defined and justified
|
✅ Strategic MVP approach defined and justified
|
||||||
✅ Clear MVP feature boundaries established
|
✅ Clear feature boundaries established (phased or single-release, per user preference)
|
||||||
✅ Phased development roadmap created
|
✅ All user-specified requirements accounted for — none silently removed or deferred
|
||||||
|
✅ Any scope reduction recommendations presented to user with rationale and explicit confirmation obtained
|
||||||
✅ Key risks identified and mitigation strategies defined
|
✅ Key risks identified and mitigation strategies defined
|
||||||
✅ User explicitly agrees to scope decisions
|
✅ User explicitly agrees to scope decisions
|
||||||
✅ A/P/C menu presented and handled correctly
|
✅ A/P/C menu presented and handled correctly
|
||||||
|
|
@ -202,8 +246,11 @@ When user selects 'C', append the content directly to the document using the str
|
||||||
❌ Making scope decisions without strategic rationale
|
❌ Making scope decisions without strategic rationale
|
||||||
❌ Not getting explicit user agreement on MVP boundaries
|
❌ Not getting explicit user agreement on MVP boundaries
|
||||||
❌ Missing critical risk analysis
|
❌ Missing critical risk analysis
|
||||||
❌ Not creating clear phased development approach
|
|
||||||
❌ Not presenting A/P/C menu after content generation
|
❌ Not presenting A/P/C menu after content generation
|
||||||
|
❌ **CRITICAL**: Silently de-scoping or deferring requirements that the user explicitly included in their input documents
|
||||||
|
❌ **CRITICAL**: Inventing phasing (MVP/Growth/Vision) when the user did not request phased delivery
|
||||||
|
❌ **CRITICAL**: Making consequential scoping decisions (what is in/out of scope) without explicit user confirmation
|
||||||
|
❌ **CRITICAL**: Creating phase-based artifacts (tags, labels, follow-on prompts) without explicit user approval
|
||||||
|
|
||||||
❌ **CRITICAL**: Reading only partial step file - leads to incomplete understanding and poor decisions
|
❌ **CRITICAL**: Reading only partial step file - leads to incomplete understanding and poor decisions
|
||||||
❌ **CRITICAL**: Proceeding with 'C' without fully reading and understanding the next step file
|
❌ **CRITICAL**: Proceeding with 'C' without fully reading and understanding the next step file
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ Make targeted improvements:
|
||||||
- All user success criteria
|
- All user success criteria
|
||||||
- All functional requirements (capability contract)
|
- All functional requirements (capability contract)
|
||||||
- All user journey narratives
|
- All user journey narratives
|
||||||
- All scope decisions (MVP, Growth, Vision)
|
- All scope decisions (whether phased or single-release), including consent-critical evidence (explicit user confirmations and rationales for any scope changes from step 8)
|
||||||
- All non-functional requirements
|
- All non-functional requirements
|
||||||
- Product differentiator and vision
|
- Product differentiator and vision
|
||||||
- Domain-specific requirements
|
- Domain-specific requirements
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ failed_layers: '' # set at runtime: comma-separated list of layers that failed o
|
||||||
- The Blind Hunter subagent receives NO project context — diff only.
|
- The Blind Hunter subagent receives NO project context — diff only.
|
||||||
- The Edge Case Hunter subagent receives diff and project read access.
|
- The Edge Case Hunter subagent receives diff and project read access.
|
||||||
- The Acceptance Auditor subagent receives diff, spec, and context docs.
|
- The Acceptance Auditor subagent receives diff, spec, and context docs.
|
||||||
|
- All review subagents must run at the same model capability as the current session.
|
||||||
|
|
||||||
## INSTRUCTIONS
|
## INSTRUCTIONS
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
||||||
spec_file: '' # set at runtime for both routes before leaving this step
|
spec_file: '' # set at runtime for both routes before leaving this step
|
||||||
|
story_key: '' # set at runtime to the current story's full sprint-status key (e.g. 3-2-digest-delivery) when the intent is an epic story and sprint-status resolution succeeds
|
||||||
---
|
---
|
||||||
|
|
||||||
# Step 1: Clarify and Route
|
# Step 1: Clarify and Route
|
||||||
|
|
@ -20,7 +21,7 @@ Before listing artifacts or prompting the user, check whether you already know t
|
||||||
|
|
||||||
1. Explicit argument
|
1. Explicit argument
|
||||||
Did the user pass a specific file path, spec name, or clear instruction this message?
|
Did the user pass a specific file path, spec name, or clear instruction this message?
|
||||||
- If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: draft, ready-for-dev, in-progress, in-review, or done) → set `spec_file` and **EARLY EXIT** to the appropriate step (step-02 for draft, step-03 for ready/in-progress, step-04 for review). For `done`, ingest as context and proceed to INSTRUCTIONS — do not resume.
|
- If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: draft, ready-for-dev, in-progress, in-review, or done) → set `spec_file`. Before exiting, run **Story-key resolution** (below). Then **EARLY EXIT** to the appropriate step (step-02 for draft, step-03 for ready/in-progress, step-04 for review). For `done`, ingest as context and proceed to INSTRUCTIONS — do not resume.
|
||||||
- Anything else (intent files, external docs, plans, descriptions) → ingest it as starting intent and proceed to INSTRUCTIONS. Do not attempt to infer a workflow state from it.
|
- Anything else (intent files, external docs, plans, descriptions) → ingest it as starting intent and proceed to INSTRUCTIONS. Do not attempt to infer a workflow state from it.
|
||||||
|
|
||||||
2. Recent conversation
|
2. Recent conversation
|
||||||
|
|
@ -29,13 +30,19 @@ Before listing artifacts or prompting the user, check whether you already know t
|
||||||
|
|
||||||
3. Otherwise — scan artifacts and ask
|
3. Otherwise — scan artifacts and ask
|
||||||
- Active specs (`draft`, `ready-for-dev`, `in-progress`, `in-review`) in `{implementation_artifacts}`? → List them and HALT. Ask user which to resume (or `[N]` for new).
|
- Active specs (`draft`, `ready-for-dev`, `in-progress`, `in-review`) in `{implementation_artifacts}`? → List them and HALT. Ask user which to resume (or `[N]` for new).
|
||||||
- If `draft` selected: Set `spec_file`. **EARLY EXIT** → `./step-02-plan.md` (resume planning from the draft)
|
- If `draft` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `./step-02-plan.md` (resume planning from the draft)
|
||||||
- If `ready-for-dev` or `in-progress` selected: Set `spec_file`. **EARLY EXIT** → `./step-03-implement.md`
|
- If `ready-for-dev` or `in-progress` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `./step-03-implement.md`
|
||||||
- If `in-review` selected: Set `spec_file`. **EARLY EXIT** → `./step-04-review.md`
|
- If `in-review` selected: Set `spec_file`. Run **Story-key resolution** (below). **EARLY EXIT** → `./step-04-review.md`
|
||||||
- Unformatted spec or intent file lacking `status` frontmatter? → Suggest treating its contents as the starting intent. Do NOT attempt to infer a state and resume it.
|
- Unformatted spec or intent file lacking `status` frontmatter? → Suggest treating its contents as the starting intent. Do NOT attempt to infer a state and resume it.
|
||||||
|
|
||||||
Never ask extra questions if you already understand what the user intends.
|
Never ask extra questions if you already understand what the user intends.
|
||||||
|
|
||||||
|
### Story-key resolution
|
||||||
|
|
||||||
|
This runs on ALL paths (early-exit and INSTRUCTIONS) whenever `spec_file` is set. Determine whether the spec is an epic story — use the spec's filename, frontmatter, and any loaded epics file to identify `{epic_num}` and `{story_num}`. If the spec is not an epic story, skip silently and leave `{story_key}` unset.
|
||||||
|
|
||||||
|
If the spec is an epic story and `{sprint_status}` exists: find the `development_status` key matching `{epic_num}-{story_num}` by exact numeric equality on the first two segments (so `1-1` never collides with `1-10`). Exactly one match → set `{story_key}` to that full key. Zero or multiple matches → leave `{story_key}` unset (warn on multiple).
|
||||||
|
|
||||||
## INSTRUCTIONS
|
## INSTRUCTIONS
|
||||||
|
|
||||||
1. Load context.
|
1. Load context.
|
||||||
|
|
@ -45,7 +52,7 @@ Never ask extra questions if you already understand what the user intends.
|
||||||
|
|
||||||
**A) Epic story path** — if the intent is clearly an epic story:
|
**A) Epic story path** — if the intent is clearly an epic story:
|
||||||
|
|
||||||
1. Identify the epic number and (if present) the story number. If you can't identify an epic number, use path B.
|
1. Identify the epic number `{epic_num}` and (if present) the story number `{story_num}`. If you can't identify an epic number, use path B.
|
||||||
|
|
||||||
2. **Check for a valid cached epic context.** Look for `{implementation_artifacts}/epic-<N>-context.md` (where `<N>` is the epic number). A file is **valid** when it exists, is non-empty, starts with `# Epic <N> Context:` (with the correct epic number), and no file in `{planning_artifacts}` is newer.
|
2. **Check for a valid cached epic context.** Look for `{implementation_artifacts}/epic-<N>-context.md` (where `<N>` is the epic number). A file is **valid** when it exists, is non-empty, starts with `# Epic <N> Context:` (with the correct epic number), and no file in `{planning_artifacts}` is newer.
|
||||||
- **If valid:** load it as the primary planning context. Do not load raw planning docs (PRD, architecture, UX, etc.). Skip to step 5.
|
- **If valid:** load it as the primary planning context. Do not load raw planning docs (PRD, architecture, UX, etc.). Skip to step 5.
|
||||||
|
|
@ -59,6 +66,8 @@ Never ask extra questions if you already understand what the user intends.
|
||||||
|
|
||||||
5. **Previous story continuity.** Regardless of which context source succeeded above, scan `{implementation_artifacts}` for specs from the same epic with `status: done` and a lower story number. Load the most recent one (highest story number below current). Extract its **Code Map**, **Design Notes**, **Spec Change Log**, and **task list** as continuity context for step-02 planning. If no `done` spec is found but an `in-review` spec exists for the same epic with a lower story number, note it to the user and ask whether to load it.
|
5. **Previous story continuity.** Regardless of which context source succeeded above, scan `{implementation_artifacts}` for specs from the same epic with `status: done` and a lower story number. Load the most recent one (highest story number below current). Extract its **Code Map**, **Design Notes**, **Spec Change Log**, and **task list** as continuity context for step-02 planning. If no `done` spec is found but an `in-review` spec exists for the same epic with a lower story number, note it to the user and ask whether to load it.
|
||||||
|
|
||||||
|
6. **Resolve `{story_key}`.** If not already set by an earlier early-exit path, run **Story-key resolution** (above) now.
|
||||||
|
|
||||||
**B) Freeform path** — if the intent is not an epic story:
|
**B) Freeform path** — if the intent is not an epic story:
|
||||||
- Planning artifacts are the output of BMAD phases 1-3. Typical files include:
|
- Planning artifacts are the output of BMAD phases 1-3. Typical files include:
|
||||||
- **PRD** (`*prd*`) — product requirements and success criteria
|
- **PRD** (`*prd*`) — product requirements and success criteria
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ Capture `baseline_commit` (current HEAD, or `NO_VCS` if version control is unava
|
||||||
|
|
||||||
Change `{spec_file}` status to `in-progress` in the frontmatter before starting implementation.
|
Change `{spec_file}` status to `in-progress` in the frontmatter before starting implementation.
|
||||||
|
|
||||||
|
Follow `./sync-sprint-status.md` with `{target_status}` = `in-progress`.
|
||||||
|
|
||||||
If `{spec_file}` has a non-empty `context:` list in its frontmatter, load those files before implementation begins. When handing to a sub-agent, include them in the sub-agent prompt so it has access to the referenced context.
|
If `{spec_file}` has a non-empty `context:` list in its frontmatter, load those files before implementation begins. When handing to a sub-agent, include them in the sub-agent prompt so it has access to the referenced context.
|
||||||
|
|
||||||
Hand `{spec_file}` to a sub-agent/task and let it implement. If no sub-agents are available, implement directly.
|
Hand `{spec_file}` to a sub-agent/task and let it implement. If no sub-agents are available, implement directly.
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ specLoopIteration: 1
|
||||||
|
|
||||||
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
||||||
- Review subagents get NO conversation context.
|
- Review subagents get NO conversation context.
|
||||||
|
- All review subagents must run at the same model capability as the current session.
|
||||||
|
|
||||||
## INSTRUCTIONS
|
## INSTRUCTIONS
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,16 +48,25 @@ Format each stop as framing first, link on the next indented line:
|
||||||
|
|
||||||
When there is only one concern, omit the bold label — just list the stops directly.
|
When there is only one concern, omit the bold label — just list the stops directly.
|
||||||
|
|
||||||
### Commit and Present
|
### Mark Spec Done
|
||||||
|
|
||||||
1. Change `{spec_file}` status to `done` in the frontmatter.
|
Change `{spec_file}` status to `done` in the frontmatter.
|
||||||
2. If version control is available and the tree is dirty, create a local commit with a conventional message derived from the spec title.
|
|
||||||
3. Open the spec in the user's editor so they can click through the Suggested Review Order:
|
Follow `./sync-sprint-status.md` with `{target_status}` = `review`.
|
||||||
|
|
||||||
|
### Commit and Open
|
||||||
|
|
||||||
|
1. If version control is available and the tree is dirty, create a local commit with a conventional message derived from the spec title.
|
||||||
|
2. Open the spec in the user's editor so they can click through the Suggested Review Order:
|
||||||
- Resolve two absolute paths: (1) the repository root (`git rev-parse --show-toplevel` — returns the worktree root when in a worktree, project root otherwise; if this fails, fall back to the current working directory), (2) `{spec_file}`. Run `code -r "{absolute-root}" "{absolute-spec-file}"` — the root first so VS Code opens in the right context, then the spec file. Always double-quote paths to handle spaces and special characters.
|
- Resolve two absolute paths: (1) the repository root (`git rev-parse --show-toplevel` — returns the worktree root when in a worktree, project root otherwise; if this fails, fall back to the current working directory), (2) `{spec_file}`. Run `code -r "{absolute-root}" "{absolute-spec-file}"` — the root first so VS Code opens in the right context, then the spec file. Always double-quote paths to handle spaces and special characters.
|
||||||
- If `code` is not available (command fails), skip gracefully and tell the user the spec file path instead.
|
- If `code` is not available (command fails), skip gracefully and tell the user the spec file path instead.
|
||||||
4. Display summary of your work to the user, including the commit hash if one was created. Any file paths shown in conversation/terminal output must use CWD-relative format (no leading `/`) with `:line` notation (e.g., `src/path/file.ts:42`) for terminal clickability — the goal is to make paths clickable in terminal emulators. Include:
|
|
||||||
- A note that the spec is open in their editor (or the file path if it couldn't be opened). Mention that `{spec_file}` now contains a Suggested Review Order.
|
### Display Summary
|
||||||
- **Navigation tip:** "Ctrl+click (Cmd+click on macOS) the links in the Suggested Review Order to jump to each stop."
|
|
||||||
- Offer to push and/or create a pull request.
|
Display summary of your work to the user, including the commit hash if one was created. Any file paths shown in conversation/terminal output must use CWD-relative format (no leading `/`) with `:line` notation (e.g., `src/path/file.ts:42`) for terminal clickability — the goal is to make paths clickable in terminal emulators. Include:
|
||||||
|
|
||||||
|
- A note that the spec is open in their editor (or the file path if it couldn't be opened). Mention that `{spec_file}` now contains a Suggested Review Order.
|
||||||
|
- **Navigation tip:** "Ctrl+click (Cmd+click on macOS) the links in the Suggested Review Order to jump to each stop."
|
||||||
|
- Offer to push and/or create a pull request.
|
||||||
|
|
||||||
Workflow complete.
|
Workflow complete.
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,13 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
||||||
|
|
||||||
### Implement
|
### Implement
|
||||||
|
|
||||||
|
Follow `./sync-sprint-status.md` with `{target_status}` = `in-progress`.
|
||||||
|
|
||||||
Implement the clarified intent directly.
|
Implement the clarified intent directly.
|
||||||
|
|
||||||
### Review
|
### Review
|
||||||
|
|
||||||
Invoke the `bmad-review-adversarial-general` skill in a subagent with the changed files. The subagent gets NO conversation context — to avoid anchoring bias. If no sub-agents are available, write the changed files to a review prompt file in `{implementation_artifacts}` and HALT. Ask the human to run the review in a separate session and paste back the findings.
|
Invoke the `bmad-review-adversarial-general` skill in a subagent with the changed files. The subagent gets NO conversation context — to avoid anchoring bias. Launch at the same model capability as the current session. If no sub-agents are available, write the changed files to a review prompt file in `{implementation_artifacts}` and HALT. Ask the human to run the review in a separate session and paste back the findings.
|
||||||
|
|
||||||
### Classify
|
### Classify
|
||||||
|
|
||||||
|
|
@ -39,6 +41,8 @@ Write `{spec_file}` using `./spec-template.md`. Fill only these sections — del
|
||||||
2. **Title and Intent** — `# {title}` heading and `## Intent` with **Problem** and **Approach** lines. Reuse the summary you already generated for the terminal.
|
2. **Title and Intent** — `# {title}` heading and `## Intent` with **Problem** and **Approach** lines. Reuse the summary you already generated for the terminal.
|
||||||
3. **Suggested Review Order** — append after Intent. Build using the same convention as `./step-05-present.md` § "Generate Suggested Review Order" (spec-file-relative links, concern-based ordering, ultra-concise framing).
|
3. **Suggested Review Order** — append after Intent. Build using the same convention as `./step-05-present.md` § "Generate Suggested Review Order" (spec-file-relative links, concern-based ordering, ultra-concise framing).
|
||||||
|
|
||||||
|
Follow `./sync-sprint-status.md` with `{target_status}` = `review`.
|
||||||
|
|
||||||
### Commit
|
### Commit
|
||||||
|
|
||||||
If version control is available and the tree is dirty, create a local commit with a conventional message derived from the intent. If VCS is unavailable, skip.
|
If version control is available and the tree is dirty, create a local commit with a conventional message derived from the intent. If VCS is unavailable, skip.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Sync Sprint Status
|
||||||
|
|
||||||
|
Shared sub-step for updating `sprint-status.yaml` during quick-dev. Called from any route (plan-code-review, one-shot, future routes) with a `{target_status}` parameter.
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
|
||||||
|
Skip this entire file (return to caller) if ANY of:
|
||||||
|
- `{story_key}` is unset
|
||||||
|
- `{sprint_status}` does not exist on disk
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. Load the FULL `{sprint_status}` file.
|
||||||
|
2. Find the `development_status` entry matching `{story_key}`. If not found, warn the user once (`"{story_key} not found in sprint-status; skipping sprint sync"`) and return to caller.
|
||||||
|
3. **Idempotency check.** If `development_status[{story_key}]` is already at `{target_status}` or a later state (`review` is later than `in-progress`; `done` is later than both), return to caller — no write needed. Never regress a story's status.
|
||||||
|
4. Set `development_status[{story_key}]` to `{target_status}`.
|
||||||
|
5. **Epic lift (only when `{target_status}` = `in-progress`).** Derive the parent epic key as `epic-{N}` from the leading numeric segment of `{story_key}` (e.g., `3-2-digest-delivery` → `epic-3`). If that entry exists and is `backlog`, set it to `in-progress`. Leave it alone otherwise. Skip this sub-step entirely when `{target_status}` is not `in-progress`.
|
||||||
|
6. Refresh `last_updated` to the current date.
|
||||||
|
7. Save the file, preserving ALL comments and structure including STATUS DEFINITIONS and WORKFLOW NOTES.
|
||||||
|
|
@ -65,6 +65,7 @@ Load and read full config from `{main_config}` and resolve:
|
||||||
- `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
|
- `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
|
||||||
- `communication_language`, `document_output_language`, `user_skill_level`
|
- `communication_language`, `document_output_language`, `user_skill_level`
|
||||||
- `date` as system-generated current datetime
|
- `date` as system-generated current datetime
|
||||||
|
- `sprint_status` = `{implementation_artifacts}/sprint-status.yaml`
|
||||||
- `project_context` = `**/project-context.md` (load if exists)
|
- `project_context` = `**/project-context.md` (load if exists)
|
||||||
- CLAUDE.md / memory files (load if exist)
|
- CLAUDE.md / memory files (load if exist)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1728,36 +1728,6 @@ async function runTests() {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
console.log(`${colors.yellow}Test Suite 33: Community & Custom Module Managers${colors.reset}\n`);
|
console.log(`${colors.yellow}Test Suite 33: Community & Custom Module Managers${colors.reset}\n`);
|
||||||
|
|
||||||
// --- CustomModuleManager.validateGitHubUrl ---
|
|
||||||
{
|
|
||||||
const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager');
|
|
||||||
const mgr = new CustomModuleManager();
|
|
||||||
|
|
||||||
const https1 = mgr.validateGitHubUrl('https://github.com/owner/repo');
|
|
||||||
assert(https1.isValid === true, 'validateGitHubUrl accepts HTTPS URL');
|
|
||||||
assert(https1.owner === 'owner' && https1.repo === 'repo', 'validateGitHubUrl extracts owner/repo from HTTPS');
|
|
||||||
|
|
||||||
const https2 = mgr.validateGitHubUrl('https://github.com/owner/repo.git');
|
|
||||||
assert(https2.isValid === true, 'validateGitHubUrl accepts HTTPS URL with .git');
|
|
||||||
assert(https2.repo === 'repo', 'validateGitHubUrl strips .git suffix');
|
|
||||||
|
|
||||||
const ssh1 = mgr.validateGitHubUrl('git@github.com:owner/repo.git');
|
|
||||||
assert(ssh1.isValid === true, 'validateGitHubUrl accepts SSH URL');
|
|
||||||
assert(ssh1.owner === 'owner' && ssh1.repo === 'repo', 'validateGitHubUrl extracts owner/repo from SSH');
|
|
||||||
|
|
||||||
const bad1 = mgr.validateGitHubUrl('https://gitlab.com/owner/repo');
|
|
||||||
assert(bad1.isValid === false, 'validateGitHubUrl rejects non-GitHub URL');
|
|
||||||
|
|
||||||
const bad2 = mgr.validateGitHubUrl('');
|
|
||||||
assert(bad2.isValid === false, 'validateGitHubUrl rejects empty string');
|
|
||||||
|
|
||||||
const bad3 = mgr.validateGitHubUrl(null);
|
|
||||||
assert(bad3.isValid === false, 'validateGitHubUrl rejects null');
|
|
||||||
|
|
||||||
const bad4 = mgr.validateGitHubUrl('https://github.com/owner');
|
|
||||||
assert(bad4.isValid === false, 'validateGitHubUrl rejects URL without repo');
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- CustomModuleManager._normalizeCustomModule ---
|
// --- CustomModuleManager._normalizeCustomModule ---
|
||||||
{
|
{
|
||||||
const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager');
|
const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager');
|
||||||
|
|
@ -1954,25 +1924,6 @@ async function runTests() {
|
||||||
assert(notFound === null, 'getModuleByCode returns null for unknown code');
|
assert(notFound === null, 'getModuleByCode returns null for unknown code');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- CustomModuleManager URL edge cases ---
|
|
||||||
{
|
|
||||||
const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager');
|
|
||||||
const mgr = new CustomModuleManager();
|
|
||||||
|
|
||||||
// HTTP (not HTTPS) should work
|
|
||||||
const http = mgr.validateGitHubUrl('http://github.com/owner/repo');
|
|
||||||
assert(http.isValid === true, 'validateGitHubUrl accepts HTTP URL');
|
|
||||||
|
|
||||||
// Trailing slash should be rejected (strict matching)
|
|
||||||
const trailing = mgr.validateGitHubUrl('https://github.com/owner/repo/');
|
|
||||||
assert(trailing.isValid === false, 'validateGitHubUrl rejects trailing slash');
|
|
||||||
|
|
||||||
// SSH without .git should work
|
|
||||||
const sshNoDotGit = mgr.validateGitHubUrl('git@github.com:owner/repo');
|
|
||||||
assert(sshNoDotGit.isValid === true, 'validateGitHubUrl accepts SSH without .git');
|
|
||||||
assert(sshNoDotGit.repo === 'repo', 'validateGitHubUrl extracts repo from SSH without .git');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,6 @@
|
||||||
const path = require('node:path');
|
|
||||||
const os = require('node:os');
|
|
||||||
const prompts = require('./prompts');
|
const prompts = require('./prompts');
|
||||||
|
|
||||||
const CLIUtils = {
|
const CLIUtils = {
|
||||||
/**
|
|
||||||
* Get version from package.json
|
|
||||||
*/
|
|
||||||
getVersion() {
|
|
||||||
try {
|
|
||||||
const packageJson = require(path.join(__dirname, '..', '..', 'package.json'));
|
|
||||||
return packageJson.version || 'Unknown';
|
|
||||||
} catch {
|
|
||||||
return 'Unknown';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display BMAD logo and version using @clack intro + box
|
* Display BMAD logo and version using @clack intro + box
|
||||||
*/
|
*/
|
||||||
|
|
@ -52,37 +38,6 @@ const CLIUtils = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Display section header
|
|
||||||
* @param {string} title - Section title
|
|
||||||
* @param {string} subtitle - Optional subtitle
|
|
||||||
*/
|
|
||||||
async displaySection(title, subtitle = null) {
|
|
||||||
await prompts.note(subtitle || '', title);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display info box
|
|
||||||
* @param {string|Array} content - Content to display
|
|
||||||
* @param {Object} options - Box options
|
|
||||||
*/
|
|
||||||
async displayBox(content, options = {}) {
|
|
||||||
let text = content;
|
|
||||||
if (Array.isArray(content)) {
|
|
||||||
text = content.join('\n\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
const color = await prompts.getColor();
|
|
||||||
const borderColor = options.borderColor || 'cyan';
|
|
||||||
const colorMap = { green: color.green, red: color.red, yellow: color.yellow, cyan: color.cyan, blue: color.blue };
|
|
||||||
const formatBorder = colorMap[borderColor] || color.cyan;
|
|
||||||
|
|
||||||
await prompts.box(text, options.title, {
|
|
||||||
rounded: options.borderStyle === 'round' || options.borderStyle === undefined,
|
|
||||||
formatBorder,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display module configuration header
|
* Display module configuration header
|
||||||
* @param {string} moduleName - Module name (fallback if no custom header)
|
* @param {string} moduleName - Module name (fallback if no custom header)
|
||||||
|
|
@ -93,98 +48,6 @@ const CLIUtils = {
|
||||||
const title = header || `Configuring ${moduleName.toUpperCase()} Module`;
|
const title = header || `Configuring ${moduleName.toUpperCase()} Module`;
|
||||||
await prompts.note(subheader || '', title);
|
await prompts.note(subheader || '', title);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Display module with no custom configuration
|
|
||||||
* @param {string} moduleName - Module name (fallback if no custom header)
|
|
||||||
* @param {string} header - Custom header from module.yaml
|
|
||||||
* @param {string} subheader - Custom subheader from module.yaml
|
|
||||||
*/
|
|
||||||
async displayModuleNoConfig(moduleName, header = null, subheader = null) {
|
|
||||||
const title = header || `${moduleName.toUpperCase()} Module - No Custom Configuration`;
|
|
||||||
await prompts.note(subheader || '', title);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display step indicator
|
|
||||||
* @param {number} current - Current step
|
|
||||||
* @param {number} total - Total steps
|
|
||||||
* @param {string} description - Step description
|
|
||||||
*/
|
|
||||||
async displayStep(current, total, description) {
|
|
||||||
const progress = `[${current}/${total}]`;
|
|
||||||
await prompts.log.step(`${progress} ${description}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display completion message
|
|
||||||
* @param {string} message - Completion message
|
|
||||||
*/
|
|
||||||
async displayComplete(message) {
|
|
||||||
const color = await prompts.getColor();
|
|
||||||
await prompts.box(`\u2728 ${message}`, 'Complete', {
|
|
||||||
rounded: true,
|
|
||||||
formatBorder: color.green,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display error message
|
|
||||||
* @param {string} message - Error message
|
|
||||||
*/
|
|
||||||
async displayError(message) {
|
|
||||||
const color = await prompts.getColor();
|
|
||||||
await prompts.box(`\u2717 ${message}`, 'Error', {
|
|
||||||
rounded: true,
|
|
||||||
formatBorder: color.red,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format list for display
|
|
||||||
* @param {Array} items - Items to display
|
|
||||||
* @param {string} prefix - Item prefix
|
|
||||||
*/
|
|
||||||
formatList(items, prefix = '\u2022') {
|
|
||||||
return items.map((item) => ` ${prefix} ${item}`).join('\n');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear previous lines
|
|
||||||
* @param {number} lines - Number of lines to clear
|
|
||||||
*/
|
|
||||||
clearLines(lines) {
|
|
||||||
for (let i = 0; i < lines; i++) {
|
|
||||||
process.stdout.moveCursor(0, -1);
|
|
||||||
process.stdout.clearLine(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display module completion message
|
|
||||||
* @param {string} moduleName - Name of the completed module
|
|
||||||
* @param {boolean} clearScreen - Whether to clear the screen first (deprecated, always false now)
|
|
||||||
*/
|
|
||||||
displayModuleComplete(moduleName, clearScreen = false) {
|
|
||||||
// No longer clear screen or show boxes - just a simple completion message
|
|
||||||
// This is deprecated but kept for backwards compatibility
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expand path with ~ expansion
|
|
||||||
* @param {string} inputPath - Path to expand
|
|
||||||
* @returns {string} Expanded path
|
|
||||||
*/
|
|
||||||
expandPath(inputPath) {
|
|
||||||
if (!inputPath) return inputPath;
|
|
||||||
|
|
||||||
// Expand ~ to home directory
|
|
||||||
if (inputPath.startsWith('~')) {
|
|
||||||
return path.join(os.homedir(), inputPath.slice(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
return inputPath;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { CLIUtils };
|
module.exports = { CLIUtils };
|
||||||
|
|
|
||||||
|
|
@ -107,117 +107,6 @@ class Manifest {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update existing manifest
|
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
|
||||||
* @param {Object} updates - Fields to update
|
|
||||||
* @param {Array} installedFiles - Updated list of installed files
|
|
||||||
*/
|
|
||||||
async update(bmadDir, updates, installedFiles = null) {
|
|
||||||
const yaml = require('yaml');
|
|
||||||
const manifest = (await this._readRaw(bmadDir)) || {
|
|
||||||
installation: {},
|
|
||||||
modules: [],
|
|
||||||
ides: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle module updates
|
|
||||||
if (updates.modules) {
|
|
||||||
// If modules is being updated, we need to preserve detailed module info
|
|
||||||
const existingDetailed = manifest.modules || [];
|
|
||||||
const incomingNames = updates.modules;
|
|
||||||
|
|
||||||
// Build updated modules array
|
|
||||||
const updatedModules = [];
|
|
||||||
for (const name of incomingNames) {
|
|
||||||
const existing = existingDetailed.find((m) => m.name === name);
|
|
||||||
if (existing) {
|
|
||||||
// Preserve existing details, update lastUpdated if this module is being updated
|
|
||||||
updatedModules.push({
|
|
||||||
...existing,
|
|
||||||
lastUpdated: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// New module - add with minimal details
|
|
||||||
updatedModules.push({
|
|
||||||
name,
|
|
||||||
version: null,
|
|
||||||
installDate: new Date().toISOString(),
|
|
||||||
lastUpdated: new Date().toISOString(),
|
|
||||||
source: 'unknown',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest.modules = updatedModules;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge other updates
|
|
||||||
if (updates.version) {
|
|
||||||
manifest.installation.version = updates.version;
|
|
||||||
}
|
|
||||||
if (updates.installDate) {
|
|
||||||
manifest.installation.installDate = updates.installDate;
|
|
||||||
}
|
|
||||||
manifest.installation.lastUpdated = new Date().toISOString();
|
|
||||||
|
|
||||||
if (updates.ides) {
|
|
||||||
manifest.ides = updates.ides;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle per-module version updates
|
|
||||||
if (updates.moduleVersions) {
|
|
||||||
for (const [moduleName, versionInfo] of Object.entries(updates.moduleVersions)) {
|
|
||||||
const moduleIndex = manifest.modules.findIndex((m) => m.name === moduleName);
|
|
||||||
if (moduleIndex !== -1) {
|
|
||||||
manifest.modules[moduleIndex] = {
|
|
||||||
...manifest.modules[moduleIndex],
|
|
||||||
...versionInfo,
|
|
||||||
lastUpdated: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle adding a new module with version info
|
|
||||||
if (updates.addModule) {
|
|
||||||
const { name, version, source, npmPackage, repoUrl, localPath } = updates.addModule;
|
|
||||||
const existing = manifest.modules.find((m) => m.name === name);
|
|
||||||
if (!existing) {
|
|
||||||
const entry = {
|
|
||||||
name,
|
|
||||||
version: version || null,
|
|
||||||
installDate: new Date().toISOString(),
|
|
||||||
lastUpdated: new Date().toISOString(),
|
|
||||||
source: source || 'external',
|
|
||||||
npmPackage: npmPackage || null,
|
|
||||||
repoUrl: repoUrl || null,
|
|
||||||
};
|
|
||||||
if (localPath) entry.localPath = localPath;
|
|
||||||
manifest.modules.push(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
|
|
||||||
await fs.ensureDir(path.dirname(manifestPath));
|
|
||||||
|
|
||||||
// Clean the manifest data to remove any non-serializable values
|
|
||||||
const cleanManifestData = structuredClone(manifest);
|
|
||||||
|
|
||||||
const yamlContent = yaml.stringify(cleanManifestData, {
|
|
||||||
indent: 2,
|
|
||||||
lineWidth: 0,
|
|
||||||
sortKeys: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensure POSIX-compliant final newline
|
|
||||||
const content = yamlContent.endsWith('\n') ? yamlContent : yamlContent + '\n';
|
|
||||||
await fs.writeFile(manifestPath, content, 'utf8');
|
|
||||||
|
|
||||||
// Return the flattened format for compatibility
|
|
||||||
return this._flattenManifest(manifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read raw manifest data without flattening
|
* Read raw manifest data without flattening
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
* @param {string} bmadDir - Path to bmad directory
|
||||||
|
|
@ -310,62 +199,6 @@ class Manifest {
|
||||||
await this._writeRaw(bmadDir, manifest);
|
await this._writeRaw(bmadDir, manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a module from the manifest
|
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
|
||||||
* @param {string} moduleName - Module name to remove
|
|
||||||
*/
|
|
||||||
async removeModule(bmadDir, moduleName) {
|
|
||||||
const manifest = await this._readRaw(bmadDir);
|
|
||||||
if (!manifest || !manifest.modules) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = manifest.modules.findIndex((m) => m.name === moduleName);
|
|
||||||
if (index !== -1) {
|
|
||||||
manifest.modules.splice(index, 1);
|
|
||||||
await this._writeRaw(bmadDir, manifest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a single module's version info
|
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
|
||||||
* @param {string} moduleName - Module name
|
|
||||||
* @param {Object} versionInfo - Version info to update
|
|
||||||
*/
|
|
||||||
async updateModuleVersion(bmadDir, moduleName, versionInfo) {
|
|
||||||
const manifest = await this._readRaw(bmadDir);
|
|
||||||
if (!manifest || !manifest.modules) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = manifest.modules.findIndex((m) => m.name === moduleName);
|
|
||||||
if (index !== -1) {
|
|
||||||
manifest.modules[index] = {
|
|
||||||
...manifest.modules[index],
|
|
||||||
...versionInfo,
|
|
||||||
lastUpdated: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
await this._writeRaw(bmadDir, manifest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get version info for a specific module
|
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
|
||||||
* @param {string} moduleName - Module name
|
|
||||||
* @returns {Object|null} Module version info or null
|
|
||||||
*/
|
|
||||||
async getModuleVersion(bmadDir, moduleName) {
|
|
||||||
const manifest = await this._readRaw(bmadDir);
|
|
||||||
if (!manifest || !manifest.modules) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return manifest.modules.find((m) => m.name === moduleName) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all modules with their version info
|
* Get all modules with their version info
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
* @param {string} bmadDir - Path to bmad directory
|
||||||
|
|
@ -403,27 +236,6 @@ class Manifest {
|
||||||
await fs.writeFile(manifestPath, content, 'utf8');
|
await fs.writeFile(manifestPath, content, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add an IDE configuration to the manifest
|
|
||||||
* @param {string} bmadDir - Path to bmad directory
|
|
||||||
* @param {string} ideName - IDE name to add
|
|
||||||
*/
|
|
||||||
async addIde(bmadDir, ideName) {
|
|
||||||
const manifest = await this.read(bmadDir);
|
|
||||||
if (!manifest) {
|
|
||||||
throw new Error('No manifest found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!manifest.ides) {
|
|
||||||
manifest.ides = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!manifest.ides.includes(ideName)) {
|
|
||||||
manifest.ides.push(ideName);
|
|
||||||
await this.update(bmadDir, { ides: manifest.ides });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate SHA256 hash of a file
|
* Calculate SHA256 hash of a file
|
||||||
* @param {string} filePath - Path to file
|
* @param {string} filePath - Path to file
|
||||||
|
|
@ -438,354 +250,6 @@ class Manifest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse installed files to extract metadata
|
|
||||||
* @param {Array} installedFiles - List of installed file paths
|
|
||||||
* @param {string} bmadDir - Path to bmad directory for relative paths
|
|
||||||
* @returns {Array} Array of file metadata objects
|
|
||||||
*/
|
|
||||||
async parseInstalledFiles(installedFiles, bmadDir) {
|
|
||||||
const fileMetadata = [];
|
|
||||||
|
|
||||||
for (const filePath of installedFiles) {
|
|
||||||
const fileExt = path.extname(filePath).toLowerCase();
|
|
||||||
// Make path relative to parent of bmad directory, starting with 'bmad/'
|
|
||||||
const relativePath = 'bmad' + filePath.replace(bmadDir, '').replaceAll('\\', '/');
|
|
||||||
|
|
||||||
// Calculate file hash
|
|
||||||
const hash = await this.calculateFileHash(filePath);
|
|
||||||
|
|
||||||
// Handle markdown files - extract XML metadata if present
|
|
||||||
if (fileExt === '.md') {
|
|
||||||
try {
|
|
||||||
if (await fs.pathExists(filePath)) {
|
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
|
||||||
const metadata = this.extractXmlNodeAttributes(content, filePath, relativePath);
|
|
||||||
|
|
||||||
if (metadata) {
|
|
||||||
// Has XML metadata
|
|
||||||
metadata.hash = hash;
|
|
||||||
fileMetadata.push(metadata);
|
|
||||||
} else {
|
|
||||||
// No XML metadata - still track the file
|
|
||||||
fileMetadata.push({
|
|
||||||
file: relativePath,
|
|
||||||
type: 'md',
|
|
||||||
name: path.basename(filePath, fileExt),
|
|
||||||
title: null,
|
|
||||||
hash: hash,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
await prompts.log.warn(`Could not parse ${filePath}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Handle other file types (CSV, JSON, YAML, etc.)
|
|
||||||
else {
|
|
||||||
fileMetadata.push({
|
|
||||||
file: relativePath,
|
|
||||||
type: fileExt.slice(1), // Remove the dot
|
|
||||||
name: path.basename(filePath, fileExt),
|
|
||||||
title: null,
|
|
||||||
hash: hash,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract XML node attributes from MD file content
|
|
||||||
* @param {string} content - File content
|
|
||||||
* @param {string} filePath - File path for context
|
|
||||||
* @param {string} relativePath - Relative path starting with 'bmad/'
|
|
||||||
* @returns {Object|null} Extracted metadata or null
|
|
||||||
*/
|
|
||||||
extractXmlNodeAttributes(content, filePath, relativePath) {
|
|
||||||
// Look for XML blocks in code fences
|
|
||||||
const xmlBlockMatch = content.match(/```xml\s*([\s\S]*?)```/);
|
|
||||||
if (!xmlBlockMatch) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const xmlContent = xmlBlockMatch[1];
|
|
||||||
|
|
||||||
// Extract root XML node (agent, task, template, etc.)
|
|
||||||
const rootNodeMatch = xmlContent.match(/<(\w+)([^>]*)>/);
|
|
||||||
if (!rootNodeMatch) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeType = rootNodeMatch[1];
|
|
||||||
const attributes = rootNodeMatch[2];
|
|
||||||
|
|
||||||
// Extract name and title attributes (id not needed since we have path)
|
|
||||||
const nameMatch = attributes.match(/name="([^"]*)"/);
|
|
||||||
const titleMatch = attributes.match(/title="([^"]*)"/);
|
|
||||||
|
|
||||||
return {
|
|
||||||
file: relativePath,
|
|
||||||
type: nodeType,
|
|
||||||
name: nameMatch ? nameMatch[1] : null,
|
|
||||||
title: titleMatch ? titleMatch[1] : null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate CSV manifest content
|
|
||||||
* @param {Object} data - Manifest data
|
|
||||||
* @param {Array} fileMetadata - File metadata array
|
|
||||||
* @param {Object} moduleConfigs - Module configuration data
|
|
||||||
* @returns {string} CSV content
|
|
||||||
*/
|
|
||||||
generateManifestCsv(data, fileMetadata, moduleConfigs = {}) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
let csv = [];
|
|
||||||
|
|
||||||
// Header section
|
|
||||||
csv.push(
|
|
||||||
'# BMAD Manifest',
|
|
||||||
`# Generated: ${timestamp}`,
|
|
||||||
'',
|
|
||||||
'## Installation Info',
|
|
||||||
'Property,Value',
|
|
||||||
`Version,${data.version}`,
|
|
||||||
`InstallDate,${data.installDate || timestamp}`,
|
|
||||||
`LastUpdated,${data.lastUpdated || timestamp}`,
|
|
||||||
);
|
|
||||||
if (data.language) {
|
|
||||||
csv.push(`Language,${data.language}`);
|
|
||||||
}
|
|
||||||
csv.push('');
|
|
||||||
|
|
||||||
// Modules section
|
|
||||||
if (data.modules && data.modules.length > 0) {
|
|
||||||
csv.push('## Modules', 'Name,Version,ShortTitle');
|
|
||||||
for (const moduleName of data.modules) {
|
|
||||||
const config = moduleConfigs[moduleName] || {};
|
|
||||||
csv.push([moduleName, config.version || '', config['short-title'] || ''].map((v) => this.escapeCsv(v)).join(','));
|
|
||||||
}
|
|
||||||
csv.push('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// IDEs section
|
|
||||||
if (data.ides && data.ides.length > 0) {
|
|
||||||
csv.push('## IDEs', 'IDE');
|
|
||||||
for (const ide of data.ides) {
|
|
||||||
csv.push(this.escapeCsv(ide));
|
|
||||||
}
|
|
||||||
csv.push('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Files section - NO LONGER USED
|
|
||||||
// Files are now tracked in files-manifest.csv by ManifestGenerator
|
|
||||||
|
|
||||||
return csv.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse CSV manifest content back to object
|
|
||||||
* @param {string} csvContent - CSV content to parse
|
|
||||||
* @returns {Object} Parsed manifest data
|
|
||||||
*/
|
|
||||||
parseManifestCsv(csvContent) {
|
|
||||||
const result = {
|
|
||||||
modules: [],
|
|
||||||
ides: [],
|
|
||||||
files: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const lines = csvContent.split('\n');
|
|
||||||
let section = '';
|
|
||||||
|
|
||||||
for (const line_ of lines) {
|
|
||||||
const line = line_.trim();
|
|
||||||
|
|
||||||
// Skip empty lines and comments
|
|
||||||
if (!line || line.startsWith('#')) {
|
|
||||||
// Check for section headers
|
|
||||||
if (line.startsWith('## ')) {
|
|
||||||
section = line.slice(3).toLowerCase();
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse based on current section
|
|
||||||
switch (section) {
|
|
||||||
case 'installation info': {
|
|
||||||
// Skip header row
|
|
||||||
if (line === 'Property,Value') continue;
|
|
||||||
|
|
||||||
const [property, ...valueParts] = line.split(',');
|
|
||||||
const value = this.unescapeCsv(valueParts.join(','));
|
|
||||||
|
|
||||||
switch (property) {
|
|
||||||
// Path no longer stored in manifest
|
|
||||||
case 'Version': {
|
|
||||||
result.version = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'InstallDate': {
|
|
||||||
result.installDate = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'LastUpdated': {
|
|
||||||
result.lastUpdated = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'Language': {
|
|
||||||
result.language = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'modules': {
|
|
||||||
// Skip header row
|
|
||||||
if (line === 'Name,Version,ShortTitle') continue;
|
|
||||||
|
|
||||||
const parts = this.parseCsvLine(line);
|
|
||||||
if (parts[0]) {
|
|
||||||
result.modules.push(parts[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'ides': {
|
|
||||||
// Skip header row
|
|
||||||
if (line === 'IDE') continue;
|
|
||||||
|
|
||||||
result.ides.push(this.unescapeCsv(line));
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'files': {
|
|
||||||
// Skip header rows (support both old and new format)
|
|
||||||
if (line === 'Type,Path,Name,Title' || line === 'Type,Path,Name,Title,Hash') continue;
|
|
||||||
|
|
||||||
const parts = this.parseCsvLine(line);
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
result.files.push({
|
|
||||||
type: parts[0] || '',
|
|
||||||
file: parts[1] || '',
|
|
||||||
name: parts[2] || null,
|
|
||||||
title: parts[3] || null,
|
|
||||||
hash: parts[4] || null, // Hash column (may not exist in old manifests)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// No default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a CSV line handling quotes and commas
|
|
||||||
* @param {string} line - CSV line to parse
|
|
||||||
* @returns {Array} Array of values
|
|
||||||
*/
|
|
||||||
parseCsvLine(line) {
|
|
||||||
const result = [];
|
|
||||||
let current = '';
|
|
||||||
let inQuotes = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < line.length; i++) {
|
|
||||||
const char = line[i];
|
|
||||||
|
|
||||||
if (char === '"') {
|
|
||||||
if (inQuotes && line[i + 1] === '"') {
|
|
||||||
// Escaped quote
|
|
||||||
current += '"';
|
|
||||||
i++;
|
|
||||||
} else {
|
|
||||||
// Toggle quote state
|
|
||||||
inQuotes = !inQuotes;
|
|
||||||
}
|
|
||||||
} else if (char === ',' && !inQuotes) {
|
|
||||||
// Field separator
|
|
||||||
result.push(this.unescapeCsv(current));
|
|
||||||
current = '';
|
|
||||||
} else {
|
|
||||||
current += char;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the last field
|
|
||||||
result.push(this.unescapeCsv(current));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Escape CSV special characters
|
|
||||||
* @param {string} text - Text to escape
|
|
||||||
* @returns {string} Escaped text
|
|
||||||
*/
|
|
||||||
escapeCsv(text) {
|
|
||||||
if (!text) return '';
|
|
||||||
const str = String(text);
|
|
||||||
|
|
||||||
// If contains comma, newline, or quote, wrap in quotes and escape quotes
|
|
||||||
if (str.includes(',') || str.includes('\n') || str.includes('"')) {
|
|
||||||
return '"' + str.replaceAll('"', '""') + '"';
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unescape CSV field
|
|
||||||
* @param {string} text - Text to unescape
|
|
||||||
* @returns {string} Unescaped text
|
|
||||||
*/
|
|
||||||
unescapeCsv(text) {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// Remove surrounding quotes if present
|
|
||||||
if (text.startsWith('"') && text.endsWith('"')) {
|
|
||||||
text = text.slice(1, -1);
|
|
||||||
// Unescape doubled quotes
|
|
||||||
text = text.replaceAll('""', '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load module configuration files
|
|
||||||
* @param {Array} modules - List of module names
|
|
||||||
* @returns {Object} Module configurations indexed by name
|
|
||||||
*/
|
|
||||||
async loadModuleConfigs(modules) {
|
|
||||||
const configs = {};
|
|
||||||
|
|
||||||
for (const moduleName of modules) {
|
|
||||||
// Handle core module differently - it's in src/core-skills not src/modules/core
|
|
||||||
const configPath =
|
|
||||||
moduleName === 'core'
|
|
||||||
? path.join(process.cwd(), 'src', 'core-skills', 'config.yaml')
|
|
||||||
: path.join(process.cwd(), 'src', 'modules', moduleName, 'config.yaml');
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (await fs.pathExists(configPath)) {
|
|
||||||
const yaml = require('yaml');
|
|
||||||
const content = await fs.readFile(configPath, 'utf8');
|
|
||||||
configs[moduleName] = yaml.parse(content);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
await prompts.log.warn(`Could not load config for module ${moduleName}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return configs;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Get module version info from source
|
* Get module version info from source
|
||||||
* @param {string} moduleName - Module name/code
|
* @param {string} moduleName - Module name/code
|
||||||
|
|
@ -986,47 +450,6 @@ class Manifest {
|
||||||
|
|
||||||
return updates;
|
return updates;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare two semantic versions
|
|
||||||
* @param {string} v1 - First version
|
|
||||||
* @param {string} v2 - Second version
|
|
||||||
* @returns {number} -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
|
|
||||||
*/
|
|
||||||
compareVersions(v1, v2) {
|
|
||||||
if (!v1 || !v2) return 0;
|
|
||||||
|
|
||||||
const normalize = (v) => {
|
|
||||||
// Remove leading 'v' if present
|
|
||||||
v = v.replace(/^v/, '');
|
|
||||||
// Handle prerelease tags
|
|
||||||
const parts = v.split('-');
|
|
||||||
const main = parts[0].split('.');
|
|
||||||
const prerelease = parts[1];
|
|
||||||
return { main, prerelease };
|
|
||||||
};
|
|
||||||
|
|
||||||
const n1 = normalize(v1);
|
|
||||||
const n2 = normalize(v2);
|
|
||||||
|
|
||||||
// Compare main version parts
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const num1 = parseInt(n1.main[i] || '0', 10);
|
|
||||||
const num2 = parseInt(n2.main[i] || '0', 10);
|
|
||||||
if (num1 !== num2) {
|
|
||||||
return num1 < num2 ? -1 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If main versions are equal, compare prerelease
|
|
||||||
if (n1.prerelease && n2.prerelease) {
|
|
||||||
return n1.prerelease < n2.prerelease ? -1 : n1.prerelease > n2.prerelease ? 1 : 0;
|
|
||||||
}
|
|
||||||
if (n1.prerelease) return -1; // Prerelease is older than stable
|
|
||||||
if (n2.prerelease) return 1; // Stable is newer than prerelease
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { Manifest };
|
module.exports = { Manifest };
|
||||||
|
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates launcher command files for each agent
|
|
||||||
*/
|
|
||||||
class AgentCommandGenerator {
|
|
||||||
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
|
|
||||||
this.templatePath = path.join(__dirname, '../templates/agent-command-template.md');
|
|
||||||
this.bmadFolderName = bmadFolderName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect agent artifacts for IDE installation
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {Array} selectedModules - Modules to include
|
|
||||||
* @returns {Object} Artifacts array with metadata
|
|
||||||
*/
|
|
||||||
async collectAgentArtifacts(bmadDir, selectedModules = []) {
|
|
||||||
const { getAgentsFromBmad } = require('./bmad-artifacts');
|
|
||||||
|
|
||||||
// Get agents from INSTALLED bmad/ directory
|
|
||||||
const agents = await getAgentsFromBmad(bmadDir, selectedModules);
|
|
||||||
|
|
||||||
const artifacts = [];
|
|
||||||
|
|
||||||
for (const agent of agents) {
|
|
||||||
const launcherContent = await this.generateLauncherContent(agent);
|
|
||||||
// Use relativePath if available (for nested agents), otherwise just name with .md
|
|
||||||
const agentPathInModule = agent.relativePath || `${agent.name}.md`;
|
|
||||||
// Calculate the relative agent path (e.g., bmm/agents/pm.md)
|
|
||||||
let agentRelPath = agent.path || '';
|
|
||||||
// Normalize path separators for cross-platform compatibility
|
|
||||||
agentRelPath = agentRelPath.replaceAll('\\', '/');
|
|
||||||
// Remove _bmad/ prefix if present to get relative path from project root
|
|
||||||
// Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
|
|
||||||
if (agentRelPath.includes('_bmad/')) {
|
|
||||||
const parts = agentRelPath.split(/_bmad\//);
|
|
||||||
if (parts.length > 1) {
|
|
||||||
agentRelPath = parts.slice(1).join('/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
artifacts.push({
|
|
||||||
type: 'agent-launcher',
|
|
||||||
name: agent.name,
|
|
||||||
description: agent.description || `${agent.name} agent`,
|
|
||||||
module: agent.module,
|
|
||||||
canonicalId: agent.canonicalId || '',
|
|
||||||
relativePath: path.join(agent.module, 'agents', agentPathInModule), // For command filename
|
|
||||||
agentPath: agentRelPath, // Relative path to actual agent file
|
|
||||||
content: launcherContent,
|
|
||||||
sourcePath: agent.path,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
artifacts,
|
|
||||||
counts: {
|
|
||||||
agents: agents.length,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate launcher content for an agent
|
|
||||||
* @param {Object} agent - Agent metadata
|
|
||||||
* @returns {string} Launcher file content
|
|
||||||
*/
|
|
||||||
async generateLauncherContent(agent) {
|
|
||||||
// Load the template
|
|
||||||
const template = await fs.readFile(this.templatePath, 'utf8');
|
|
||||||
|
|
||||||
// Replace template variables
|
|
||||||
// Use relativePath if available (for nested agents), otherwise just name with .md
|
|
||||||
const agentPathInModule = agent.relativePath || `${agent.name}.md`;
|
|
||||||
return template
|
|
||||||
.replaceAll('{{name}}', agent.name)
|
|
||||||
.replaceAll('{{module}}', agent.module)
|
|
||||||
.replaceAll('{{path}}', agentPathInModule)
|
|
||||||
.replaceAll('{{description}}', agent.description || `${agent.name} agent`)
|
|
||||||
.replaceAll('_bmad', this.bmadFolderName)
|
|
||||||
.replaceAll('_bmad', '_bmad');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write agent launcher artifacts to IDE commands directory
|
|
||||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
|
||||||
* @param {Array} artifacts - Agent launcher artifacts
|
|
||||||
* @returns {number} Count of launchers written
|
|
||||||
*/
|
|
||||||
async writeAgentLaunchers(baseCommandsDir, artifacts) {
|
|
||||||
let writtenCount = 0;
|
|
||||||
|
|
||||||
for (const artifact of artifacts) {
|
|
||||||
if (artifact.type === 'agent-launcher') {
|
|
||||||
const moduleAgentsDir = path.join(baseCommandsDir, artifact.module, 'agents');
|
|
||||||
await fs.ensureDir(moduleAgentsDir);
|
|
||||||
|
|
||||||
const launcherPath = path.join(moduleAgentsDir, `${artifact.name}.md`);
|
|
||||||
await fs.writeFile(launcherPath, artifact.content);
|
|
||||||
writtenCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return writtenCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write agent launcher artifacts using underscore format (Windows-compatible)
|
|
||||||
* Creates flat files like: bmad_bmm_pm.md
|
|
||||||
*
|
|
||||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
|
||||||
* @param {Array} artifacts - Agent launcher artifacts
|
|
||||||
* @returns {number} Count of launchers written
|
|
||||||
*/
|
|
||||||
async writeColonArtifacts(baseCommandsDir, artifacts) {
|
|
||||||
let writtenCount = 0;
|
|
||||||
|
|
||||||
for (const artifact of artifacts) {
|
|
||||||
if (artifact.type === 'agent-launcher') {
|
|
||||||
// Convert relativePath to underscore format: bmm/agents/pm.md → bmad_bmm_pm.md
|
|
||||||
const flatName = toColonPath(artifact.relativePath);
|
|
||||||
const launcherPath = path.join(baseCommandsDir, flatName);
|
|
||||||
await fs.ensureDir(path.dirname(launcherPath));
|
|
||||||
await fs.writeFile(launcherPath, artifact.content);
|
|
||||||
writtenCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return writtenCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write agent launcher artifacts using dash format (NEW STANDARD)
|
|
||||||
* Creates flat files like: bmad-agent-bmm-pm.md
|
|
||||||
*
|
|
||||||
* The bmad-agent- prefix distinguishes agents from workflows/tasks/tools.
|
|
||||||
*
|
|
||||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
|
||||||
* @param {Array} artifacts - Agent launcher artifacts
|
|
||||||
* @returns {number} Count of launchers written
|
|
||||||
*/
|
|
||||||
async writeDashArtifacts(baseCommandsDir, artifacts) {
|
|
||||||
let writtenCount = 0;
|
|
||||||
|
|
||||||
for (const artifact of artifacts) {
|
|
||||||
if (artifact.type === 'agent-launcher') {
|
|
||||||
// Convert relativePath to dash format: bmm/agents/pm.md → bmad-agent-bmm-pm.md
|
|
||||||
const flatName = toDashPath(artifact.relativePath);
|
|
||||||
const launcherPath = path.join(baseCommandsDir, flatName);
|
|
||||||
await fs.ensureDir(path.dirname(launcherPath));
|
|
||||||
await fs.writeFile(launcherPath, artifact.content);
|
|
||||||
writtenCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return writtenCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the custom agent name in underscore format (Windows-compatible)
|
|
||||||
* @param {string} agentName - Custom agent name
|
|
||||||
* @returns {string} Underscore-formatted filename
|
|
||||||
*/
|
|
||||||
getCustomAgentColonName(agentName) {
|
|
||||||
return customAgentColonName(agentName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the custom agent name in underscore format (Windows-compatible)
|
|
||||||
* @param {string} agentName - Custom agent name
|
|
||||||
* @returns {string} Underscore-formatted filename
|
|
||||||
*/
|
|
||||||
getCustomAgentDashName(agentName) {
|
|
||||||
return customAgentDashName(agentName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { AgentCommandGenerator };
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const { loadSkillManifest, getCanonicalId } = require('./skill-manifest');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helpers for gathering BMAD agents/tasks from the installed tree.
|
|
||||||
* Shared by installers that need Claude-style exports.
|
|
||||||
*
|
|
||||||
* TODO: Dead code cleanup — compiled XML agents are retired.
|
|
||||||
*
|
|
||||||
* All agents now use the SKILL.md directory format with bmad-skill-manifest.yaml
|
|
||||||
* (type: agent). The legacy pipeline below only discovers compiled .md files
|
|
||||||
* containing <agent> XML tags, which no longer exist. The following are dead:
|
|
||||||
*
|
|
||||||
* - getAgentsFromBmad() — scans {module}/agents/ for .md files with <agent> tags
|
|
||||||
* - getAgentsFromDir() — recursive helper for the above
|
|
||||||
* - AgentCommandGenerator — (agent-command-generator.js) generates launcher .md files
|
|
||||||
* that tell the LLM to load a compiled agent .md file
|
|
||||||
* - agent-command-template.md — (templates/) the launcher template with hardcoded
|
|
||||||
* {module}/agents/{{path}} reference
|
|
||||||
*
|
|
||||||
* Agent metadata for agent-manifest.csv is now handled entirely by
|
|
||||||
* ManifestGenerator.getAgentsFromDirRecursive() in manifest-generator.js,
|
|
||||||
* which walks the full module tree and finds type:agent directories.
|
|
||||||
*
|
|
||||||
* IDE installation of agents is handled by the native skill pipeline —
|
|
||||||
* each agent's SKILL.md directory is installed directly to the IDE's
|
|
||||||
* skills path, so no launcher intermediary is needed.
|
|
||||||
*
|
|
||||||
* Cleanup: remove getAgentsFromBmad, getAgentsFromDir, their exports,
|
|
||||||
* AgentCommandGenerator, agent-command-template.md, and all call sites
|
|
||||||
* in IDE installers that invoke collectAgentArtifacts / writeAgentLaunchers /
|
|
||||||
* writeColonArtifacts / writeDashArtifacts.
|
|
||||||
* getTasksFromBmad and getTasksFromDir may still be live — verify before removing.
|
|
||||||
*/
|
|
||||||
async function getAgentsFromBmad(bmadDir, selectedModules = []) {
|
|
||||||
const agents = [];
|
|
||||||
|
|
||||||
// Get core agents
|
|
||||||
if (await fs.pathExists(path.join(bmadDir, 'core', 'agents'))) {
|
|
||||||
const coreAgents = await getAgentsFromDir(path.join(bmadDir, 'core', 'agents'), 'core');
|
|
||||||
agents.push(...coreAgents);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get module agents
|
|
||||||
for (const moduleName of selectedModules) {
|
|
||||||
const agentsPath = path.join(bmadDir, moduleName, 'agents');
|
|
||||||
|
|
||||||
if (await fs.pathExists(agentsPath)) {
|
|
||||||
const moduleAgents = await getAgentsFromDir(agentsPath, moduleName);
|
|
||||||
agents.push(...moduleAgents);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get standalone agents from bmad/agents/ directory
|
|
||||||
const standaloneAgentsDir = path.join(bmadDir, 'agents');
|
|
||||||
if (await fs.pathExists(standaloneAgentsDir)) {
|
|
||||||
const agentDirs = await fs.readdir(standaloneAgentsDir, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const agentDir of agentDirs) {
|
|
||||||
if (!agentDir.isDirectory()) continue;
|
|
||||||
|
|
||||||
const agentDirPath = path.join(standaloneAgentsDir, agentDir.name);
|
|
||||||
const agentFiles = await fs.readdir(agentDirPath);
|
|
||||||
const skillManifest = await loadSkillManifest(agentDirPath);
|
|
||||||
|
|
||||||
for (const file of agentFiles) {
|
|
||||||
if (!file.endsWith('.md')) continue;
|
|
||||||
if (file.includes('.customize.')) continue;
|
|
||||||
|
|
||||||
const filePath = path.join(agentDirPath, file);
|
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
|
||||||
|
|
||||||
if (content.includes('localskip="true"')) continue;
|
|
||||||
|
|
||||||
agents.push({
|
|
||||||
path: filePath,
|
|
||||||
name: file.replace('.md', ''),
|
|
||||||
module: 'standalone', // Mark as standalone agent
|
|
||||||
canonicalId: getCanonicalId(skillManifest, file),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return agents;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getTasksFromBmad(bmadDir, selectedModules = []) {
|
|
||||||
const tasks = [];
|
|
||||||
|
|
||||||
if (await fs.pathExists(path.join(bmadDir, 'core', 'tasks'))) {
|
|
||||||
const coreTasks = await getTasksFromDir(path.join(bmadDir, 'core', 'tasks'), 'core');
|
|
||||||
tasks.push(...coreTasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const moduleName of selectedModules) {
|
|
||||||
const tasksPath = path.join(bmadDir, moduleName, 'tasks');
|
|
||||||
|
|
||||||
if (await fs.pathExists(tasksPath)) {
|
|
||||||
const moduleTasks = await getTasksFromDir(tasksPath, moduleName);
|
|
||||||
tasks.push(...moduleTasks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAgentsFromDir(dirPath, moduleName, relativePath = '') {
|
|
||||||
const agents = [];
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(dirPath))) {
|
|
||||||
return agents;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
||||||
const skillManifest = await loadSkillManifest(dirPath);
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
// Skip if entry.name is undefined or not a string
|
|
||||||
if (!entry.name || typeof entry.name !== 'string') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullPath = path.join(dirPath, entry.name);
|
|
||||||
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
// Recurse into subdirectories
|
|
||||||
const subDirAgents = await getAgentsFromDir(fullPath, moduleName, newRelativePath);
|
|
||||||
agents.push(...subDirAgents);
|
|
||||||
} else if (entry.name.endsWith('.md')) {
|
|
||||||
// Skip README files and other non-agent files
|
|
||||||
if (entry.name.toLowerCase() === 'readme.md' || entry.name.toLowerCase().startsWith('readme-')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.name.includes('.customize.')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await fs.readFile(fullPath, 'utf8');
|
|
||||||
|
|
||||||
if (content.includes('localskip="true"')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only include files that have agent-specific content (compiled agents have <agent> tag)
|
|
||||||
if (!content.includes('<agent')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
agents.push({
|
|
||||||
path: fullPath,
|
|
||||||
name: entry.name.replace('.md', ''),
|
|
||||||
module: moduleName,
|
|
||||||
relativePath: newRelativePath, // Keep the .md extension for the full path
|
|
||||||
canonicalId: getCanonicalId(skillManifest, entry.name),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return agents;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getTasksFromDir(dirPath, moduleName) {
|
|
||||||
const tasks = [];
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(dirPath))) {
|
|
||||||
return tasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = await fs.readdir(dirPath);
|
|
||||||
const skillManifest = await loadSkillManifest(dirPath);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
// Include both .md and .xml task files
|
|
||||||
if (!file.endsWith('.md') && !file.endsWith('.xml')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = path.join(dirPath, file);
|
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
|
||||||
|
|
||||||
// Skip internal/engine files (not user-facing tasks)
|
|
||||||
if (content.includes('internal="true"')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove extension to get task name
|
|
||||||
const ext = file.endsWith('.xml') ? '.xml' : '.md';
|
|
||||||
tasks.push({
|
|
||||||
path: filePath,
|
|
||||||
name: file.replace(ext, ''),
|
|
||||||
module: moduleName,
|
|
||||||
canonicalId: getCanonicalId(skillManifest, file),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return tasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getAgentsFromBmad,
|
|
||||||
getTasksFromBmad,
|
|
||||||
getAgentsFromDir,
|
|
||||||
getTasksFromDir,
|
|
||||||
};
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const yaml = require('yaml');
|
|
||||||
const { glob } = require('glob');
|
|
||||||
const { getSourcePath } = require('../../project-root');
|
|
||||||
|
|
||||||
async function loadModuleInjectionConfig(handler, moduleName) {
|
|
||||||
const sourceModulesPath = getSourcePath('modules');
|
|
||||||
const handlerBaseDir = path.join(sourceModulesPath, moduleName, 'sub-modules', handler);
|
|
||||||
const configPath = path.join(handlerBaseDir, 'injections.yaml');
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(configPath))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configContent = await fs.readFile(configPath, 'utf8');
|
|
||||||
const config = yaml.parse(configContent) || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
config,
|
|
||||||
handlerBaseDir,
|
|
||||||
configPath,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldApplyInjection(injection, subagentChoices) {
|
|
||||||
if (!subagentChoices || subagentChoices.install === 'none') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subagentChoices.install === 'all') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subagentChoices.install === 'selective') {
|
|
||||||
const selected = subagentChoices.selected || [];
|
|
||||||
|
|
||||||
if (injection.requires === 'any' && selected.length > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (injection.requires) {
|
|
||||||
const required = `${injection.requires}.md`;
|
|
||||||
return selected.includes(required);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (injection.point) {
|
|
||||||
const selectedNames = selected.map((file) => file.replace('.md', ''));
|
|
||||||
return selectedNames.some((name) => injection.point.includes(name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterAgentInstructions(content, selectedFiles) {
|
|
||||||
if (!selectedFiles || selectedFiles.length === 0) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedAgents = selectedFiles.map((file) => file.replace('.md', ''));
|
|
||||||
const lines = content.split('\n');
|
|
||||||
const filteredLines = [];
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.includes('<llm') || line.includes('</llm>')) {
|
|
||||||
filteredLines.push(line);
|
|
||||||
} else if (line.includes('subagent')) {
|
|
||||||
let shouldInclude = false;
|
|
||||||
for (const agent of selectedAgents) {
|
|
||||||
if (line.includes(agent)) {
|
|
||||||
shouldInclude = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldInclude) {
|
|
||||||
filteredLines.push(line);
|
|
||||||
}
|
|
||||||
} else if (line.includes('When creating PRDs') || line.includes('ACTIVELY delegate')) {
|
|
||||||
filteredLines.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filteredLines.length > 2) {
|
|
||||||
return filteredLines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveSubagentFiles(handlerBaseDir, subagentConfig, subagentChoices) {
|
|
||||||
if (!subagentConfig || !subagentConfig.files) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subagentChoices || subagentChoices.install === 'none') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
let filesToCopy = subagentConfig.files;
|
|
||||||
|
|
||||||
if (subagentChoices.install === 'selective') {
|
|
||||||
filesToCopy = subagentChoices.selected || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceDir = path.join(handlerBaseDir, subagentConfig.source || '');
|
|
||||||
const resolved = [];
|
|
||||||
|
|
||||||
for (const file of filesToCopy) {
|
|
||||||
// Use forward slashes for glob pattern (works on both Windows and Unix)
|
|
||||||
// Convert backslashes to forward slashes for glob compatibility
|
|
||||||
const normalizedSourceDir = sourceDir.replaceAll('\\', '/');
|
|
||||||
const pattern = `${normalizedSourceDir}/**/${file}`;
|
|
||||||
const matches = await glob(pattern);
|
|
||||||
|
|
||||||
if (matches.length > 0) {
|
|
||||||
const absolutePath = matches[0];
|
|
||||||
resolved.push({
|
|
||||||
file,
|
|
||||||
absolutePath,
|
|
||||||
relativePath: path.relative(sourceDir, absolutePath),
|
|
||||||
sourceDir,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
loadModuleInjectionConfig,
|
|
||||||
shouldApplyInjection,
|
|
||||||
filterAgentInstructions,
|
|
||||||
resolveSubagentFiles,
|
|
||||||
};
|
|
||||||
|
|
@ -15,8 +15,6 @@
|
||||||
* - standalone/agents/fred.md → bmad-agent-standalone-fred.md
|
* - standalone/agents/fred.md → bmad-agent-standalone-fred.md
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Type segments - agents are included in naming, others are filtered out
|
|
||||||
const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools'];
|
|
||||||
const AGENT_SEGMENT = 'agents';
|
const AGENT_SEGMENT = 'agents';
|
||||||
|
|
||||||
// BMAD installation folder name - centralized constant for all installers
|
// BMAD installation folder name - centralized constant for all installers
|
||||||
|
|
@ -194,125 +192,6 @@ function parseDashName(filename) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// LEGACY FUNCTIONS (underscore format) - kept for backward compatibility
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert hierarchical path to flat underscore-separated name (LEGACY)
|
|
||||||
* @deprecated Use toDashName instead
|
|
||||||
*/
|
|
||||||
function toUnderscoreName(module, type, name) {
|
|
||||||
const isAgent = type === AGENT_SEGMENT;
|
|
||||||
if (module === 'core') {
|
|
||||||
return isAgent ? `bmad_agent_${name}.md` : `bmad_${name}.md`;
|
|
||||||
}
|
|
||||||
if (module === 'standalone') {
|
|
||||||
return isAgent ? `bmad_agent_standalone_${name}.md` : `bmad_standalone_${name}.md`;
|
|
||||||
}
|
|
||||||
return isAgent ? `bmad_${module}_agent_${name}.md` : `bmad_${module}_${name}.md`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert relative path to flat underscore-separated name (LEGACY)
|
|
||||||
* @deprecated Use toDashPath instead
|
|
||||||
*/
|
|
||||||
function toUnderscorePath(relativePath) {
|
|
||||||
// Strip common file extensions (same as toDashPath for consistency)
|
|
||||||
const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, '');
|
|
||||||
const parts = withoutExt.split(/[/\\]/);
|
|
||||||
|
|
||||||
const module = parts[0];
|
|
||||||
const type = parts[1];
|
|
||||||
const name = parts.slice(2).join('_');
|
|
||||||
|
|
||||||
return toUnderscoreName(module, type, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create custom agent underscore name (LEGACY)
|
|
||||||
* @deprecated Use customAgentDashName instead
|
|
||||||
*/
|
|
||||||
function customAgentUnderscoreName(agentName) {
|
|
||||||
return `bmad_custom_${agentName}.md`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a filename uses underscore format (LEGACY)
|
|
||||||
* @deprecated Use isDashFormat instead
|
|
||||||
*/
|
|
||||||
function isUnderscoreFormat(filename) {
|
|
||||||
return filename.startsWith('bmad_') && filename.includes('_');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract parts from an underscore-formatted filename (LEGACY)
|
|
||||||
* @deprecated Use parseDashName instead
|
|
||||||
*/
|
|
||||||
function parseUnderscoreName(filename) {
|
|
||||||
const withoutExt = filename.replace('.md', '');
|
|
||||||
const parts = withoutExt.split('_');
|
|
||||||
|
|
||||||
if (parts.length < 2 || parts[0] !== 'bmad') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentIndex = parts.indexOf('agent');
|
|
||||||
|
|
||||||
if (agentIndex !== -1) {
|
|
||||||
if (agentIndex === 1) {
|
|
||||||
// bmad_agent_... - check for standalone
|
|
||||||
if (parts.length >= 4 && parts[2] === 'standalone') {
|
|
||||||
return {
|
|
||||||
prefix: parts[0],
|
|
||||||
module: 'standalone',
|
|
||||||
type: 'agents',
|
|
||||||
name: parts.slice(3).join('_'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
prefix: parts[0],
|
|
||||||
module: 'core',
|
|
||||||
type: 'agents',
|
|
||||||
name: parts.slice(agentIndex + 1).join('_'),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
prefix: parts[0],
|
|
||||||
module: parts[1],
|
|
||||||
type: 'agents',
|
|
||||||
name: parts.slice(agentIndex + 1).join('_'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts.length === 2) {
|
|
||||||
return {
|
|
||||||
prefix: parts[0],
|
|
||||||
module: 'core',
|
|
||||||
type: 'workflows',
|
|
||||||
name: parts[1],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for standalone non-agent: bmad_standalone_name
|
|
||||||
if (parts[1] === 'standalone') {
|
|
||||||
return {
|
|
||||||
prefix: parts[0],
|
|
||||||
module: 'standalone',
|
|
||||||
type: 'workflows',
|
|
||||||
name: parts.slice(2).join('_'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
prefix: parts[0],
|
|
||||||
module: parts[1],
|
|
||||||
type: 'workflows',
|
|
||||||
name: parts.slice(2).join('_'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the skill name for an artifact.
|
* Resolve the skill name for an artifact.
|
||||||
* Prefers canonicalId from a bmad-skill-manifest.yaml sidecar when available,
|
* Prefers canonicalId from a bmad-skill-manifest.yaml sidecar when available,
|
||||||
|
|
@ -328,37 +207,13 @@ function resolveSkillName(artifact) {
|
||||||
return toDashPath(artifact.relativePath);
|
return toDashPath(artifact.relativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward compatibility aliases (colon format was same as underscore)
|
|
||||||
const toColonName = toUnderscoreName;
|
|
||||||
const toColonPath = toUnderscorePath;
|
|
||||||
const customAgentColonName = customAgentUnderscoreName;
|
|
||||||
const isColonFormat = isUnderscoreFormat;
|
|
||||||
const parseColonName = parseUnderscoreName;
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// New standard (dash-based)
|
|
||||||
toDashName,
|
toDashName,
|
||||||
toDashPath,
|
toDashPath,
|
||||||
resolveSkillName,
|
resolveSkillName,
|
||||||
customAgentDashName,
|
customAgentDashName,
|
||||||
isDashFormat,
|
isDashFormat,
|
||||||
parseDashName,
|
parseDashName,
|
||||||
|
|
||||||
// Legacy (underscore-based) - kept for backward compatibility
|
|
||||||
toUnderscoreName,
|
|
||||||
toUnderscorePath,
|
|
||||||
customAgentUnderscoreName,
|
|
||||||
isUnderscoreFormat,
|
|
||||||
parseUnderscoreName,
|
|
||||||
|
|
||||||
// Backward compatibility aliases
|
|
||||||
toColonName,
|
|
||||||
toColonPath,
|
|
||||||
customAgentColonName,
|
|
||||||
isColonFormat,
|
|
||||||
parseColonName,
|
|
||||||
|
|
||||||
TYPE_SEGMENTS,
|
|
||||||
AGENT_SEGMENT,
|
AGENT_SEGMENT,
|
||||||
BMAD_FOLDER_NAME,
|
BMAD_FOLDER_NAME,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
---
|
|
||||||
name: '{{name}}'
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
|
||||||
|
|
||||||
<agent-activation CRITICAL="TRUE">
|
|
||||||
1. LOAD the FULL agent file from {project-root}/_bmad/{{module}}/agents/{{path}}
|
|
||||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
|
||||||
3. Execute ALL activation steps exactly as written in the agent file
|
|
||||||
4. Follow the agent's persona and menu system precisely
|
|
||||||
5. Stay in character throughout the session
|
|
||||||
</agent-activation>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
---
|
|
||||||
name: '{{name}}'
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}}
|
|
||||||
|
|
||||||
Follow all instructions in the workflow file exactly as written.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
default-agent.md
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
default-workflow.md
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
---
|
|
||||||
name: '{{name}}'
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
|
||||||
|
|
||||||
<agent-activation CRITICAL="TRUE">
|
|
||||||
1. LOAD the FULL agent file from {project-root}/_bmad/{{path}}
|
|
||||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
|
||||||
3. FOLLOW every step in the <activation> section precisely
|
|
||||||
4. DISPLAY the welcome/greeting as instructed
|
|
||||||
5. PRESENT the numbered menu
|
|
||||||
6. WAIT for user input before proceeding
|
|
||||||
</agent-activation>
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
---
|
|
||||||
name: '{{name}}'
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
Read the entire task file at: {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
|
|
||||||
Follow all instructions in the task file exactly as written.
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
---
|
|
||||||
name: '{{name}}'
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
Read the entire tool file at: {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
|
|
||||||
Follow all instructions in the tool file exactly as written.
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
---
|
|
||||||
name: '{{name}}'
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL {project-root}/{{bmadFolderName}}/{{path}}, READ its entire contents and follow its directions exactly!
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
description = "Activates the {{name}} agent from the BMad Method."
|
|
||||||
prompt = """
|
|
||||||
CRITICAL: You are now the BMad '{{name}}' agent.
|
|
||||||
|
|
||||||
PRE-FLIGHT CHECKLIST:
|
|
||||||
1. [ ] IMMEDIATE ACTION: Load and parse {project-root}/{{bmadFolderName}}/{{module}}/config.yaml - store ALL config values in memory for use throughout the session.
|
|
||||||
2. [ ] IMMEDIATE ACTION: Read and internalize the full agent definition at {project-root}/{{bmadFolderName}}/{{path}}.
|
|
||||||
3. [ ] CONFIRM: The user's name from config is {user_name}.
|
|
||||||
|
|
||||||
Only after all checks are complete, greet the user by name and display the menu.
|
|
||||||
Acknowledge this checklist is complete in your first response.
|
|
||||||
|
|
||||||
AGENT DEFINITION: {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
"""
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
description = "Executes the {{name}} task from the BMAD Method."
|
|
||||||
prompt = """
|
|
||||||
Execute the BMAD '{{name}}' task.
|
|
||||||
|
|
||||||
TASK INSTRUCTIONS:
|
|
||||||
1. LOAD the task file from {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
2. READ its entire contents
|
|
||||||
3. FOLLOW every instruction precisely as specified
|
|
||||||
|
|
||||||
TASK FILE: {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
"""
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
description = "Executes the {{name}} tool from the BMAD Method."
|
|
||||||
prompt = """
|
|
||||||
Execute the BMAD '{{name}}' tool.
|
|
||||||
|
|
||||||
TOOL INSTRUCTIONS:
|
|
||||||
1. LOAD the tool file from {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
2. READ its entire contents
|
|
||||||
3. FOLLOW every instruction precisely as specified
|
|
||||||
|
|
||||||
TOOL FILE: {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
"""
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
description = '{{description}}'
|
|
||||||
prompt = """
|
|
||||||
Execute the BMAD '{{name}}' workflow.
|
|
||||||
|
|
||||||
CRITICAL: This is a structured YAML workflow. Follow these steps precisely:
|
|
||||||
|
|
||||||
1. LOAD the workflow definition from {project-root}/{{bmadFolderName}}/{{workflow_path}}
|
|
||||||
2. PARSE the YAML structure to understand:
|
|
||||||
- Workflow phases and steps
|
|
||||||
- Required inputs and outputs
|
|
||||||
- Dependencies between steps
|
|
||||||
3. EXECUTE each step in order
|
|
||||||
4. VALIDATE outputs before proceeding to next step
|
|
||||||
|
|
||||||
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{workflow_path}}
|
|
||||||
"""
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
description = '{{description}}'
|
|
||||||
prompt = """
|
|
||||||
Execute the BMAD '{{name}}' workflow.
|
|
||||||
|
|
||||||
CRITICAL: You must load and follow the workflow definition exactly.
|
|
||||||
|
|
||||||
WORKFLOW INSTRUCTIONS:
|
|
||||||
1. LOAD the workflow file from {project-root}/{{bmadFolderName}}/{{workflow_path}}
|
|
||||||
2. READ its entire contents
|
|
||||||
3. FOLLOW every step precisely as specified
|
|
||||||
4. DO NOT skip or modify any steps
|
|
||||||
|
|
||||||
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{workflow_path}}
|
|
||||||
"""
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
---
|
|
||||||
inclusion: manual
|
|
||||||
---
|
|
||||||
|
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
|
||||||
|
|
||||||
<agent-activation CRITICAL="TRUE">
|
|
||||||
1. LOAD the FULL agent file from #[[file:{{bmadFolderName}}/{{path}}]]
|
|
||||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
|
||||||
3. FOLLOW every step in the <activation> section precisely
|
|
||||||
4. DISPLAY the welcome/greeting as instructed
|
|
||||||
5. PRESENT the numbered menu
|
|
||||||
6. WAIT for user input before proceeding
|
|
||||||
</agent-activation>
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
inclusion: manual
|
|
||||||
---
|
|
||||||
|
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
Read the entire task file at: #[[file:{{bmadFolderName}}/{{path}}]]
|
|
||||||
|
|
||||||
Follow all instructions in the task file exactly as written.
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
inclusion: manual
|
|
||||||
---
|
|
||||||
|
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
Read the entire tool file at: #[[file:{{bmadFolderName}}/{{path}}]]
|
|
||||||
|
|
||||||
Follow all instructions in the tool file exactly as written.
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
---
|
|
||||||
inclusion: manual
|
|
||||||
---
|
|
||||||
|
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL #[[file:{{bmadFolderName}}/{{path}}]], READ its entire contents and follow its directions exactly!
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
---
|
|
||||||
mode: all
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
|
||||||
|
|
||||||
<agent-activation CRITICAL="TRUE">
|
|
||||||
1. LOAD the FULL agent file from {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
|
||||||
3. FOLLOW every step in the <activation> section precisely
|
|
||||||
4. DISPLAY the welcome/greeting as instructed
|
|
||||||
5. PRESENT the numbered menu
|
|
||||||
6. WAIT for user input before proceeding
|
|
||||||
</agent-activation>
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
---
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
Execute the BMAD '{{name}}' task.
|
|
||||||
|
|
||||||
TASK INSTRUCTIONS:
|
|
||||||
|
|
||||||
1. LOAD the task file from {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
2. READ its entire contents
|
|
||||||
3. FOLLOW every instruction precisely as specified
|
|
||||||
|
|
||||||
TASK FILE: {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
---
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
Execute the BMAD '{{name}}' tool.
|
|
||||||
|
|
||||||
TOOL INSTRUCTIONS:
|
|
||||||
|
|
||||||
1. LOAD the tool file from {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
2. READ its entire contents
|
|
||||||
3. FOLLOW every instruction precisely as specified
|
|
||||||
|
|
||||||
TOOL FILE: {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
---
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
Execute the BMAD '{{name}}' workflow.
|
|
||||||
|
|
||||||
CRITICAL: You must load and follow the workflow definition exactly.
|
|
||||||
|
|
||||||
WORKFLOW INSTRUCTIONS:
|
|
||||||
|
|
||||||
1. LOAD the workflow file from {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
2. READ its entire contents
|
|
||||||
3. FOLLOW every step precisely as specified
|
|
||||||
4. DO NOT skip or modify any steps
|
|
||||||
|
|
||||||
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
---
|
|
||||||
description: '{{description}}'
|
|
||||||
---
|
|
||||||
|
|
||||||
Execute the BMAD '{{name}}' workflow.
|
|
||||||
|
|
||||||
CRITICAL: You must load and follow the workflow definition exactly.
|
|
||||||
|
|
||||||
WORKFLOW INSTRUCTIONS:
|
|
||||||
|
|
||||||
1. LOAD the workflow file from {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
2. READ its entire contents
|
|
||||||
3. FOLLOW every step precisely as specified
|
|
||||||
4. DO NOT skip or modify any steps
|
|
||||||
|
|
||||||
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{path}}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
{{description}}
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}}
|
|
||||||
|
|
||||||
Follow all instructions in the workflow file exactly as written.
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
{{description}}
|
|
||||||
|
|
||||||
## Instructions
|
|
||||||
|
|
||||||
Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}}
|
|
||||||
|
|
||||||
Follow all instructions in the workflow file exactly as written.
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
---
|
|
||||||
description: '{{description}}'
|
|
||||||
auto_execution_mode: "iterate"
|
|
||||||
---
|
|
||||||
|
|
||||||
# {{name}}
|
|
||||||
|
|
||||||
Read the entire workflow file at {project-root}/_bmad/{{workflow_path}}
|
|
||||||
|
|
||||||
Follow all instructions in the workflow file exactly as written.
|
|
||||||
|
|
@ -155,33 +155,6 @@ class CustomModuleManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use parseSource() instead. Kept for backward compatibility.
|
|
||||||
* Parse and validate a GitHub repository URL.
|
|
||||||
* @param {string} url - GitHub URL to validate
|
|
||||||
* @returns {Object} { owner, repo, isValid, error }
|
|
||||||
*/
|
|
||||||
validateGitHubUrl(url) {
|
|
||||||
if (!url || typeof url !== 'string') {
|
|
||||||
return { owner: null, repo: null, isValid: false, error: 'URL is required' };
|
|
||||||
}
|
|
||||||
const trimmed = url.trim();
|
|
||||||
|
|
||||||
// HTTPS format: https://github.com/owner/repo[.git] (strict, no trailing path)
|
|
||||||
const httpsMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
||||||
if (httpsMatch) {
|
|
||||||
return { owner: httpsMatch[1], repo: httpsMatch[2], isValid: true, error: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
// SSH format: git@github.com:owner/repo[.git]
|
|
||||||
const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
||||||
if (sshMatch) {
|
|
||||||
return { owner: sshMatch[1], repo: sshMatch[2], isValid: true, error: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { owner: null, repo: null, isValid: false, error: 'Not a valid GitHub URL (expected https://github.com/owner/repo)' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Marketplace JSON ─────────────────────────────────────────────────────
|
// ─── Marketplace JSON ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -109,46 +109,6 @@ class ExternalModuleManager {
|
||||||
return modules.find((m) => m.code === code) || null;
|
return modules.find((m) => m.code === code) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get module info by key
|
|
||||||
* @param {string} key - The module key (e.g., 'bmad-creative-intelligence-suite')
|
|
||||||
* @returns {Object|null} Module info or null if not found
|
|
||||||
*/
|
|
||||||
async getModuleByKey(key) {
|
|
||||||
const modules = await this.listAvailable();
|
|
||||||
return modules.find((m) => m.key === key) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a module code exists in external modules
|
|
||||||
* @param {string} code - The module code to check
|
|
||||||
* @returns {boolean} True if the module exists
|
|
||||||
*/
|
|
||||||
async hasModule(code) {
|
|
||||||
const module = await this.getModuleByCode(code);
|
|
||||||
return module !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the URL for a module by code
|
|
||||||
* @param {string} code - The module code
|
|
||||||
* @returns {string|null} The URL or null if not found
|
|
||||||
*/
|
|
||||||
async getModuleUrl(code) {
|
|
||||||
const module = await this.getModuleByCode(code);
|
|
||||||
return module ? module.url : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the module definition path for a module by code
|
|
||||||
* @param {string} code - The module code
|
|
||||||
* @returns {string|null} The module definition path or null if not found
|
|
||||||
*/
|
|
||||||
async getModuleDefinition(code) {
|
|
||||||
const module = await this.getModuleByCode(code);
|
|
||||||
return module ? module.moduleDefinition : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the cache directory for external modules
|
* Get the cache directory for external modules
|
||||||
* @returns {string} Path to the external modules cache directory
|
* @returns {string} Path to the external modules cache directory
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ class OfficialModules {
|
||||||
// Config collection state (merged from ConfigCollector)
|
// Config collection state (merged from ConfigCollector)
|
||||||
this.collectedConfig = {};
|
this.collectedConfig = {};
|
||||||
this._existingConfig = null;
|
this._existingConfig = null;
|
||||||
|
// Tracked during interactive config collection so {directory_name}
|
||||||
|
// placeholder defaults can be resolved in buildQuestion().
|
||||||
this.currentProjectDir = null;
|
this.currentProjectDir = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -500,32 +502,6 @@ class OfficialModules {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all .md agent files recursively in a directory
|
|
||||||
* @param {string} dir - Directory to search
|
|
||||||
* @returns {Array} List of .md agent file paths
|
|
||||||
*/
|
|
||||||
async findAgentMdFiles(dir) {
|
|
||||||
const agentFiles = [];
|
|
||||||
|
|
||||||
async function searchDirectory(searchDir) {
|
|
||||||
const entries = await fs.readdir(searchDir, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = path.join(searchDir, entry.name);
|
|
||||||
|
|
||||||
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
||||||
agentFiles.push(fullPath);
|
|
||||||
} else if (entry.isDirectory()) {
|
|
||||||
await searchDirectory(fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await searchDirectory(dir);
|
|
||||||
return agentFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create directories declared in module.yaml's `directories` key
|
* Create directories declared in module.yaml's `directories` key
|
||||||
* This replaces the security-risky module installer pattern with declarative config
|
* This replaces the security-risky module installer pattern with declarative config
|
||||||
|
|
@ -699,29 +675,6 @@ class OfficialModules {
|
||||||
return { createdDirs, movedDirs, createdWdsFolders };
|
return { createdDirs, movedDirs, createdWdsFolders };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Private: Process module configuration
|
|
||||||
* @param {string} modulePath - Path to installed module
|
|
||||||
* @param {string} moduleName - Module name
|
|
||||||
*/
|
|
||||||
async processModuleConfig(modulePath, moduleName) {
|
|
||||||
const configPath = path.join(modulePath, 'config.yaml');
|
|
||||||
|
|
||||||
if (await fs.pathExists(configPath)) {
|
|
||||||
try {
|
|
||||||
let configContent = await fs.readFile(configPath, 'utf8');
|
|
||||||
|
|
||||||
// Replace path placeholders
|
|
||||||
configContent = configContent.replaceAll('{project-root}', `bmad/${moduleName}`);
|
|
||||||
configContent = configContent.replaceAll('{module}', moduleName);
|
|
||||||
|
|
||||||
await fs.writeFile(configPath, configContent, 'utf8');
|
|
||||||
} catch (error) {
|
|
||||||
await prompts.log.warn(`Failed to process module config: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private: Sync module files (preserving user modifications)
|
* Private: Sync module files (preserving user modifications)
|
||||||
* @param {string} sourcePath - Source module path
|
* @param {string} sourcePath - Source module path
|
||||||
|
|
@ -1091,7 +1044,6 @@ class OfficialModules {
|
||||||
*/
|
*/
|
||||||
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
||||||
this.currentProjectDir = projectDir;
|
this.currentProjectDir = projectDir;
|
||||||
|
|
||||||
// Load existing config if not already loaded
|
// Load existing config if not already loaded
|
||||||
if (!this._existingConfig) {
|
if (!this._existingConfig) {
|
||||||
await this.loadExistingConfig(projectDir);
|
await this.loadExistingConfig(projectDir);
|
||||||
|
|
|
||||||
|
|
@ -50,17 +50,6 @@ class RegistryClient {
|
||||||
const content = await this.fetch(url, timeout);
|
const content = await this.fetch(url, timeout);
|
||||||
return yaml.parse(content);
|
return yaml.parse(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch a URL and parse the response as JSON.
|
|
||||||
* @param {string} url - URL to fetch
|
|
||||||
* @param {number} [timeout] - Timeout in ms
|
|
||||||
* @returns {Promise<Object>} Parsed JSON content
|
|
||||||
*/
|
|
||||||
async fetchJson(url, timeout) {
|
|
||||||
const content = await this.fetch(url, timeout);
|
|
||||||
return JSON.parse(content);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { RegistryClient };
|
module.exports = { RegistryClient };
|
||||||
|
|
|
||||||
|
|
@ -498,26 +498,6 @@ async function password(options) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Group multiple prompts together
|
|
||||||
* @param {Object} prompts - Object of prompt functions
|
|
||||||
* @param {Object} [options] - Group options
|
|
||||||
* @returns {Promise<Object>} Object with all answers
|
|
||||||
*/
|
|
||||||
async function group(prompts, options = {}) {
|
|
||||||
const clack = await getClack();
|
|
||||||
|
|
||||||
const result = await clack.group(prompts, {
|
|
||||||
onCancel: () => {
|
|
||||||
clack.cancel('Operation cancelled');
|
|
||||||
process.exit(0);
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run tasks with spinner feedback
|
* Run tasks with spinner feedback
|
||||||
* @param {Array} tasks - Array of task objects [{title, task, enabled?}]
|
* @param {Array} tasks - Array of task objects [{title, task, enabled?}]
|
||||||
|
|
@ -578,42 +558,6 @@ async function box(content, title, options) {
|
||||||
clack.box(content, title, options);
|
clack.box(content, title, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a progress bar for visualizing task completion
|
|
||||||
* @param {Object} [options] - Progress options (max, style, etc.)
|
|
||||||
* @returns {Promise<Object>} Progress controller with start, advance, stop methods
|
|
||||||
*/
|
|
||||||
async function progress(options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.progress(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a task log for displaying scrolling subprocess output
|
|
||||||
* @param {Object} options - TaskLog options (title, limit, retainLog)
|
|
||||||
* @returns {Promise<Object>} TaskLog controller with message, success, error methods
|
|
||||||
*/
|
|
||||||
async function taskLog(options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.taskLog(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* File system path prompt with autocomplete
|
|
||||||
* @param {Object} options - Path options
|
|
||||||
* @param {string} options.message - The prompt message
|
|
||||||
* @param {string} [options.initialValue] - Initial path value
|
|
||||||
* @param {boolean} [options.directory=false] - Only allow directories
|
|
||||||
* @param {Function} [options.validate] - Validation function
|
|
||||||
* @returns {Promise<string>} Selected path
|
|
||||||
*/
|
|
||||||
async function pathPrompt(options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
const result = await clack.path(options);
|
|
||||||
await handleCancel(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Autocomplete single-select prompt with type-ahead filtering
|
* Autocomplete single-select prompt with type-ahead filtering
|
||||||
* @param {Object} options - Autocomplete options
|
* @param {Object} options - Autocomplete options
|
||||||
|
|
@ -631,50 +575,6 @@ async function autocomplete(options) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Key-based instant selection prompt
|
|
||||||
* @param {Object} options - SelectKey options
|
|
||||||
* @param {string} options.message - The prompt message
|
|
||||||
* @param {Array} options.options - Array of choices [{value, label, hint?}]
|
|
||||||
* @returns {Promise<any>} Selected value
|
|
||||||
*/
|
|
||||||
async function selectKey(options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
const result = await clack.selectKey(options);
|
|
||||||
await handleCancel(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stream messages with dynamic content (for LLMs, generators, etc.)
|
|
||||||
*/
|
|
||||||
const stream = {
|
|
||||||
async info(generator) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.stream.info(generator);
|
|
||||||
},
|
|
||||||
async success(generator) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.stream.success(generator);
|
|
||||||
},
|
|
||||||
async step(generator) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.stream.step(generator);
|
|
||||||
},
|
|
||||||
async warn(generator) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.stream.warn(generator);
|
|
||||||
},
|
|
||||||
async error(generator) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.stream.error(generator);
|
|
||||||
},
|
|
||||||
async message(generator, options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.stream.message(generator, options);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the color utility (picocolors instance from @clack/prompts)
|
* Get the color utility (picocolors instance from @clack/prompts)
|
||||||
* @returns {Promise<Object>} The color utility (picocolors)
|
* @returns {Promise<Object>} The color utility (picocolors)
|
||||||
|
|
@ -790,20 +690,14 @@ module.exports = {
|
||||||
note,
|
note,
|
||||||
box,
|
box,
|
||||||
spinner,
|
spinner,
|
||||||
progress,
|
|
||||||
taskLog,
|
|
||||||
select,
|
select,
|
||||||
multiselect,
|
multiselect,
|
||||||
autocompleteMultiselect,
|
autocompleteMultiselect,
|
||||||
autocomplete,
|
autocomplete,
|
||||||
selectKey,
|
|
||||||
confirm,
|
confirm,
|
||||||
text,
|
text,
|
||||||
path: pathPrompt,
|
|
||||||
password,
|
password,
|
||||||
group,
|
|
||||||
tasks,
|
tasks,
|
||||||
log,
|
log,
|
||||||
stream,
|
|
||||||
prompt,
|
prompt,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue