bmad-module-skill-forge 4.0.0 → 5.0.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 (85) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +3 -3
  3. package/docs/_data/pinned.yaml +19 -13
  4. package/docs/architecture.md +2 -2
  5. package/docs/concepts.md +4 -4
  6. package/docs/examples.md +32 -25
  7. package/docs/getting-started.md +3 -3
  8. package/docs/how-it-works.md +2 -2
  9. package/docs/index.md +1 -1
  10. package/docs/skill-model.md +41 -39
  11. package/docs/verifying-a-skill.md +41 -20
  12. package/docs/why-skf.md +1 -1
  13. package/docs/workflows.md +2 -2
  14. package/package.json +1 -1
  15. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +11 -3
  16. package/src/shared/scripts/schemas/skill-brief.v1.json +1 -1
  17. package/src/shared/scripts/skf-build-change-manifest.py +881 -168
  18. package/src/shared/scripts/skf-count-tokens.py +54 -10
  19. package/src/shared/scripts/skf-detect-workspaces.py +67 -0
  20. package/src/shared/scripts/skf-emit-result-envelope.py +6 -5
  21. package/src/shared/scripts/skf-extract-public-api.py +439 -107
  22. package/src/shared/scripts/skf-extraction-inventory.py +10 -6
  23. package/src/shared/scripts/skf-extraction-snapshot.py +6 -6
  24. package/src/shared/scripts/skf-parse-gaps.py +135 -26
  25. package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
  26. package/src/shared/scripts/skf-resolve-customization.py +197 -0
  27. package/src/shared/scripts/skf-shard-body.py +239 -41
  28. package/src/shared/scripts/skf-validate-output.py +12 -3
  29. package/src/shared/scripts/skf-verify-provenance-completeness.py +470 -38
  30. package/src/shared/tool-requirements.yaml +8 -2
  31. package/src/skf-analyze-source/SKILL.md +2 -2
  32. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  33. package/src/skf-audit-skill/SKILL.md +2 -2
  34. package/src/skf-audit-skill/references/re-index.md +2 -2
  35. package/src/skf-brief-skill/SKILL.md +2 -2
  36. package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
  37. package/src/skf-brief-skill/references/scope-definition.md +4 -4
  38. package/src/skf-campaign/SKILL.md +2 -2
  39. package/src/skf-campaign/references/step-05-skill-loop.md +6 -1
  40. package/src/skf-create-skill/SKILL.md +2 -2
  41. package/src/skf-create-skill/assets/skill-sections.md +1 -1
  42. package/src/skf-create-skill/references/extract.md +2 -1
  43. package/src/skf-create-skill/references/extraction-patterns.md +3 -3
  44. package/src/skf-create-skill/references/report.md +6 -1
  45. package/src/skf-create-stack-skill/SKILL.md +2 -2
  46. package/src/skf-create-stack-skill/references/validate.md +1 -1
  47. package/src/skf-drop-skill/SKILL.md +2 -2
  48. package/src/skf-export-skill/SKILL.md +2 -2
  49. package/src/skf-export-skill/assets/snippet-format.md +5 -5
  50. package/src/skf-export-skill/references/generate-snippet.md +22 -15
  51. package/src/skf-export-skill/references/load-skill.md +1 -1
  52. package/src/skf-export-skill/references/token-report.md +29 -9
  53. package/src/skf-forger/SKILL.md +2 -2
  54. package/src/skf-quick-skill/SKILL.md +2 -2
  55. package/src/skf-refine-architecture/SKILL.md +6 -8
  56. package/src/skf-rename-skill/SKILL.md +2 -2
  57. package/src/skf-setup/SKILL.md +2 -2
  58. package/src/skf-test-skill/SKILL.md +2 -2
  59. package/src/skf-test-skill/assets/output-section-formats.md +6 -4
  60. package/src/skf-test-skill/references/coherence-check.md +4 -3
  61. package/src/skf-test-skill/references/coverage-check-branches.md +61 -0
  62. package/src/skf-test-skill/references/coverage-check-tiers.md +1 -1
  63. package/src/skf-test-skill/references/coverage-check.md +45 -69
  64. package/src/skf-test-skill/references/init.md +2 -1
  65. package/src/skf-test-skill/references/migration-section-rules.md +26 -14
  66. package/src/skf-test-skill/references/report.md +2 -2
  67. package/src/skf-test-skill/references/scoring-rules.md +5 -4
  68. package/src/skf-test-skill/references/source-access-protocol.md +6 -6
  69. package/src/skf-test-skill/scripts/gap-ledger.py +378 -163
  70. package/src/skf-test-skill/scripts/load-coverage-inputs.py +171 -70
  71. package/src/skf-test-skill/scripts/locate-export-segments.py +101 -20
  72. package/src/skf-test-skill/scripts/reconcile-coverage.py +192 -39
  73. package/src/skf-test-skill/scripts/score-signatures.py +329 -54
  74. package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
  75. package/src/skf-test-skill/scripts/validate-inventory.py +108 -9
  76. package/src/skf-test-skill/templates/test-report-template.md +1 -0
  77. package/src/skf-update-skill/SKILL.md +2 -2
  78. package/src/skf-update-skill/references/detect-changes.md +1 -1
  79. package/src/skf-update-skill/references/gap-driven.md +36 -21
  80. package/src/skf-update-skill/references/merge.md +15 -10
  81. package/src/skf-update-skill/references/re-extract.md +3 -1
  82. package/src/skf-update-skill/references/report.md +5 -4
  83. package/src/skf-update-skill/references/write.md +39 -18
  84. package/src/skf-verify-stack/SKILL.md +2 -2
  85. package/tools/cli/lib/tool-check.js +158 -2
