Compare commits
21 Commits
b6ea7846bd
...
95951b8aa9
| Author | SHA1 | Date |
|---|---|---|
|
|
95951b8aa9 | |
|
|
1786d1debc | |
|
|
4cb58ba9f3 | |
|
|
871d921072 | |
|
|
3fad46849f | |
|
|
3bb953f18b | |
|
|
43c59f0cff | |
|
|
3c8d865457 | |
|
|
1b8424cf6d | |
|
|
9973b3c35a | |
|
|
1a0da0278f | |
|
|
52ebc3330d | |
|
|
8b13628496 | |
|
|
642b6a0cf4 | |
|
|
fd1e24c5c2 | |
|
|
84bade9a95 | |
|
|
4f1894908c | |
|
|
7a214cc7d8 | |
|
|
ac5cb552c0 | |
|
|
22035ef015 | |
|
|
5a1f356e2c |
|
|
@ -108,3 +108,6 @@ jobs:
|
|||
|
||||
- name: Validate file references
|
||||
run: npm run validate:refs
|
||||
|
||||
- name: Validate skills
|
||||
run: npm run validate:skills
|
||||
|
|
|
|||
|
|
@ -9,3 +9,4 @@ Open source framework for structured, agent-assisted software delivery.
|
|||
`quality` mirrors the checks in `.github/workflows/quality.yaml`.
|
||||
|
||||
- Skill validation rules are in `tools/skill-validator.md`.
|
||||
- Deterministic skill checks run via `npm run validate:skills` (included in `quality`).
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ your-project/
|
|||
| ----------------- | ----------------------------- |
|
||||
| **Index/Landing** | `core-concepts/index.md` |
|
||||
| **Concept** | `what-are-agents.md` |
|
||||
| **Feature** | `quick-flow.md` |
|
||||
| **Feature** | `quick-dev.md` |
|
||||
| **Philosophy** | `why-solutioning-matters.md` |
|
||||
| **FAQ** | `established-projects-faq.md` |
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
title: "Quick Dev"
|
||||
description: Reduce human-in-the-loop friction without giving up the checkpoints that protect output quality
|
||||
sidebar:
|
||||
order: 2
|
||||
---
|
||||
|
||||
Intent in, code changes out, with as few human-in-the-loop turns as possible — without sacrificing quality.
|
||||
|
||||
It lets the model run longer between checkpoints, then brings the human back only when the task cannot safely continue without human judgment or when it is time to review the end result.
|
||||
|
||||

|
||||
|
||||
## Why This Exists
|
||||
|
||||
Human-in-the-loop turns are necessary and expensive.
|
||||
|
||||
Current LLMs still fail in predictable ways: they misread intent, fill gaps with confident guesses, drift into unrelated work, and generate noisy review output. At the same time, constant human intervention limits development velocity. Human attention is the bottleneck.
|
||||
|
||||
`bmad-quick-dev` rebalances that tradeoff. It trusts the model to run unsupervised for longer stretches, but only after the workflow has created a strong enough boundary to make that safe.
|
||||
|
||||
## The Core Design
|
||||
|
||||
### 1. Compress intent first
|
||||
|
||||
The workflow starts by having the human and the model compress the request into one coherent goal. The input can begin as a rough expression of intent, but before the workflow runs autonomously it has to become small enough, clear enough, and contradiction-free enough to execute.
|
||||
|
||||
Intent can come in many forms: a couple of phrases, a bug tracker link, output from plan mode, text copied from a chat session, or even a story number from BMAD's own `epics.md`. In that last case, the workflow will not understand BMAD story-tracking semantics, but it can still take the story itself and run with it.
|
||||
|
||||
This workflow does not eliminate human control. It relocates it to a small number of high-value moments:
|
||||
|
||||
- **Intent clarification** - turning a messy request into one coherent goal without hidden contradictions
|
||||
- **Spec approval** - confirming that the frozen understanding is the right thing to build
|
||||
- **Review of the final product** - the primary checkpoint, where the human decides whether the result is acceptable at the end
|
||||
|
||||
### 2. Route to the smallest safe path
|
||||
|
||||
Once the goal is clear, the workflow decides whether this is a true one-shot change or whether it needs the fuller path. Small, zero-blast-radius changes can go straight to implementation. Everything else goes through planning so the model has a stronger boundary before it runs longer on its own.
|
||||
|
||||
### 3. Run longer with less supervision
|
||||
|
||||
After that routing decision, the model can carry more of the work on its own. On the fuller path, the approved spec becomes the boundary the model executes against with less supervision, which is the whole point of the design.
|
||||
|
||||
### 4. Diagnose failure at the right layer
|
||||
|
||||
If the implementation is wrong because the intent was wrong, patching the code is the wrong fix. If the code is wrong because the spec was weak, patching the diff is also the wrong fix. The workflow is designed to diagnose where the failure entered the system, go back to that layer, and regenerate from there.
|
||||
|
||||
Review findings are used to decide whether the problem came from intent, spec generation, or local implementation. Only truly local problems get patched locally.
|
||||
|
||||
### 5. Bring the human back only when needed
|
||||
|
||||
The intent interview is human-in-the-loop, but it is not the same kind of interruption as a recurring checkpoint. The workflow tries to keep those recurring checkpoints to a minimum. After the initial shaping of intent, the human mainly comes back when the workflow cannot safely continue without judgment and at the end, when it is time to review the result.
|
||||
|
||||
- **Intent-gap resolution** - stepping back in when review proves the workflow could not safely infer what was meant
|
||||
|
||||
Everything else is a candidate for longer autonomous execution. That tradeoff is deliberate. Older patterns spend more human attention on continuous supervision. Quick Dev spends more trust on the model, but saves human attention for the moments where human reasoning has the highest leverage.
|
||||
|
||||
## Why the Review System Matters
|
||||
|
||||
The review phase is not just there to find bugs. It is there to route correction without destroying momentum.
|
||||
|
||||
This workflow works best on a platform that can spawn subagents, or at least invoke another LLM through the command line and wait for a result. If your platform does not support that natively, you can add a skill to do it. Context-free subagents are a cornerstone of the review design.
|
||||
|
||||
Agentic reviews often go wrong in two ways:
|
||||
|
||||
- They generate too many findings, forcing the human to sift through noise.
|
||||
- They derail the current change by surfacing unrelated issues and turning every run into an ad hoc cleanup project.
|
||||
|
||||
Quick Dev addresses both by treating review as triage.
|
||||
|
||||
Some findings belong to the current change. Some do not. If a finding is incidental rather than causally tied to the current work, the workflow can defer it instead of forcing the human to handle it immediately. That keeps the run focused and prevents random tangents from consuming the budget of attention.
|
||||
|
||||
That triage will sometimes be imperfect. That is acceptable. It is usually better to misjudge some findings than to flood the human with thousands of low-value review comments. The system is optimizing for signal quality, not exhaustive recall.
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
---
|
||||
title: "Quick Flow"
|
||||
description: Fast-track for small changes - skip the full methodology
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
Skip the ceremony. Quick Flow takes you from intent to working code in a single workflow — no Product Brief, no PRD, no Architecture doc.
|
||||
|
||||
## When to Use It
|
||||
|
||||
- Bug fixes and patches
|
||||
- Refactoring existing code
|
||||
- Small, well-understood features
|
||||
- Prototyping and spikes
|
||||
- Single-agent work where one developer can hold the full scope
|
||||
|
||||
## When NOT to Use It
|
||||
|
||||
- New products or platforms that need stakeholder alignment
|
||||
- Major features spanning multiple components or teams
|
||||
- Work that requires architectural decisions (database schema, API contracts, service boundaries)
|
||||
- Anything where requirements are unclear or contested
|
||||
|
||||
:::caution[Scope Creep]
|
||||
If you start a Quick Flow and realize the scope is bigger than expected, `bmad-quick-dev` will detect this and offer to escalate. You can switch to a full PRD workflow at any point without losing your work.
|
||||
:::
|
||||
|
||||
## How It Works
|
||||
|
||||
Run `bmad-quick-dev` and the workflow handles everything — clarifying intent, planning, implementing, reviewing, and presenting results.
|
||||
|
||||
### 1. Clarify intent
|
||||
|
||||
You describe what you want. The workflow compresses your request into one coherent goal — small enough, clear enough, and contradiction-free enough to execute safely. Intent can come from many sources: a few phrases, a bug tracker link, plan mode output, chat session text, or even a story number from your epics.
|
||||
|
||||
### 2. Route to the smallest safe path
|
||||
|
||||
Once the goal is clear, the workflow decides whether this is a true one-shot change or needs the fuller path. Small, zero-blast-radius changes go straight to implementation. Everything else goes through planning so the model has a stronger boundary before running autonomously.
|
||||
|
||||
### 3. Plan and implement
|
||||
|
||||
On the planning path, the workflow produces a complete tech-spec with ordered implementation tasks, acceptance criteria in Given/When/Then format, and testing strategy. After you approve the spec, it becomes the boundary the model executes against with less supervision.
|
||||
|
||||
### 4. Review and present
|
||||
|
||||
After implementation, the workflow runs a self-check audit and adversarial code review of the diff. Review acts as triage — findings tied to the current change are addressed, while incidental findings are deferred to keep the run focused. Results are presented for your sign-off.
|
||||
|
||||
### Human-in-the-loop checkpoints
|
||||
|
||||
The workflow relocates human control to a small number of high-value moments:
|
||||
|
||||
- **Intent clarification** — turning a messy request into one coherent goal
|
||||
- **Spec approval** — confirming the frozen understanding is the right thing to build
|
||||
- **Final review** — deciding whether the result is acceptable
|
||||
|
||||
Between these checkpoints, the model runs longer with less supervision. This is deliberate — it trades continuous supervision for focused human attention at moments with the highest leverage.
|
||||
|
||||
## What Quick Flow Skips
|
||||
|
||||
The full BMad Method produces a Product Brief, PRD, Architecture doc, and Epic/Story breakdown before any code is written. Quick Flow replaces all of that with a single tech-spec. This works because Quick Flow targets changes where:
|
||||
|
||||
- The product direction is already established
|
||||
- Architecture decisions are already made
|
||||
- A single developer can reason about the full scope
|
||||
- Requirements fit in one conversation
|
||||
|
||||
## Escalating to Full BMad Method
|
||||
|
||||
Quick Flow includes built-in guardrails for scope detection. When you run `bmad-quick-dev`, it evaluates signals like multi-component mentions, system-level language, and uncertainty about approach. If it detects the work is bigger than a quick flow:
|
||||
|
||||
- **Light escalation** — Recommends creating a plan before implementation
|
||||
- **Heavy escalation** — Recommends switching to the full BMad Method PRD process
|
||||
|
||||
You can also escalate manually at any time. Your tech-spec work carries forward — it becomes input for the broader planning process rather than being discarded.
|
||||
|
|
@ -5,119 +5,91 @@ sidebar:
|
|||
order: 5
|
||||
---
|
||||
|
||||
Use the **DEV agent** directly for bug fixes, refactorings, or small targeted changes that don't require the full BMad Method or Quick Flow.
|
||||
Use **Quick Dev** for bug fixes, refactorings, or small targeted changes that don't require the full BMad Method.
|
||||
|
||||
## When to Use This
|
||||
|
||||
- Bug fixes with a clear, known cause
|
||||
- Small refactorings (rename, extract, restructure) contained within a few files
|
||||
- Minor feature tweaks or configuration changes
|
||||
- Exploratory work to understand an unfamiliar codebase
|
||||
- Dependency updates
|
||||
|
||||
:::note[Prerequisites]
|
||||
- BMad Method installed (`npx bmad-method install`)
|
||||
- An AI-powered IDE (Claude Code, Cursor, or similar)
|
||||
:::
|
||||
|
||||
## Choose Your Approach
|
||||
|
||||
| Situation | Agent | Why |
|
||||
| --- | --- | --- |
|
||||
| Fix a specific bug or make a small, scoped change | **DEV agent** | Jumps straight into implementation without planning overhead |
|
||||
| Change touches several files or you want a written plan first | **Quick Flow Solo Dev** | Clarifies intent, plans, implements, and reviews in a single workflow so the agent stays aligned to your standards |
|
||||
|
||||
If you are unsure, start with the DEV agent. You can always escalate to Quick Flow if the change grows.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Invoke the DEV Agent
|
||||
### 1. Start a Fresh Chat
|
||||
|
||||
Start a **fresh chat** in your AI IDE and invoke the DEV agent skill:
|
||||
Open a **fresh chat session** in your AI IDE. Reusing a session from a previous workflow can cause context conflicts.
|
||||
|
||||
### 2. Give It Your Intent
|
||||
|
||||
Quick Dev accepts free-form intent — before, with, or after the invocation. Examples:
|
||||
|
||||
```text
|
||||
bmad-dev
|
||||
run quick-dev — Fix the login validation bug that allows empty passwords.
|
||||
```
|
||||
|
||||
This loads the agent's persona and capabilities into the session. If you decide you need Quick Flow instead, invoke the **Quick Flow Solo Dev** agent skill in a fresh chat:
|
||||
|
||||
```text
|
||||
bmad-quick-flow-solo-dev
|
||||
run quick-dev — fix https://github.com/org/repo/issues/42
|
||||
```
|
||||
|
||||
Once the Solo Dev agent is loaded, describe your change and tell it to run **quick-dev**. The workflow will clarify your intent, create a plan, implement the change, run a code review, and present results — all in a single run.
|
||||
```text
|
||||
run quick-dev — implement the intent in _bmad-output/implementation-artifacts/my-intent.md
|
||||
```
|
||||
|
||||
:::tip[Fresh Chats]
|
||||
Always start a new chat session when loading an agent. Reusing a session from a previous workflow can cause context conflicts.
|
||||
:::
|
||||
```text
|
||||
I think the problem is in the auth middleware, it's not checking token expiry.
|
||||
Let me look at it... yeah, src/auth/middleware.ts line 47 skips
|
||||
the exp check entirely. run quick-dev
|
||||
```
|
||||
|
||||
### 2. Describe the Change
|
||||
```text
|
||||
run quick-dev
|
||||
> What would you like to do?
|
||||
Refactor UserService to use async/await instead of callbacks.
|
||||
```
|
||||
|
||||
Tell the agent what you need in plain language. Be specific about the problem and, if you know it, where the relevant code lives.
|
||||
Plain text, file paths, GitHub issue URLs, bug tracker links — anything the LLM can resolve to a concrete intent.
|
||||
|
||||
:::note[Example Prompts]
|
||||
**Bug fix** -- "Fix the login validation bug that allows empty passwords. The validation logic is in `src/auth/validate.ts`."
|
||||
### 3. Answer Questions and Approve
|
||||
|
||||
**Refactoring** -- "Refactor the UserService to use async/await instead of callbacks."
|
||||
Quick Dev may ask clarifying questions or present a short spec for your approval before implementing. Answer its questions and approve when you're satisfied with the plan.
|
||||
|
||||
**Configuration change** -- "Update the CI pipeline to cache node_modules between runs."
|
||||
### 4. Review and Push
|
||||
|
||||
**Dependency update** -- "Upgrade the express dependency to the latest v5 release and fix any breaking changes."
|
||||
:::
|
||||
Quick Dev implements the change, reviews its own work, patches issues, and commits locally. When it's done, it opens the affected files in your editor.
|
||||
|
||||
You don't need to provide every detail. The agent will read the relevant source files and ask clarifying questions when needed.
|
||||
- Skim the diff to confirm the change matches your intent
|
||||
- If something looks off, tell the agent what to fix — it can iterate in the same session
|
||||
|
||||
### 3. Let the Agent Work
|
||||
|
||||
The agent will:
|
||||
|
||||
- Read and analyze the relevant source files
|
||||
- Propose a solution and explain its reasoning
|
||||
- Implement the change across the affected files
|
||||
- Run your project's test suite if one exists
|
||||
|
||||
If your project has tests, the agent runs them automatically after making changes and iterates until tests pass. For projects without a test suite, verify the change manually (run the app, hit the endpoint, check the output).
|
||||
|
||||
### 4. Review and Verify
|
||||
|
||||
Before committing, review what changed:
|
||||
|
||||
- Read through the diff to confirm the change matches your intent
|
||||
- Run the application or tests yourself to double-check
|
||||
- If something looks wrong, tell the agent what to fix -- it can iterate in the same session
|
||||
|
||||
Once satisfied, commit the changes with a clear message describing the fix.
|
||||
Once satisfied, push the commit. Quick Dev will offer to push and create a PR for you.
|
||||
|
||||
:::caution[If Something Breaks]
|
||||
If a committed change causes unexpected issues, use `git revert HEAD` to undo the last commit cleanly. Then start a fresh chat with the DEV agent to try a different approach.
|
||||
If a pushed change causes unexpected issues, use `git revert HEAD` to undo the last commit cleanly. Then start a fresh chat and run Quick Dev again to try a different approach.
|
||||
:::
|
||||
|
||||
## Learning Your Codebase
|
||||
|
||||
The DEV agent is also useful for exploring unfamiliar code. Load it in a fresh chat and ask questions:
|
||||
|
||||
:::note[Example Prompts]
|
||||
"Explain how the authentication system works in this codebase."
|
||||
|
||||
"Show me where error handling happens in the API layer."
|
||||
|
||||
"What does the `ProcessOrder` function do and what calls it?"
|
||||
:::
|
||||
|
||||
Use the agent to learn about your project, understand how components connect, and explore unfamiliar areas before making changes.
|
||||
|
||||
## What You Get
|
||||
|
||||
- Modified source files with the fix or refactoring applied
|
||||
- Passing tests (if your project has a test suite)
|
||||
- A clean commit describing the change
|
||||
- A ready-to-push commit with a conventional commit message
|
||||
|
||||
No planning artifacts are produced -- that's the point of this approach.
|
||||
## Deferred Work
|
||||
|
||||
Quick Dev keeps each run focused on a single goal. If your request contains multiple independent goals, or if the review surfaces pre-existing issues unrelated to your change, Quick Dev defers them to a file (`deferred-work.md` in your implementation artifacts directory) rather than trying to tackle everything at once.
|
||||
|
||||
Check this file after a run — it's your backlog of things to come back to. Each deferred item can be fed into a fresh Quick Dev run later.
|
||||
|
||||
## When to Upgrade to Formal Planning
|
||||
|
||||
Consider using [Quick Flow](../explanation/quick-flow.md) or the full BMad Method when:
|
||||
Consider using the full BMad Method when:
|
||||
|
||||
- The change affects multiple systems or requires coordinated updates across many files
|
||||
- You are unsure about the scope and need a spec to think it through
|
||||
- The fix keeps growing in complexity as you work on it
|
||||
- You are unsure about the scope and need requirements discovery first
|
||||
- You need documentation or architectural decisions recorded for the team
|
||||
|
||||
See [Quick Dev](../explanation/quick-dev.md) for more on how Quick Dev fits into the BMad Method.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ This page lists the default BMM (Agile suite) agents that install with BMad Meth
|
|||
| Scrum Master (Bob) | `bmad-sm` | `SP`, `CS`, `ER`, `CC` | Sprint Planning, Create Story, Epic Retrospective, Correct Course |
|
||||
| Developer (Amelia) | `bmad-dev` | `DS`, `CR` | Dev Story, Code Review |
|
||||
| QA Engineer (Quinn) | `bmad-qa` | `QA` | Automate (generate tests for existing features) |
|
||||
| Quick Flow Solo Dev (Barry) | `bmad-master` | `QS`, `QD`, `CR` | Quick Spec, Quick Dev, Code Review |
|
||||
| Quick Flow Solo Dev (Barry) | `bmad-master` | `QD`, `CR` | Quick Dev, Code Review |
|
||||
| UX Designer (Sally) | `bmad-ux-designer` | `CU` | Create UX Design |
|
||||
| Technical Writer (Paige) | `bmad-tech-writer` | `DP`, `WD`, `US`, `MG`, `VD`, `EC` | Document Project, Write Document, Update Standards, Mermaid Generate, Validate Doc, Explain Concept |
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ Agent menu triggers use two different invocation types. Knowing which type a tri
|
|||
|
||||
Most triggers load a structured workflow file. Type the trigger code and the agent starts the workflow, prompting you for input at each step.
|
||||
|
||||
Examples: `CP` (Create PRD), `DS` (Dev Story), `CA` (Create Architecture), `QS` (Quick Spec)
|
||||
Examples: `CP` (Create PRD), `DS` (Dev Story), `CA` (Create Architecture), `QD` (Quick Dev)
|
||||
|
||||
### Conversational triggers (arguments required)
|
||||
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ your-project/
|
|||
| ----------------- | ----------------------------- |
|
||||
| **Index/Landing** | `core-concepts/index.md` |
|
||||
| **Concept** | `what-are-agents.md` |
|
||||
| **Feature** | `quick-flow.md` |
|
||||
| **Feature** | `quick-dev.md` |
|
||||
| **Philosophy** | `why-solutioning-matters.md` |
|
||||
| **FAQ** | `established-projects-faq.md` |
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
title: "快速开发"
|
||||
description: 在不牺牲输出质量检查点的情况下减少人机交互的摩擦
|
||||
sidebar:
|
||||
order: 2
|
||||
---
|
||||
|
||||
输入意图,输出代码变更,尽可能少的人机交互轮次——同时不牺牲质量。
|
||||
|
||||
它让模型在检查点之间运行更长时间,只有在任务无法在没有人类判断的情况下安全继续时,或者需要审查最终结果时,才会让人类介入。
|
||||
|
||||

