Merge branch 'main' into fix-code-review-actions

This commit is contained in:
Alex Verkhovsky 2026-03-19 00:15:30 -06:00 committed by GitHub
commit 027738121f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1076 additions and 384 deletions

View File

@ -108,3 +108,6 @@ jobs:
- name: Validate file references
run: npm run validate:refs
- name: Validate skills
run: npm run validate:skills

View File

@ -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`).

View File

@ -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` |

View File

@ -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.
![Quick Dev workflow diagram](/diagrams/quick-dev-diagram.png)
## 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.

View File

@ -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.

View File

@ -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.

View File

@ -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)

View File

@ -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` |

View File

@ -0,0 +1,73 @@
---
title: "快速开发"
description: 在不牺牲输出质量检查点的情况下减少人机交互的摩擦
sidebar:
order: 2
---
输入意图,输出代码变更,尽可能少的人机交互轮次——同时不牺牲质量。
它让模型在检查点之间运行更长时间,只有在任务无法在没有人类判断的情况下安全继续时,或者需要审查最终结果时,才会让人类介入。
![快速开发工作流图](/diagrams/quick-dev-diagram.png)
## 为什么需要这个功能
人机交互轮次既必要又昂贵。
当前的 LLM 仍然会以可预测的方式失败:它们误读意图、用自信的猜测填补空白、偏离到不相关的工作中,并生成嘈杂的审查输出。与此同时,持续的人工干预限制了开发速度。人类注意力是瓶颈。
`bmad-quick-dev` 重新平衡了这种权衡。它信任模型在更长的时间段内无监督运行,但前提是工作流已经创建了足够强的边界来确保安全。
## 核心设计
### 1. 首先压缩意图
工作流首先让人类和模型将请求压缩成一个连贯的目标。输入可以从粗略的意图表达开始,但在工作流自主运行之前,它必须变得足够小、足够清晰、没有矛盾。
意图可以以多种形式出现:几句话、一个错误追踪器链接、计划模式的输出、从聊天会话复制的文本,甚至来自 BMAD 自己的 `epics.md` 的故事编号。在最后一种情况下,工作流不会理解 BMAD 故事跟踪语义,但它仍然可以获取故事本身并继续执行。
这个工作流并不会消除人类的控制。它将其重新定位到少数几个高价值时刻:
- **意图澄清** - 将混乱的请求转化为一个没有隐藏矛盾的连贯目标
- **规范审批** - 确认冻结的理解是正确要构建的东西
- **最终产品审查** - 主要检查点,人类在最后决定结果是否可接受
### 2. 路由到最小安全路径
一旦目标清晰,工作流就会决定这是一个真正的单次变更还是需要更完整的路径。小的、零爆炸半径的变更可以直接进入实现。其他所有内容都需要经过规划,这样模型在独自运行更长时间之前就有更强的边界。
### 3. 以更少的监督运行更长时间
在那个路由决策之后,模型可以自己承担更多工作。在更完整的路径上,批准的规范成为模型在较少监督下执行的边界,这正是设计的全部意义。
### 4. 在正确的层诊断失败
如果实现是错误的,因为意图是错误的,修补代码是错误的修复。如果代码是错误的,因为规范太弱,修补差异也是错误的修复。工作流旨在诊断失败从系统的哪个层面进入,回到那个层面,并从那里重新生成。
审查发现用于确定问题来自意图、规范生成还是本地实现。只有真正的本地问题才会在本地修补。
### 5. 只在需要时让人类回来
意图访谈是人机交互,但它不是与重复检查点相同类型的中断。工作流试图将那些重复检查点保持在最低限度。在初始意图塑造之后,人类主要在工作流无法在没有判断的情况下安全继续时,以及在最后需要审查结果时才回来。
- **意图差距解决** - 当审查证明工作流无法安全推断出原本意图时重新介入
其他一切都是更长自主执行的候选。这种权衡是经过深思熟虑的。旧模式在持续监督上花费更多的人类注意力。快速开发在模型上投入更多信任,但将人类注意力保留在人类推理具有最高杠杆作用的时刻。
## 为什么审查系统很重要
审查阶段不仅仅是为了发现错误。它是为了在不破坏动力的情况下路由修正。
这个工作流在能够生成子智能体的平台上效果最好,或者至少可以通过命令行调用另一个 LLM 并等待结果。如果你的平台本身不支持这一点,你可以添加一个技能来做。无上下文子智能体是审查设计的基石。
智能体审查经常以两种方式出错:
- 它们生成太多发现,迫使人类在噪音中筛选
- 它们通过提出不相关的问题并使每次运行变成临时清理项目来使当前变更脱轨
快速开发通过将审查视为分诊来解决这两个问题。
一些发现属于当前变更。一些不属于。如果一个发现是附带的而不是与当前工作有因果关系,工作流可以推迟它,而不是强迫人类立即处理它。这使运行保持专注,并防止随机的分支话题消耗注意力的预算。
那个分诊有时会不完美。这是可以接受的。通常,误判一些发现比用成千上万个低价值的审查评论淹没人类要好。系统正在优化信号质量,而不是详尽的召回率。

View File

@ -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**:探索性开发。用于探索技术可行性或解决方案的短期研究活动。

View File

@ -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 驱动的 IDEClaude 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**:约定式提交。遵循标准格式的提交消息。

View File

@ -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 生成、验证文档、解释概念 |

View File

@ -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}": [

View File

@ -26,7 +26,7 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
Present summary. If token count exceeded 1600 and user chose [K], include the token count and explain why it may be a problem. HALT and ask human: `[A] Approve` | `[E] Edit`
- **A**: Rename `{wipFile}` to `{spec_file}`, set status `ready-for-dev`. Everything inside `<frozen-after-approval>` is now locked — only the human can change it. → Step 3.
- **A**: Rename `{wipFile}` to `{spec_file}`, set status `ready-for-dev`. Everything inside `<frozen-after-approval>` is now locked — only the human can change it. Display the finalized spec path to the user as a CWD-relative path (no leading `/`) so it is clickable in the terminal. → Step 3.
- **E**: Apply changes, then return to CHECKPOINT 1.

View File

@ -22,7 +22,7 @@ Build the trail as an ordered sequence of **stops** — clickable `path:line` re
2. **Lead with the entry point** — the single highest-leverage file:line a reviewer should look at first to grasp the design intent.
3. **Inside each concern**, order stops from most important / architecturally interesting to supporting. Lightly bias toward higher-risk or boundary-crossing stops.
4. **End with peripherals** — tests, config, types, and other supporting changes come last.
5. **Every code reference is a clickable workspace-relative link.** Format each stop as a markdown link: `[short-name:line](/project-root-relative/path/to/file.ts#L42)`. The link target uses a leading `/` (workspace root) with a `#L` line anchor. Use the file's basename (or shortest unambiguous suffix) plus line number as the link text.
5. **Every code reference is a clickable workspace-relative link** (project-root-relative for clickability in the editor). Format each stop as a markdown link: `[short-name:line](/project-root-relative/path/to/file.ts#L42)`. The link target uses a leading `/` (workspace root) with a `#L` line anchor. Use the file's basename (or shortest unambiguous suffix) plus line number as the link text.
6. **Each stop gets one ultra-concise line of framing** (≤15 words) — why this approach was chosen here and what it achieves in the context of the change. No paragraphs.
Format each stop as framing first, link on the next indented line:
@ -53,7 +53,7 @@ When there is only one concern, omit the bold label — just list the stops dire
3. Open the spec in the user's editor so they can click through the Suggested Review Order:
- Run `code -r "{spec_file}"` to open the spec in the current VS Code window (reuses the window where the project or worktree is open). Always double-quote the path to handle spaces and special characters.
- If `code` is not available (command fails), skip gracefully and tell the user the spec file path instead.
4. Display summary of your work to the user, including the commit hash if one was created. Include:
4. Display summary of your work to the user, including the commit hash if one was created. Any file paths shown in conversation/terminal output must use CWD-relative format (no leading `/`) for terminal clickability — this differs from spec-file links which use project-root-relative paths. Include:
- A note that the spec is open in their editor (or the file path if it couldn't be opened). Mention that `{spec_file}` now contains a Suggested Review Order.
- **Navigation tip:** "Ctrl+click (Cmd+click on macOS) the links in the Suggested Review Order to jump to each stop."
- Offer to push and/or create a pull request.

View File

@ -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.

View File

@ -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);

View File

@ -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)) {

View File

@ -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 {

View File

@ -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]}`;
}

View File

@ -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) {

View File

@ -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);
}

View File

@ -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';
}
}

View File

@ -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,11 +318,11 @@ When reporting findings, use this format:
## Summary
| Severity | Count |
|----------|-------|
| CRITICAL | N |
| HIGH | N |
| MEDIUM | N |
| LOW | N |
| -------- | ----- |
| CRITICAL | N |
| HIGH | N |
| MEDIUM | N |
| LOW | N |
## Findings
@ -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

736
tools/validate-skills.js Normal file
View File

@ -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 };