Skip to content

Commit 12ca6ca

Browse files
Merge pull request #99 from bmad-code-org/feat/support-claude-cowork
feat: support claude cowork
2 parents b0ee2a5 + 4525c23 commit 12ca6ca

4 files changed

Lines changed: 215 additions & 5 deletions

File tree

.github/workflows/quality.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,6 @@ jobs:
115115

116116
- name: Test agent compilation components
117117
run: npm run test:install
118+
119+
- name: Validate marketplace manifest
120+
run: npm run validate:marketplace

README.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,32 @@ npx bmad-method install
108108

109109
**Note:** TEA is automatically added to party mode after installation. Use `/party` to collaborate with TEA alongside other BMad agents.
110110

111+
### Claude Cowork
112+
113+
Claude.ai web and the Claude desktop chat have no access to your project files, so the `npx` installer's writes can't reach them. [Claude Cowork](https://www.claude.com/product/claude-code) does — it sandboxes your project in a VM and exposes a plugin manager. The `npx` installer still can't write into that sandbox, but Cowork accepts plugins via its marketplace API, which TEA's `.claude-plugin/marketplace.json` ships with.
114+
115+
**Install** (two steps — register the marketplace, then install the plugin):
116+
117+
```
118+
/plugin marketplace add bmad-code-org/bmad-method-test-architecture-enterprise
119+
/plugin install bmad-method-test-architecture-enterprise@bmad-method-test-architecture-enterprise
120+
```
121+
122+
Restart the Cowork session, then `/bmad-method-test-architecture-enterprise:*` slash commands appear.
123+
124+
**Update**: `/plugin marketplace update bmad-method-test-architecture-enterprise`
125+
126+
**Uninstall**: `/plugin uninstall bmad-method-test-architecture-enterprise@bmad-method-test-architecture-enterprise`
127+
128+
**Known issue**: Cowork's plugin reconciler currently has open bugs ([anthropics/claude-code#38429](https://github.com/anthropics/claude-code/issues/38429), [#39274](https://github.com/anthropics/claude-code/issues/39274)) that can purge third-party marketplace plugins on session sync. If your slash commands disappear, re-run the `/plugin install` line.
129+
111130
### Tool-specific invocation
112131

113-
| Tool | Invocation style | Example |
114-
| ------------------------------- | ------------------------------- | -------------------------------------------- |
115-
| Claude Code / Cursor / Windsurf | Slash command | `/bmad:tea:automate` |
116-
| Codex | `$` skill from `.agents/skills` | `$bmad-tea` or `$bmad-tea-testarch-automate` |
132+
| Tool | Invocation style | Example |
133+
| ------------------------------- | -------------------------------- | ------------------------------------------------------------------ |
134+
| Claude Code / Cursor / Windsurf | Slash command | `/bmad:tea:automate` |
135+
| Codex | `$` skill from `.agents/skills` | `$bmad-tea` or `$bmad-tea-testarch-automate` |
136+
| Claude Cowork | Marketplace plugin slash command | `/bmad-method-test-architecture-enterprise:bmad-testarch-automate` |
117137

118138
## Quickstart
119139

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,14 @@
3737
"release:minor": "gh workflow run publish.yaml -f channel=latest -f bump=minor",
3838
"release:next": "gh workflow run publish.yaml -f channel=next",
3939
"release:patch": "gh workflow run publish.yaml -f channel=latest -f bump=patch",
40-
"test": "npm run test:schemas && npm run test:install && npm run test:knowledge && npm run test:release-metadata && npm run test:tea-workflow-descriptions && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check",
40+
"test": "npm run test:schemas && npm run test:install && npm run test:knowledge && npm run test:release-metadata && npm run test:tea-workflow-descriptions && npm run validate:schemas && npm run validate:marketplace && npm run lint && npm run lint:md && npm run format:check",
4141
"test:coverage": "c8 npm test",
4242
"test:install": "node test/test-installation-components.js",
4343
"test:knowledge": "node test/test-knowledge-base.js",
4444
"test:release-metadata": "node test/test-release-metadata.js",
4545
"test:schemas": "node test/test-agent-schema.js",
4646
"test:tea-workflow-descriptions": "node tools/validate-tea-workflow-descriptions.js",
47+
"validate:marketplace": "node tools/validate-marketplace.js --strict",
4748
"validate:schemas": "node tools/validate-agent-schema.js",
4849
"validate:tea-workflow-descriptions": "node tools/validate-tea-workflow-descriptions.js"
4950
},

