bmad-plus 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +14 -14
  3. package/SECURITY.md +62 -0
  4. package/osint-agent-package/skills/bmad-osint-investigate/osint/scripts/_http.py +68 -24
  5. package/package.json +1 -1
  6. package/readme-international/README.de.md +14 -14
  7. package/readme-international/README.es.md +14 -14
  8. package/readme-international/README.fr.md +14 -14
  9. package/src/bmad-plus/agents/agent-quality/SKILL.md +1 -1
  10. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +26 -5
  11. package/src/bmad-plus/packs/pack-seo/SKILL.md +3 -1
  12. package/src/bmad-plus/packs/pack-seo/ref/cwv-thresholds.md +2 -2
  13. package/src/bmad-plus/packs/pack-seo/requirements.txt +1 -1
  14. package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +72 -30
  15. package/src/bmad-plus/packs/pack-seo/scripts/seo_crawl.py +36 -24
  16. package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +179 -59
  17. package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +5 -6
  18. package/src/bmad-plus/packs/pack-seo/scripts/seo_screenshot.py +176 -14
  19. package/src/bmad-plus/packs/pack-shield/README.md +12 -0
  20. package/src/bmad-plus/packs/pack-shield/SKILL.md +7 -1
  21. package/src/bmad-plus/packs/pack-shield/review-rules/access-control.md +10 -0
  22. package/src/bmad-plus/packs/pack-shield/review-rules/ai-integrations.md +10 -0
  23. package/src/bmad-plus/packs/pack-shield/review-rules/change-and-supply-chain.md +10 -0
  24. package/src/bmad-plus/packs/pack-shield/review-rules/cryptography.md +10 -0
  25. package/src/bmad-plus/packs/pack-shield/review-rules/index.yaml +134 -0
  26. package/src/bmad-plus/packs/pack-shield/review-rules/logging.md +10 -0
  27. package/src/bmad-plus/packs/pack-shield/review-rules/personal-data.md +10 -0
  28. package/src/bmad-plus/packs/pack-shield/shared/ai-processing-register-template.yaml +53 -0
  29. package/src/bmad-plus/packs/pack-shield/shared/ai-processing-register.md +32 -0
  30. package/src/bmad-plus/packs/pack-shield/shared/assurance-case-template.yaml +87 -0
  31. package/src/bmad-plus/packs/pack-shield/shared/assurance-case.md +50 -0
  32. package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +24 -1
  33. package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +1 -0
  34. package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +5 -4
  35. package/tools/build/generate-adapters.js +7 -0
  36. package/tools/build/generate.js +14 -0
  37. package/tools/cli/bmad-plus-cli.js +2 -0
  38. package/tools/cli/commands/ai-register.js +63 -0
  39. package/tools/cli/commands/assurance.js +162 -0
  40. package/tools/cli/commands/review.js +141 -7
  41. package/tools/cli/lib/ai-register.js +393 -0
  42. package/tools/cli/lib/assurance.js +822 -0
  43. package/tools/cli/lib/control-refs.js +132 -0
  44. package/tools/cli/lib/installation-health.js +17 -0
  45. package/tools/cli/lib/packs.js +60 -2
  46. package/tools/cli/lib/page-origins.js +582 -0
  47. package/tools/cli/lib/review-rules.js +124 -26
  48. package/tools/cli/lib/review.js +493 -10
  49. package/tools/cli/lib/uat.js +22 -5
  50. package/tools/cli/review-rules/index.yaml +9 -0
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Review checklists chosen by path. Built-in rules ship with the CLI; a project adds,
3
- * replaces or disables rules in `_bmad/review-rules.yaml`. Every rule whose patterns match
4
- * a file applies to it — rules add up, so a broad project rule never silences the built-in
5
- * ones by accident. The resolved set is hashed into the review scope.
2
+ * Review checklists chosen by path. Built-in rules ship with the CLI; an installed pack may
3
+ * add its own (Shield's compliance rules); a project adds, replaces or disables rules in
4
+ * `_bmad/review-rules.yaml`. Every rule whose patterns match a file applies to it — rules
5
+ * add up, so a broad project rule never silences the built-in ones by accident. A rule may
6
+ * name the compliance controls it examines. The resolved set is hashed into the review scope.
6
7
  */
7
8
  'use strict';
8
9
 
@@ -11,12 +12,16 @@ const path = require('node:path');
11
12
  const crypto = require('node:crypto');
12
13
  const yaml = require('js-yaml');
13
14
  const { compile, matchesAny } = require('./glob');
15
+ const { parseControls } = require('./control-refs');
16
+ const { PACKS } = require('./packs');
14
17
 
15
18
  const SCHEMA = 'bmad-plus/review-rules/1';
16
19
  const BUILTIN_DIR = path.join(__dirname, '..', 'review-rules');
17
20
  const PROJECT_FILE = path.join('_bmad', 'review-rules.yaml');
21
+ /** Where an installed pack keeps its rules, below its own folder in `.agents/skills`. */
22
+ const PACK_RULES_DIR = 'review-rules';
18
23
  const RULE_ID = /^[a-z0-9][a-z0-9-]{0,60}$/;
19
- const RULE_KEYS = ['id', 'title', 'globs', 'doc'];
24
+ const RULE_KEYS = ['id', 'title', 'group', 'globs', 'doc', 'controls'];
20
25
  const MAX_DOC_BYTES = 64 * 1024;
21
26
  const MAX_INDEX_BYTES = 256 * 1024;
22
27
 
@@ -47,8 +52,8 @@ function parseLayer(root, indexName, layer) {
47
52
  if (!doc || doc.schema !== SCHEMA) throw new Error(`${where}: schema must be "${SCHEMA}"`);
48
53
  const extra = Object.keys(doc).filter((key) => !['schema', 'rules', 'disable'].includes(key));
49
54
  if (extra.length) throw new Error(`${where}: unknown key(s) ${extra.join(', ')}`);
50
- if (layer === 'builtin' && doc.disable)
51
- throw new Error(`${where}: built-in rules cannot disable`);
55
+ if (layer !== 'project' && doc.disable)
56
+ throw new Error(`${where}: only a project's rules can disable`);
52
57
  const disable = doc.disable || [];
53
58
  if (!Array.isArray(disable) || !disable.every((id) => typeof id === 'string'))
54
59
  throw new Error(`${where}: disable must be a list of rule ids`);
@@ -60,11 +65,13 @@ function parseLayer(root, indexName, layer) {
60
65
  if (!rule || typeof rule !== 'object') throw new Error(`${at}: must be a mapping`);
61
66
  const unknown = Object.keys(rule).filter((key) => !RULE_KEYS.includes(key));
62
67
  if (unknown.length) throw new Error(`${at}: unknown key(s) ${unknown.join(', ')}`);
63
- if (!RULE_ID.test(String(rule.id))) throw new Error(`${at}: invalid id`);
68
+ if (typeof rule.id !== 'string' || !RULE_ID.test(rule.id)) throw new Error(`${at}: invalid id`);
64
69
  if (seen.has(rule.id)) throw new Error(`${at}: duplicate id`);
65
70
  seen.add(rule.id);
66
71
  if (typeof rule.title !== 'string' || !rule.title.trim())
67
72
  throw new Error(`${at}: title is required`);
73
+ if (rule.group !== undefined && !(typeof rule.group === 'string' && RULE_ID.test(rule.group)))
74
+ throw new Error(`${at}: invalid group`);
68
75
  if (
69
76
  !Array.isArray(rule.globs) ||
70
77
  !rule.globs.length ||
@@ -72,13 +79,17 @@ function parseLayer(root, indexName, layer) {
72
79
  )
73
80
  throw new Error(`${at}: globs must be a non-empty list of patterns`);
74
81
  for (const glob of rule.globs) compile(glob);
82
+ const controls = rule.controls === undefined ? [] : parseControls(rule.controls, at);
75
83
  if (!/\.md$/i.test(String(rule.doc))) throw new Error(`${at}: doc must be a Markdown file`);
76
84
  const text = readConfined(root, rule.doc, MAX_DOC_BYTES, at).trim();
77
85
  if (!text) throw new Error(`${at}: ${rule.doc} is empty`);
78
86
  return {
79
87
  id: rule.id,
80
88
  title: rule.title.trim(),
89
+ // A rule family reviewers can split by; a rule without one is a family of its own.
90
+ group: rule.group === undefined ? rule.id : rule.group,
81
91
  globs: [...rule.globs],
92
+ controls,
82
93
  layer,
83
94
  source: rule.doc,
84
95
  text,
@@ -87,13 +98,35 @@ function parseLayer(root, indexName, layer) {
87
98
  return { rules: parsed, disable };
88
99
  }
89
100
 
101
+ /** Rule indexes of the packs installed in the project, in pack order. */
102
+ function installedPackLayers(projectDir) {
103
+ const layers = [];
104
+ for (const [pack, { packDir }] of Object.entries(PACKS)) {
105
+ if (!packDir) continue;
106
+ const relative = ['.agents', 'skills', packDir, PACK_RULES_DIR, 'index.yaml'];
107
+ if (fs.existsSync(path.join(projectDir, ...relative)))
108
+ layers.push({ pack, file: relative.join('/') });
109
+ }
110
+ return layers;
111
+ }
112
+
90
113
  /**
91
- * The effective rule set for a project: built-in rules, then the project's own. A project
92
- * rule with a built-in id replaces it; `disable` removes rules by id and is reported.
114
+ * The effective rule set for a project: built-in rules, then those of installed packs, then
115
+ * the project's own. A pack only adds; a project rule with an existing id replaces it, and
116
+ * `disable` removes rules by id and is reported.
93
117
  */
94
118
  function loadRuleset(projectDir, { builtinDir = BUILTIN_DIR } = {}) {
95
119
  const builtin = parseLayer(builtinDir, 'index.yaml', 'builtin');
96
120
  const ordered = new Map(builtin.rules.map((rule) => [rule.id, rule]));
121
+ const packFiles = installedPackLayers(projectDir);
122
+ for (const { pack, file } of packFiles) {
123
+ const layer = parseLayer(path.join(projectDir, path.dirname(file)), 'index.yaml', 'pack');
124
+ for (const rule of layer.rules) {
125
+ if (ordered.has(rule.id))
126
+ throw new Error(`${pack} review rules: rule ${rule.id} already exists; a pack only adds`);
127
+ ordered.set(rule.id, { ...rule, pack });
128
+ }
129
+ }
97
130
  let disabled = [];
98
131
  const projectIndex = path.join(projectDir, PROJECT_FILE);
99
132
  let project = null;
@@ -107,10 +140,20 @@ function loadRuleset(projectDir, { builtinDir = BUILTIN_DIR } = {}) {
107
140
  for (const id of disabled) ordered.delete(id);
108
141
  }
109
142
  const list = [...ordered.values()];
110
- const fingerprint = list.map((rule) => [rule.id, rule.layer, rule.globs, sha256(rule.text)]);
143
+ // Controls join the fingerprint only when a rule names some, so a rule set without any
144
+ // keeps the hash it had before rules could carry controls.
145
+ const fingerprint = list.map((rule) => [
146
+ rule.id,
147
+ rule.group,
148
+ rule.layer,
149
+ rule.globs,
150
+ sha256(rule.text),
151
+ ...(rule.controls.length ? [rule.controls] : []),
152
+ ]);
111
153
  return {
112
154
  rules: list,
113
155
  disabled,
156
+ packFiles: packFiles.map(({ file }) => file),
114
157
  projectFile: project ? PROJECT_FILE.split(path.sep).join('/') : null,
115
158
  sha256: sha256(JSON.stringify({ rules: fingerprint, disabled })),
116
159
  };
@@ -121,36 +164,91 @@ function rulesFor(ruleset, file) {
121
164
  return ruleset.rules.filter((rule) => matchesAny(file, rule.globs)).map((rule) => rule.id);
122
165
  }
123
166
 
167
+ /** The rule families of a set of rule ids, each with its rules, in rule order. */
168
+ function groupsOf(ruleset, ids) {
169
+ const groups = new Map();
170
+ for (const rule of ruleset.rules) {
171
+ if (!ids.includes(rule.id)) continue;
172
+ groups.set(rule.group, [...(groups.get(rule.group) || []), rule.id]);
173
+ }
174
+ return [...groups].map(([id, rules]) => ({ id, rules }));
175
+ }
176
+
124
177
  /**
125
- * The reviewer's checklist for a scope: each applicable rule once, with the files it
126
- * covers. Rules that match nothing in the scope are left out.
178
+ * The compliance controls a set of rule ids examines, each with the rules that name it, in
179
+ * rule order. A control touched is a prompt to look at its requirement, not a finding.
180
+ */
181
+ function controlsOf(ruleset, ids) {
182
+ const controls = new Map();
183
+ for (const rule of ruleset.rules) {
184
+ if (!ids.includes(rule.id)) continue;
185
+ for (const control of rule.controls)
186
+ controls.set(control, [...(controls.get(control) || []), rule.id]);
187
+ }
188
+ return [...controls].map(([id, rules]) => ({ id, rules }));
189
+ }
190
+
191
+ /**
192
+ * The reviewer's checklist for a scope: the controls the change touches, then each
193
+ * applicable rule once, with the files it covers. Rules that match nothing are left out.
127
194
  */
128
195
  function checklist(ruleset, byPath, { title = 'Review checklist' } = {}) {
129
196
  const files = new Map();
130
197
  for (const [file, ids] of Object.entries(byPath)) {
131
198
  for (const id of ids) files.set(id, [...(files.get(id) || []), file]);
132
199
  }
133
- const sections = ruleset.rules
134
- .filter((rule) => files.has(rule.id))
135
- .map((rule) =>
136
- [
137
- `## ${rule.title} \`${rule.id}\`${rule.layer === 'project' ? ' (project rule)' : ''}`,
200
+ const applied = ruleset.rules.filter((rule) => files.has(rule.id));
201
+ const controls = controlsOf(
202
+ ruleset,
203
+ applied.map((rule) => rule.id)
204
+ );
205
+ const touched = controls.length
206
+ ? [
207
+ '## Controls this change touches',
208
+ '',
209
+ ...controls.map(
210
+ ({ id, rules }) => `- \`${id}\` — ${rules.map((rule) => `\`${rule}\``).join(', ')}`
211
+ ),
138
212
  '',
139
- `Applies to: ${files
140
- .get(rule.id)
141
- .map((file) => `\`${file}\``)
142
- .join(', ')}`,
213
+ 'List in the `controls` of a finding the control it breaks; a control listed here is not a finding.',
143
214
  '',
144
- rule.text,
145
- ].join('\n')
146
- );
215
+ ]
216
+ : [];
217
+ const sections = applied.map((rule) =>
218
+ [
219
+ `## ${rule.title} \`${rule.id}\`${rule.layer === 'project' ? ' (project rule)' : ''}${rule.pack ? ` (${rule.pack} rule)` : ''}`,
220
+ '',
221
+ `Group: \`${rule.group}\``,
222
+ ...(rule.controls.length
223
+ ? ['', `Controls: ${rule.controls.map((control) => `\`${control}\``).join(', ')}`]
224
+ : []),
225
+ '',
226
+ `Applies to: ${files
227
+ .get(rule.id)
228
+ .map((file) => `\`${file}\``)
229
+ .join(', ')}`,
230
+ '',
231
+ rule.text,
232
+ ].join('\n')
233
+ );
147
234
  return [
148
235
  `# ${title}`,
149
236
  '',
150
237
  `Rule set ${ruleset.sha256.slice(0, 12)}. Report a finding only with its quoted code; a rule is a prompt to look, not a finding.`,
151
238
  '',
239
+ ...touched,
152
240
  ...sections.flatMap((section) => [section, '']),
153
241
  ].join('\n');
154
242
  }
155
243
 
156
- module.exports = { SCHEMA, BUILTIN_DIR, PROJECT_FILE, loadRuleset, rulesFor, checklist };
244
+ module.exports = {
245
+ SCHEMA,
246
+ BUILTIN_DIR,
247
+ PROJECT_FILE,
248
+ PACK_RULES_DIR,
249
+ loadRuleset,
250
+ rulesFor,
251
+ groupsOf,
252
+ controlsOf,
253
+ checklist,
254
+ };