@@ -7,12 +7,16 @@
7
7
  *
8
8
  * Each probe runs a binary resolved on PATH outside the project folder, with
9
9
  * no shell and fixed arguments, and never npx, which can download. A .cmd or
10
- * .bat shim on Windows runs through cmd.exe. A missing, hanging or failing
11
- * probe reads as unknown: nothing here throws or changes an exit code.
10
+ * .bat shim on Windows runs through cmd.exe. tessl and skill-check are never
11
+ * run: tessl's version is read from the name of the binary it installs, and
12
+ * skill-check's, which has no --version, from its package.json. A missing,
13
+ * hanging or failing probe reads as unknown: nothing here throws or changes
14
+ * an exit code.
12
15
  */
13
16
 
14
17
  const { spawn } = require('node:child_process');
15
18
  const fs = require('node:fs');
19
+ const os = require('node:os');
16
20
  const path = require('node:path');
17
21
  const chalk = require('chalk');
18
22
  const yaml = require('js-yaml');
@@ -27,6 +31,25 @@ const MINIMUM = /^\d+(?:\.\d+)*$/;
27
31
  const NPX_TOOLS = new Set(['tessl', 'skill_check']);
28
32
  // ccc has no --version: `uv tool list` names each package uv installed, with its version.
29
33
  const CCC_PACKAGE = /^cocoindex-code v(\S+)/m;
34
+ // skill-check has no --version either (1.2.0 exits 2 on it). Its version is
35
+ // read from the package.json of the install its binary belongs to. A
36
+ // package.json is looked for at most this many folders above a binary's real
37
+ // path.
38
+ const SKILL_CHECK_PACKAGE = 'skill-check';
39
+ const PACKAGE_DEPTH = 4;
40
+ const MAX_PACKAGE_JSON = 1024 * 1024;
41
+ // tessl is not run either: on a tessl that has never run, any command,
42
+ // --version included, accepts tessl's Terms and turns on usage sharing for
43
+ // the user, and a run can start tessl's own update. tessl's installers name
44
+ // each binary tessl-<version>-<platform> in a tessl/versions folder and point
45
+ // the PATH entry at one: a symlink on Linux and macOS, a copy in tessl\bin on
46
+ // Windows.
47
+ const TESSL_BINARY = /^tessl-(.+)-(?:linux|darwin|win32)-(?:x64|arm64)(?:-musl)?(?:\.exe)?$/;
48
+ // The npm (@tessl/cli) and Homebrew (tessl) packages hold a launcher that
49
+ // runs the binary tessl installed, so their version is not the one that runs.
50
+ // @tessl/cli before 0.63.0 held the CLI itself.
51
+ const TESSL_LAUNCHERS = ['@tessl/cli', 'tessl'];
52
+ const FIRST_TESSL_LAUNCHER = '0.63.0';
30
53
  const MAX_OUTPUT = 64 * 1024;
