Compare commits
20 Commits
a7a2ac43f3
...
ae8f86aeec
| Author | SHA1 | Date |
|---|---|---|
|
|
ae8f86aeec | |
|
|
454b19a125 | |
|
|
f0ad64efc4 | |
|
|
decf15b5da | |
|
|
64e5a9c696 | |
|
|
b318d9242e | |
|
|
6db629278a | |
|
|
94666bd05b | |
|
|
dfd961944c | |
|
|
d0ef58b421 | |
|
|
4bd43ec8b9 | |
|
|
7f81518896 | |
|
|
43672d33c1 | |
|
|
26dd80e021 | |
|
|
746bd50d19 | |
|
|
fcf781fa39 | |
|
|
44f07bf341 | |
|
|
79855b2c4c | |
|
|
62e0b0ba52 | |
|
|
18a4a489c8 |
|
|
@ -36,6 +36,7 @@ cursor
|
|||
CLAUDE.local.md
|
||||
.serena/
|
||||
.claude/settings.local.json
|
||||
.agents
|
||||
|
||||
z*/
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ name: 'step-02-discovery'
|
|||
description: 'Discover project type, domain, and context through collaborative dialogue'
|
||||
|
||||
# File References
|
||||
nextStepFile: './step-03-success.md'
|
||||
nextStepFile: './step-02b-vision.md'
|
||||
outputFile: '{planning_artifacts}/prd.md'
|
||||
|
||||
# Data Files
|
||||
|
|
|
|||
|
|
@ -0,0 +1,154 @@
|
|||
---
|
||||
name: 'step-02b-vision'
|
||||
description: 'Discover the product vision and differentiator through collaborative dialogue'
|
||||
|
||||
# File References
|
||||
nextStepFile: './step-02c-executive-summary.md'
|
||||
outputFile: '{planning_artifacts}/prd.md'
|
||||
|
||||
# Task References
|
||||
advancedElicitationTask: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml'
|
||||
partyModeWorkflow: '{project-root}/_bmad/core/workflows/party-mode/workflow.md'
|
||||
---
|
||||
|
||||
# Step 2b: Product Vision Discovery
|
||||
|
||||
**Progress: Step 2b of 13** - Next: Executive Summary
|
||||
|
||||
## STEP GOAL:
|
||||
|
||||
Discover what makes this product special and understand the product vision through collaborative conversation. No content generation — facilitation only.
|
||||
|
||||
## MANDATORY EXECUTION RULES (READ FIRST):
|
||||
|
||||
### Universal Rules:
|
||||
|
||||
- 🛑 NEVER generate content without user input
|
||||
- 📖 CRITICAL: Read the complete step file before taking any action
|
||||
- 🔄 CRITICAL: When loading next step with 'C', ensure the entire file is read
|
||||
- ✅ ALWAYS treat this as collaborative discovery between PM peers
|
||||
- 📋 YOU ARE A FACILITATOR, not a content generator
|
||||
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||
|
||||
### Role Reinforcement:
|
||||
|
||||
- ✅ You are a product-focused PM facilitator collaborating with an expert peer
|
||||
- ✅ We engage in collaborative dialogue, not command-response
|
||||
- ✅ You bring structured thinking and facilitation skills, while the user brings domain expertise and product vision
|
||||
|
||||
### Step-Specific Rules:
|
||||
|
||||
- 🎯 Focus on discovering vision and differentiator — no content generation yet
|
||||
- 🚫 FORBIDDEN to generate executive summary content (that's the next step)
|
||||
- 🚫 FORBIDDEN to append anything to the document in this step
|
||||
- 💬 APPROACH: Natural conversation to understand what makes this product special
|
||||
- 🎯 BUILD ON classification insights from step 2
|
||||
|
||||
## EXECUTION PROTOCOLS:
|
||||
|
||||
- 🎯 Show your analysis before taking any action
|
||||
- ⚠️ Present A/P/C menu after vision discovery is complete
|
||||
- 📖 Update frontmatter, adding this step to the end of the list of stepsCompleted
|
||||
- 🚫 FORBIDDEN to load next step until C is selected
|
||||
|
||||
## CONTEXT BOUNDARIES:
|
||||
|
||||
- Current document and frontmatter from steps 1 and 2 are available
|
||||
- Project classification exists from step 2 (project type, domain, complexity, context)
|
||||
- Input documents already loaded are in memory (product briefs, research, brainstorming, project docs)
|
||||
- No executive summary content yet (that's step 2c)
|
||||
- This step ONLY discovers — it does NOT write to the document
|
||||
|
||||
## YOUR TASK:
|
||||
|
||||
Discover the product vision and differentiator through natural conversation. Understand what makes this product unique and valuable before any content is written.
|
||||
|
||||
## VISION DISCOVERY SEQUENCE:
|
||||
|
||||
### 1. Acknowledge Classification Context
|
||||
|
||||
Reference the classification from step 2 and use it to frame the vision conversation:
|
||||
|
||||
"We've established this is a {{projectType}} in the {{domain}} domain with {{complexityLevel}} complexity. Now let's explore what makes this product special."
|
||||
|
||||
### 2. Explore What Makes It Special
|
||||
|
||||
Guide the conversation to uncover the product's unique value:
|
||||
|
||||
- **User delight:** "What would make users say 'this is exactly what I needed'?"
|
||||
- **Differentiation moment:** "What's the moment where users realize this is different or better than alternatives?"
|
||||
- **Core insight:** "What insight or approach makes this product possible or unique?"
|
||||
- **Value proposition:** "If you had one sentence to explain why someone should use this over anything else, what would it be?"
|
||||
|
||||
### 3. Understand the Vision
|
||||
|
||||
Dig deeper into the product vision:
|
||||
|
||||
- **Problem framing:** "What's the real problem you're solving — not the surface symptom, but the deeper need?"
|
||||
- **Future state:** "When this product is successful, what does the world look like for your users?"
|
||||
- **Why now:** "Why is this the right time to build this?"
|
||||
|
||||
### 4. Validate Understanding
|
||||
|
||||
Reflect back what you've heard and confirm:
|
||||
|
||||
"Here's what I'm hearing about your vision and differentiator:
|
||||
|
||||
**Vision:** {{summarized_vision}}
|
||||
**What Makes It Special:** {{summarized_differentiator}}
|
||||
**Core Insight:** {{summarized_insight}}
|
||||
|
||||
Does this capture it? Anything I'm missing?"
|
||||
|
||||
Let the user confirm or refine your understanding.
|
||||
|
||||
### N. Present MENU OPTIONS
|
||||
|
||||
Present your understanding of the product vision for review, then display menu:
|
||||
|
||||
"Based on our conversation, I have a clear picture of your product vision and what makes it special. I'll use these insights to draft the Executive Summary in the next step.
|
||||
|
||||
**What would you like to do?**"
|
||||
|
||||
Display: "**Select:** [A] Advanced Elicitation [P] Party Mode [C] Continue to Executive Summary (Step 2c of 13)"
|
||||
|
||||
#### Menu Handling Logic:
|
||||
- IF A: Read fully and follow: {advancedElicitationTask} with the current vision insights, process the enhanced insights that come back, ask user if they accept the improvements, if yes update understanding then redisplay menu, if no keep original understanding then redisplay menu
|
||||
- IF P: Read fully and follow: {partyModeWorkflow} with the current vision insights, process the collaborative insights, ask user if they accept the changes, if yes update understanding then redisplay menu, if no keep original understanding then redisplay menu
|
||||
- IF C: Update {outputFile} frontmatter by adding this step name to the end of stepsCompleted array, then read fully and follow: {nextStepFile}
|
||||
- IF Any other: help user respond, then redisplay menu
|
||||
|
||||
#### EXECUTION RULES:
|
||||
- ALWAYS halt and wait for user input after presenting menu
|
||||
- ONLY proceed to next step when user selects 'C'
|
||||
- After other menu items execution, return to this menu
|
||||
|
||||
## CRITICAL STEP COMPLETION NOTE
|
||||
|
||||
ONLY WHEN [C continue option] is selected and [stepsCompleted updated], will you then read fully and follow: `{nextStepFile}` to generate the Executive Summary.
|
||||
|
||||
---
|
||||
|
||||
## 🚨 SYSTEM SUCCESS/FAILURE METRICS
|
||||
|
||||
### ✅ SUCCESS:
|
||||
|
||||
- Classification context from step 2 acknowledged and built upon
|
||||
- Natural conversation to understand product vision and differentiator
|
||||
- User's existing documents (briefs, research, brainstorming) leveraged for vision insights
|
||||
- Vision and differentiator validated with user before proceeding
|
||||
- Clear understanding established that will inform Executive Summary generation
|
||||
- Frontmatter updated with stepsCompleted when C selected
|
||||
|
||||
### ❌ SYSTEM FAILURE:
|
||||
|
||||
- Generating executive summary or any document content (that's step 2c!)
|
||||
- Appending anything to the PRD document
|
||||
- Not building on classification from step 2
|
||||
- Being prescriptive instead of having natural conversation
|
||||
- Proceeding without user selecting 'C'
|
||||
|
||||
❌ **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
|
||||
|
||||
**Master Rule:** This step is vision discovery only. No content generation, no document writing. Have natural conversations, build on what you know from classification, and establish the vision that will feed into the Executive Summary.
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
---
|
||||
name: 'step-02c-executive-summary'
|
||||
description: 'Generate and append the Executive Summary section to the PRD document'
|
||||
|
||||
# File References
|
||||
nextStepFile: './step-03-success.md'
|
||||
outputFile: '{planning_artifacts}/prd.md'
|
||||
|
||||
# Task References
|
||||
advancedElicitationTask: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml'
|
||||
partyModeWorkflow: '{project-root}/_bmad/core/workflows/party-mode/workflow.md'
|
||||
---
|
||||
|
||||
# Step 2c: Executive Summary Generation
|
||||
|
||||
**Progress: Step 2c of 13** - Next: Success Criteria
|
||||
|
||||
## STEP GOAL:
|
||||
|
||||
Generate the Executive Summary content using insights from classification (step 2) and vision discovery (step 2b), then append it to the PRD document.
|
||||
|
||||
## MANDATORY EXECUTION RULES (READ FIRST):
|
||||
|
||||
### Universal Rules:
|
||||
|
||||
- 🛑 NEVER generate content without user input
|
||||
- 📖 CRITICAL: Read the complete step file before taking any action
|
||||
- 🔄 CRITICAL: When loading next step with 'C', ensure the entire file is read
|
||||
- ✅ ALWAYS treat this as collaborative discovery between PM peers
|
||||
- 📋 YOU ARE A FACILITATOR, not a content generator
|
||||
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||
|
||||
### Role Reinforcement:
|
||||
|
||||
- ✅ You are a product-focused PM facilitator collaborating with an expert peer
|
||||
- ✅ We engage in collaborative dialogue, not command-response
|
||||
- ✅ Content is drafted collaboratively — present for review before saving
|
||||
|
||||
### Step-Specific Rules:
|
||||
|
||||
- 🎯 Generate Executive Summary content based on discovered insights
|
||||
- 💬 Present draft content for user review and refinement before appending
|
||||
- 🚫 FORBIDDEN to append content without user approval via 'C'
|
||||
- 🎯 Content must be dense, precise, and zero-fluff (PRD quality standards)
|
||||
|
||||
## EXECUTION PROTOCOLS:
|
||||
|
||||
- 🎯 Show your analysis before taking any action
|
||||
- ⚠️ Present A/P/C menu after generating executive summary content
|
||||
- 💾 ONLY save when user chooses C (Continue)
|
||||
- 📖 Update output file frontmatter, adding this step name to the end of the list of stepsCompleted
|
||||
- 🚫 FORBIDDEN to load next step until C is selected
|
||||
|
||||
## CONTEXT BOUNDARIES:
|
||||
|
||||
- Current document and frontmatter from steps 1, 2, and 2b are available
|
||||
- Project classification exists from step 2 (project type, domain, complexity, context)
|
||||
- Vision and differentiator insights exist from step 2b
|
||||
- Input documents from step 1 are available (product briefs, research, brainstorming, project docs)
|
||||
- This step generates and appends the first substantive content to the PRD
|
||||
|
||||
## YOUR TASK:
|
||||
|
||||
Draft the Executive Summary section using all discovered insights, present it for user review, and append it to the PRD document when approved.
|
||||
|
||||
## EXECUTIVE SUMMARY GENERATION SEQUENCE:
|
||||
|
||||
### 1. Synthesize Available Context
|
||||
|
||||
Review all available context before drafting:
|
||||
- Classification from step 2: project type, domain, complexity, project context
|
||||
- Vision and differentiator from step 2b: what makes this special, core insight
|
||||
- Input documents: product briefs, research, brainstorming, project docs
|
||||
|
||||
### 2. Draft Executive Summary Content
|
||||
|
||||
Generate the Executive Summary section using the content structure below. Apply PRD quality standards:
|
||||
- High information density — every sentence carries weight
|
||||
- Zero fluff — no filler phrases or vague language
|
||||
- Precise and actionable — clear, specific statements
|
||||
- Dual-audience optimized — readable by humans, consumable by LLMs
|
||||
|
||||
### 3. Present Draft for Review
|
||||
|
||||
Present the drafted content to the user for review:
|
||||
|
||||
"Here's the Executive Summary I've drafted based on our discovery work. Please review and let me know if you'd like any changes:"
|
||||
|
||||
Show the full drafted content using the structure from the Content Structure section below.
|
||||
|
||||
Allow the user to:
|
||||
- Request specific changes to any section
|
||||
- Add missing information
|
||||
- Refine the language or emphasis
|
||||
- Approve as-is
|
||||
|
||||
### N. Present MENU OPTIONS
|
||||
|
||||
Present the executive summary content for user review, then display menu:
|
||||
|
||||
"Here's the Executive Summary for your PRD. Review the content above and let me know what you'd like to do."
|
||||
|
||||
Display: "**Select:** [A] Advanced Elicitation [P] Party Mode [C] Continue to Success Criteria (Step 3 of 13)"
|
||||
|
||||
#### Menu Handling Logic:
|
||||
- IF A: Read fully and follow: {advancedElicitationTask} with the current executive summary content, process the enhanced content that comes 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: Read fully and follow: {partyModeWorkflow} with the current executive summary content, process the collaborative improvements, 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: {nextStepFile}
|
||||
- IF Any other: help user respond, then redisplay menu
|
||||
|
||||
#### EXECUTION RULES:
|
||||
- ALWAYS halt and wait for user input after presenting menu
|
||||
- ONLY proceed to next step when user selects 'C'
|
||||
- After other menu items execution, return to this menu
|
||||
|
||||
## APPEND TO DOCUMENT:
|
||||
|
||||
When user selects 'C', append the following content structure directly to the document:
|
||||
|
||||
```markdown
|
||||
## Executive Summary
|
||||
|
||||
{vision_alignment_content}
|
||||
|
||||
### What Makes This Special
|
||||
|
||||
{product_differentiator_content}
|
||||
|
||||
## Project Classification
|
||||
|
||||
{project_classification_content}
|
||||
```
|
||||
|
||||
Where:
|
||||
- `{vision_alignment_content}` — Product vision, target users, and the problem being solved. Dense, precise summary drawn from step 2b vision discovery.
|
||||
- `{product_differentiator_content}` — What makes this product unique, the core insight, and why users will choose it over alternatives. Drawn from step 2b differentiator discovery.
|
||||
- `{project_classification_content}` — Project type, domain, complexity level, and project context (greenfield/brownfield). Drawn from step 2 classification.
|
||||
|
||||
## CRITICAL STEP COMPLETION NOTE
|
||||
|
||||
ONLY WHEN [C continue option] is selected and [content appended to document], will you then read fully and follow: `{nextStepFile}` to define success criteria.
|
||||
|
||||
---
|
||||
|
||||
## 🚨 SYSTEM SUCCESS/FAILURE METRICS
|
||||
|
||||
### ✅ SUCCESS:
|
||||
|
||||
- Executive Summary drafted using insights from steps 2 and 2b
|
||||
- Content meets PRD quality standards (dense, precise, zero-fluff)
|
||||
- Draft presented to user for review before saving
|
||||
- User given opportunity to refine content
|
||||
- Content properly appended to document when C selected
|
||||
- A/P/C menu presented and handled correctly
|
||||
- Frontmatter updated with stepsCompleted when C selected
|
||||
|
||||
### ❌ SYSTEM FAILURE:
|
||||
|
||||
- Generating content without incorporating discovered vision and classification
|
||||
- Appending content without user selecting 'C'
|
||||
- Producing vague, fluffy, or low-density content
|
||||
- Not presenting draft for user review
|
||||
- Not presenting A/P/C menu after content generation
|
||||
- Skipping directly to next step without appending content
|
||||
|
||||
❌ **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**: Making decisions without complete understanding of step requirements and protocols
|
||||
|
||||
**Master Rule:** Generate high-quality Executive Summary content from discovered insights. Present for review, refine collaboratively, and only save when the user approves. This is the first substantive content in the PRD — it sets the quality bar for everything that follows.
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
/**
|
||||
* Tests for CodexSetup.transformToSkillFormat
|
||||
*
|
||||
* Validates that descriptions round-trip correctly through parse/stringify,
|
||||
* producing valid YAML regardless of input quoting style.
|
||||
*
|
||||
* Usage: node test/test-codex-transform.js
|
||||
*/
|
||||
|
||||
const path = require('node:path');
|
||||
const yaml = require('yaml');
|
||||
|
||||
// ANSI colors
|
||||
const colors = {
|
||||
reset: '\u001B[0m',
|
||||
green: '\u001B[32m',
|
||||
red: '\u001B[31m',
|
||||
cyan: '\u001B[36m',
|
||||
dim: '\u001B[2m',
|
||||
};
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, testName, detail) {
|
||||
if (condition) {
|
||||
console.log(` ${colors.green}PASS${colors.reset} ${testName}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(` ${colors.red}FAIL${colors.reset} ${testName}`);
|
||||
if (detail) console.log(` ${colors.dim}${detail}${colors.reset}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the output frontmatter and return the description value.
|
||||
* Validates the output is well-formed YAML that parses back correctly.
|
||||
*/
|
||||
function parseOutputDescription(output) {
|
||||
const match = output.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!match) return null;
|
||||
const parsed = yaml.parse(match[1]);
|
||||
return parsed?.description;
|
||||
}
|
||||
|
||||
// Import the class under test
|
||||
const { CodexSetup } = require(path.join(__dirname, '..', 'tools', 'cli', 'installers', 'lib', 'ide', 'codex.js'));
|
||||
|
||||
const setup = new CodexSetup();
|
||||
|
||||
console.log(`\n${colors.cyan}CodexSetup.transformToSkillFormat tests${colors.reset}\n`);
|
||||
|
||||
// --- Simple description, no quotes ---
|
||||
{
|
||||
const input = `---\ndescription: A simple description\n---\n\nBody content here.`;
|
||||
const result = setup.transformToSkillFormat(input, 'my-skill');
|
||||
const desc = parseOutputDescription(result);
|
||||
assert(desc === 'A simple description', 'simple description round-trips', `got description: ${JSON.stringify(desc)}`);
|
||||
assert(result.includes('\nBody content here.'), 'body preserved for simple description');
|
||||
}
|
||||
|
||||
// --- Description with embedded single quotes (from double-quoted YAML input) ---
|
||||
{
|
||||
const input = `---\ndescription: "can't stop won't stop"\n---\n\nBody content here.`;
|
||||
const result = setup.transformToSkillFormat(input, 'my-skill');
|
||||
const desc = parseOutputDescription(result);
|
||||
assert(desc === "can't stop won't stop", 'description with apostrophes round-trips', `got description: ${JSON.stringify(desc)}`);
|
||||
assert(result.includes('\nBody content here.'), 'body preserved for quoted description');
|
||||
}
|
||||
|
||||
// --- Description with embedded single quote ---
|
||||
{
|
||||
const input = `---\ndescription: "it's a test"\n---\n\nBody.`;
|
||||
const result = setup.transformToSkillFormat(input, 'test-skill');
|
||||
const desc = parseOutputDescription(result);
|
||||
assert(desc === "it's a test", 'description with apostrophe round-trips', `got description: ${JSON.stringify(desc)}`);
|
||||
}
|
||||
|
||||
// --- Single-quoted input with pre-escaped apostrophe (YAML '' escape) ---
|
||||
{
|
||||
const input = `---\ndescription: 'don''t panic'\n---\n\nBody.`;
|
||||
const result = setup.transformToSkillFormat(input, 'test-skill');
|
||||
const desc = parseOutputDescription(result);
|
||||
assert(desc === "don't panic", 'single-quoted escaped apostrophe round-trips', `got description: ${JSON.stringify(desc)}`);
|
||||
}
|
||||
|
||||
// --- Verify name is set correctly ---
|
||||
{
|
||||
const input = `---\ndescription: test\n---\n\nBody.`;
|
||||
const result = setup.transformToSkillFormat(input, 'my-custom-skill');
|
||||
const match = result.match(/^---\n([\s\S]*?)\n---/);
|
||||
const parsed = yaml.parse(match[1]);
|
||||
assert(parsed.name === 'my-custom-skill', 'name field matches skillName argument', `got name: ${JSON.stringify(parsed.name)}`);
|
||||
}
|
||||
|
||||
// --- Extra frontmatter keys are stripped ---
|
||||
{
|
||||
const input = `---\ndescription: foo\ndisable-model-invocation: true\ncustom-field: bar\n---\n\nBody.`;
|
||||
const result = setup.transformToSkillFormat(input, 'strip-extra');
|
||||
const desc = parseOutputDescription(result);
|
||||
assert(desc === 'foo', 'description preserved when extra keys present', `got description: ${JSON.stringify(desc)}`);
|
||||
const match = result.match(/^---\n([\s\S]*?)\n---/);
|
||||
const parsed = yaml.parse(match[1]);
|
||||
assert(parsed.name === 'strip-extra', 'name equals skillName after stripping extras', `got name: ${JSON.stringify(parsed.name)}`);
|
||||
assert(!('disable-model-invocation' in parsed), 'disable-model-invocation stripped', `keys: ${Object.keys(parsed).join(', ')}`);
|
||||
assert(!('custom-field' in parsed), 'custom-field stripped', `keys: ${Object.keys(parsed).join(', ')}`);
|
||||
const keys = Object.keys(parsed).sort();
|
||||
assert(
|
||||
keys.length === 2 && keys[0] === 'description' && keys[1] === 'name',
|
||||
'only name and description remain',
|
||||
`keys: ${keys.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
// --- No frontmatter wraps content ---
|
||||
{
|
||||
const input = 'Just some content without frontmatter.';
|
||||
const result = setup.transformToSkillFormat(input, 'bare-skill');
|
||||
const desc = parseOutputDescription(result);
|
||||
assert(desc === 'bare-skill', 'no-frontmatter fallback uses skillName as description', `got description: ${JSON.stringify(desc)}`);
|
||||
assert(result.includes('Just some content without frontmatter.'), 'body preserved when no frontmatter');
|
||||
}
|
||||
|
||||
// --- No frontmatter with single-quote in skillName ---
|
||||
{
|
||||
const input = 'Body content for the skill.';
|
||||
const result = setup.transformToSkillFormat(input, "it's-a-task");
|
||||
const desc = parseOutputDescription(result);
|
||||
assert(desc === "it's-a-task", 'no-frontmatter skillName with single quote round-trips', `got description: ${JSON.stringify(desc)}`);
|
||||
assert(result.includes('Body content for the skill.'), 'body preserved for single-quote skillName');
|
||||
}
|
||||
|
||||
// --- CRLF frontmatter is parsed correctly (Windows line endings) ---
|
||||
{
|
||||
const input = '---\r\ndescription: windows line endings\r\n---\r\n\r\nBody.';
|
||||
const result = setup.transformToSkillFormat(input, 'crlf-skill');
|
||||
const desc = parseOutputDescription(result);
|
||||
assert(desc === 'windows line endings', 'CRLF frontmatter parses correctly', `got description: ${JSON.stringify(desc)}`);
|
||||
assert(result.includes('Body.'), 'body preserved for CRLF input');
|
||||
}
|
||||
|
||||
// --- Summary ---
|
||||
console.log(`\n${passed} passed, ${failed} failed\n`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
/**
|
||||
* Tests for CodexSetup.writeSkillArtifacts
|
||||
*
|
||||
* Validates directory creation, SKILL.md file writing, type filtering,
|
||||
* and integration with transformToSkillFormat.
|
||||
*
|
||||
* Usage: node test/test-codex-write-skills.js
|
||||
*/
|
||||
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const os = require('node:os');
|
||||
const yaml = require('yaml');
|
||||
|
||||
// ANSI colors
|
||||
const colors = {
|
||||
reset: '\u001B[0m',
|
||||
green: '\u001B[32m',
|
||||
red: '\u001B[31m',
|
||||
cyan: '\u001B[36m',
|
||||
dim: '\u001B[2m',
|
||||
};
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, testName, detail) {
|
||||
if (condition) {
|
||||
console.log(` ${colors.green}PASS${colors.reset} ${testName}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(` ${colors.red}FAIL${colors.reset} ${testName}`);
|
||||
if (detail) console.log(` ${colors.dim}${detail}${colors.reset}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// Import the class under test
|
||||
const { CodexSetup } = require(path.join(__dirname, '..', 'tools', 'cli', 'installers', 'lib', 'ide', 'codex.js'));
|
||||
|
||||
const setup = new CodexSetup();
|
||||
|
||||
// Create a temp directory for each test run
|
||||
let tmpDir;
|
||||
|
||||
async function createTmpDir() {
|
||||
tmpDir = path.join(os.tmpdir(), `bmad-test-skills-${Date.now()}`);
|
||||
await fs.ensureDir(tmpDir);
|
||||
return tmpDir;
|
||||
}
|
||||
|
||||
async function cleanTmpDir() {
|
||||
if (tmpDir) {
|
||||
await fs.remove(tmpDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
console.log(`\n${colors.cyan}CodexSetup.writeSkillArtifacts tests${colors.reset}\n`);
|
||||
|
||||
// --- Writes a single artifact as a skill directory with SKILL.md ---
|
||||
{
|
||||
const destDir = await createTmpDir();
|
||||
const artifacts = [
|
||||
{
|
||||
type: 'task',
|
||||
relativePath: 'bmm/tasks/create-story.md',
|
||||
content: '---\ndescription: Create a user story\n---\n\nStory creation instructions.',
|
||||
},
|
||||
];
|
||||
const count = await setup.writeSkillArtifacts(destDir, artifacts, 'task');
|
||||
assert(count === 1, 'single artifact returns count 1');
|
||||
|
||||
const skillDir = path.join(destDir, 'bmad-bmm-create-story');
|
||||
assert(await fs.pathExists(skillDir), 'skill directory created');
|
||||
|
||||
const skillFile = path.join(skillDir, 'SKILL.md');
|
||||
assert(await fs.pathExists(skillFile), 'SKILL.md file created');
|
||||
|
||||
const content = await fs.readFile(skillFile, 'utf8');
|
||||
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
assert(fmMatch !== null, 'SKILL.md has frontmatter');
|
||||
|
||||
const parsed = yaml.parse(fmMatch[1]);
|
||||
assert(parsed.name === 'bmad-bmm-create-story', 'name matches skill directory name', `got: ${parsed.name}`);
|
||||
assert(parsed.description === 'Create a user story', 'description preserved', `got: ${parsed.description}`);
|
||||
assert(content.includes('Story creation instructions.'), 'body content preserved');
|
||||
await cleanTmpDir();
|
||||
}
|
||||
|
||||
// --- Filters artifacts by type ---
|
||||
{
|
||||
const destDir = await createTmpDir();
|
||||
const artifacts = [
|
||||
{
|
||||
type: 'task',
|
||||
relativePath: 'bmm/tasks/create-story.md',
|
||||
content: '---\ndescription: A task\n---\n\nTask body.',
|
||||
},
|
||||
{
|
||||
type: 'workflow-command',
|
||||
relativePath: 'bmm/workflows/plan-project.md',
|
||||
content: '---\ndescription: A workflow\n---\n\nWorkflow body.',
|
||||
},
|
||||
{
|
||||
type: 'agent-launcher',
|
||||
relativePath: 'bmm/agents/pm.md',
|
||||
content: '---\ndescription: An agent\n---\n\nAgent body.',
|
||||
},
|
||||
];
|
||||
const count = await setup.writeSkillArtifacts(destDir, artifacts, 'task');
|
||||
assert(count === 1, 'only matching type is written when filtering for task');
|
||||
|
||||
const entries = await fs.readdir(destDir);
|
||||
assert(entries.length === 1, 'only one skill directory created', `got ${entries.length}: ${entries.join(', ')}`);
|
||||
assert(entries[0] === 'bmad-bmm-create-story', 'correct artifact was written', `got: ${entries[0]}`);
|
||||
await cleanTmpDir();
|
||||
}
|
||||
|
||||
// --- Writes multiple artifacts of the same type ---
|
||||
{
|
||||
const destDir = await createTmpDir();
|
||||
const artifacts = [
|
||||
{
|
||||
type: 'workflow-command',
|
||||
relativePath: 'bmm/workflows/plan-project.md',
|
||||
content: '---\ndescription: Plan\n---\n\nPlan body.',
|
||||
},
|
||||
{
|
||||
type: 'workflow-command',
|
||||
relativePath: 'core/workflows/review.md',
|
||||
content: '---\ndescription: Review\n---\n\nReview body.',
|
||||
},
|
||||
];
|
||||
const count = await setup.writeSkillArtifacts(destDir, artifacts, 'workflow-command');
|
||||
assert(count === 2, 'two artifacts written');
|
||||
|
||||
const entries = new Set((await fs.readdir(destDir)).sort());
|
||||
assert(entries.has('bmad-bmm-plan-project'), 'first skill directory exists');
|
||||
assert(entries.has('bmad-review'), 'second skill directory exists (core module)');
|
||||
await cleanTmpDir();
|
||||
}
|
||||
|
||||
// --- Returns 0 when no artifacts match type ---
|
||||
{
|
||||
const destDir = await createTmpDir();
|
||||
const artifacts = [
|
||||
{
|
||||
type: 'agent-launcher',
|
||||
relativePath: 'bmm/agents/pm.md',
|
||||
content: '---\ndescription: An agent\n---\n\nBody.',
|
||||
},
|
||||
];
|
||||
const count = await setup.writeSkillArtifacts(destDir, artifacts, 'task');
|
||||
assert(count === 0, 'returns 0 when no types match');
|
||||
|
||||
const entries = await fs.readdir(destDir);
|
||||
assert(entries.length === 0, 'no directories created when no types match');
|
||||
await cleanTmpDir();
|
||||
}
|
||||
|
||||
// --- Handles empty artifacts array ---
|
||||
{
|
||||
const destDir = await createTmpDir();
|
||||
const count = await setup.writeSkillArtifacts(destDir, [], 'task');
|
||||
assert(count === 0, 'returns 0 for empty artifacts array');
|
||||
await cleanTmpDir();
|
||||
}
|
||||
|
||||
// --- Artifacts without type field are always written ---
|
||||
{
|
||||
const destDir = await createTmpDir();
|
||||
const artifacts = [
|
||||
{
|
||||
relativePath: 'bmm/tasks/no-type.md',
|
||||
content: '---\ndescription: No type field\n---\n\nBody.',
|
||||
},
|
||||
];
|
||||
const count = await setup.writeSkillArtifacts(destDir, artifacts, 'task');
|
||||
assert(count === 1, 'artifact without type field is written (no filtering)');
|
||||
await cleanTmpDir();
|
||||
}
|
||||
|
||||
// --- Content without frontmatter gets minimal frontmatter added ---
|
||||
{
|
||||
const destDir = await createTmpDir();
|
||||
const artifacts = [
|
||||
{
|
||||
type: 'task',
|
||||
relativePath: 'bmm/tasks/bare.md',
|
||||
content: 'Just plain content, no frontmatter.',
|
||||
},
|
||||
];
|
||||
const count = await setup.writeSkillArtifacts(destDir, artifacts, 'task');
|
||||
assert(count === 1, 'bare content artifact written');
|
||||
|
||||
const skillFile = path.join(destDir, 'bmad-bmm-bare', 'SKILL.md');
|
||||
const content = await fs.readFile(skillFile, 'utf8');
|
||||
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
assert(fmMatch !== null, 'frontmatter added to bare content');
|
||||
|
||||
const parsed = yaml.parse(fmMatch[1]);
|
||||
assert(parsed.name === 'bmad-bmm-bare', 'name set for bare content', `got: ${parsed.name}`);
|
||||
assert(content.includes('Just plain content, no frontmatter.'), 'original content preserved');
|
||||
await cleanTmpDir();
|
||||
}
|
||||
|
||||
// --- Summary ---
|
||||
console.log(`\n${passed} passed, ${failed} failed\n`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests().catch((error) => {
|
||||
console.error('Test runner error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -7,10 +7,13 @@ const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
||||
const { getTasksFromBmad } = require('./shared/bmad-artifacts');
|
||||
const { toDashPath, customAgentDashName } = require('./shared/path-utils');
|
||||
const yaml = require('yaml');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
/**
|
||||
* Codex setup handler (CLI mode)
|
||||
* Writes BMAD artifacts as Agent Skills (agentskills.io format)
|
||||
* into .agents/skills/ directories.
|
||||
*/
|
||||
class CodexSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
|
|
@ -23,35 +26,35 @@ class CodexSetup extends BaseIdeSetup {
|
|||
* @returns {Object} Collected configuration
|
||||
*/
|
||||
async collectConfiguration(options = {}) {
|
||||
// Non-interactive mode: use default (global)
|
||||
// Non-interactive mode: use default (project)
|
||||
if (options.skipPrompts) {
|
||||
return { installLocation: 'global' };
|
||||
return { installLocation: 'project' };
|
||||
}
|
||||
|
||||
let confirmed = false;
|
||||
let installLocation = 'global';
|
||||
let installLocation = 'project';
|
||||
|
||||
while (!confirmed) {
|
||||
installLocation = await prompts.select({
|
||||
message: 'Where would you like to install Codex CLI prompts?',
|
||||
message: 'Where would you like to install Codex CLI skills?',
|
||||
choices: [
|
||||
{
|
||||
name: 'Global - Simple for single project ' + '(~/.codex/prompts, but references THIS project only)',
|
||||
value: 'global',
|
||||
},
|
||||
{
|
||||
name: `Project-specific - Recommended for real work (requires CODEX_HOME=<project-dir>${path.sep}.codex)`,
|
||||
name: 'Project-specific - Recommended (<project>/.agents/skills)',
|
||||
value: 'project',
|
||||
},
|
||||
{
|
||||
name: 'Global - ($HOME/.agents/skills)',
|
||||
value: 'global',
|
||||
},
|
||||
],
|
||||
default: 'global',
|
||||
default: 'project',
|
||||
});
|
||||
|
||||
// Show brief confirmation hint (detailed instructions available via verbose)
|
||||
if (installLocation === 'project') {
|
||||
await prompts.log.info('Prompts installed to: <project>/.codex/prompts (requires CODEX_HOME)');
|
||||
await prompts.log.info('Skills installed to: <project>/.agents/skills');
|
||||
} else {
|
||||
await prompts.log.info('Prompts installed to: ~/.codex/prompts');
|
||||
await prompts.log.info('Skills installed to: $HOME/.agents/skills');
|
||||
}
|
||||
|
||||
// Confirm the choice
|
||||
|
|
@ -80,20 +83,21 @@ class CodexSetup extends BaseIdeSetup {
|
|||
// Always use CLI mode
|
||||
const mode = 'cli';
|
||||
|
||||
// Get installation location from pre-collected config or default to global
|
||||
const installLocation = options.preCollectedConfig?.installLocation || 'global';
|
||||
// Get installation location from pre-collected config or default to project
|
||||
const installLocation = options.preCollectedConfig?.installLocation || 'project';
|
||||
|
||||
const { artifacts, counts } = await this.collectClaudeArtifacts(projectDir, bmadDir, options);
|
||||
|
||||
const destDir = this.getCodexPromptDir(projectDir, installLocation);
|
||||
const destDir = this.getCodexSkillsDir(projectDir, installLocation);
|
||||
await fs.ensureDir(destDir);
|
||||
await this.clearOldBmadFiles(destDir, options);
|
||||
await this.clearOldBmadSkills(destDir, options);
|
||||
|
||||
// Collect artifacts and write using underscore format
|
||||
// Collect and write agent skills
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
const agentCount = await agentGen.writeDashArtifacts(destDir, agentArtifacts);
|
||||
const agentCount = await this.writeSkillArtifacts(destDir, agentArtifacts, 'agent-launcher');
|
||||
|
||||
// Collect and write task skills
|
||||
const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []);
|
||||
const taskArtifacts = [];
|
||||
for (const task of tasks) {
|
||||
|
|
@ -117,19 +121,23 @@ class CodexSetup extends BaseIdeSetup {
|
|||
});
|
||||
}
|
||||
|
||||
const ttGen = new TaskToolCommandGenerator(this.bmadFolderName);
|
||||
const taskSkillArtifacts = taskArtifacts.map((artifact) => ({
|
||||
...artifact,
|
||||
content: ttGen.generateCommandContent(artifact, artifact.type),
|
||||
}));
|
||||
const tasksWritten = await this.writeSkillArtifacts(destDir, taskSkillArtifacts, 'task');
|
||||
|
||||
// Collect and write workflow skills
|
||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
||||
const workflowCount = await workflowGenerator.writeDashArtifacts(destDir, workflowArtifacts);
|
||||
|
||||
// Also write tasks using underscore format
|
||||
const ttGen = new TaskToolCommandGenerator(this.bmadFolderName);
|
||||
const tasksWritten = await ttGen.writeDashArtifacts(destDir, taskArtifacts);
|
||||
const workflowCount = await this.writeSkillArtifacts(destDir, workflowArtifacts, 'workflow-command');
|
||||
|
||||
const written = agentCount + workflowCount + tasksWritten;
|
||||
|
||||
if (!options.silent) {
|
||||
await prompts.log.success(
|
||||
`${this.name} configured: ${counts.agents} agents, ${counts.workflows} workflows, ${counts.tasks} tasks, ${written} files → ${destDir}`,
|
||||
`${this.name} configured: ${counts.agents} agents, ${counts.workflows} workflows, ${counts.tasks} tasks, ${written} skills → ${destDir}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -145,13 +153,82 @@ class CodexSetup extends BaseIdeSetup {
|
|||
}
|
||||
|
||||
/**
|
||||
* Detect Codex installation by checking for BMAD prompt exports
|
||||
* Write artifacts as Agent Skills (agentskills.io format).
|
||||
* Each artifact becomes a directory containing SKILL.md.
|
||||
* @param {string} destDir - Base skills directory
|
||||
* @param {Array} artifacts - Artifacts to write
|
||||
* @param {string} artifactType - Type filter (e.g., 'agent-launcher', 'workflow-command', 'task')
|
||||
* @returns {number} Number of skills written
|
||||
*/
|
||||
async writeSkillArtifacts(destDir, artifacts, artifactType) {
|
||||
let writtenCount = 0;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
// Filter by type if the artifact has a type field
|
||||
if (artifact.type && artifact.type !== artifactType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the dash-format name (e.g., bmad-bmm-create-prd.md) and remove .md
|
||||
const flatName = toDashPath(artifact.relativePath);
|
||||
const skillName = flatName.replace(/\.md$/, '');
|
||||
|
||||
// Create skill directory
|
||||
const skillDir = path.join(destDir, skillName);
|
||||
await fs.ensureDir(skillDir);
|
||||
|
||||
// Transform content: rewrite frontmatter for skills format
|
||||
const skillContent = this.transformToSkillFormat(artifact.content, skillName);
|
||||
|
||||
// Write SKILL.md
|
||||
await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillContent);
|
||||
writtenCount++;
|
||||
}
|
||||
|
||||
return writtenCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform artifact content from Codex prompt format to Agent Skills format.
|
||||
* Removes disable-model-invocation, ensures name matches directory.
|
||||
* @param {string} content - Original content with YAML frontmatter
|
||||
* @param {string} skillName - Skill name (must match directory name)
|
||||
* @returns {string} Transformed content
|
||||
*/
|
||||
transformToSkillFormat(content, skillName) {
|
||||
// Parse frontmatter
|
||||
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
||||
if (!fmMatch) {
|
||||
// No frontmatter -- wrap with minimal frontmatter
|
||||
const fm = yaml.stringify({ name: skillName, description: skillName }).trimEnd();
|
||||
return `---\n${fm}\n---\n\n${content}`;
|
||||
}
|
||||
|
||||
const frontmatter = fmMatch[1];
|
||||
const body = fmMatch[2];
|
||||
|
||||
// Parse frontmatter with yaml library to handle all quoting variants
|
||||
let description;
|
||||
try {
|
||||
const parsed = yaml.parse(frontmatter);
|
||||
description = parsed?.description || `${skillName} skill`;
|
||||
} catch {
|
||||
description = `${skillName} skill`;
|
||||
}
|
||||
|
||||
// Build new frontmatter with only skills-spec fields, let yaml handle quoting
|
||||
const newFrontmatter = yaml.stringify({ name: skillName, description }).trimEnd();
|
||||
return `---\n${newFrontmatter}\n---\n${body}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect Codex installation by checking for BMAD skills
|
||||
*/
|
||||
async detect(projectDir) {
|
||||
// Check both global and project-specific locations
|
||||
const globalDir = this.getCodexPromptDir(null, 'global');
|
||||
const globalDir = this.getCodexSkillsDir(null, 'global');
|
||||
const projectDir_local = projectDir || process.cwd();
|
||||
const projectSpecificDir = this.getCodexPromptDir(projectDir_local, 'project');
|
||||
const projectSpecificDir = this.getCodexSkillsDir(projectDir_local, 'project');
|
||||
|
||||
// Check global location
|
||||
if (await fs.pathExists(globalDir)) {
|
||||
|
|
@ -240,27 +317,20 @@ class CodexSetup extends BaseIdeSetup {
|
|||
};
|
||||
}
|
||||
|
||||
getCodexPromptDir(projectDir = null, location = 'global') {
|
||||
getCodexSkillsDir(projectDir = null, location = 'project') {
|
||||
if (location === 'project' && projectDir) {
|
||||
return path.join(projectDir, '.codex', 'prompts');
|
||||
return path.join(projectDir, '.agents', 'skills');
|
||||
}
|
||||
return path.join(os.homedir(), '.codex', 'prompts');
|
||||
if (location === 'project' && !projectDir) {
|
||||
throw new Error('projectDir is required for project-scoped skill installation');
|
||||
}
|
||||
return path.join(os.homedir(), '.agents', 'skills');
|
||||
}
|
||||
|
||||
async flattenAndWriteArtifacts(artifacts, destDir) {
|
||||
let written = 0;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
const flattenedName = this.flattenFilename(artifact.relativePath);
|
||||
const targetPath = path.join(destDir, flattenedName);
|
||||
await fs.writeFile(targetPath, artifact.content);
|
||||
written++;
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
async clearOldBmadFiles(destDir, options = {}) {
|
||||
/**
|
||||
* Remove existing BMAD skill directories from the skills directory.
|
||||
*/
|
||||
async clearOldBmadSkills(destDir, options = {}) {
|
||||
if (!(await fs.pathExists(destDir))) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -299,7 +369,8 @@ class CodexSetup extends BaseIdeSetup {
|
|||
}
|
||||
|
||||
async readAndProcessWithProject(filePath, metadata, projectDir) {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const rawContent = await fs.readFile(filePath, 'utf8');
|
||||
const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
||||
return super.processContent(content, metadata, projectDir);
|
||||
}
|
||||
|
||||
|
|
@ -311,14 +382,14 @@ class CodexSetup extends BaseIdeSetup {
|
|||
const lines = [
|
||||
'IMPORTANT: Codex Configuration',
|
||||
'',
|
||||
'/prompts installed globally to your HOME DIRECTORY.',
|
||||
'Skills installed globally to your HOME DIRECTORY ($HOME/.agents/skills).',
|
||||
'',
|
||||
'These prompts reference a specific _bmad path.',
|
||||
'These skills reference a specific _bmad path.',
|
||||
"To use with other projects, you'd need to copy the _bmad dir.",
|
||||
'',
|
||||
'You can now use /commands in Codex CLI',
|
||||
' Example: /bmad_bmm_pm',
|
||||
' Type / to see all available commands',
|
||||
'Skills are available in Codex CLI automatically.',
|
||||
' Use /skills to see available skills',
|
||||
' Skills can also be invoked implicitly based on task description',
|
||||
];
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
|
@ -330,40 +401,15 @@ class CodexSetup extends BaseIdeSetup {
|
|||
* @returns {string} Instructions text
|
||||
*/
|
||||
getProjectSpecificInstructions(projectDir = null, destDir = null) {
|
||||
const isWindows = os.platform() === 'win32';
|
||||
|
||||
const commonLines = [
|
||||
const lines = [
|
||||
'Project-Specific Codex Configuration',
|
||||
'',
|
||||
`Prompts will be installed to: ${destDir || '<project>/.codex/prompts'}`,
|
||||
'',
|
||||
'REQUIRED: You must set CODEX_HOME to use these prompts',
|
||||
`Skills installed to: ${destDir || '<project>/.agents/skills'}`,
|
||||
'',
|
||||
'Codex automatically discovers skills in .agents/skills/ at and above the current directory and in your home directory.',
|
||||
'No additional configuration is needed.',
|
||||
];
|
||||
|
||||
const windowsLines = [
|
||||
'Create a codex.cmd file in your project root:',
|
||||
'',
|
||||
' @echo off',
|
||||
' set CODEX_HOME=%~dp0.codex',
|
||||
' codex %*',
|
||||
'',
|
||||
String.raw`Then run: .\codex instead of codex`,
|
||||
'(The %~dp0 gets the directory of the .cmd file)',
|
||||
];
|
||||
|
||||
const unixLines = [
|
||||
'Add this alias to your ~/.bashrc or ~/.zshrc:',
|
||||
'',
|
||||
' alias codex=\'CODEX_HOME="$PWD/.codex" codex\'',
|
||||
'',
|
||||
'After adding, run: source ~/.bashrc (or source ~/.zshrc)',
|
||||
'(The $PWD uses your current working directory)',
|
||||
];
|
||||
const closingLines = ['', 'This tells Codex CLI to use prompts from this project instead of ~/.codex'];
|
||||
|
||||
const lines = [...commonLines, ...(isWindows ? windowsLines : unixLines), ...closingLines];
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
|
|
@ -372,31 +418,34 @@ class CodexSetup extends BaseIdeSetup {
|
|||
*/
|
||||
async cleanup(projectDir = null) {
|
||||
// Clean both global and project-specific locations
|
||||
const globalDir = this.getCodexPromptDir(null, 'global');
|
||||
await this.clearOldBmadFiles(globalDir);
|
||||
const globalDir = this.getCodexSkillsDir(null, 'global');
|
||||
await this.clearOldBmadSkills(globalDir);
|
||||
|
||||
if (projectDir) {
|
||||
const projectSpecificDir = this.getCodexPromptDir(projectDir, 'project');
|
||||
await this.clearOldBmadFiles(projectSpecificDir);
|
||||
const projectSpecificDir = this.getCodexSkillsDir(projectDir, 'project');
|
||||
await this.clearOldBmadSkills(projectSpecificDir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for Codex
|
||||
* @param {string} projectDir - Project directory (not used, Codex installs to home)
|
||||
* Install a custom agent launcher for Codex as an Agent Skill
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object|null} Info about created command
|
||||
* @returns {Object|null} Info about created skill
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const destDir = this.getCodexPromptDir(projectDir, 'project');
|
||||
await fs.ensureDir(destDir);
|
||||
const destDir = this.getCodexSkillsDir(projectDir, 'project');
|
||||
|
||||
const launcherContent = `---
|
||||
name: '${agentName}'
|
||||
description: '${agentName} agent'
|
||||
disable-model-invocation: true
|
||||
// Skill name from the dash name (without .md)
|
||||
const skillName = customAgentDashName(agentName).replace(/\.md$/, '');
|
||||
const skillDir = path.join(destDir, skillName);
|
||||
await fs.ensureDir(skillDir);
|
||||
|
||||
const fm = yaml.stringify({ name: skillName, description: `${agentName} agent` }).trimEnd();
|
||||
const skillContent = `---
|
||||
${fm}
|
||||
---
|
||||
|
||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||
|
|
@ -411,14 +460,12 @@ You must fully embody this agent's persona and follow all activation instruction
|
|||
</agent-activation>
|
||||
`;
|
||||
|
||||
// Use underscore format: bmad_custom_fred-commit-poet.md
|
||||
const fileName = customAgentDashName(agentName);
|
||||
const launcherPath = path.join(destDir, fileName);
|
||||
await fs.writeFile(launcherPath, launcherContent, 'utf8');
|
||||
const skillPath = path.join(skillDir, 'SKILL.md');
|
||||
await fs.writeFile(skillPath, skillContent, 'utf8');
|
||||
|
||||
return {
|
||||
path: path.relative(projectDir, launcherPath),
|
||||
command: `/${fileName.replace('.md', '')}`,
|
||||
path: path.relative(projectDir, skillPath),
|
||||
command: `$${skillName}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue