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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +3 -3
- package/docs/_data/pinned.yaml +19 -13
- package/docs/architecture.md +2 -2
- package/docs/concepts.md +4 -4
- package/docs/examples.md +32 -25
- package/docs/getting-started.md +3 -3
- package/docs/how-it-works.md +2 -2
- package/docs/index.md +1 -1
- package/docs/skill-model.md +41 -39
- package/docs/verifying-a-skill.md +41 -20
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +2 -2
- package/package.json +1 -1
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +11 -3
- package/src/shared/scripts/schemas/skill-brief.v1.json +1 -1
- package/src/shared/scripts/skf-build-change-manifest.py +881 -168
- package/src/shared/scripts/skf-count-tokens.py +54 -10
- package/src/shared/scripts/skf-detect-workspaces.py +67 -0
- package/src/shared/scripts/skf-emit-result-envelope.py +6 -5
- package/src/shared/scripts/skf-extract-public-api.py +439 -107
- package/src/shared/scripts/skf-extraction-inventory.py +10 -6
- package/src/shared/scripts/skf-extraction-snapshot.py +6 -6
- package/src/shared/scripts/skf-parse-gaps.py +135 -26
- package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
- package/src/shared/scripts/skf-resolve-customization.py +197 -0
- package/src/shared/scripts/skf-shard-body.py +239 -41
- package/src/shared/scripts/skf-validate-output.py +12 -3
- package/src/shared/scripts/skf-verify-provenance-completeness.py +470 -38
- package/src/shared/tool-requirements.yaml +8 -2
- package/src/skf-analyze-source/SKILL.md +2 -2
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-audit-skill/SKILL.md +2 -2
- package/src/skf-audit-skill/references/re-index.md +2 -2
- package/src/skf-brief-skill/SKILL.md +2 -2
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
- package/src/skf-brief-skill/references/scope-definition.md +4 -4
- package/src/skf-campaign/SKILL.md +2 -2
- package/src/skf-campaign/references/step-05-skill-loop.md +6 -1
- package/src/skf-create-skill/SKILL.md +2 -2
- package/src/skf-create-skill/assets/skill-sections.md +1 -1
- package/src/skf-create-skill/references/extract.md +2 -1
- package/src/skf-create-skill/references/extraction-patterns.md +3 -3
- package/src/skf-create-skill/references/report.md +6 -1
- package/src/skf-create-stack-skill/SKILL.md +2 -2
- package/src/skf-create-stack-skill/references/validate.md +1 -1
- package/src/skf-drop-skill/SKILL.md +2 -2
- package/src/skf-export-skill/SKILL.md +2 -2
- package/src/skf-export-skill/assets/snippet-format.md +5 -5
- package/src/skf-export-skill/references/generate-snippet.md +22 -15
- package/src/skf-export-skill/references/load-skill.md +1 -1
- package/src/skf-export-skill/references/token-report.md +29 -9
- package/src/skf-forger/SKILL.md +2 -2
- package/src/skf-quick-skill/SKILL.md +2 -2
- package/src/skf-refine-architecture/SKILL.md +6 -8
- package/src/skf-rename-skill/SKILL.md +2 -2
- package/src/skf-setup/SKILL.md +2 -2
- package/src/skf-test-skill/SKILL.md +2 -2
- package/src/skf-test-skill/assets/output-section-formats.md +6 -4
- package/src/skf-test-skill/references/coherence-check.md +4 -3
- package/src/skf-test-skill/references/coverage-check-branches.md +61 -0
- package/src/skf-test-skill/references/coverage-check-tiers.md +1 -1
- package/src/skf-test-skill/references/coverage-check.md +45 -69
- package/src/skf-test-skill/references/init.md +2 -1
- package/src/skf-test-skill/references/migration-section-rules.md +26 -14
- package/src/skf-test-skill/references/report.md +2 -2
- package/src/skf-test-skill/references/scoring-rules.md +5 -4
- package/src/skf-test-skill/references/source-access-protocol.md +6 -6
- package/src/skf-test-skill/scripts/gap-ledger.py +378 -163
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +171 -70
- package/src/skf-test-skill/scripts/locate-export-segments.py +101 -20
- package/src/skf-test-skill/scripts/reconcile-coverage.py +192 -39
- package/src/skf-test-skill/scripts/score-signatures.py +329 -54
- package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
- package/src/skf-test-skill/scripts/validate-inventory.py +108 -9
- package/src/skf-test-skill/templates/test-report-template.md +1 -0
- package/src/skf-update-skill/SKILL.md +2 -2
- package/src/skf-update-skill/references/detect-changes.md +1 -1
- package/src/skf-update-skill/references/gap-driven.md +36 -21
- package/src/skf-update-skill/references/merge.md +15 -10
- package/src/skf-update-skill/references/re-extract.md +3 -1
- package/src/skf-update-skill/references/report.md +5 -4
- package/src/skf-update-skill/references/write.md +39 -18
- package/src/skf-verify-stack/SKILL.md +2 -2
- 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.
|
|
11
|
-
*
|
|
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 };
|