31
54
  const STYLES = { ok: chalk.green, upgrade: chalk.yellow, unknown: chalk.yellow, missing: chalk.red, optional: chalk.dim };
32
55
 
@@ -172,6 +195,129 @@ async function versionOf(file, args, context) {
172
195
  return ok ? findVersion(stdout) : null;
173
196
  }
174
197
 
198
+ /**
199
+ * The package.json at `file` as an object, or null when there is none, it is
200
+ * not a regular file under MAX_PACKAGE_JSON (a FIFO would block the check
201
+ * past its timeout) or it cannot be read.
202
+ */
203
+ function readPackage(file) {
204
+ try {
205
+ const stat = fs.statSync(file);
206
+ if (!stat.isFile() || stat.size > MAX_PACKAGE_JSON) return null;
207
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
208
+ return data && typeof data === 'object' && !Array.isArray(data) ? data : null;
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * The package.json of the package named one of `names` that installed
216
+ * `file`, the binary `bin` on PATH, or null: the shim folder's
217
+ * node_modules/<name>/package.json (npm's global layout on Windows), else the
218
+ * first package.json with a name above the binary's real path, when it is
219
+ * one of `names` and declares that file as its `bin` (npm, yarn, bun and
220
+ * Homebrew link the binary into its package on Linux and macOS). null for
221
+ * any other layout, such as pnpm's or Volta's shims.
222
+ */
223
+ function packageOf(file, bin, names) {
224
+ for (const name of names) {
225
+ const shim = readPackage(path.join(path.dirname(file), 'node_modules', ...name.split('/'), 'package.json'));
226
+ if (shim?.name === name) return shim;
227
+ }
228
+ const target = realpath(file);
229
+ const declares = (data, dir) => {
230
+ const entry = typeof data.bin === 'string' ? data.bin : data.bin?.[bin];
231
+ return typeof entry === 'string' && realpath(path.resolve(dir, entry)) === target;
232
+ };
233
+ let dir = path.dirname(target);
234
+ for (let depth = 0; depth < PACKAGE_DEPTH; depth++) {
235
+ const data = readPackage(path.join(dir, 'package.json'));
236
+ if (typeof data?.name === 'string') return names.includes(data.name) && declares(data, dir) ? data : null;
237
+ const parent = path.dirname(dir);
238
+ if (parent === dir) break;
239
+ dir = parent;
240
+ }
241
+ return null;
242
+ }
243
+
244
+ /** A file's stat, or null when it cannot be read (a dangling link, say). */
245
+ function statOf(file) {
246
+ try {
247
+ return fs.statSync(file);
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
252
+
253
+ /** The last part of `file`, in lower case on Windows, where names ignore case. */
254
+ function nameOf(file) {
255
+ return process.platform === 'win32' ? path.basename(file).toLowerCase() : path.basename(file);
256
+ }
257
+
258
+ /**
259
+ * The version of the tessl binary `file`, from its name: tessl-<version>-
260
+ * <platform> once its real path is resolved, or, for a copy at
261
+ * tessl/bin/tessl or tessl.exe (tessl's layout on Windows), the name of the
262
+ * one file of tessl/versions that has its size. null when there is no such
263
+ * file, or more than one.
264
+ */
265
+ function tesslBinaryVersion(file) {
266
+ const target = realpath(file);
267
+ const named = TESSL_BINARY.exec(nameOf(target));
268
+ if (named) return findVersion(named[1]);
269
+ const bin = path.dirname(target);
270
+ if (!/^tessl(?:\.exe)?$/.test(nameOf(target)) || nameOf(bin) !== 'bin' || nameOf(path.dirname(bin)) !== 'tessl') return null;
271
+ const size = statOf(target)?.size;
272
+ const versions = path.join(path.dirname(bin), 'versions');
273
+ let names;
274
+ try {
275
+ names = fs.readdirSync(versions);
276
+ } catch {
277
+ return null;
278
+ }
279
+ const copied = names.filter((name) => {
280
+ const stat = TESSL_BINARY.test(nameOf(name)) ? statOf(path.join(versions, name)) : null;
281
+ return stat?.isFile() && stat.size === size;
282
+ });
283
+ return size !== undefined && copied.length === 1 ? findVersion(TESSL_BINARY.exec(nameOf(copied[0]))[1]) : null;
284
+ }
285
+
286
+ /**
287
+ * The binary tessl's launchers run: ~/.local/bin/tessl, or
288
+ * %LOCALAPPDATA%\tessl\bin\tessl.exe on Windows. null when the home folder
289
+ * is not an absolute path, which would be read from the project folder.
290
+ */
291
+ function tesslLauncherTarget(env) {
292
+ const target =
293
+ process.platform === 'win32'
294
+ ? path.join(
295
+ envValue(env, 'LOCALAPPDATA') ?? path.join(envValue(env, 'USERPROFILE') || os.homedir(), 'AppData', 'Local'),
296
+ 'tessl',
297
+ 'bin',
298
+ 'tessl.exe',
299
+ )
300
+ : path.join(env.HOME || os.homedir(), '.local', 'bin', 'tessl');
301
+ return path.isAbsolute(target) ? target : null;
302
+ }
303
+
304
+ /**
305
+ * tessl's version, read without running it: from the binary on PATH (see
306
+ * tesslBinaryVersion), or, when that is the npm or Homebrew launcher, from
307
+ * the binary the launcher runs. An @tessl/cli older than the first launcher
308
+ * is the CLI itself, so its package.json has the version.
309
+ */
310
+ function tesslVersion(file, env) {
311
+ const own = tesslBinaryVersion(file);
312
+ if (own) return own;
313
+ const launcher = packageOf(file, 'tessl', TESSL_LAUNCHERS);
314
+ if (!launcher) return null;
315
+ const packaged = findVersion(launcher.version);
316
+ if (launcher.name === '@tessl/cli' && packaged && compareVersions(packaged, FIRST_TESSL_LAUNCHER)) return packaged;
317
+ const target = tesslLauncherTarget(env);
318
+ return target ? tesslBinaryVersion(target) : null;
319
+ }
320
+
175
321
  /** One tool: { found, version }, the version null when it cannot be read. */
176
322
  async function probe(key, tool, context) {
177
323
  if (key === 'node') return { found: true, version: process.versions.node };
@@ -190,6 +336,16 @@ async function probe(key, tool, context) {
190
336
  const match = ok ? CCC_PACKAGE.exec(stdout) : null;
191
337
  return { found: true, version: match ? findVersion(match[1]) : null };
192
338
  }
339
+ if (key === 'skill_check') {
340
+ const file = resolveOutsideProject(SKILL_CHECK_PACKAGE, context);
341
+ return file
342
+ ? { found: true, version: findVersion(packageOf(file, SKILL_CHECK_PACKAGE, [SKILL_CHECK_PACKAGE])?.version) }
343
+ : { found: false };
344
+ }
345
+ if (key === 'tessl') {
346
+ const file = resolveOutsideProject('tessl', context);
347
+ return file ? { found: true, version: tesslVersion(file, context.env) } : { found: false };
348
+ }
193
349
  const [name, ...args] = Array.isArray(tool.version_command) ? tool.version_command.map(String) : [];
194
350
  const file = name ? resolveOutsideProject(name, context) : null;
195
351
  if (!file) return { found: false };