mustflow 2.113.0 → 2.114.5
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/README.md +2 -0
- package/dist/cli/commands/skill.js +77 -4
- package/dist/cli/lib/command-registry.js +1 -1
- package/dist/cli/lib/external-skill-import.js +461 -7
- package/dist/core/public-json-contracts.js +9 -0
- package/package.json +3 -1
- package/schemas/README.md +4 -0
- package/schemas/skill-update-report.schema.json +256 -0
- package/templates/default/i18n.toml +12 -12
- package/templates/default/locales/en/.mustflow/skills/async-timing-boundary-review/SKILL.md +6 -1
- package/templates/default/locales/en/.mustflow/skills/client-bundle-pruning-review/SKILL.md +40 -34
- package/templates/default/locales/en/.mustflow/skills/frame-render-performance-review/SKILL.md +28 -22
- package/templates/default/locales/en/.mustflow/skills/hot-path-performance-review/SKILL.md +22 -10
- package/templates/default/locales/en/.mustflow/skills/javascript-code-change/SKILL.md +29 -11
- package/templates/default/locales/en/.mustflow/skills/memory-lifetime-review/SKILL.md +8 -2
- package/templates/default/locales/en/.mustflow/skills/node-code-change/SKILL.md +19 -5
- package/templates/default/locales/en/.mustflow/skills/performance-budget-check/SKILL.md +15 -7
- package/templates/default/locales/en/.mustflow/skills/quadratic-scan-review/SKILL.md +25 -22
- package/templates/default/locales/en/.mustflow/skills/web-render-performance-review/SKILL.md +18 -17
- package/templates/default/manifest.toml +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, renameSync, rmSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, renameSync, rmSync } from 'node:fs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { readCommandContract, readCommandContractIncludePaths, } from '../../core/config-loading.js';
|
|
@@ -7,10 +7,13 @@ const EXTERNAL_SKILL_ROOT = '.mustflow/external-skills';
|
|
|
7
7
|
const PROVENANCE_FILE = 'mustflow-skill-source.json';
|
|
8
8
|
const COMMANDS_CONFIG_PATH = '.mustflow/config/commands.toml';
|
|
9
9
|
const COMMAND_FRAGMENT_DIRECTORY = '.mustflow/config/commands';
|
|
10
|
+
const EXTERNAL_SKILL_UPDATE_CHECK_STATE_PATH = '.mustflow/state/external-skills/update-check.json';
|
|
10
11
|
const DEFAULT_GITHUB_REF = 'HEAD';
|
|
11
12
|
const MAX_IMPORTED_FILES = 40;
|
|
12
13
|
const MAX_TOTAL_BYTES = 512 * 1024;
|
|
13
14
|
const MAX_FILE_BYTES = 256 * 1024;
|
|
15
|
+
const UPDATE_CHECK_STALE_AFTER_DAYS = 7;
|
|
16
|
+
const UPDATE_CHECK_STALE_AFTER_MS = UPDATE_CHECK_STALE_AFTER_DAYS * 24 * 60 * 60 * 1000;
|
|
14
17
|
const ALLOWED_SUPPORT_DIRECTORIES = new Set(['assets', 'references', 'scripts']);
|
|
15
18
|
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
16
19
|
const GITHUB_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/u;
|
|
@@ -365,6 +368,212 @@ function createTarget(skillName) {
|
|
|
365
368
|
provenance_path: `${skillDir}/${PROVENANCE_FILE}`,
|
|
366
369
|
};
|
|
367
370
|
}
|
|
371
|
+
function sourceToParsed(source) {
|
|
372
|
+
return {
|
|
373
|
+
host: source.host,
|
|
374
|
+
owner: source.owner,
|
|
375
|
+
repo: source.repo,
|
|
376
|
+
ref: source.ref,
|
|
377
|
+
skillPath: source.skill_path,
|
|
378
|
+
sourceUrl: source.source_url,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function provenancePathForSkill(skillName) {
|
|
382
|
+
return `${EXTERNAL_SKILL_ROOT}/${skillName}/${PROVENANCE_FILE}`;
|
|
383
|
+
}
|
|
384
|
+
function readJsonFile(projectRoot, relativePath, maxBytes = 512 * 1024) {
|
|
385
|
+
const content = readUtf8FileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, ...relativePath.split('/')), { maxBytes });
|
|
386
|
+
return JSON.parse(content);
|
|
387
|
+
}
|
|
388
|
+
function assertExternalSkillSource(value, skillName) {
|
|
389
|
+
if (!value || typeof value !== 'object') {
|
|
390
|
+
throw new Error(`External skill provenance is not an object: ${skillName}`);
|
|
391
|
+
}
|
|
392
|
+
const record = value;
|
|
393
|
+
if (record.schema_version !== '1' || record.kind !== 'external_skill_source') {
|
|
394
|
+
throw new Error(`External skill provenance has an unsupported shape: ${skillName}`);
|
|
395
|
+
}
|
|
396
|
+
if (!record.source || typeof record.source !== 'object') {
|
|
397
|
+
throw new Error(`External skill provenance is missing source metadata: ${skillName}`);
|
|
398
|
+
}
|
|
399
|
+
if (!Array.isArray(record.files)) {
|
|
400
|
+
throw new Error(`External skill provenance is missing file hashes: ${skillName}`);
|
|
401
|
+
}
|
|
402
|
+
return record;
|
|
403
|
+
}
|
|
404
|
+
function installedExternalSkillNames(projectRoot) {
|
|
405
|
+
const rootPath = path.join(projectRoot, ...EXTERNAL_SKILL_ROOT.split('/'));
|
|
406
|
+
if (!existsSync(rootPath)) {
|
|
407
|
+
return [];
|
|
408
|
+
}
|
|
409
|
+
return readdirSync(rootPath, { withFileTypes: true })
|
|
410
|
+
.filter((entry) => entry.isDirectory())
|
|
411
|
+
.map((entry) => entry.name)
|
|
412
|
+
.filter((name) => SLUG_PATTERN.test(name) && existsSync(path.join(rootPath, name, PROVENANCE_FILE)))
|
|
413
|
+
.sort((left, right) => left.localeCompare(right));
|
|
414
|
+
}
|
|
415
|
+
function readExternalSkillProvenance(projectRoot, skillName) {
|
|
416
|
+
return assertExternalSkillSource(readJsonFile(projectRoot, provenancePathForSkill(skillName)), skillName);
|
|
417
|
+
}
|
|
418
|
+
function readExternalSkillUpdateCheckStateFile(projectRoot) {
|
|
419
|
+
const statePath = path.join(projectRoot, ...EXTERNAL_SKILL_UPDATE_CHECK_STATE_PATH.split('/'));
|
|
420
|
+
if (!existsSync(statePath)) {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
const value = readJsonFile(projectRoot, EXTERNAL_SKILL_UPDATE_CHECK_STATE_PATH, 64 * 1024);
|
|
425
|
+
if (!value || typeof value !== 'object') {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
const record = value;
|
|
429
|
+
if (record.schema_version !== '1' ||
|
|
430
|
+
record.kind !== 'external_skill_update_check_state' ||
|
|
431
|
+
typeof record.last_checked_at !== 'string' ||
|
|
432
|
+
Number.isNaN(Date.parse(record.last_checked_at))) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
schema_version: '1',
|
|
437
|
+
kind: 'external_skill_update_check_state',
|
|
438
|
+
last_checked_at: record.last_checked_at,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function createExternalSkillUpdateCheckState(projectRoot, now) {
|
|
446
|
+
const previous = readExternalSkillUpdateCheckStateFile(projectRoot);
|
|
447
|
+
const checkedAt = now.toISOString();
|
|
448
|
+
const nextCheckDueAt = new Date(now.getTime() + UPDATE_CHECK_STALE_AFTER_MS).toISOString();
|
|
449
|
+
const previousTime = previous ? Date.parse(previous.last_checked_at) : Number.NaN;
|
|
450
|
+
const hasInstalledExternalSkills = installedExternalSkillNames(projectRoot).length > 0;
|
|
451
|
+
const staleBeforeCommand = hasInstalledExternalSkills && (!previous ||
|
|
452
|
+
Number.isNaN(previousTime) ||
|
|
453
|
+
now.getTime() - previousTime >= UPDATE_CHECK_STALE_AFTER_MS);
|
|
454
|
+
return {
|
|
455
|
+
state_path: EXTERNAL_SKILL_UPDATE_CHECK_STATE_PATH,
|
|
456
|
+
stale_after_days: UPDATE_CHECK_STALE_AFTER_DAYS,
|
|
457
|
+
checked_at: checkedAt,
|
|
458
|
+
previous_checked_at: previous?.last_checked_at ?? null,
|
|
459
|
+
next_check_due_at: nextCheckDueAt,
|
|
460
|
+
stale_before_command: staleBeforeCommand,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function writeExternalSkillUpdateCheckState(projectRoot, checkedAt) {
|
|
464
|
+
writeJsonFileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, ...EXTERNAL_SKILL_UPDATE_CHECK_STATE_PATH.split('/')), {
|
|
465
|
+
schema_version: '1',
|
|
466
|
+
kind: 'external_skill_update_check_state',
|
|
467
|
+
last_checked_at: checkedAt,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
export function createExternalSkillUpdateReminder(projectRoot, now = new Date()) {
|
|
471
|
+
const hasInstalledExternalSkills = installedExternalSkillNames(projectRoot).length > 0;
|
|
472
|
+
if (!hasInstalledExternalSkills) {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
const previous = readExternalSkillUpdateCheckStateFile(projectRoot);
|
|
476
|
+
const previousTime = previous ? Date.parse(previous.last_checked_at) : Number.NaN;
|
|
477
|
+
if (previous && !Number.isNaN(previousTime) && now.getTime() - previousTime < UPDATE_CHECK_STALE_AFTER_MS) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
return `External skill update check is stale; run mf skill outdated --json or mf skill update --all --dry-run --json. Last checked: ${previous?.last_checked_at ?? 'never'}.`;
|
|
481
|
+
}
|
|
482
|
+
function readInstalledRelativeFilePaths(projectRoot, target) {
|
|
483
|
+
const targetPath = path.join(projectRoot, ...target.skill_dir.split('/'));
|
|
484
|
+
const pending = [''];
|
|
485
|
+
const files = [];
|
|
486
|
+
const stack = [...pending];
|
|
487
|
+
while (stack.length > 0) {
|
|
488
|
+
const relativeDirectory = stack.pop() ?? '';
|
|
489
|
+
const absoluteDirectory = relativeDirectory
|
|
490
|
+
? path.join(targetPath, ...relativeDirectory.split('/'))
|
|
491
|
+
: targetPath;
|
|
492
|
+
const entries = readdirSync(absoluteDirectory, { withFileTypes: true })
|
|
493
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
494
|
+
for (const entry of entries) {
|
|
495
|
+
const relativePath = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
|
|
496
|
+
if (relativePath === PROVENANCE_FILE) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (entry.isDirectory()) {
|
|
500
|
+
stack.push(relativePath);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
files.push(relativePath);
|
|
504
|
+
if (files.length > MAX_IMPORTED_FILES) {
|
|
505
|
+
throw new Error(`External skill ${target.skill_name} exceeds ${MAX_IMPORTED_FILES} installed files.`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
510
|
+
}
|
|
511
|
+
function readInstalledFileReports(projectRoot, target, provenanceFiles) {
|
|
512
|
+
return provenanceFiles.map((file) => {
|
|
513
|
+
const relativePath = `${target.skill_dir}/${file.relative_path}`;
|
|
514
|
+
const content = readUtf8FileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, ...relativePath.split('/')), { maxBytes: MAX_FILE_BYTES });
|
|
515
|
+
return {
|
|
516
|
+
relative_path: file.relative_path,
|
|
517
|
+
kind: file.kind,
|
|
518
|
+
bytes: Buffer.byteLength(content, 'utf8'),
|
|
519
|
+
sha256: hashContent(content),
|
|
520
|
+
};
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
function collectFileChanges(currentFiles, remoteFiles) {
|
|
524
|
+
const currentByPath = new Map(currentFiles.map((file) => [file.relative_path, file]));
|
|
525
|
+
const remoteByPath = new Map(remoteFiles.map((file) => [file.relative_path, file]));
|
|
526
|
+
const paths = [...new Set([...currentByPath.keys(), ...remoteByPath.keys()])].sort((left, right) => left.localeCompare(right));
|
|
527
|
+
const changes = [];
|
|
528
|
+
for (const relativePath of paths) {
|
|
529
|
+
const current = currentByPath.get(relativePath);
|
|
530
|
+
const remote = remoteByPath.get(relativePath);
|
|
531
|
+
if (!current && remote) {
|
|
532
|
+
changes.push({
|
|
533
|
+
relative_path: relativePath,
|
|
534
|
+
status: 'added',
|
|
535
|
+
current_sha256: null,
|
|
536
|
+
remote_sha256: remote.sha256,
|
|
537
|
+
});
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (current && !remote) {
|
|
541
|
+
changes.push({
|
|
542
|
+
relative_path: relativePath,
|
|
543
|
+
status: 'removed',
|
|
544
|
+
current_sha256: current.sha256,
|
|
545
|
+
remote_sha256: null,
|
|
546
|
+
});
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (current && remote && current.sha256 !== remote.sha256) {
|
|
550
|
+
changes.push({
|
|
551
|
+
relative_path: relativePath,
|
|
552
|
+
status: 'modified',
|
|
553
|
+
current_sha256: current.sha256,
|
|
554
|
+
remote_sha256: remote.sha256,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return changes;
|
|
559
|
+
}
|
|
560
|
+
function localDriftIssues(skillName, provenanceFiles, installedFiles, installedRelativeFilePaths) {
|
|
561
|
+
const provenanceByPath = new Map(provenanceFiles.map((file) => [file.relative_path, file]));
|
|
562
|
+
const provenancePaths = new Set(provenanceFiles.map((file) => file.relative_path));
|
|
563
|
+
const issues = [];
|
|
564
|
+
for (const installed of installedFiles) {
|
|
565
|
+
const provenance = provenanceByPath.get(installed.relative_path);
|
|
566
|
+
if (!provenance || provenance.sha256 !== installed.sha256) {
|
|
567
|
+
issues.push(`External skill ${skillName} has local file drift: ${installed.relative_path}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
for (const relativePath of installedRelativeFilePaths) {
|
|
571
|
+
if (!provenancePaths.has(relativePath)) {
|
|
572
|
+
issues.push(`External skill ${skillName} has local file drift: ${relativePath}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return issues;
|
|
576
|
+
}
|
|
368
577
|
function scriptNameForIntent(relativePath) {
|
|
369
578
|
const basename = path.posix.basename(relativePath);
|
|
370
579
|
const withoutExtension = basename.replace(/\.[^.]+$/u, '');
|
|
@@ -402,7 +611,7 @@ function createTrustedScriptIntent(skillName, scriptPath) {
|
|
|
402
611
|
approval_required: ['network_access', 'destructive_command'],
|
|
403
612
|
};
|
|
404
613
|
}
|
|
405
|
-
function createScriptTrust(projectRoot, target, files, trustScripts, mode) {
|
|
614
|
+
function createScriptTrust(projectRoot, target, files, trustScripts, mode, existingTrust) {
|
|
406
615
|
const scripts = files.filter((file) => file.kind === 'script');
|
|
407
616
|
if (!trustScripts) {
|
|
408
617
|
return {
|
|
@@ -434,7 +643,7 @@ function createScriptTrust(projectRoot, target, files, trustScripts, mode) {
|
|
|
434
643
|
if (duplicateIntent) {
|
|
435
644
|
throw new Error(`External skill trusted script intents contain a duplicate name: ${duplicateIntent}`);
|
|
436
645
|
}
|
|
437
|
-
validateTrustedScriptCommandPlan(projectRoot, includeEntry, fragmentPath, intents);
|
|
646
|
+
validateTrustedScriptCommandPlan(projectRoot, includeEntry, fragmentPath, intents, existingTrust);
|
|
438
647
|
return {
|
|
439
648
|
requested: true,
|
|
440
649
|
status: mode === 'install' ? 'trusted' : 'planned',
|
|
@@ -455,17 +664,24 @@ function findDuplicate(values) {
|
|
|
455
664
|
}
|
|
456
665
|
return null;
|
|
457
666
|
}
|
|
458
|
-
function validateTrustedScriptCommandPlan(projectRoot, includeEntry, fragmentPath, intents) {
|
|
667
|
+
function validateTrustedScriptCommandPlan(projectRoot, includeEntry, fragmentPath, intents, existingTrust) {
|
|
459
668
|
const existingIncludes = new Set(readCommandContractIncludePaths(projectRoot).map((entry) => entry.replace(/^\.mustflow\/config\//u, '')));
|
|
460
|
-
|
|
669
|
+
const fragmentExists = existsSync(path.join(projectRoot, ...fragmentPath.split('/')));
|
|
670
|
+
const mayReuseExistingPlan = existingTrust?.grants_command_authority === true &&
|
|
671
|
+
existingTrust.include_entry === includeEntry &&
|
|
672
|
+
existingTrust.fragment_path === fragmentPath &&
|
|
673
|
+
existingIncludes.has(includeEntry) &&
|
|
674
|
+
fragmentExists;
|
|
675
|
+
const reusableIntentNames = new Set(mayReuseExistingPlan ? existingTrust.intents.map((intent) => intent.intent) : []);
|
|
676
|
+
if (existingIncludes.has(includeEntry) && !mayReuseExistingPlan) {
|
|
461
677
|
throw new Error(`External skill trusted script command include already exists: ${includeEntry}`);
|
|
462
678
|
}
|
|
463
|
-
if (
|
|
679
|
+
if (fragmentExists && !mayReuseExistingPlan) {
|
|
464
680
|
throw new Error(`External skill trusted script command fragment already exists: ${fragmentPath}`);
|
|
465
681
|
}
|
|
466
682
|
const contract = readCommandContract(projectRoot);
|
|
467
683
|
for (const intent of intents) {
|
|
468
|
-
if (Object.prototype.hasOwnProperty.call(contract.intents, intent.intent)) {
|
|
684
|
+
if (Object.prototype.hasOwnProperty.call(contract.intents, intent.intent) && !reusableIntentNames.has(intent.intent)) {
|
|
469
685
|
throw new Error(`External skill trusted script intent already exists in command contract: ${intent.intent}`);
|
|
470
686
|
}
|
|
471
687
|
}
|
|
@@ -517,6 +733,12 @@ function updateCommandIncludeText(content, includeEntry) {
|
|
|
517
733
|
if (closingLineIndex < 0) {
|
|
518
734
|
throw new Error(`[include].files in ${COMMANDS_CONFIG_PATH} must be a TOML array`);
|
|
519
735
|
}
|
|
736
|
+
const existingValues = lines
|
|
737
|
+
.slice(filesLineIndex + 1, closingLineIndex)
|
|
738
|
+
.flatMap((line) => [...line.matchAll(/"([^"]+)"/gu)].map((match) => match[1]));
|
|
739
|
+
if (existingValues.includes(includeEntry)) {
|
|
740
|
+
return lines.join('\n');
|
|
741
|
+
}
|
|
520
742
|
lines.splice(closingLineIndex, 0, includeLine);
|
|
521
743
|
return lines.join('\n');
|
|
522
744
|
}
|
|
@@ -565,6 +787,45 @@ function writeImportedSkillFiles(projectRoot, target, source, files, fileReport,
|
|
|
565
787
|
throw error;
|
|
566
788
|
}
|
|
567
789
|
}
|
|
790
|
+
function writeUpdatedSkillFiles(projectRoot, target, source, files, fileReport, warnings, scriptTrust) {
|
|
791
|
+
const targetPath = path.join(projectRoot, ...target.skill_dir.split('/'));
|
|
792
|
+
if (!existsSync(targetPath)) {
|
|
793
|
+
throw new Error(`External skill is not installed: ${target.skill_dir}`);
|
|
794
|
+
}
|
|
795
|
+
const timestamp = Date.now();
|
|
796
|
+
const tempSkillDir = `${EXTERNAL_SKILL_ROOT}/.${target.skill_name}.update-${process.pid}-${timestamp}`;
|
|
797
|
+
const backupSkillDir = `${EXTERNAL_SKILL_ROOT}/.${target.skill_name}.backup-${process.pid}-${timestamp}`;
|
|
798
|
+
const tempPath = path.join(projectRoot, ...tempSkillDir.split('/'));
|
|
799
|
+
const backupPath = path.join(projectRoot, ...backupSkillDir.split('/'));
|
|
800
|
+
const tempSkillPath = path.join(tempPath, 'SKILL.md');
|
|
801
|
+
ensureFileTargetInsideWithoutSymlinks(projectRoot, tempSkillPath, { allowMissingLeaf: true });
|
|
802
|
+
try {
|
|
803
|
+
for (const file of files) {
|
|
804
|
+
writeUtf8FileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, ...tempSkillDir.split('/'), ...file.relativePath.split('/')), file.content);
|
|
805
|
+
}
|
|
806
|
+
writeJsonFileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, ...tempSkillDir.split('/'), PROVENANCE_FILE), {
|
|
807
|
+
schema_version: '1',
|
|
808
|
+
kind: 'external_skill_source',
|
|
809
|
+
source,
|
|
810
|
+
files: fileReport,
|
|
811
|
+
script_trust: scriptTrust,
|
|
812
|
+
warnings,
|
|
813
|
+
});
|
|
814
|
+
renameSync(targetPath, backupPath);
|
|
815
|
+
renameSync(tempPath, targetPath);
|
|
816
|
+
rmSync(backupPath, { recursive: true, force: true });
|
|
817
|
+
}
|
|
818
|
+
catch (error) {
|
|
819
|
+
rmSync(tempPath, { recursive: true, force: true });
|
|
820
|
+
if (!existsSync(targetPath) && existsSync(backupPath)) {
|
|
821
|
+
renameSync(backupPath, targetPath);
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
rmSync(backupPath, { recursive: true, force: true });
|
|
825
|
+
}
|
|
826
|
+
throw error;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
568
829
|
function rejectionReport(mode, issue) {
|
|
569
830
|
return {
|
|
570
831
|
schema_version: '1',
|
|
@@ -582,6 +843,199 @@ function rejectionReport(mode, issue) {
|
|
|
582
843
|
wrote_files: false,
|
|
583
844
|
};
|
|
584
845
|
}
|
|
846
|
+
function rejectedUpdateItem(skillName, issue) {
|
|
847
|
+
return {
|
|
848
|
+
skill_name: skillName,
|
|
849
|
+
ok: false,
|
|
850
|
+
status: 'rejected',
|
|
851
|
+
source: null,
|
|
852
|
+
target: createTarget(skillName),
|
|
853
|
+
current_files: [],
|
|
854
|
+
remote_files: [],
|
|
855
|
+
changed_files: [],
|
|
856
|
+
warnings: [],
|
|
857
|
+
issues: [issue],
|
|
858
|
+
wrote_files: false,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
async function createExternalSkillUpdateItem(projectRoot, skillName, options) {
|
|
862
|
+
const target = createTarget(skillName);
|
|
863
|
+
try {
|
|
864
|
+
const provenance = readExternalSkillProvenance(projectRoot, skillName);
|
|
865
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
866
|
+
if (typeof fetchImpl !== 'function') {
|
|
867
|
+
throw new Error('This runtime does not provide fetch.');
|
|
868
|
+
}
|
|
869
|
+
const installedRelativeFilePaths = readInstalledRelativeFilePaths(projectRoot, target);
|
|
870
|
+
const installedFiles = readInstalledFileReports(projectRoot, target, provenance.files);
|
|
871
|
+
const driftIssues = localDriftIssues(skillName, provenance.files, installedFiles, installedRelativeFilePaths);
|
|
872
|
+
if (driftIssues.length > 0) {
|
|
873
|
+
return {
|
|
874
|
+
skill_name: skillName,
|
|
875
|
+
ok: false,
|
|
876
|
+
status: 'rejected',
|
|
877
|
+
source: provenance.source,
|
|
878
|
+
target,
|
|
879
|
+
current_files: installedFiles,
|
|
880
|
+
remote_files: [],
|
|
881
|
+
changed_files: [],
|
|
882
|
+
script_trust: provenance.script_trust,
|
|
883
|
+
warnings: [],
|
|
884
|
+
issues: driftIssues,
|
|
885
|
+
wrote_files: false,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
const remoteSourceFiles = await loadExternalSkillFiles(fetchImpl, sourceToParsed(provenance.source));
|
|
889
|
+
const normalizedRemoteFiles = normalizeImportedSkillFiles(remoteSourceFiles);
|
|
890
|
+
const remoteReports = fileReports(normalizedRemoteFiles);
|
|
891
|
+
const changedFiles = collectFileChanges(provenance.files, remoteReports);
|
|
892
|
+
const mode = options.mode ?? (options.action === 'outdated' ? 'check' : 'install');
|
|
893
|
+
const trustScripts = options.action === 'update' && options.trustScripts === true;
|
|
894
|
+
const hasScriptFiles = remoteReports.some((file) => file.kind === 'script');
|
|
895
|
+
if (options.action === 'update' &&
|
|
896
|
+
mode === 'install' &&
|
|
897
|
+
hasScriptFiles &&
|
|
898
|
+
provenance.script_trust?.grants_command_authority === true &&
|
|
899
|
+
!trustScripts) {
|
|
900
|
+
return {
|
|
901
|
+
skill_name: skillName,
|
|
902
|
+
ok: false,
|
|
903
|
+
status: 'rejected',
|
|
904
|
+
source: provenance.source,
|
|
905
|
+
target,
|
|
906
|
+
current_files: installedFiles,
|
|
907
|
+
remote_files: remoteReports,
|
|
908
|
+
changed_files: changedFiles,
|
|
909
|
+
script_trust: provenance.script_trust,
|
|
910
|
+
warnings: [],
|
|
911
|
+
issues: [`External skill ${skillName} has trusted scripts; rerun update with --trust-scripts to refresh script files.`],
|
|
912
|
+
wrote_files: false,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
const scriptTrust = createScriptTrust(projectRoot, target, remoteReports, trustScripts, mode === 'install' ? 'install' : 'dry_run', provenance.script_trust);
|
|
916
|
+
const warnings = [
|
|
917
|
+
...(changedFiles.length === 0 ? ['External skill is already current.'] : []),
|
|
918
|
+
...(hasScriptFiles && scriptTrust.status !== 'trusted'
|
|
919
|
+
? ['Imported scripts are inert reference files; mustflow does not grant command authority for external scripts.']
|
|
920
|
+
: []),
|
|
921
|
+
...(scriptTrust.status === 'trusted'
|
|
922
|
+
? ['Imported scripts were trusted by request; mustflow created command-contract intents gated by network and destructive approvals.']
|
|
923
|
+
: []),
|
|
924
|
+
...(scriptTrust.status === 'planned'
|
|
925
|
+
? ['Imported scripts would be trusted by request during install; dry-run only reports the command-contract plan.']
|
|
926
|
+
: []),
|
|
927
|
+
'External skills are untrusted until the agent reads and evaluates the selected SKILL.md.',
|
|
928
|
+
];
|
|
929
|
+
if (options.action === 'update' && mode === 'install' && changedFiles.length > 0) {
|
|
930
|
+
writeUpdatedSkillFiles(projectRoot, target, provenance.source, normalizedRemoteFiles, remoteReports, warnings, scriptTrust);
|
|
931
|
+
writeTrustedScriptCommandContract(projectRoot, target, provenance.source, scriptTrust);
|
|
932
|
+
}
|
|
933
|
+
return {
|
|
934
|
+
skill_name: skillName,
|
|
935
|
+
ok: true,
|
|
936
|
+
status: options.action === 'update' && mode === 'install' && changedFiles.length > 0
|
|
937
|
+
? 'updated'
|
|
938
|
+
: changedFiles.length > 0
|
|
939
|
+
? 'outdated'
|
|
940
|
+
: 'current',
|
|
941
|
+
source: provenance.source,
|
|
942
|
+
target,
|
|
943
|
+
current_files: installedFiles,
|
|
944
|
+
remote_files: remoteReports,
|
|
945
|
+
changed_files: changedFiles,
|
|
946
|
+
script_trust: scriptTrust,
|
|
947
|
+
warnings,
|
|
948
|
+
issues: [],
|
|
949
|
+
wrote_files: options.action === 'update' && mode === 'install' && changedFiles.length > 0,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
catch (error) {
|
|
953
|
+
return rejectedUpdateItem(skillName, error instanceof Error ? error.message : String(error));
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function selectedExternalSkillNames(projectRoot, options) {
|
|
957
|
+
if (options.all === true || (options.action === 'outdated' && (!options.skillNames || options.skillNames.length === 0))) {
|
|
958
|
+
return installedExternalSkillNames(projectRoot);
|
|
959
|
+
}
|
|
960
|
+
return [...new Set(options.skillNames ?? [])].sort((left, right) => left.localeCompare(right));
|
|
961
|
+
}
|
|
962
|
+
export async function createExternalSkillUpdateReport(projectRoot, options) {
|
|
963
|
+
const action = options.action;
|
|
964
|
+
const mode = action === 'outdated' ? 'check' : options.mode ?? 'install';
|
|
965
|
+
const checkState = createExternalSkillUpdateCheckState(projectRoot, options.now ?? new Date());
|
|
966
|
+
const warnings = [];
|
|
967
|
+
const issues = [];
|
|
968
|
+
if (action === 'update' && options.all !== true && (!options.skillNames || options.skillNames.length === 0)) {
|
|
969
|
+
issues.push('mf skill update requires a skill name or --all.');
|
|
970
|
+
}
|
|
971
|
+
if (options.all === true && options.skillNames && options.skillNames.length > 0) {
|
|
972
|
+
issues.push(`mf skill ${action} accepts either --all or skill names, not both.`);
|
|
973
|
+
}
|
|
974
|
+
if (action === 'update' && options.skillNames && options.skillNames.length > 1 && options.all !== true) {
|
|
975
|
+
issues.push('mf skill update accepts one skill name at a time unless --all is used.');
|
|
976
|
+
}
|
|
977
|
+
const requestedNames = selectedExternalSkillNames(projectRoot, options);
|
|
978
|
+
for (const skillName of requestedNames) {
|
|
979
|
+
if (!SLUG_PATTERN.test(skillName)) {
|
|
980
|
+
issues.push(`Invalid external skill name: ${skillName}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (issues.length > 0) {
|
|
984
|
+
return {
|
|
985
|
+
schema_version: '1',
|
|
986
|
+
kind: 'skill_update_report',
|
|
987
|
+
command: 'skill',
|
|
988
|
+
action,
|
|
989
|
+
ok: false,
|
|
990
|
+
mode,
|
|
991
|
+
status: 'rejected',
|
|
992
|
+
check_state: checkState,
|
|
993
|
+
skills: [],
|
|
994
|
+
warnings,
|
|
995
|
+
issues,
|
|
996
|
+
wrote_files: false,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
const installedNames = new Set(installedExternalSkillNames(projectRoot));
|
|
1000
|
+
const missingNames = requestedNames.filter((name) => !installedNames.has(name));
|
|
1001
|
+
const existingNames = requestedNames.filter((name) => installedNames.has(name));
|
|
1002
|
+
const skills = [
|
|
1003
|
+
...missingNames.map((name) => rejectedUpdateItem(name, `External skill is not installed: ${name}`)),
|
|
1004
|
+
];
|
|
1005
|
+
for (const skillName of existingNames) {
|
|
1006
|
+
skills.push(await createExternalSkillUpdateItem(projectRoot, skillName, {
|
|
1007
|
+
action,
|
|
1008
|
+
mode,
|
|
1009
|
+
trustScripts: options.trustScripts,
|
|
1010
|
+
fetch: options.fetch,
|
|
1011
|
+
}));
|
|
1012
|
+
}
|
|
1013
|
+
if (installedNames.size > 0 || requestedNames.length > 0) {
|
|
1014
|
+
writeExternalSkillUpdateCheckState(projectRoot, checkState.checked_at);
|
|
1015
|
+
}
|
|
1016
|
+
const wroteFiles = skills.some((skill) => skill.wrote_files);
|
|
1017
|
+
const ok = skills.every((skill) => skill.ok);
|
|
1018
|
+
return {
|
|
1019
|
+
schema_version: '1',
|
|
1020
|
+
kind: 'skill_update_report',
|
|
1021
|
+
command: 'skill',
|
|
1022
|
+
action,
|
|
1023
|
+
ok,
|
|
1024
|
+
mode,
|
|
1025
|
+
status: ok
|
|
1026
|
+
? wroteFiles
|
|
1027
|
+
? 'updated'
|
|
1028
|
+
: mode === 'dry_run'
|
|
1029
|
+
? 'preview'
|
|
1030
|
+
: 'checked'
|
|
1031
|
+
: 'rejected',
|
|
1032
|
+
check_state: checkState,
|
|
1033
|
+
skills,
|
|
1034
|
+
warnings,
|
|
1035
|
+
issues: skills.flatMap((skill) => skill.issues),
|
|
1036
|
+
wrote_files: wroteFiles,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
585
1039
|
export async function createExternalSkillImportReport(projectRoot, inputUrl, options) {
|
|
586
1040
|
const mode = options.mode;
|
|
587
1041
|
try {
|
|
@@ -685,6 +685,15 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
685
685
|
],
|
|
686
686
|
expectedExitCodes: [0, 1],
|
|
687
687
|
},
|
|
688
|
+
{
|
|
689
|
+
id: 'skill-update-report',
|
|
690
|
+
schemaFile: 'skill-update-report.schema.json',
|
|
691
|
+
producer: 'mf skill outdated --json / mf skill update <skill-name>|--all --json',
|
|
692
|
+
packaged: true,
|
|
693
|
+
documented: true,
|
|
694
|
+
installedCommand: ['mf', 'skill', 'outdated', '--json'],
|
|
695
|
+
expectedExitCodes: [0, 1],
|
|
696
|
+
},
|
|
688
697
|
{
|
|
689
698
|
id: 'route-fixture',
|
|
690
699
|
schemaFile: 'route-fixture.schema.json',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mustflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.114.5",
|
|
4
4
|
"description": "Agent workflow documents and CLI for mustflow repository roots.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT-0",
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
"test:cli": "node scripts/run-cli-tests.mjs --build cli",
|
|
36
36
|
"test:coverage": "node scripts/run-cli-tests.mjs --build coverage",
|
|
37
37
|
"test:audit": "node scripts/audit-tests.mjs --json",
|
|
38
|
+
"test:audit:related": "node scripts/audit-related-selection.mjs",
|
|
39
|
+
"test:ops": "node scripts/analyze-test-ops.mjs",
|
|
38
40
|
"test:release": "node scripts/run-cli-tests.mjs --build release",
|
|
39
41
|
"test:fast:node": "node scripts/run-cli-tests.mjs --build-runner=npm fast",
|
|
40
42
|
"test:release:node": "node scripts/run-cli-tests.mjs --build-runner=npm release",
|
package/schemas/README.md
CHANGED
|
@@ -195,6 +195,10 @@ Current schemas:
|
|
|
195
195
|
- `skill-import-report.schema.json`: output of `mf skill import <github-url> --json`, containing
|
|
196
196
|
GitHub source provenance, target `.mustflow/external-skills/<name>/` paths, imported file hashes,
|
|
197
197
|
warnings for inert external scripts, rejection issues, and whether files were written
|
|
198
|
+
- `skill-update-report.schema.json`: output of
|
|
199
|
+
`mf skill outdated --json` and `mf skill update <skill-name>|--all --json`, containing saved
|
|
200
|
+
external-skill provenance, current and remote file hashes, changed-file summaries, local-drift
|
|
201
|
+
rejection issues, script-trust status, last update-check state, and whether skill files were written
|
|
198
202
|
- `route-fixture.schema.json`: parsed `.mustflow/skills/route-fixtures.json`, containing strict
|
|
199
203
|
skill-route golden cases with required and forbidden route expectations
|
|
200
204
|
- `latest-run-pointer.schema.json`: `.mustflow/state/runs/latest.json` when `mf verify` writes a
|