tools/validate-marketplace.js

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
'use strict';
2+
3+
/**
4+
* Marketplace Drift Validator
5+
*
6+
* Verifies .claude-plugin/marketplace.json stays in sync with src/**\/SKILL.md.
7+
* The marketplace.json is what Claude Code (and Claude Cowork) consume when a
8+
* user runs `/plugin marketplace add bmad-code-org/bmad-method-test-architecture-enterprise` — every skill
9+
* shipped to other IDEs through the regular installer must also be reachable
10+
* through the marketplace, or Cowork users silently miss skills.
11+
*
12+
* Checks:
13+
* - Every src/**\/SKILL.md path is declared in some plugin's `skills` array.
14+
* - Every declared skill path resolves to an existing src/.../SKILL.md.
15+
* - No skill path is declared by more than one plugin.
16+
*
17+
* Usage:
18+
* node tools/validate-marketplace.js human-readable report
19+
* node tools/validate-marketplace.js --strict exit 1 on any drift (CI)
20+
* node tools/validate-marketplace.js --json JSON output
21+
*/
22+
23+
const fs = require('node:fs');
24+
const path = require('node:path');
25+
26+
const PROJECT_ROOT = path.resolve(__dirname, '..');
27+
const SRC_DIR = path.join(PROJECT_ROOT, 'src');
28+
const MARKETPLACE_PATH = path.join(PROJECT_ROOT, '.claude-plugin', 'marketplace.json');
29+
30+
const args = new Set(process.argv.slice(2));
31+
const STRICT = args.has('--strict');
32+
const JSON_OUTPUT = args.has('--json');
33+
34+
function findSkillPaths(dir, acc = []) {
35+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
36+
const full = path.join(dir, entry.name);
37+
if (entry.isDirectory()) {
38+
findSkillPaths(full, acc);
39+
} else if (entry.name === 'SKILL.md') {
40+
// Normalize to forward slashes so the string-equal comparison against
41+
// marketplace.json paths works on Windows (where path.relative uses '\').
42+
const rel = path.relative(PROJECT_ROOT, path.dirname(full)).split(path.sep).join('/');
43+
acc.push('./' + rel);
44+
}
45+
}
46+
return acc;
47+
}
48+
49+
function suggestPlugin(skillPath, plugins) {
50+
// Score by shared path-segment depth (not raw character prefix), so a new
51+
// module like ./src/cis-skills/foo doesn't get falsely suggested under
52+
// bmad-pro-skills just because both share './src/'.
53+
// Suggest only when the match goes beyond the './src/<family>/' boundary.
54+
const skillSegments = skillPath.split('/');
55+
let best = null;
56+
let bestScore = 0;
57+
for (const plugin of plugins) {
58+
for (const declared of plugin.skills || []) {
59+
const declaredSegments = declared.split('/');
60+
let i = 0;
61+
while (i < skillSegments.length && i < declaredSegments.length && skillSegments[i] === declaredSegments[i]) {
62+
i++;
63+
}
64+
if (i > bestScore) {
65+
bestScore = i;
66+
best = plugin.name;
67+
}
68+
}
69+
}
70+
// Two skills in the same module family (e.g., both under ./src/core-skills/)
71+
// share exactly 3 segments: '.', 'src', '<family>'. A skill in a brand-new
72+
// family (e.g., ./src/cis-skills/) only shares 2 segments. Require >= 3
73+
// so suggestions stay within the same family and don't leak across modules.
74+
return bestScore >= 3 ? best : null;
75+
}
76+
77+
function validate() {
78+
if (!fs.existsSync(MARKETPLACE_PATH)) {
79+
return { ok: false, fatal: `marketplace.json not found at ${MARKETPLACE_PATH}` };
80+
}
81+
82+
let marketplace;
83+
try {
84+
marketplace = JSON.parse(fs.readFileSync(MARKETPLACE_PATH, 'utf8'));
85+
} catch (error) {
86+
return { ok: false, fatal: `marketplace.json is not valid JSON: ${error.message}` };
87+
}
88+
89+
if (!fs.existsSync(SRC_DIR)) {
90+
return { ok: false, fatal: `src directory not found at ${SRC_DIR}` };
91+
}
92+
93+
const plugins = Array.isArray(marketplace.plugins) ? marketplace.plugins : [];
94+
const declaredBy = new Map(); // skillPath -> [pluginName]
95+
for (const plugin of plugins) {
96+
const skills = Array.isArray(plugin.skills) ? plugin.skills : [];
97+
for (const skillPath of skills) {
98+
if (!declaredBy.has(skillPath)) declaredBy.set(skillPath, []);
99+
declaredBy.get(skillPath).push(plugin.name);
100+
}
101+
}
102+
103+
const onDisk = new Set(findSkillPaths(SRC_DIR));
104+
105+
const missing = []; // SKILL.md exists in src/ but no plugin declares it
106+
for (const skillPath of [...onDisk].sort()) {
107+
if (!declaredBy.has(skillPath)) {
108+
missing.push({ path: skillPath, suggestedPlugin: suggestPlugin(skillPath, plugins) });
109+
}
110+
}
111+
112+
const orphans = []; // plugin declares a path that has no SKILL.md
113+
for (const skillPath of declaredBy.keys()) {
114+
if (!onDisk.has(skillPath)) {
115+
orphans.push({ path: skillPath, declaredBy: declaredBy.get(skillPath) });
116+
}
117+
}
118+
119+
const duplicates = []; // same path declared more than once (within or across plugins)
120+
for (const [skillPath, names] of declaredBy) {
121+
if (names.length > 1) {
122+
const uniquePlugins = [...new Set(names)];
123+
duplicates.push({
124+
path: skillPath,
125+
declaredBy: uniquePlugins,
126+
withinSamePlugin: uniquePlugins.length === 1,
127+
});
128+
}
129+
}
130+
131+
return {
132+
ok: missing.length === 0 && orphans.length === 0 && duplicates.length === 0,
133+
totals: { onDisk: onDisk.size, declared: declaredBy.size, plugins: plugins.length },
134+
missing,
135+
orphans,
136+
duplicates,
137+
};
138+
}
139+
140+
function reportHuman(result) {
141+
if (result.fatal) {
142+
console.error(`✗ ${result.fatal}`);
143+
return;
144+
}
145+
const { totals, missing, orphans, duplicates, ok } = result;
146+
console.log(`Marketplace coverage: ${totals.declared} declared / ${totals.onDisk} on disk across ${totals.plugins} plugin(s)`);
147+
148+
if (missing.length > 0) {
149+
console.log(`\n✗ ${missing.length} skill(s) on disk are not declared in marketplace.json:`);
150+
for (const m of missing) {
151+
const hint = m.suggestedPlugin ? ` → likely belongs in "${m.suggestedPlugin}"` : '';
152+
console.log(` ${m.path}${hint}`);
153+
}
154+
}
155+
156+
if (orphans.length > 0) {
157+
console.log(`\n✗ ${orphans.length} declared skill path(s) do not exist on disk:`);
158+
for (const o of orphans) {
159+
console.log(` ${o.path} (declared by: ${o.declaredBy.join(', ')})`);
160+
}
161+
}
162+
163+
if (duplicates.length > 0) {
164+
console.log(`\n✗ ${duplicates.length} skill path(s) declared more than once:`);
165+
for (const d of duplicates) {
166+
const where = d.withinSamePlugin
167+
? `listed multiple times in "${d.declaredBy[0]}"`
168+
: `in multiple plugins: ${d.declaredBy.join(', ')}`;
169+
console.log(` ${d.path} (${where})`);
170+
}
171+
}
172+
173+
if (ok) console.log('\n✓ marketplace.json is in sync with src/');
174+
}
175+
176+
const result = validate();
177+
178+
if (JSON_OUTPUT) {
179+
console.log(JSON.stringify(result, null, 2));
180+
} else {
181+
reportHuman(result);
182+
}
183+
184+
if (!result.ok && (STRICT || result.fatal)) {
185+
process.exit(1);
186+
}

0 commit comments

Comments
 (0)