|
||||
|
||||
## 为什么需要这个功能
|
||||
|
||||
人机交互轮次既必要又昂贵。
|
||||
|
||||
当前的 LLM 仍然会以可预测的方式失败:它们误读意图、用自信的猜测填补空白、偏离到不相关的工作中,并生成嘈杂的审查输出。与此同时,持续的人工干预限制了开发速度。人类注意力是瓶颈。
|
||||
|
||||
`bmad-quick-dev` 重新平衡了这种权衡。它信任模型在更长的时间段内无监督运行,但前提是工作流已经创建了足够强的边界来确保安全。
|
||||
|
||||
## 核心设计
|
||||
|
||||
### 1. 首先压缩意图
|
||||
|
||||
工作流首先让人类和模型将请求压缩成一个连贯的目标。输入可以从粗略的意图表达开始,但在工作流自主运行之前,它必须变得足够小、足够清晰、没有矛盾。
|
||||
|
||||
意图可以以多种形式出现:几句话、一个错误追踪器链接、计划模式的输出、从聊天会话复制的文本,甚至来自 BMAD 自己的 `epics.md` 的故事编号。在最后一种情况下,工作流不会理解 BMAD 故事跟踪语义,但它仍然可以获取故事本身并继续执行。
|
||||
|
||||
这个工作流并不会消除人类的控制。它将其重新定位到少数几个高价值时刻:
|
||||
|
||||
- **意图澄清** - 将混乱的请求转化为一个没有隐藏矛盾的连贯目标
|
||||
- **规范审批** - 确认冻结的理解是正确要构建的东西
|
||||
- **最终产品审查** - 主要检查点,人类在最后决定结果是否可接受
|
||||
|
||||
### 2. 路由到最小安全路径
|
||||
|
||||
一旦目标清晰,工作流就会决定这是一个真正的单次变更还是需要更完整的路径。小的、零爆炸半径的变更可以直接进入实现。其他所有内容都需要经过规划,这样模型在独自运行更长时间之前就有更强的边界。
|
||||
|
||||
### 3. 以更少的监督运行更长时间
|
||||
|
||||
在那个路由决策之后,模型可以自己承担更多工作。在更完整的路径上,批准的规范成为模型在较少监督下执行的边界,这正是设计的全部意义。
|
||||
|
||||
### 4. 在正确的层诊断失败
|
||||
|
||||
如果实现是错误的,因为意图是错误的,修补代码是错误的修复。如果代码是错误的,因为规范太弱,修补差异也是错误的修复。工作流旨在诊断失败从系统的哪个层面进入,回到那个层面,并从那里重新生成。
|
||||
|
||||
审查发现用于确定问题来自意图、规范生成还是本地实现。只有真正的本地问题才会在本地修补。
|
||||
|
||||
### 5. 只在需要时让人类回来
|
||||
|
||||
意图访谈是人机交互,但它不是与重复检查点相同类型的中断。工作流试图将那些重复检查点保持在最低限度。在初始意图塑造之后,人类主要在工作流无法在没有判断的情况下安全继续时,以及在最后需要审查结果时才回来。
|
||||
|
||||
- **意图差距解决** - 当审查证明工作流无法安全推断出原本意图时重新介入
|
||||
|
||||
其他一切都是更长自主执行的候选。这种权衡是经过深思熟虑的。旧模式在持续监督上花费更多的人类注意力。快速开发在模型上投入更多信任,但将人类注意力保留在人类推理具有最高杠杆作用的时刻。
|
||||
|
||||
## 为什么审查系统很重要
|
||||
|
||||
审查阶段不仅仅是为了发现错误。它是为了在不破坏动力的情况下路由修正。
|
||||
|
||||
这个工作流在能够生成子智能体的平台上效果最好,或者至少可以通过命令行调用另一个 LLM 并等待结果。如果你的平台本身不支持这一点,你可以添加一个技能来做。无上下文子智能体是审查设计的基石。
|
||||
|
||||
智能体审查经常以两种方式出错:
|
||||
|
||||
- 它们生成太多发现,迫使人类在噪音中筛选
|
||||
- 它们通过提出不相关的问题并使每次运行变成临时清理项目来使当前变更脱轨
|
||||
|
||||
快速开发通过将审查视为分诊来解决这两个问题。
|
||||
|
||||
一些发现属于当前变更。一些不属于。如果一个发现是附带的而不是与当前工作有因果关系,工作流可以推迟它,而不是强迫人类立即处理它。这使运行保持专注,并防止随机的分支话题消耗注意力的预算。
|
||||
|
||||
那个分诊有时会不完美。这是可以接受的。通常,误判一些发现比用成千上万个低价值的审查评论淹没人类要好。系统正在优化信号质量,而不是详尽的召回率。
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
---
|
||||
title: "快速流程"
|
||||
description: 小型变更的快速通道 - 跳过完整方法论
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
跳过繁琐流程。快速流程通过单个工作流将你从意图带到可运行的代码 — 无需产品简报、无需 PRD、无需架构文档。
|
||||
|
||||
## 何时使用
|
||||
|
||||
- Bug 修复和补丁
|
||||
- 重构现有代码
|
||||
- 小型、易于理解的功能
|
||||
- 原型设计和探索性开发
|
||||
- 单智能体工作,一名开发者可以掌控完整范围
|
||||
|
||||
## 何时不使用
|
||||
|
||||
- 需要利益相关者对齐的新产品或平台
|
||||
- 跨越多个组件或团队的主要功能
|
||||
- 需要架构决策的工作(数据库架构、API 契约、服务边界)
|
||||
- 需求不明确或有争议的任何工作
|
||||
|
||||
:::caution[Scope Creep]
|
||||
如果你启动快速流程后发现范围超出预期,`bmad-quick-dev` 会检测到并提供升级选项。你可以在任何时间切换到完整的 PRD 工作流程,而不会丢失你的工作。
|
||||
:::
|
||||
|
||||
## 工作原理
|
||||
|
||||
运行 `bmad-quick-dev`,工作流会处理一切 — 澄清意图、规划、实现、审查和呈现结果。
|
||||
|
||||
### 1. 澄清意图
|
||||
|
||||
你描述想要什么。工作流将你的请求压缩成一个连贯的目标 — 足够小、足够清晰、没有矛盾,可以安全执行。意图可以来自多种来源:几句话、一个错误追踪器链接、计划模式输出、聊天会话文本,甚至来自你的史诗的故事编号。
|
||||
|
||||
### 2. 路由到最小安全路径
|
||||
|
||||
一旦目标清晰,工作流就会决定这是一个真正的单次变更还是需要更完整的路径。小的、零爆炸半径的变更可以直接进入实现。其他所有内容都需要经过规划,这样模型在自主运行之前就有更强的边界。
|
||||
|
||||
### 3. 规划和实现
|
||||
|
||||
在规划路径上,工作流生成完整的技术规范,包含有序的实现任务、Given/When/Then 格式的验收标准和测试策略。你批准规范后,它成为模型在较少监督下执行的边界。
|
||||
|
||||
### 4. 审查和呈现
|
||||
|
||||
实现后,工作流运行自检审计和差异的对抗性代码审查。审查充当分诊 — 与当前变更相关的发现会被处理,附带的发现会被推迟以保持运行专注。结果呈现供你确认。
|
||||
|
||||
### 人机交互检查点
|
||||
|
||||
工作流将人类控制重新定位到少数几个高价值时刻:
|
||||
|
||||
- **意图澄清** — 将混乱的请求转化为一个连贯的目标
|
||||
- **规范审批** — 确认冻结的理解是正确要构建的东西
|
||||
- **最终审查** — 决定结果是否可接受
|
||||
|
||||
在这些检查点之间,模型以更少的监督运行更长时间。这是经过深思熟虑的 — 它用持续监督换取在最高杠杆时刻的集中人类注意力。
|
||||
|
||||
## 快速流程跳过的内容
|
||||
|
||||
完整的 BMad 方法在编写任何代码之前会生成产品简报、PRD、架构文档和 Epic/Story 分解。Quick Flow 用单个技术规范替代所有这些。这之所以有效,是因为 Quick Flow 针对以下变更:
|
||||
|
||||
- 产品方向已确立
|
||||
- 架构决策已做出
|
||||
- 单个开发者可以推理完整范围
|
||||
- 需求可以在一次对话中涵盖
|
||||
|
||||
## 升级到完整 BMad 方法
|
||||
|
||||
快速流程包含内置的范围检测护栏。当你运行 `bmad-quick-dev` 时,它会评估多组件提及、系统级语言和方法不确定性等信号。如果检测到工作超出快速流程范围:
|
||||
|
||||
- **轻度升级** — 建议在实现前创建计划
|
||||
- **重度升级** — 建议切换到完整的 BMad 方法 PRD 流程
|
||||
|
||||
你也可以随时手动升级。你的技术规范工作会继续推进 — 它将成为更广泛规划过程的输入,而不是被丢弃。
|
||||
|
||||
---
|
||||
## 术语说明
|
||||
|
||||
- **Quick Flow**:快速流程。BMad 方法中用于小型变更的简化工作流程,跳过完整的产品规划和架构文档阶段。
|
||||
- **PRD**:Product Requirements Document,产品需求文档。详细描述产品功能、需求和验收标准的文档。
|
||||
- **Product Brief**:产品简报。概述产品愿景、目标和范围的高层文档。
|
||||
- **Architecture doc**:架构文档。描述系统架构、组件设计和技术决策的文档。
|
||||
- **Epic/Story**:史诗/故事。敏捷开发中的工作单元,Epic 是大型功能集合,Story 是具体用户故事。
|
||||
- **agent**:智能体。在人工智能与编程文档中,指具备自主决策或执行能力的单元。
|
||||
- **Scope Creep**:范围蔓延。项目范围在开发过程中逐渐扩大,超出原始计划的现象。
|
||||
- **tech-spec**:技术规范。详细描述技术实现方案、任务分解和验收标准的文档。
|
||||
- **Given/When/Then**:一种行为驱动开发(BDD)的测试场景描述格式,用于定义验收标准。
|
||||
- **adversarial review**:对抗性审查。一种代码审查方法,模拟攻击者视角以发现潜在问题和漏洞。
|
||||
- **stakeholder**:利益相关者。对项目有利益或影响的个人或组织。
|
||||
- **API contracts**:API 契约。定义 API 接口规范、请求/响应格式和行为约定的文档。
|
||||
- **service boundaries**:服务边界。定义服务职责范围和边界的架构概念。
|
||||
- **spikes**:探索性开发。用于探索技术可行性或解决方案的短期研究活动。
|
||||
|
|
@ -5,135 +5,103 @@ sidebar:
|
|||
order: 5
|
||||
---
|
||||
|
||||
直接使用 **DEV 智能体**进行 bug 修复、重构或小型针对性更改,这些操作不需要完整的 BMad Method 或 Quick Flow。
|
||||
使用 **Quick Dev** 进行 bug 修复、重构或小型针对性更改,这些操作不需要完整的 BMad Method。
|
||||
|
||||
## 何时使用此方法
|
||||
|
||||
- 原因明确且已知的 bug 修复
|
||||
- 包含在少数文件中的小型重构(重命名、提取、重组)
|
||||
- 次要功能调整或配置更改
|
||||
- 探索性工作,以了解不熟悉的代码库
|
||||
- 依赖更新
|
||||
|
||||
:::note[前置条件]
|
||||
- 已安装 BMad Method(`npx bmad-method install`)
|
||||
- AI 驱动的 IDE(Claude Code、Cursor 或类似工具)
|
||||
:::
|
||||
|
||||
## 选择你的方法
|
||||
|
||||
| 情况 | 智能体 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 修复特定 bug 或进行小型、范围明确的更改 | **DEV agent** | 直接进入实现,无需规划开销 |
|
||||
| 更改涉及多个文件,或希望先有书面计划 | **Quick Flow Solo Dev** | 在单个工作流中澄清意图、规划、实现和审查,使智能体与你的标准保持一致 |
|
||||
|
||||
如果不确定,请从 DEV 智能体开始。如果更改范围扩大,你始终可以升级到 Quick Flow。
|
||||
|
||||
## 步骤
|
||||
|
||||
### 1. 加载 DEV 智能体
|
||||
### 1. 启动新的聊天
|
||||
|
||||
在 AI IDE 中启动一个**新的聊天**,并使用斜杠命令加载 DEV 智能体:
|
||||
在 AI IDE 中打开一个**新的聊天会话**。重用之前工作流的会话可能导致上下文冲突。
|
||||
|
||||
### 2. 提供你的意图
|
||||
|
||||
Quick Dev 接受自由形式的意图——可以在调用之前、同时或之后提供。示例:
|
||||
|
||||
```text
|
||||
/bmad-agent-bmm-dev
|
||||
run quick-dev — 修复允许空密码的登录验证 bug。
|
||||
```
|
||||
|
||||
这会将智能体的角色和能力加载到会话中。如果你决定需要 Quick Flow,请在新的聊天中加载 **Quick Flow Solo Dev** 智能体:
|
||||
|
||||
```text
|
||||
/bmad-agent-bmm-quick-flow-solo-dev
|
||||
run quick-dev — fix https://github.com/org/repo/issues/42
|
||||
```
|
||||
|
||||
加载 Solo Dev 智能体后,描述你的更改并告诉它运行 **quick-dev**。工作流将澄清你的意图、创建计划、实现更改、运行代码审查并呈现结果 — 全部在单次运行中完成。
|
||||
```text
|
||||
run quick-dev — 实现 _bmad-output/implementation-artifacts/my-intent.md 中的意图
|
||||
```
|
||||
|
||||
:::tip[新聊天]
|
||||
加载智能体时始终启动新的聊天会话。重用之前工作流的会话可能导致上下文冲突。
|
||||
:::
|
||||
```text
|
||||
我觉得问题在 auth 中间件,它没有检查 token 过期。
|
||||
让我看看... 是的,src/auth/middleware.ts 第 47 行完全跳过了
|
||||
exp 检查。run quick-dev
|
||||
```
|
||||
|
||||
### 2. 描述更改
|
||||
```text
|
||||
run quick-dev
|
||||
> 你想做什么?
|
||||
重构 UserService 以使用 async/await 而不是回调。
|
||||
```
|
||||
|
||||
用通俗语言告诉智能体你需要什么。具体说明问题,如果你知道相关代码的位置,也请说明。
|
||||
纯文本、文件路径、GitHub issue URL、bug 跟踪器链接——任何 LLM 能解析为具体意图的内容都可以。
|
||||
|
||||
:::note[示例提示词]
|
||||
**Bug 修复** -- "修复允许空密码的登录验证 bug。验证逻辑位于 `src/auth/validate.ts`。"
|
||||
### 3. 回答问题并批准
|
||||
|
||||
**重构** -- "重构 UserService 以使用 async/await 而不是回调。"
|
||||
Quick Dev 可能会提出澄清问题,或在实现之前呈现简短的规范供你批准。回答它的问题,并在你对计划满意时批准。
|
||||
|
||||
**配置更改** -- "更新 CI 流水线以在运行之间缓存 node_modules。"
|
||||
### 4. 审查和推送
|
||||
|
||||
**依赖更新** -- "将 express 依赖升级到最新的 v5 版本并修复任何破坏性更改。"
|
||||
:::
|
||||
Quick Dev 实现更改、审查自己的工作、修复问题,并在本地提交。完成后,它会在编辑器中打开受影响的文件。
|
||||
|
||||
你不需要提供每个细节。智能体会读取相关的源文件,并在需要时提出澄清问题。
|
||||
|
||||
### 3. 让智能体工作
|
||||
|
||||
智能体将:
|
||||
|
||||
- 读取并分析相关的源文件
|
||||
- 提出解决方案并解释其推理
|
||||
- 在受影响的文件中实现更改
|
||||
- 如果存在测试套件,则运行项目的测试套件
|
||||
|
||||
如果你的项目有测试,智能体会在进行更改后自动运行它们,并迭代直到测试通过。对于没有测试套件的项目,请手动验证更改(运行应用、访问端点、检查输出)。
|
||||
|
||||
### 4. 审查和验证
|
||||
|
||||
在提交之前,审查更改内容:
|
||||
|
||||
- 通读 diff 以确认更改符合你的意图
|
||||
- 自己运行应用程序或测试以再次检查
|
||||
- 浏览 diff 以确认更改符合你的意图
|
||||
- 如果看起来有问题,告诉智能体需要修复什么——它可以在同一会话中迭代
|
||||
|
||||
满意后,使用描述修复的清晰消息提交更改。
|
||||
满意后,推送提交。Quick Dev 会提供推送和创建 PR 的选项。
|
||||
|
||||
:::caution[如果出现问题]
|
||||
如果提交的更改导致意外问题,请使用 `git revert HEAD` 干净地撤销最后一次提交。然后启动与 DEV 智能体的新聊天以尝试不同的方法。
|
||||
如果推送的更改导致意外问题,请使用 `git revert HEAD` 干净地撤销最后一次提交。然后启动新聊天并再次运行 Quick Dev 以尝试不同的方法。
|
||||
:::
|
||||
|
||||
## 学习你的代码库
|
||||
|
||||
DEV 智能体也适用于探索不熟悉的代码。在新的聊天中加载它并提出问题:
|
||||
|
||||
:::note[示例提示词]
|
||||
"解释此代码库中的身份验证系统是如何工作的。"
|
||||
|
||||
"向我展示 API 层中的错误处理发生在哪里。"
|
||||
|
||||
"`ProcessOrder` 函数的作用是什么,什么调用了它?"
|
||||
:::
|
||||
|
||||
使用智能体了解你的项目,理解组件如何连接,并在进行更改之前探索不熟悉的区域。
|
||||
|
||||
## 你将获得
|
||||
|
||||
- 已应用修复或重构的修改后的源文件
|
||||
- 通过的测试(如果你的项目有测试套件)
|
||||
- 描述更改的干净提交
|
||||
- 带有约定式提交消息的准备推送的提交
|
||||
|
||||
不会生成规划产物——这就是这种方法的意义所在。
|
||||
## 延迟工作
|
||||
|
||||
Quick Dev 保持每次运行聚焦于单一目标。如果你的请求包含多个独立目标,或者审查发现了与你的更改无关的已有问题,Quick Dev 会将它们延迟到一个文件中(实现产物目录中的 `deferred-work.md`),而不是试图一次解决所有问题。
|
||||
|
||||
运行后检查此文件——它是你的待办事项积压。每个延迟项目都可以稍后输入到新的 Quick Dev 运行中。
|
||||
|
||||
## 何时升级到正式规划
|
||||
|
||||
在以下情况下考虑使用 [Quick Flow](../explanation/quick-flow.md) 或完整的 BMad Method:
|
||||
在以下情况下考虑使用完整的 BMad Method:
|
||||
|
||||
- 更改影响多个系统或需要在许多文件中进行协调更新
|
||||
- 你不确定范围,需要规范来理清思路
|
||||
- 修复在工作过程中变得越来越复杂
|
||||
- 你不确定范围,需要先进行需求发现
|
||||
- 你需要为团队记录文档或架构决策
|
||||
|
||||
参见 [Quick Dev](../explanation/quick-dev.md) 了解 Quick Dev 如何融入 BMad Method。
|
||||
|
||||
---
|
||||
## 术语说明
|
||||
|
||||
- **agent**:智能体。在人工智能与编程文档中,指具备自主决策或执行能力的单元。
|
||||
- **Quick Flow**:快速流程。BMad Method 中的一种统一工作流程,用于快速澄清意图、规划、实现和审查小型更改。
|
||||
- **Quick Dev**:快速开发。BMad Method 中的快速工作流,用于小型更改的完整实现周期。
|
||||
- **refactoring**:重构。在不改变代码外部行为的情况下改进其内部结构的过程。
|
||||
- **breaking changes**:破坏性更改。可能导致现有代码或功能不再正常工作的更改。
|
||||
- **test suite**:测试套件。一组用于验证软件功能的测试用例集合。
|
||||
- **CI pipeline**:CI 流水线。持续集成流水线,用于自动化构建、测试和部署代码。
|
||||
- **async/await**:异步编程语法。JavaScript/TypeScript 中用于处理异步操作的语法糖。
|
||||
- **callbacks**:回调函数。作为参数传递给其他函数并在适当时候被调用的函数。
|
||||
- **endpoint**:端点。API 中可访问的特定 URL 路径。
|
||||
- **diff**:差异。文件或代码更改前后的对比。
|
||||
- **commit**:提交。将更改保存到版本控制系统的操作。
|
||||
- **git revert HEAD**:Git 命令,用于撤销最后一次提交。
|
||||
- **conventional commit**:约定式提交。遵循标准格式的提交消息。
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ sidebar:
|
|||
| Scrum Master (Bob) | `SP`, `CS`, `ER`, `CC` | 冲刺规划、创建用户故事、史诗回顾、纠正方向 |
|
||||
| Developer (Amelia) | `DS`, `CR` | 开发用户故事、代码评审 |
|
||||
| QA Engineer (Quinn) | `QA` | 自动化(为现有功能生成测试) |
|
||||
| Quick Flow Solo Dev (Barry) | `QS`, `QD`, `CR` | 快速规格、快速开发、代码评审 |
|
||||
| Quick Flow Solo Dev (Barry) | `QD`, `CR` | 快速开发、代码评审 |
|
||||
| UX Designer (Sally) | `CU` | 创建 UX 设计 |
|
||||
| Technical Writer (Paige) | `DP`, `WD`, `US`, `MG`, `VD`, `EC` | 文档化项目、撰写文档、更新标准、Mermaid 生成、验证文档、解释概念 |
|
||||
|
||||
|
|
|
|||
|
|
@ -39,12 +39,13 @@
|
|||
"lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
|
||||
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
||||
"prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
|
||||
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run validate:refs",
|
||||
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run validate:refs && npm run validate:skills",
|
||||
"rebundle": "node tools/cli/bundlers/bundle-web.js rebundle",
|
||||
"test": "npm run test:refs && npm run test:install && npm run lint && npm run lint:md && npm run format:check",
|
||||
"test:install": "node test/test-installation-components.js",
|
||||
"test:refs": "node test/test-file-refs-csv.js",
|
||||
"validate:refs": "node tools/validate-file-refs.js --strict"
|
||||
"validate:refs": "node tools/validate-file-refs.js --strict",
|
||||
"validate:skills": "node tools/validate-skills.js --strict"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,cjs,mjs}": [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: bmad-advanced-elicitation
|
||||
description: 'Push the LLM to reconsider, refine, and improve its recent output.'
|
||||
description: 'Push the LLM to reconsider, refine, and improve its recent output. Use when user asks for deeper critique or mentions a known deeper critique method, e.g. socratic, first principles, pre-mortem, red team.'
|
||||
---
|
||||
|
||||
Follow the instructions in ./workflow.md.
|
||||
|
|
|
|||
|
|
@ -1635,6 +1635,15 @@ async function runTests() {
|
|||
);
|
||||
await fs.writeFile(path.join(taskSkillDir29, 'workflow.md'), '# Task Skill\n\nSkill in tasks\n');
|
||||
|
||||
// --- Native agent entrypoint inside agents/: core/agents/bmad-tea/ ---
|
||||
const nativeAgentDir29 = path.join(tempFixture29, 'core', 'agents', 'bmad-tea');
|
||||
await fs.ensureDir(nativeAgentDir29);
|
||||
await fs.writeFile(path.join(nativeAgentDir29, 'bmad-skill-manifest.yaml'), 'type: agent\ncanonicalId: bmad-tea\n');
|
||||
await fs.writeFile(
|
||||
path.join(nativeAgentDir29, 'SKILL.md'),
|
||||
'---\nname: bmad-tea\ndescription: Native agent entrypoint\n---\n\nPresent a capability menu.\n',
|
||||
);
|
||||
|
||||
// Minimal agent so core module is detected
|
||||
await fs.ensureDir(path.join(tempFixture29, 'core', 'agents'));
|
||||
const minimalAgent29 = '<agent name="Test" title="T"><persona>p</persona></agent>';
|
||||
|
|
@ -1664,6 +1673,17 @@ async function runTests() {
|
|||
const inTasks29 = generator29.tasks.find((t) => t.name === 'task-skill');
|
||||
assert(inTasks29 === undefined, 'Skill in tasks/ dir does NOT appear in tasks[]');
|
||||
|
||||
// Native agent entrypoint should be installed as a verbatim skill and also
|
||||
// remain visible to the agent manifest pipeline.
|
||||
const nativeAgentEntry29 = generator29.skills.find((s) => s.canonicalId === 'bmad-tea');
|
||||
assert(nativeAgentEntry29 !== undefined, 'Native type:agent SKILL.md dir appears in skills[]');
|
||||
assert(
|
||||
nativeAgentEntry29 && nativeAgentEntry29.path.includes('agents/bmad-tea/SKILL.md'),
|
||||
'Native type:agent SKILL.md path points to the agent directory entrypoint',
|
||||
);
|
||||
const nativeAgentManifest29 = generator29.agents.find((a) => a.name === 'bmad-tea');
|
||||
assert(nativeAgentManifest29 !== undefined, 'Native type:agent SKILL.md dir appears in agents[] for agent metadata');
|
||||
|
||||
// Regular workflow should be in workflows, NOT in skills
|
||||
const regularWf29 = generator29.workflows.find((w) => w.name === 'Regular Workflow');
|
||||
assert(regularWf29 !== undefined, 'Regular type:workflow appears in workflows[]');
|
||||
|
|
@ -1689,6 +1709,37 @@ async function runTests() {
|
|||
|
||||
const scannedModules29 = await generator29.scanInstalledModules(tempFixture29);
|
||||
assert(scannedModules29.includes('skill-only-mod'), 'scanInstalledModules recognizes skill-only module');
|
||||
|
||||
// Test scanInstalledModules recognizes native-agent-only modules too
|
||||
const agentOnlyModDir29 = path.join(tempFixture29, 'agent-only-mod');
|
||||
await fs.ensureDir(path.join(agentOnlyModDir29, 'deep', 'nested', 'bmad-tea'));
|
||||
await fs.writeFile(path.join(agentOnlyModDir29, 'deep', 'nested', 'bmad-tea', 'bmad-skill-manifest.yaml'), 'type: agent\n');
|
||||
await fs.writeFile(
|
||||
path.join(agentOnlyModDir29, 'deep', 'nested', 'bmad-tea', 'SKILL.md'),
|
||||
'---\nname: bmad-tea\ndescription: desc\n---\n\nAgent menu.\n',
|
||||
);
|
||||
|
||||
const rescannedModules29 = await generator29.scanInstalledModules(tempFixture29);
|
||||
assert(rescannedModules29.includes('agent-only-mod'), 'scanInstalledModules recognizes native-agent-only module');
|
||||
|
||||
// Test scanInstalledModules recognizes multi-entry manifests keyed under SKILL.md
|
||||
const multiEntryModDir29 = path.join(tempFixture29, 'multi-entry-mod');
|
||||
await fs.ensureDir(path.join(multiEntryModDir29, 'deep', 'nested', 'bmad-tea'));
|
||||
await fs.writeFile(
|
||||
path.join(multiEntryModDir29, 'deep', 'nested', 'bmad-tea', 'bmad-skill-manifest.yaml'),
|
||||
'SKILL.md:\n type: agent\n canonicalId: bmad-tea\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(multiEntryModDir29, 'deep', 'nested', 'bmad-tea', 'SKILL.md'),
|
||||
'---\nname: bmad-tea\ndescription: desc\n---\n\nAgent menu.\n',
|
||||
);
|
||||
|
||||
const rescannedModules29b = await generator29.scanInstalledModules(tempFixture29);
|
||||
assert(rescannedModules29b.includes('multi-entry-mod'), 'scanInstalledModules recognizes multi-entry native-agent module');
|
||||
|
||||
// skill-manifest.csv should include the native agent entrypoint
|
||||
const skillManifestCsv29 = await fs.readFile(path.join(tempFixture29, '_config', 'skill-manifest.csv'), 'utf8');
|
||||
assert(skillManifestCsv29.includes('bmad-tea'), 'skill-manifest.csv includes native type:agent SKILL.md entrypoint');
|
||||
} catch (error) {
|
||||
assert(false, 'Unified skill scanner test succeeds', error.message);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -82,11 +82,11 @@ class DependencyResolver {
|
|||
// Check if this is a source directory (has 'src' subdirectory)
|
||||
const srcDir = path.join(bmadDir, 'src');
|
||||
if (await fs.pathExists(srcDir)) {
|
||||
// Source directory structure: src/core or src/bmm
|
||||
// Source directory structure: src/core-skills or src/bmm-skills
|
||||
if (module === 'core') {
|
||||
moduleDir = path.join(srcDir, 'core');
|
||||
moduleDir = path.join(srcDir, 'core-skills');
|
||||
} else if (module === 'bmm') {
|
||||
moduleDir = path.join(srcDir, 'bmm');
|
||||
moduleDir = path.join(srcDir, 'bmm-skills');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -401,8 +401,8 @@ class DependencyResolver {
|
|||
const bmadPath = dep.dependency.replace(/^bmad\//, '');
|
||||
|
||||
// Try to resolve as if it's in src structure
|
||||
// bmad/core/tasks/foo.md -> src/core/tasks/foo.md
|
||||
// bmad/bmm/tasks/bar.md -> src/bmm/tasks/bar.md (bmm is directly under src/)
|
||||
// bmad/core/tasks/foo.md -> src/core-skills/tasks/foo.md
|
||||
// bmad/bmm/tasks/bar.md -> src/bmm-skills/tasks/bar.md (bmm is directly under src/)
|
||||
// bmad/cis/agents/bar.md -> src/modules/cis/agents/bar.md
|
||||
|
||||
if (bmadPath.startsWith('core/')) {
|
||||
|
|
@ -584,11 +584,11 @@ class DependencyResolver {
|
|||
const relative = path.relative(bmadDir, filePath);
|
||||
const parts = relative.split(path.sep);
|
||||
|
||||
// Handle source directory structure (src/core, src/bmm, or src/modules/xxx)
|
||||
// Handle source directory structure (src/core-skills, src/bmm-skills, or src/modules/xxx)
|
||||
if (parts[0] === 'src') {
|
||||
if (parts[1] === 'core') {
|
||||
if (parts[1] === 'core-skills') {
|
||||
return 'core';
|
||||
} else if (parts[1] === 'bmm') {
|
||||
} else if (parts[1] === 'bmm-skills') {
|
||||
return 'bmm';
|
||||
} else if (parts[1] === 'modules' && parts.length > 2) {
|
||||
return parts[2];
|
||||
|
|
@ -631,11 +631,11 @@ class DependencyResolver {
|
|||
let moduleBase;
|
||||
|
||||
// Check if file is in source directory structure
|
||||
if (file.includes('/src/core/') || file.includes('/src/bmm/')) {
|
||||
if (file.includes('/src/core-skills/') || file.includes('/src/bmm-skills/')) {
|
||||
if (module === 'core') {
|
||||
moduleBase = path.join(bmadDir, 'src', 'core');
|
||||
moduleBase = path.join(bmadDir, 'src', 'core-skills');
|
||||
} else if (module === 'bmm') {
|
||||
moduleBase = path.join(bmadDir, 'src', 'bmm');
|
||||
moduleBase = path.join(bmadDir, 'src', 'bmm-skills');
|
||||
}
|
||||
} else {
|
||||
moduleBase = module === 'core' ? path.join(bmadDir, 'core') : path.join(bmadDir, 'modules', module);
|
||||
|
|
|
|||
|
|
@ -1789,8 +1789,8 @@ class Installer {
|
|||
.filter((entry) => entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs' && entry.name !== '_memory')
|
||||
.map((entry) => entry.name);
|
||||
|
||||
// Add core module to scan (it's installed at root level as _config, but we check src/core)
|
||||
const coreModulePath = getSourcePath('core');
|
||||
// Add core module to scan (it's installed at root level as _config, but we check src/core-skills)
|
||||
const coreModulePath = getSourcePath('core-skills');
|
||||
const modulePaths = new Map();
|
||||
|
||||
// Map all module source paths
|
||||
|
|
@ -2709,7 +2709,7 @@ class Installer {
|
|||
// Get source path
|
||||
let sourcePath;
|
||||
if (moduleId === 'core') {
|
||||
sourcePath = getSourcePath('core');
|
||||
sourcePath = getSourcePath('core-skills');
|
||||
} else {
|
||||
// First check if it's in the custom cache
|
||||
if (customModuleSources.has(moduleId)) {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,29 @@ class ManifestGenerator {
|
|||
return getInstallToBmadShared(manifest, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Native SKILL.md entrypoints can be packaged as either skills or agents.
|
||||
* Both need verbatim installation for skill-format IDEs.
|
||||
* @param {string|null} artifactType - Manifest type resolved for SKILL.md
|
||||
* @returns {boolean} True when the directory should be installed verbatim
|
||||
*/
|
||||
isNativeSkillDirType(artifactType) {
|
||||
return artifactType === 'skill' || artifactType === 'agent';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a loaded bmad-skill-manifest.yaml declares a native
|
||||
* SKILL.md entrypoint, either as a single-entry manifest or a multi-entry map.
|
||||
* @param {Object|null} manifest - Loaded manifest
|
||||
* @returns {boolean} True when the manifest contains a native skill/agent entrypoint
|
||||
*/
|
||||
hasNativeSkillManifest(manifest) {
|
||||
if (!manifest) return false;
|
||||
if (manifest.__single) return this.isNativeSkillDirType(manifest.__single.type);
|
||||
|
||||
return Object.values(manifest).some((entry) => this.isNativeSkillDirType(entry?.type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean text for CSV output by normalizing whitespace.
|
||||
* Note: Quote escaping is handled by escapeCsv() at write time.
|
||||
|
|
@ -146,9 +169,10 @@ class ManifestGenerator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Recursively walk a module directory tree, collecting skill directories.
|
||||
* A skill directory is one that contains both a bmad-skill-manifest.yaml with
|
||||
* type: skill AND a SKILL.md file with name/description frontmatter.
|
||||
* Recursively walk a module directory tree, collecting native SKILL.md entrypoints.
|
||||
* A native entrypoint directory is one that contains both a
|
||||
* bmad-skill-manifest.yaml with type: skill or type: agent AND a SKILL.md file
|
||||
* with name/description frontmatter.
|
||||
* Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths).
|
||||
*/
|
||||
async collectSkills() {
|
||||
|
|
@ -172,11 +196,11 @@ class ManifestGenerator {
|
|||
// Check this directory for skill manifest
|
||||
const manifest = await this.loadSkillManifest(dir);
|
||||
|
||||
// Determine if this directory is a skill (type: skill in manifest)
|
||||
// Determine if this directory is a native SKILL.md entrypoint
|
||||
const skillFile = 'SKILL.md';
|
||||
const artifactType = this.getArtifactType(manifest, skillFile);
|
||||
|
||||
if (artifactType === 'skill' || artifactType === 'agent') {
|
||||
if (this.isNativeSkillDirType(artifactType)) {
|
||||
const skillMdPath = path.join(dir, 'SKILL.md');
|
||||
const dirName = path.basename(dir);
|
||||
|
||||
|
|
@ -190,11 +214,12 @@ class ManifestGenerator {
|
|||
? `${this.bmadFolderName}/${moduleName}/${relativePath}/${skillFile}`
|
||||
: `${this.bmadFolderName}/${moduleName}/${skillFile}`;
|
||||
|
||||
// Skills derive canonicalId from directory name — never from manifest
|
||||
// (agent-type skills legitimately use canonicalId for agent-manifest mapping, so skip warning)
|
||||
// Native SKILL.md entrypoints derive canonicalId from directory name.
|
||||
// Agent entrypoints may keep canonicalId metadata for compatibility, so
|
||||
// only warn for non-agent SKILL.md directories.
|
||||
if (manifest && manifest.__single && manifest.__single.canonicalId && artifactType !== 'agent') {
|
||||
console.warn(
|
||||
`Warning: Skill manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for skills (directory name is the canonical ID)`,
|
||||
`Warning: Native entrypoint manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for SKILL.md directories (directory name is the canonical ID)`,
|
||||
);
|
||||
}
|
||||
const canonicalId = dirName;
|
||||
|
|
@ -224,21 +249,21 @@ class ManifestGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
// Warn if manifest says type:skill but directory was not claimed
|
||||
// Warn if manifest says this is a native entrypoint but the directory was not claimed
|
||||
if (manifest && !this.skillClaimedDirs.has(dir)) {
|
||||
let hasSkillType = false;
|
||||
let hasNativeSkillType = false;
|
||||
if (manifest.__single) {
|
||||
hasSkillType = manifest.__single.type === 'skill' || manifest.__single.type === 'agent';
|
||||
hasNativeSkillType = this.isNativeSkillDirType(manifest.__single.type);
|
||||
} else {
|
||||
for (const key of Object.keys(manifest)) {
|
||||
if (manifest[key]?.type === 'skill' || manifest[key]?.type === 'agent') {
|
||||
hasSkillType = true;
|
||||
if (this.isNativeSkillDirType(manifest[key]?.type)) {
|
||||
hasNativeSkillType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasSkillType && debug) {
|
||||
console.log(`[DEBUG] collectSkills: dir has type:skill manifest but failed validation: ${dir}`);
|
||||
if (hasNativeSkillType && debug) {
|
||||
console.log(`[DEBUG] collectSkills: dir has native SKILL.md manifest but failed validation: ${dir}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1359,7 +1384,8 @@ class ManifestGenerator {
|
|||
const hasTasks = await fs.pathExists(path.join(modulePath, 'tasks'));
|
||||
const hasTools = await fs.pathExists(path.join(modulePath, 'tools'));
|
||||
|
||||
// Check for skill-only modules: recursive scan for bmad-skill-manifest.yaml with type: skill
|
||||
// Check for native-entrypoint-only modules: recursive scan for
|
||||
// bmad-skill-manifest.yaml with type: skill or type: agent
|
||||
let hasSkills = false;
|
||||
if (!hasAgents && !hasWorkflows && !hasTasks && !hasTools) {
|
||||
hasSkills = await this._hasSkillManifestRecursive(modulePath);
|
||||
|
|
@ -1378,7 +1404,8 @@ class ManifestGenerator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Recursively check if a directory tree contains a bmad-skill-manifest.yaml with type: skill.
|
||||
* Recursively check if a directory tree contains a bmad-skill-manifest.yaml that
|
||||
* declares a native SKILL.md entrypoint (type: skill or type: agent).
|
||||
* Skips directories starting with . or _.
|
||||
* @param {string} dir - Directory to search
|
||||
* @returns {boolean} True if a skill manifest is found
|
||||
|
|
@ -1393,10 +1420,7 @@ class ManifestGenerator {
|
|||
|
||||
// Check for manifest in this directory
|
||||
const manifest = await this.loadSkillManifest(dir);
|
||||
if (manifest) {
|
||||
const type = this.getArtifactType(manifest, 'workflow.md');
|
||||
if (type === 'skill') return true;
|
||||
}
|
||||
if (this.hasNativeSkillManifest(manifest)) return true;
|
||||
|
||||
// Recurse into subdirectories
|
||||
for (const entry of entries) {
|
||||
|
|
|
|||
|
|
@ -764,10 +764,10 @@ class Manifest {
|
|||
const configs = {};
|
||||
|
||||
for (const moduleName of modules) {
|
||||
// Handle core module differently - it's in src/core not src/modules/core
|
||||
// Handle core module differently - it's in src/core-skills not src/modules/core
|
||||
const configPath =
|
||||
moduleName === 'core'
|
||||
? path.join(process.cwd(), 'src', 'core', 'config.yaml')
|
||||
? path.join(process.cwd(), 'src', 'core-skills', 'config.yaml')
|
||||
: path.join(process.cwd(), 'src', 'modules', moduleName, 'config.yaml');
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -630,7 +630,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
|||
}
|
||||
|
||||
/**
|
||||
* Install verbatim skill directories (type: skill entries from skill-manifest.csv).
|
||||
* Install verbatim native SKILL.md directories from skill-manifest.csv.
|
||||
* Copies the entire source directory as-is into the IDE skill directory.
|
||||
* The source SKILL.md is used directly — no frontmatter transformation or file generation.
|
||||
* @param {string} projectDir - Project directory
|
||||
|
|
|
|||
|
|
@ -146,13 +146,13 @@ When running any workflow:
|
|||
transformWorkflowPath(workflowPath) {
|
||||
let transformed = workflowPath;
|
||||
|
||||
if (workflowPath.includes('/src/bmm/')) {
|
||||
const match = workflowPath.match(/\/src\/bmm\/(.+)/);
|
||||
if (workflowPath.includes('/src/bmm-skills/')) {
|
||||
const match = workflowPath.match(/\/src\/bmm-skills\/(.+)/);
|
||||
if (match) {
|
||||
transformed = `{project-root}/${this.bmadFolderName}/bmm/${match[1]}`;
|
||||
}
|
||||
} else if (workflowPath.includes('/src/core/')) {
|
||||
const match = workflowPath.match(/\/src\/core\/(.+)/);
|
||||
} else if (workflowPath.includes('/src/core-skills/')) {
|
||||
const match = workflowPath.match(/\/src\/core-skills\/(.+)/);
|
||||
if (match) {
|
||||
transformed = `{project-root}/${this.bmadFolderName}/core/${match[1]}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ class ModuleManager {
|
|||
|
||||
/**
|
||||
* List all available modules (excluding core which is always installed)
|
||||
* bmm is the only built-in module, directly under src/bmm
|
||||
* bmm is the only built-in module, directly under src/bmm-skills
|
||||
* All other modules come from external-official-modules.yaml
|
||||
* @returns {Object} Object with modules array and customModules array
|
||||
*/
|
||||
|
|
@ -195,10 +195,10 @@ class ModuleManager {
|
|||
const modules = [];
|
||||
const customModules = [];
|
||||
|
||||
// Add built-in bmm module (directly under src/bmm)
|
||||
const bmmPath = getSourcePath('bmm');
|
||||
// Add built-in bmm module (directly under src/bmm-skills)
|
||||
const bmmPath = getSourcePath('bmm-skills');
|
||||
if (await fs.pathExists(bmmPath)) {
|
||||
const bmmInfo = await this.getModuleInfo(bmmPath, 'bmm', 'src/bmm');
|
||||
const bmmInfo = await this.getModuleInfo(bmmPath, 'bmm', 'src/bmm-skills');
|
||||
if (bmmInfo) {
|
||||
modules.push(bmmInfo);
|
||||
}
|
||||
|
|
@ -251,7 +251,8 @@ class ModuleManager {
|
|||
}
|
||||
|
||||
// Mark as custom if it's using custom.yaml OR if it's outside src/bmm or src/core
|
||||
const isCustomSource = sourceDescription !== 'src/bmm' && sourceDescription !== 'src/core' && sourceDescription !== 'src/modules';
|
||||
const isCustomSource =
|
||||
sourceDescription !== 'src/bmm-skills' && sourceDescription !== 'src/core-skills' && sourceDescription !== 'src/modules';
|
||||
const moduleInfo = {
|
||||
id: defaultName,
|
||||
path: modulePath,
|
||||
|
|
@ -300,9 +301,9 @@ class ModuleManager {
|
|||
return this.customModulePaths.get(moduleCode);
|
||||
}
|
||||
|
||||
// Check for built-in bmm module (directly under src/bmm)
|
||||
// Check for built-in bmm module (directly under src/bmm-skills)
|
||||
if (moduleCode === 'bmm') {
|
||||
const bmmPath = getSourcePath('bmm');
|
||||
const bmmPath = getSourcePath('bmm-skills');
|
||||
if (await fs.pathExists(bmmPath)) {
|
||||
return bmmPath;
|
||||
}
|
||||
|
|
@ -1141,10 +1142,10 @@ class ModuleManager {
|
|||
const projectRoot = path.dirname(bmadDir);
|
||||
const emptyResult = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
|
||||
|
||||
// Special handling for core module - it's in src/core not src/modules
|
||||
// Special handling for core module - it's in src/core-skills not src/modules
|
||||
let sourcePath;
|
||||
if (moduleName === 'core') {
|
||||
sourcePath = getSourcePath('core');
|
||||
sourcePath = getSourcePath('core-skills');
|
||||
} else {
|
||||
sourcePath = await this.findModuleSource(moduleName, { silent: true });
|
||||
if (!sourcePath) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ function findProjectRoot(startPath = __dirname) {
|
|||
try {
|
||||
const pkg = fs.readJsonSync(packagePath);
|
||||
// Check if this is the BMAD project
|
||||
if (pkg.name === 'bmad-method' || fs.existsSync(path.join(currentPath, 'src', 'core'))) {
|
||||
if (pkg.name === 'bmad-method' || fs.existsSync(path.join(currentPath, 'src', 'core-skills'))) {
|
||||
return currentPath;
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -24,8 +24,8 @@ function findProjectRoot(startPath = __dirname) {
|
|||
}
|
||||
}
|
||||
|
||||
// Also check for src/core as a marker
|
||||
if (fs.existsSync(path.join(currentPath, 'src', 'core', 'agents'))) {
|
||||
// Also check for src/core-skills as a marker
|
||||
if (fs.existsSync(path.join(currentPath, 'src', 'core-skills', 'agents'))) {
|
||||
return currentPath;
|
||||
}
|
||||
|
||||
|
|
@ -61,10 +61,10 @@ function getSourcePath(...segments) {
|
|||
*/
|
||||
function getModulePath(moduleName, ...segments) {
|
||||
if (moduleName === 'core') {
|
||||
return getSourcePath('core', ...segments);
|
||||
return getSourcePath('core-skills', ...segments);
|
||||
}
|
||||
if (moduleName === 'bmm') {
|
||||
return getSourcePath('bmm', ...segments);
|
||||
return getSourcePath('bmm-skills', ...segments);
|
||||
}
|
||||
return getSourcePath('modules', moduleName, ...segments);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -495,7 +495,7 @@ class YamlXmlBuilder {
|
|||
|
||||
// Extract module from path (e.g., /path/to/modules/bmm/agents/pm.yaml -> bmm)
|
||||
// or /path/to/bmad/bmm/agents/pm.yaml -> bmm
|
||||
// or /path/to/src/bmm/agents/pm.yaml -> bmm
|
||||
// or /path/to/src/bmm-skills/agents/pm.yaml -> bmm
|
||||
let module = 'core'; // default to core
|
||||
const pathParts = agentYamlPath.split(path.sep);
|
||||
|
||||
|
|
@ -515,10 +515,12 @@ class YamlXmlBuilder {
|
|||
module = potentialModule;
|
||||
}
|
||||
} else if (srcIndex !== -1 && pathParts[srcIndex + 1]) {
|
||||
// Path contains /src/{module}/ (bmm and core are directly under src/)
|
||||
// Path contains /src/{module}/ (bmm-skills and core-skills are directly under src/)
|
||||
const potentialModule = pathParts[srcIndex + 1];
|
||||
if (potentialModule === 'bmm' || potentialModule === 'core') {
|
||||
module = potentialModule;
|
||||
if (potentialModule === 'bmm-skills') {
|
||||
module = 'bmm';
|
||||
} else if (potentialModule === 'core-skills') {
|
||||
module = 'core';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,27 @@
|
|||
|
||||
An LLM-readable validation prompt for skills following the Agent Skills open standard.
|
||||
|
||||
## First Pass — Deterministic Checks
|
||||
|
||||
Before running inference-based validation, run the deterministic validator:
|
||||
|
||||
```bash
|
||||
node tools/validate-skills.js --json path/to/skill-dir
|
||||
```
|
||||
|
||||
This checks 14 rules deterministically: SKILL-01, SKILL-02, SKILL-03, SKILL-04, SKILL-05, SKILL-06, SKILL-07, WF-01, WF-02, PATH-02, STEP-01, STEP-06, STEP-07, SEQ-02.
|
||||
|
||||
Review its JSON output. For any rule that produced **zero findings** in the first pass, **skip it** during inference-based validation below — it has already been verified. If a rule produced any findings, the inference validator should still review that rule (some rules like SKILL-04 and SKILL-06 have sub-checks that benefit from judgment). Focus your inference effort on the remaining rules that require judgment (PATH-01, PATH-03, PATH-04, PATH-05, WF-03, STEP-02, STEP-03, STEP-04, STEP-05, SEQ-01, REF-01, REF-02, REF-03).
|
||||
|
||||
## How to Use
|
||||
|
||||
1. You are given a **skill directory path** to validate.
|
||||
2. Read every file in the skill directory recursively.
|
||||
3. Apply every rule in the catalog below to every applicable file.
|
||||
4. Produce a findings report using the report template at the end.
|
||||
2. Run the deterministic first pass (see above) and note which rules passed.
|
||||
3. Read every file in the skill directory recursively.
|
||||
4. Apply every rule in the catalog below to every applicable file, **skipping rules that passed the deterministic first pass**.
|
||||
5. Produce a findings report using the report template at the end, including any deterministic findings from the first pass.
|
||||
|
||||
If no findings are generated, the skill passes validation.
|
||||
If no findings are generated (from either pass), the skill passes validation.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -55,9 +68,9 @@ If no findings are generated, the skill passes validation.
|
|||
|
||||
- **Severity:** HIGH
|
||||
- **Applies to:** `SKILL.md`
|
||||
- **Rule:** The `name` value must use only lowercase letters, numbers, and hyphens. Max 64 characters. Must not contain "anthropic" or "claude".
|
||||
- **Detection:** Regex test: `^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$`. String search for forbidden substrings.
|
||||
- **Fix:** Rename to comply with the format.
|
||||
- **Rule:** The `name` value must start with `bmad-`, use only lowercase letters, numbers, and single hyphens between segments.
|
||||
- **Detection:** Regex test: `^bmad-[a-z0-9]+(-[a-z0-9]+)*$`.
|
||||
- **Fix:** Rename to comply with the format (e.g., `bmad-my-skill`).
|
||||
|
||||
### SKILL-05 — `name` Must Match Directory Name
|
||||
|
||||
|
|
@ -75,23 +88,33 @@ If no findings are generated, the skill passes validation.
|
|||
- **Detection:** Check length. Look for trigger phrases like "Use when" or "Use if" — their absence suggests the description only says _what_ but not _when_.
|
||||
- **Fix:** Append a "Use when..." clause to the description.
|
||||
|
||||
### SKILL-07 — SKILL.md Must Have Body Content
|
||||
|
||||
- **Severity:** HIGH
|
||||
- **Applies to:** `SKILL.md`
|
||||
- **Rule:** SKILL.md must have non-empty markdown body content after the frontmatter. The body provides L2 instructions — a SKILL.md with only frontmatter is incomplete.
|
||||
- **Detection:** Extract content after the closing `---` frontmatter delimiter and check it is non-empty after trimming whitespace.
|
||||
- **Fix:** Add markdown body with skill instructions after the closing `---`.
|
||||
|
||||
---
|
||||
|
||||
### WF-01 — workflow.md Must NOT Have `name` in Frontmatter
|
||||
### WF-01 — Only SKILL.md May Have `name` in Frontmatter
|
||||
|
||||
- **Severity:** HIGH
|
||||
- **Applies to:** `workflow.md` (if it exists)
|
||||
- **Rule:** The `name` field belongs only in `SKILL.md`. If `workflow.md` has YAML frontmatter, it must not contain `name:`.
|
||||
- **Detection:** Parse frontmatter and check for `name:` key.
|
||||
- **Fix:** Remove the `name:` line from workflow.md frontmatter.
|
||||
- **Applies to:** all `.md` files except `SKILL.md`
|
||||
- **Rule:** The `name` field belongs only in `SKILL.md`. No other markdown file in the skill directory may have `name:` in its frontmatter.
|
||||
- **Detection:** Parse frontmatter of every non-SKILL.md markdown file and check for `name:` key.
|
||||
- **Fix:** Remove the `name:` line from the file's frontmatter.
|
||||
- **Exception:** `bmad-agent-tech-writer` — has sub-skill files with intentional `name` fields (to be revisited).
|
||||
|
||||
### WF-02 — workflow.md Must NOT Have `description` in Frontmatter
|
||||
### WF-02 — Only SKILL.md May Have `description` in Frontmatter
|
||||
|
||||
- **Severity:** HIGH
|
||||
- **Applies to:** `workflow.md` (if it exists)
|
||||
- **Rule:** The `description` field belongs only in `SKILL.md`. If `workflow.md` has YAML frontmatter, it must not contain `description:`.
|
||||
- **Detection:** Parse frontmatter and check for `description:` key.
|
||||
- **Fix:** Remove the `description:` line from workflow.md frontmatter.
|
||||
- **Applies to:** all `.md` files except `SKILL.md`
|
||||
- **Rule:** The `description` field belongs only in `SKILL.md`. No other markdown file in the skill directory may have `description:` in its frontmatter.
|
||||
- **Detection:** Parse frontmatter of every non-SKILL.md markdown file and check for `description:` key.
|
||||
- **Fix:** Remove the `description:` line from the file's frontmatter.
|
||||
- **Exception:** `bmad-agent-tech-writer` — has sub-skill files with intentional `description` fields (to be revisited).
|
||||
|
||||
### WF-03 — workflow.md Frontmatter Variables Must Be Config or Runtime Only
|
||||
|
||||
|
|
@ -103,6 +126,7 @@ If no findings are generated, the skill passes validation.
|
|||
- A legitimate external path expression (must not violate PATH-05 — no paths into another skill's directory)
|
||||
|
||||
It must NOT be a path to a file within the skill directory (see PATH-04), nor a path into another skill's directory (see PATH-05).
|
||||
|
||||
- **Detection:** For each frontmatter variable, check if its value resolves to a file inside the skill (e.g., starts with `./`, `{installed_path}`, or is a bare relative path to a sibling file). If so, it is an intra-skill path variable. Also check if the value is a path into another skill's directory — if so, it violates PATH-05 and is not a legitimate external path.
|
||||
- **Fix:** Remove the variable. Use a hardcoded relative path inline where the file is referenced.
|
||||
|
||||
|
|
@ -294,7 +318,7 @@ When reporting findings, use this format:
|
|||
## Summary
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| -------- | ----- |
|
||||
| CRITICAL | N |
|
||||
| HIGH | N |
|
||||
| MEDIUM | N |
|
||||
|
|
@ -329,28 +353,34 @@ Quick-reference for the Agent Skills open standard.
|
|||
For the full standard, see: [Agent Skills specification](https://agentskills.io/specification)
|
||||
|
||||
### Structure
|
||||
|
||||
- Every skill is a directory with `SKILL.md` as the required entrypoint
|
||||
- YAML frontmatter between `---` markers provides metadata; markdown body provides instructions
|
||||
- Supporting files (scripts, templates, references) live alongside SKILL.md
|
||||
|
||||
### Path resolution
|
||||
|
||||
- Relative file references resolve from the directory of the file that contains the reference, not from the skill root
|
||||
- Example: from `branch-a/deep/next.md`, `./deeper/final.md` resolves to `branch-a/deep/deeper/final.md`
|
||||
- Example: from `branch-a/deep/next.md`, `./branch-b/alt/leaf.md` incorrectly resolves to `branch-a/deep/branch-b/alt/leaf.md`
|
||||
|
||||
### Frontmatter fields (standard)
|
||||
|
||||
- `name`: lowercase letters, numbers, hyphens only; max 64 chars; no "anthropic" or "claude"
|
||||
- `description`: required, max 1024 chars; should state what the skill does AND when to use it
|
||||
|
||||
### Progressive disclosure — three loading levels
|
||||
|
||||
- **L1 Metadata** (~100 tokens): `name` + `description` loaded at startup into system prompt
|
||||
- **L2 Instructions** (<5k tokens): SKILL.md body loaded only when skill is triggered
|
||||
- **L3 Resources** (unlimited): additional files + scripts loaded/executed on demand; script output enters context, script code does not
|
||||
|
||||
### Key design principle
|
||||
|
||||
- Skills are filesystem-based directories, not API payloads — Claude reads them via bash/file tools
|
||||
- Keep SKILL.md focused; offload detailed reference to separate files
|
||||
|
||||
### Practical tips
|
||||
|
||||
- Keep SKILL.md under 500 lines
|
||||
- `description` drives auto-discovery — use keywords users would naturally say
|
||||
|
|
|
|||
|
|
@ -0,0 +1,736 @@
|
|||
/**
|
||||
* Deterministic Skill Validator
|
||||
*
|
||||
* Validates 14 deterministic rules across all skill directories.
|
||||
* Acts as a fast first-pass complement to the inference-based skill validator.
|
||||
*
|
||||
* What it checks:
|
||||
* - SKILL-01: SKILL.md exists
|
||||
* - SKILL-02: SKILL.md frontmatter has name
|
||||
* - SKILL-03: SKILL.md frontmatter has description
|
||||
* - SKILL-04: name format (lowercase, hyphens, no forbidden substrings)
|
||||
* - SKILL-05: name matches directory basename
|
||||
* - SKILL-06: description quality (length, "Use when"/"Use if")
|
||||
* - SKILL-07: SKILL.md has body content after frontmatter
|
||||
* - WF-01: workflow.md frontmatter has no name
|
||||
* - WF-02: workflow.md frontmatter has no description
|
||||
* - PATH-02: no installed_path variable
|
||||
* - STEP-01: step filename format
|
||||
* - STEP-06: step frontmatter has no name/description
|
||||
* - STEP-07: step count 2-10
|
||||
* - SEQ-02: no time estimates
|
||||
*
|
||||
* Usage:
|
||||
* node tools/validate-skills.js # All skills, human-readable
|
||||
* node tools/validate-skills.js path/to/skill-dir # Single skill
|
||||
* node tools/validate-skills.js --strict # Exit 1 on HIGH+ findings
|
||||
* node tools/validate-skills.js --json # JSON output
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '..');
|
||||
const SRC_DIR = path.join(PROJECT_ROOT, 'src');
|
||||
|
||||
// --- CLI Parsing ---
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const STRICT = args.includes('--strict');
|
||||
const JSON_OUTPUT = args.includes('--json');
|
||||
const positionalArgs = args.filter((a) => !a.startsWith('--'));
|
||||
|
||||
// --- Constants ---
|
||||
|
||||
const NAME_REGEX = /^bmad-[a-z0-9]+(-[a-z0-9]+)*$/;
|
||||
const STEP_FILENAME_REGEX = /^step-\d{2}[a-z]?-[a-z0-9-]+\.md$/;
|
||||
const TIME_ESTIMATE_PATTERNS = [/takes?\s+\d+\s*min/i, /~\s*\d+\s*min/i, /estimated\s+time/i, /\bETA\b/];
|
||||
|
||||
const SEVERITY_ORDER = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
|
||||
|
||||
// --- Output Escaping ---
|
||||
|
||||
function escapeAnnotation(str) {
|
||||
return str.replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A');
|
||||
}
|
||||
|
||||
function escapeTableCell(str) {
|
||||
return String(str).replaceAll('|', String.raw`\|`);
|
||||
}
|
||||
|
||||
// --- Frontmatter Parsing ---
|
||||
|
||||
/**
|
||||
* Parse YAML frontmatter from a markdown file.
|
||||
* Returns an object with key-value pairs, or null if no frontmatter.
|
||||
*/
|
||||
function parseFrontmatter(content) {
|
||||
const trimmed = content.trimStart();
|
||||
if (!trimmed.startsWith('---')) return null;
|
||||
|
||||
let endIndex = trimmed.indexOf('\n---\n', 3);
|
||||
if (endIndex === -1) {
|
||||
// Handle file ending with \n---
|
||||
if (trimmed.endsWith('\n---')) {
|
||||
endIndex = trimmed.length - 4;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const fmBlock = trimmed.slice(3, endIndex).trim();
|
||||
if (fmBlock === '') return {};
|
||||
|
||||
const result = {};
|
||||
for (const line of fmBlock.split('\n')) {
|
||||
const colonIndex = line.indexOf(':');
|
||||
if (colonIndex === -1) continue;
|
||||
// Skip indented lines (nested YAML values)
|
||||
if (line[0] === ' ' || line[0] === '\t') continue;
|
||||
const key = line.slice(0, colonIndex).trim();
|
||||
let value = line.slice(colonIndex + 1).trim();
|
||||
// Strip surrounding quotes (single or double)
|
||||
if ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"'))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse YAML frontmatter, handling multiline values (description often spans lines).
|
||||
* Returns an object with key-value pairs, or null if no frontmatter.
|
||||
*/
|
||||
function parseFrontmatterMultiline(content) {
|
||||
const trimmed = content.trimStart();
|
||||
if (!trimmed.startsWith('---')) return null;
|
||||
|
||||
let endIndex = trimmed.indexOf('\n---\n', 3);
|
||||
if (endIndex === -1) {
|
||||
// Handle file ending with \n---
|
||||
if (trimmed.endsWith('\n---')) {
|
||||
endIndex = trimmed.length - 4;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const fmBlock = trimmed.slice(3, endIndex).trim();
|
||||
if (fmBlock === '') return {};
|
||||
|
||||
const result = {};
|
||||
let currentKey = null;
|
||||
let currentValue = '';
|
||||
|
||||
for (const line of fmBlock.split('\n')) {
|
||||
const colonIndex = line.indexOf(':');
|
||||
// New key-value pair: must start at column 0 (no leading whitespace) and have a colon
|
||||
if (colonIndex > 0 && line[0] !== ' ' && line[0] !== '\t') {
|
||||
// Save previous key
|
||||
if (currentKey !== null) {
|
||||
result[currentKey] = stripQuotes(currentValue.trim());
|
||||
}
|
||||
currentKey = line.slice(0, colonIndex).trim();
|
||||
currentValue = line.slice(colonIndex + 1);
|
||||
} else if (currentKey !== null) {
|
||||
// Skip YAML comment lines
|
||||
if (line.trimStart().startsWith('#')) continue;
|
||||
// Continuation of multiline value
|
||||
currentValue += '\n' + line;
|
||||
}
|
||||
}
|
||||
|
||||
// Save last key
|
||||
if (currentKey !== null) {
|
||||
result[currentKey] = stripQuotes(currentValue.trim());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function stripQuotes(value) {
|
||||
if ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"'))) {
|
||||
return value.slice(1, -1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// --- Safe File Reading ---
|
||||
|
||||
/**
|
||||
* Read a file safely, returning null on error.
|
||||
* Pushes a warning finding if the file cannot be read.
|
||||
*/
|
||||
function safeReadFile(filePath, findings, relFile) {
|
||||
try {
|
||||
return fs.readFileSync(filePath, 'utf-8');
|
||||
} catch (error) {
|
||||
findings.push({
|
||||
rule: 'READ-ERR',
|
||||
title: 'File Read Error',
|
||||
severity: 'MEDIUM',
|
||||
file: relFile || path.basename(filePath),
|
||||
detail: `Cannot read file: ${error.message}`,
|
||||
fix: 'Check file permissions and ensure the file exists.',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Code Block Stripping ---
|
||||
|
||||
function stripCodeBlocks(content) {
|
||||
return content.replaceAll(/```[\s\S]*?```/g, (m) => m.replaceAll(/[^\n]/g, ''));
|
||||
}
|
||||
|
||||
// --- Skill Discovery ---
|
||||
|
||||
function discoverSkillDirs(rootDirs) {
|
||||
const skillDirs = [];
|
||||
|
||||
function walk(dir) {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
||||
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
const skillMd = path.join(fullPath, 'SKILL.md');
|
||||
|
||||
if (fs.existsSync(skillMd)) {
|
||||
skillDirs.push(fullPath);
|
||||
}
|
||||
|
||||
// Keep walking into subdirectories to find nested skills
|
||||
walk(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
for (const rootDir of rootDirs) {
|
||||
walk(rootDir);
|
||||
}
|
||||
|
||||
return skillDirs.sort();
|
||||
}
|
||||
|
||||
// --- File Collection ---
|
||||
|
||||
function collectSkillFiles(skillDir) {
|
||||
const files = [];
|
||||
|
||||
function walk(dir) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(skillDir);
|
||||
return files;
|
||||
}
|
||||
|
||||
// --- Rule Checks ---
|
||||
|
||||
function validateSkill(skillDir) {
|
||||
const findings = [];
|
||||
const dirName = path.basename(skillDir);
|
||||
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
||||
const workflowMdPath = path.join(skillDir, 'workflow.md');
|
||||
const stepsDir = path.join(skillDir, 'steps');
|
||||
|
||||
// Collect all files in the skill for PATH-02 and SEQ-02
|
||||
const allFiles = collectSkillFiles(skillDir);
|
||||
|
||||
// --- SKILL-01: SKILL.md must exist ---
|
||||
if (!fs.existsSync(skillMdPath)) {
|
||||
findings.push({
|
||||
rule: 'SKILL-01',
|
||||
title: 'SKILL.md Must Exist',
|
||||
severity: 'CRITICAL',
|
||||
file: 'SKILL.md',
|
||||
detail: 'SKILL.md not found in skill directory.',
|
||||
fix: 'Create SKILL.md as the skill entrypoint.',
|
||||
});
|
||||
// Cannot check SKILL-02 through SKILL-07 without SKILL.md
|
||||
return findings;
|
||||
}
|
||||
|
||||
const skillContent = safeReadFile(skillMdPath, findings, 'SKILL.md');
|
||||
if (skillContent === null) return findings;
|
||||
const skillFm = parseFrontmatterMultiline(skillContent);
|
||||
|
||||
// --- SKILL-02: frontmatter has name ---
|
||||
if (!skillFm || !('name' in skillFm)) {
|
||||
findings.push({
|
||||
rule: 'SKILL-02',
|
||||
title: 'SKILL.md Must Have name in Frontmatter',
|
||||
severity: 'CRITICAL',
|
||||
file: 'SKILL.md',
|
||||
detail: 'Frontmatter is missing the `name` field.',
|
||||
fix: 'Add `name: <skill-name>` to the frontmatter.',
|
||||
});
|
||||
} else if (skillFm.name === '') {
|
||||
findings.push({
|
||||
rule: 'SKILL-02',
|
||||
title: 'SKILL.md Must Have name in Frontmatter',
|
||||
severity: 'CRITICAL',
|
||||
file: 'SKILL.md',
|
||||
detail: 'Frontmatter `name` field is empty.',
|
||||
fix: 'Set `name` to the skill directory name (kebab-case).',
|
||||
});
|
||||
}
|
||||
|
||||
// --- SKILL-03: frontmatter has description ---
|
||||
if (!skillFm || !('description' in skillFm)) {
|
||||
findings.push({
|
||||
rule: 'SKILL-03',
|
||||
title: 'SKILL.md Must Have description in Frontmatter',
|
||||
severity: 'CRITICAL',
|
||||
file: 'SKILL.md',
|
||||
detail: 'Frontmatter is missing the `description` field.',
|
||||
fix: 'Add `description: <what it does and when to use it>` to the frontmatter.',
|
||||
});
|
||||
} else if (skillFm.description === '') {
|
||||
findings.push({
|
||||
rule: 'SKILL-03',
|
||||
title: 'SKILL.md Must Have description in Frontmatter',
|
||||
severity: 'CRITICAL',
|
||||
file: 'SKILL.md',
|
||||
detail: 'Frontmatter `description` field is empty.',
|
||||
fix: 'Add a description stating what the skill does and when to use it.',
|
||||
});
|
||||
}
|
||||
|
||||
const name = skillFm && skillFm.name;
|
||||
const description = skillFm && skillFm.description;
|
||||
|
||||
// --- SKILL-04: name format ---
|
||||
if (name && !NAME_REGEX.test(name)) {
|
||||
findings.push({
|
||||
rule: 'SKILL-04',
|
||||
title: 'name Format',
|
||||
severity: 'HIGH',
|
||||
file: 'SKILL.md',
|
||||
detail: `name "${name}" does not match pattern: ${NAME_REGEX}`,
|
||||
fix: 'Rename to comply with lowercase letters, numbers, and hyphens only (max 64 chars).',
|
||||
});
|
||||
}
|
||||
|
||||
// --- SKILL-05: name matches directory ---
|
||||
if (name && name !== dirName) {
|
||||
findings.push({
|
||||
rule: 'SKILL-05',
|
||||
title: 'name Must Match Directory Name',
|
||||
severity: 'HIGH',
|
||||
file: 'SKILL.md',
|
||||
detail: `name "${name}" does not match directory name "${dirName}".`,
|
||||
fix: `Change name to "${dirName}" or rename the directory.`,
|
||||
});
|
||||
}
|
||||
|
||||
// --- SKILL-06: description quality ---
|
||||
if (description) {
|
||||
if (description.length > 1024) {
|
||||
findings.push({
|
||||
rule: 'SKILL-06',
|
||||
title: 'description Quality',
|
||||
severity: 'MEDIUM',
|
||||
file: 'SKILL.md',
|
||||
detail: `description is ${description.length} characters (max 1024).`,
|
||||
fix: 'Shorten the description to 1024 characters or less.',
|
||||
});
|
||||
}
|
||||
|
||||
if (!/use\s+when\b/i.test(description) && !/use\s+if\b/i.test(description)) {
|
||||
findings.push({
|
||||
rule: 'SKILL-06',
|
||||
title: 'description Quality',
|
||||
severity: 'MEDIUM',
|
||||
file: 'SKILL.md',
|
||||
detail: 'description does not contain "Use when" or "Use if" trigger phrase.',
|
||||
fix: 'Append a "Use when..." clause to explain when to invoke this skill.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- SKILL-07: SKILL.md must have body content after frontmatter ---
|
||||
{
|
||||
const trimmed = skillContent.trimStart();
|
||||
let bodyStart = -1;
|
||||
if (trimmed.startsWith('---')) {
|
||||
let endIdx = trimmed.indexOf('\n---\n', 3);
|
||||
if (endIdx !== -1) {
|
||||
bodyStart = endIdx + 4;
|
||||
} else if (trimmed.endsWith('\n---')) {
|
||||
bodyStart = trimmed.length; // no body at all
|
||||
}
|
||||
} else {
|
||||
bodyStart = 0; // no frontmatter, entire file is body
|
||||
}
|
||||
const body = bodyStart >= 0 ? trimmed.slice(bodyStart).trim() : '';
|
||||
if (body === '') {
|
||||
findings.push({
|
||||
rule: 'SKILL-07',
|
||||
title: 'SKILL.md Must Have Body Content',
|
||||
severity: 'HIGH',
|
||||
file: 'SKILL.md',
|
||||
detail: 'SKILL.md has no content after frontmatter. L2 instructions are required.',
|
||||
fix: 'Add markdown body with skill instructions after the closing ---.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- WF-01 / WF-02: non-SKILL.md files must NOT have name/description ---
|
||||
// TODO: bmad-agent-tech-writer has sub-skill files with intentional name/description
|
||||
const WF_SKIP_SKILLS = new Set(['bmad-agent-tech-writer']);
|
||||
for (const filePath of allFiles) {
|
||||
if (path.extname(filePath) !== '.md') continue;
|
||||
if (path.basename(filePath) === 'SKILL.md') continue;
|
||||
if (WF_SKIP_SKILLS.has(dirName)) continue;
|
||||
|
||||
const relFile = path.relative(skillDir, filePath);
|
||||
const content = safeReadFile(filePath, findings, relFile);
|
||||
if (content === null) continue;
|
||||
const fm = parseFrontmatter(content);
|
||||
if (!fm) continue;
|
||||
|
||||
if ('name' in fm) {
|
||||
findings.push({
|
||||
rule: 'WF-01',
|
||||
title: 'Only SKILL.md May Have name in Frontmatter',
|
||||
severity: 'HIGH',
|
||||
file: relFile,
|
||||
detail: `${relFile} frontmatter contains \`name\` — this belongs only in SKILL.md.`,
|
||||
fix: "Remove the `name:` line from this file's frontmatter.",
|
||||
});
|
||||
}
|
||||
|
||||
if ('description' in fm) {
|
||||
findings.push({
|
||||
rule: 'WF-02',
|
||||
title: 'Only SKILL.md May Have description in Frontmatter',
|
||||
severity: 'HIGH',
|
||||
file: relFile,
|
||||
detail: `${relFile} frontmatter contains \`description\` — this belongs only in SKILL.md.`,
|
||||
fix: "Remove the `description:` line from this file's frontmatter.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- PATH-02: no installed_path ---
|
||||
for (const filePath of allFiles) {
|
||||
// Only check markdown and yaml files
|
||||
const ext = path.extname(filePath);
|
||||
if (!['.md', '.yaml', '.yml'].includes(ext)) continue;
|
||||
|
||||
const relFile = path.relative(skillDir, filePath);
|
||||
const content = safeReadFile(filePath, findings, relFile);
|
||||
if (content === null) continue;
|
||||
|
||||
// Check frontmatter for installed_path key
|
||||
const fm = parseFrontmatter(content);
|
||||
if (fm && 'installed_path' in fm) {
|
||||
findings.push({
|
||||
rule: 'PATH-02',
|
||||
title: 'No installed_path Variable',
|
||||
severity: 'HIGH',
|
||||
file: relFile,
|
||||
detail: 'Frontmatter contains `installed_path:` key.',
|
||||
fix: 'Remove `installed_path` from frontmatter. Use relative paths instead.',
|
||||
});
|
||||
}
|
||||
|
||||
// Check content for any mention of installed_path (variable ref, prose, bare text)
|
||||
const stripped = stripCodeBlocks(content);
|
||||
const lines = stripped.split('\n');
|
||||
for (const [i, line] of lines.entries()) {
|
||||
if (/installed_path/i.test(line)) {
|
||||
findings.push({
|
||||
rule: 'PATH-02',
|
||||
title: 'No installed_path Variable',
|
||||
severity: 'HIGH',
|
||||
file: relFile,
|
||||
line: i + 1,
|
||||
detail: '`installed_path` reference found in content.',
|
||||
fix: 'Remove all installed_path usage. Use relative paths (`./path` or `../path`) instead.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- STEP-01: step filename format ---
|
||||
// --- STEP-06: step frontmatter no name/description ---
|
||||
// --- STEP-07: step count ---
|
||||
// Only check the literal steps/ directory (variant directories like steps-c, steps-v
|
||||
// use different naming conventions and are excluded per the rule specification)
|
||||
if (fs.existsSync(stepsDir) && fs.statSync(stepsDir).isDirectory()) {
|
||||
const stepDirName = 'steps';
|
||||
const stepFiles = fs.readdirSync(stepsDir).filter((f) => f.endsWith('.md'));
|
||||
|
||||
// STEP-01: filename format
|
||||
for (const stepFile of stepFiles) {
|
||||
if (!STEP_FILENAME_REGEX.test(stepFile)) {
|
||||
findings.push({
|
||||
rule: 'STEP-01',
|
||||
title: 'Step File Naming',
|
||||
severity: 'MEDIUM',
|
||||
file: path.join(stepDirName, stepFile),
|
||||
detail: `Filename "${stepFile}" does not match pattern: ${STEP_FILENAME_REGEX}`,
|
||||
fix: 'Rename to step-NN-description.md (NN = zero-padded number, optional letter suffix).',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// STEP-06: step frontmatter has no name/description
|
||||
for (const stepFile of stepFiles) {
|
||||
const stepPath = path.join(stepsDir, stepFile);
|
||||
const stepContent = safeReadFile(stepPath, findings, path.join(stepDirName, stepFile));
|
||||
if (stepContent === null) continue;
|
||||
const stepFm = parseFrontmatter(stepContent);
|
||||
|
||||
if (stepFm) {
|
||||
if ('name' in stepFm) {
|
||||
findings.push({
|
||||
rule: 'STEP-06',
|
||||
title: 'Step File Frontmatter: No name or description',
|
||||
severity: 'MEDIUM',
|
||||
file: path.join(stepDirName, stepFile),
|
||||
detail: 'Step file frontmatter contains `name:` — this is metadata noise.',
|
||||
fix: 'Remove `name:` from step file frontmatter.',
|
||||
});
|
||||
}
|
||||
if ('description' in stepFm) {
|
||||
findings.push({
|
||||
rule: 'STEP-06',
|
||||
title: 'Step File Frontmatter: No name or description',
|
||||
severity: 'MEDIUM',
|
||||
file: path.join(stepDirName, stepFile),
|
||||
detail: 'Step file frontmatter contains `description:` — this is metadata noise.',
|
||||
fix: 'Remove `description:` from step file frontmatter.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// STEP-07: step count 2-10
|
||||
const stepCount = stepFiles.filter((f) => f.startsWith('step-')).length;
|
||||
if (stepCount > 0 && (stepCount < 2 || stepCount > 10)) {
|
||||
const detail =
|
||||
stepCount < 2
|
||||
? `Only ${stepCount} step file found — consider inlining into workflow.md.`
|
||||
: `${stepCount} step files found — more than 10 risks LLM context degradation.`;
|
||||
findings.push({
|
||||
rule: 'STEP-07',
|
||||
title: 'Step Count',
|
||||
severity: 'LOW',
|
||||
file: stepDirName + '/',
|
||||
detail,
|
||||
fix: stepCount > 10 ? 'Consider consolidating steps.' : 'Consider expanding or inlining.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- SEQ-02: no time estimates ---
|
||||
for (const filePath of allFiles) {
|
||||
const ext = path.extname(filePath);
|
||||
if (!['.md', '.yaml', '.yml'].includes(ext)) continue;
|
||||
|
||||
const relFile = path.relative(skillDir, filePath);
|
||||
const content = safeReadFile(filePath, findings, relFile);
|
||||
if (content === null) continue;
|
||||
const stripped = stripCodeBlocks(content);
|
||||
const lines = stripped.split('\n');
|
||||
|
||||
for (const [i, line] of lines.entries()) {
|
||||
for (const pattern of TIME_ESTIMATE_PATTERNS) {
|
||||
if (pattern.test(line)) {
|
||||
findings.push({
|
||||
rule: 'SEQ-02',
|
||||
title: 'No Time Estimates',
|
||||
severity: 'LOW',
|
||||
file: relFile,
|
||||
line: i + 1,
|
||||
detail: `Time estimate pattern found: "${line.trim()}"`,
|
||||
fix: 'Remove time estimates — AI execution speed varies too much.',
|
||||
});
|
||||
break; // Only report once per line
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return findings;
|
||||
}
|
||||
|
||||
// --- Output Formatting ---
|
||||
|
||||
function formatHumanReadable(results) {
|
||||
const output = [];
|
||||
let totalFindings = 0;
|
||||
const severityCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
|
||||
|
||||
output.push(
|
||||
`\nValidating skills in: ${SRC_DIR}`,
|
||||
`Mode: ${STRICT ? 'STRICT (exit 1 on HIGH+)' : 'WARNING (exit 0)'}${JSON_OUTPUT ? ' + JSON' : ''}\n`,
|
||||
);
|
||||
|
||||
let totalSkills = 0;
|
||||
let skillsWithFindings = 0;
|
||||
|
||||
for (const { skillDir, findings } of results) {
|
||||
totalSkills++;
|
||||
const relDir = path.relative(PROJECT_ROOT, skillDir);
|
||||
|
||||
if (findings.length > 0) {
|
||||
skillsWithFindings++;
|
||||
output.push(`\n${relDir}`);
|
||||
|
||||
for (const f of findings) {
|
||||
totalFindings++;
|
||||
severityCounts[f.severity]++;
|
||||
const location = f.line ? ` (line ${f.line})` : '';
|
||||
output.push(` [${f.severity}] ${f.rule} — ${f.title}`, ` File: ${f.file}${location}`, ` ${f.detail}`);
|
||||
|
||||
if (process.env.GITHUB_ACTIONS) {
|
||||
const absFile = path.join(skillDir, f.file);
|
||||
const ghFile = path.relative(PROJECT_ROOT, absFile);
|
||||
const line = f.line || 1;
|
||||
const level = f.severity === 'LOW' ? 'notice' : 'warning';
|
||||
console.log(`::${level} file=${ghFile},line=${line}::${escapeAnnotation(`${f.rule}: ${f.detail}`)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
output.push(
|
||||
`\n${'─'.repeat(60)}`,
|
||||
`\nSummary:`,
|
||||
` Skills scanned: ${totalSkills}`,
|
||||
` Skills with findings: ${skillsWithFindings}`,
|
||||
` Total findings: ${totalFindings}`,
|
||||
);
|
||||
|
||||
if (totalFindings > 0) {
|
||||
output.push('', ` | Severity | Count |`, ` |----------|-------|`);
|
||||
for (const sev of ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']) {
|
||||
if (severityCounts[sev] > 0) {
|
||||
output.push(` | ${sev.padEnd(8)} | ${String(severityCounts[sev]).padStart(5)} |`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasHighPlus = severityCounts.CRITICAL > 0 || severityCounts.HIGH > 0;
|
||||
|
||||
if (totalFindings === 0) {
|
||||
output.push(`\n All skills passed validation!`);
|
||||
} else if (STRICT && hasHighPlus) {
|
||||
output.push(`\n [STRICT MODE] HIGH+ findings found — exiting with failure.`);
|
||||
} else if (STRICT) {
|
||||
output.push(`\n [STRICT MODE] Only MEDIUM/LOW findings — pass.`);
|
||||
} else {
|
||||
output.push(`\n Run with --strict to treat HIGH+ findings as errors.`);
|
||||
}
|
||||
|
||||
output.push('');
|
||||
|
||||
// Write GitHub Actions step summary
|
||||
if (process.env.GITHUB_STEP_SUMMARY) {
|
||||
let summary = '## Skill Validation\n\n';
|
||||
if (totalFindings > 0) {
|
||||
summary += '| Skill | Rule | Severity | File | Detail |\n';
|
||||
summary += '|-------|------|----------|------|--------|\n';
|
||||
for (const { skillDir, findings } of results) {
|
||||
const relDir = path.relative(PROJECT_ROOT, skillDir);
|
||||
for (const f of findings) {
|
||||
summary += `| ${escapeTableCell(relDir)} | ${f.rule} | ${f.severity} | ${escapeTableCell(f.file)} | ${escapeTableCell(f.detail)} |\n`;
|
||||
}
|
||||
}
|
||||
summary += '\n';
|
||||
}
|
||||
summary += `**${totalSkills} skills scanned, ${totalFindings} findings**\n`;
|
||||
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
|
||||
}
|
||||
|
||||
return { output: output.join('\n'), hasHighPlus };
|
||||
}
|
||||
|
||||
function formatJson(results) {
|
||||
const allFindings = [];
|
||||
for (const { skillDir, findings } of results) {
|
||||
const relDir = path.relative(PROJECT_ROOT, skillDir);
|
||||
for (const f of findings) {
|
||||
allFindings.push({
|
||||
skill: relDir,
|
||||
rule: f.rule,
|
||||
title: f.title,
|
||||
severity: f.severity,
|
||||
file: f.file,
|
||||
line: f.line || null,
|
||||
detail: f.detail,
|
||||
fix: f.fix,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by severity
|
||||
allFindings.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
|
||||
|
||||
const hasHighPlus = allFindings.some((f) => f.severity === 'CRITICAL' || f.severity === 'HIGH');
|
||||
|
||||
return { output: JSON.stringify(allFindings, null, 2), hasHighPlus };
|
||||
}
|
||||
|
||||
// --- Main ---
|
||||
|
||||
if (require.main === module) {
|
||||
// Determine which skills to validate
|
||||
let skillDirs;
|
||||
|
||||
if (positionalArgs.length > 0) {
|
||||
// Single skill directory specified
|
||||
const target = path.resolve(positionalArgs[0]);
|
||||
if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
|
||||
console.error(`Error: "${positionalArgs[0]}" is not a valid directory.`);
|
||||
process.exit(2);
|
||||
}
|
||||
skillDirs = [target];
|
||||
} else {
|
||||
// Discover all skills
|
||||
skillDirs = discoverSkillDirs([SRC_DIR]);
|
||||
}
|
||||
|
||||
if (skillDirs.length === 0) {
|
||||
console.error('No skill directories found.');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// Validate each skill
|
||||
const results = [];
|
||||
for (const skillDir of skillDirs) {
|
||||
const findings = validateSkill(skillDir);
|
||||
results.push({ skillDir, findings });
|
||||
}
|
||||
|
||||
// Format output
|
||||
const { output, hasHighPlus } = JSON_OUTPUT ? formatJson(results) : formatHumanReadable(results);
|
||||
console.log(output);
|
||||
|
||||
// Exit code
|
||||
if (STRICT && hasHighPlus) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Exports (for testing) ---
|
||||
module.exports = { parseFrontmatter, parseFrontmatterMultiline, validateSkill, discoverSkillDirs };
|
||||
Loading…
Reference in New Issue