mustflow 2.99.2 → 2.103.10

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 (53) hide show
  1. package/dist/cli/commands/run.js +11 -0
  2. package/dist/cli/commands/skill.js +76 -2
  3. package/dist/cli/i18n/en.js +2 -0
  4. package/dist/cli/i18n/es.js +2 -0
  5. package/dist/cli/i18n/fr.js +2 -0
  6. package/dist/cli/i18n/hi.js +2 -0
  7. package/dist/cli/i18n/ko.js +2 -0
  8. package/dist/cli/i18n/zh.js +2 -0
  9. package/dist/cli/lib/external-skill-import.js +455 -0
  10. package/dist/cli/lib/local-index/index.js +5 -1
  11. package/dist/cli/lib/local-index/sql.js +9 -1
  12. package/dist/cli/lib/run-plan.js +37 -0
  13. package/dist/core/change-impact.js +16 -0
  14. package/dist/core/code-outline.js +3 -13
  15. package/dist/core/config-chain.js +3 -13
  16. package/dist/core/dependency-graph.js +3 -13
  17. package/dist/core/docs-link-integrity.js +23 -4
  18. package/dist/core/env-contract.js +3 -13
  19. package/dist/core/export-diff.js +3 -3
  20. package/dist/core/ignored-directories.js +40 -0
  21. package/dist/core/public-json-contracts.js +16 -0
  22. package/dist/core/reference-drift.js +4 -2
  23. package/dist/core/related-files.js +3 -13
  24. package/dist/core/repo-merge-conflict-scan.js +3 -9
  25. package/dist/core/route-outline.js +3 -13
  26. package/dist/core/script-pack-suggestions.js +23 -12
  27. package/dist/core/secret-risk-scan.js +3 -13
  28. package/dist/core/skill-route-resolution.js +74 -6
  29. package/package.json +2 -2
  30. package/schemas/README.md +3 -0
  31. package/schemas/link-integrity-report.schema.json +1 -0
  32. package/schemas/reference-drift-report.schema.json +1 -0
  33. package/schemas/skill-import-report.schema.json +97 -0
  34. package/templates/default/i18n.toml +52 -10
  35. package/templates/default/locales/en/.mustflow/skills/INDEX.md +22 -2
  36. package/templates/default/locales/en/.mustflow/skills/ai-generated-code-hardening/SKILL.md +30 -7
  37. package/templates/default/locales/en/.mustflow/skills/api-request-performance-review/SKILL.md +12 -6
  38. package/templates/default/locales/en/.mustflow/skills/c-code-change/SKILL.md +371 -0
  39. package/templates/default/locales/en/.mustflow/skills/clarifying-question-gate/SKILL.md +53 -14
  40. package/templates/default/locales/en/.mustflow/skills/completion-evidence-gate/SKILL.md +26 -3
  41. package/templates/default/locales/en/.mustflow/skills/css-code-change/SKILL.md +74 -24
  42. package/templates/default/locales/en/.mustflow/skills/docs-prose-review/SKILL.md +36 -10
  43. package/templates/default/locales/en/.mustflow/skills/github-contribution-quality-gate/SKILL.md +27 -3
  44. package/templates/default/locales/en/.mustflow/skills/hot-path-performance-review/SKILL.md +20 -15
  45. package/templates/default/locales/en/.mustflow/skills/html-code-change/SKILL.md +37 -21
  46. package/templates/default/locales/en/.mustflow/skills/next-action-menu/SKILL.md +22 -7
  47. package/templates/default/locales/en/.mustflow/skills/quadratic-scan-review/SKILL.md +21 -19
  48. package/templates/default/locales/en/.mustflow/skills/react-code-change/SKILL.md +324 -0
  49. package/templates/default/locales/en/.mustflow/skills/routes.toml +24 -0
  50. package/templates/default/locales/en/.mustflow/skills/shell-code-change/SKILL.md +279 -0
  51. package/templates/default/locales/en/.mustflow/skills/structured-config-change/SKILL.md +170 -0
  52. package/templates/default/locales/en/.mustflow/skills/vertical-slice-tdd/SKILL.md +22 -8
  53. package/templates/default/manifest.toml +29 -1
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
4
5
  import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
5
6
  export const DEPENDENCY_GRAPH_PACK_ID = 'code';
6
7
  export const DEPENDENCY_GRAPH_SCRIPT_ID = 'dependency-graph';
@@ -14,17 +15,7 @@ const MAX_ISSUES = 50;
14
15
  const MAX_CYCLES = 20;
15
16
  const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
16
17
  const RESOLVE_EXTENSIONS = [...SOURCE_EXTENSIONS, '.json'];
17
- const IGNORED_DIRECTORIES = [
18
- '.git',
19
- '.mustflow/cache',
20
- '.mustflow/state',
21
- 'node_modules',
22
- 'dist',
23
- 'build',
24
- 'coverage',
25
- '.next',
26
- '.turbo',
27
- ];
18
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
28
19
  const ERROR_CODES = new Set([
29
20
  'dependency_graph_path_outside_root',
30
21
  'dependency_graph_unreadable_path',
@@ -64,8 +55,7 @@ function isSourceLanguage(language) {
64
55
  return language !== 'json' && language !== 'other';
65
56
  }
66
57
  function isIgnoredDirectory(relativePath) {
67
- const normalized = normalizeRelativePath(relativePath);
68
- return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
58
+ return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
69
59
  }
70
60
  function makeFinding(code, severity, pathValue, message) {
71
61
  return { code, severity, path: pathValue, message };
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
4
5
  import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
5
6
  export const LINK_INTEGRITY_PACK_ID = 'docs';
6
7
  export const LINK_INTEGRITY_SCRIPT_ID = 'link-integrity';
@@ -11,7 +12,7 @@ const MAX_ISSUES = 50;
11
12
  const DEFAULT_PATHS = ['README.md', 'schemas/README.md', 'docs-site/src/content/docs'];
12
13
  const PATH_FILTERS = ['*.md', '*.mdx'];
13
14
  const CHECKED_LINK_KINDS = ['local_file', 'local_anchor'];
14
- const IGNORED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.astro']);
15
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
15
16
  const ERROR_CODES = new Set([
16
17
  'link_integrity_path_outside_root',
17
18
  'link_integrity_unreadable_path',
@@ -59,7 +60,7 @@ function addCandidate(candidates, findings, issues, policy, candidate) {
59
60
  }
60
61
  function collectDocumentsFromDirectory(projectRoot, absoluteDirectory, candidates, findings, issues, policy) {
61
62
  const relativeDirectory = normalizeRelativePath(path.relative(projectRoot, absoluteDirectory));
62
- if (IGNORED_DIRECTORIES.has(path.basename(relativeDirectory)) || [...IGNORED_DIRECTORIES].some((entry) => relativeDirectory.startsWith(`${entry}/`))) {
63
+ if (isIgnoredDirectoryPath(relativeDirectory, IGNORED_DIRECTORIES)) {
63
64
  return;
64
65
  }
65
66
  let entries;
@@ -173,9 +174,26 @@ function splitTarget(target) {
173
174
  const anchor = rawAnchor === undefined ? null : decodeUriComponentSafe(rawAnchor);
174
175
  return { pathPart, anchor };
175
176
  }
177
+ function stripHtmlTagText(value) {
178
+ let result = '';
179
+ let tagDepth = 0;
180
+ for (const char of value) {
181
+ if (char === '<') {
182
+ tagDepth += 1;
183
+ continue;
184
+ }
185
+ if (char === '>') {
186
+ tagDepth = Math.max(0, tagDepth - 1);
187
+ continue;
188
+ }
189
+ if (tagDepth === 0) {
190
+ result += char;
191
+ }
192
+ }
193
+ return result;
194
+ }
176
195
  function slugHeading(value) {
177
- return value
178
- .replace(/<[^>]+>/gu, '')
196
+ return stripHtmlTagText(value)
179
197
  .replace(/[`*_~]/gu, '')
180
198
  .replace(/\[([^\]]+)\]\([^)]+\)/gu, '$1')
181
199
  .toLocaleLowerCase()
@@ -369,6 +387,7 @@ export function checkLinkIntegrity(projectRoot, options) {
369
387
  max_file_bytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES,
370
388
  default_paths: [...DEFAULT_PATHS],
371
389
  path_filters: [...PATH_FILTERS],
390
+ ignored_directories: [...IGNORED_DIRECTORIES],
372
391
  checked_link_kinds: [...CHECKED_LINK_KINDS],
373
392
  };
374
393
  const findings = [];
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
4
5
  import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
5
6
  export const ENV_CONTRACT_PACK_ID = 'repo';
6
7
  export const ENV_CONTRACT_SCRIPT_ID = 'env-contract';
@@ -36,17 +37,7 @@ const ENV_EXAMPLE_NAMES = [
36
37
  '.dev.vars.example',
37
38
  ];
38
39
  const SECRET_ENV_NAMES = ['.env', '.env.local', '.env.production', '.env.development', '.dev.vars'];
39
- const IGNORED_DIRECTORIES = [
40
- '.git',
41
- '.mustflow/cache',
42
- '.mustflow/state',
43
- 'node_modules',
44
- 'dist',
45
- 'build',
46
- 'coverage',
47
- '.next',
48
- '.turbo',
49
- ];
40
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
50
41
  const ERROR_CODES = new Set([
51
42
  'env_contract_path_outside_root',
52
43
  'env_contract_unreadable_path',
@@ -66,8 +57,7 @@ function pushIssue(issues, issue) {
66
57
  }
67
58
  }
68
59
  function isIgnoredDirectory(relativePath) {
69
- const normalized = normalizeRelativePath(relativePath);
70
- return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
60
+ return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
71
61
  }
72
62
  function isEnvExampleFile(relativePath) {
73
63
  const name = path.basename(relativePath).toLowerCase();
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
3
3
  import { existsSync, readFileSync } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { extractSymbols, languageForPath } from './code-outline.js';
6
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
6
7
  import { ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
7
8
  export const CODE_EXPORT_DIFF_SCRIPT_ID = 'export-diff';
8
9
  export const CODE_EXPORT_DIFF_SCRIPT_REF = `code/${CODE_EXPORT_DIFF_SCRIPT_ID}`;
@@ -10,7 +11,7 @@ const DEFAULT_BASE_REF = 'HEAD';
10
11
  const DEFAULT_MAX_FILES = 100;
11
12
  const DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
12
13
  const SUPPORTED_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
13
- const IGNORED_DIRECTORIES = ['.git', 'node_modules', 'dist', 'build', 'coverage', '.mustflow/cache', '.mustflow/state'];
14
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
14
15
  const ERROR_CODES = new Set([
15
16
  'export_diff_git_unavailable',
16
17
  'export_diff_invalid_ref',
@@ -46,8 +47,7 @@ function makeFinding(code, severity, pathValue, message) {
46
47
  return { code, severity, path: pathValue, message };
47
48
  }
48
49
  function isIgnoredPath(relativePath) {
49
- const normalized = normalizeRelativePath(relativePath);
50
- return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
50
+ return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
51
51
  }
52
52
  function isSupportedPath(relativePath) {
53
53
  return SUPPORTED_EXTENSIONS.includes(path.extname(relativePath).toLowerCase());
@@ -0,0 +1,40 @@
1
+ export const DEFAULT_IGNORED_DIRECTORIES = [
2
+ '.git',
3
+ '.mustflow/cache',
4
+ '.mustflow/state',
5
+ 'node_modules',
6
+ 'dist',
7
+ 'build',
8
+ 'coverage',
9
+ '.next',
10
+ '.turbo',
11
+ '.astro',
12
+ ];
13
+ function normalizeDirectoryPath(value) {
14
+ return value.replace(/\\/gu, '/').replace(/^\.\/+/u, '').replace(/\/+$/u, '') || '.';
15
+ }
16
+ function containsSegmentSequence(segments, sequence) {
17
+ if (sequence.length === 0 || sequence.length > segments.length) {
18
+ return false;
19
+ }
20
+ for (let index = 0; index <= segments.length - sequence.length; index += 1) {
21
+ if (sequence.every((segment, offset) => segments[index + offset] === segment)) {
22
+ return true;
23
+ }
24
+ }
25
+ return false;
26
+ }
27
+ export function isIgnoredDirectoryPath(relativePath, ignoredDirectories = DEFAULT_IGNORED_DIRECTORIES) {
28
+ const normalized = normalizeDirectoryPath(relativePath);
29
+ if (normalized === '.') {
30
+ return false;
31
+ }
32
+ const segments = normalized.split('/').filter((segment) => segment.length > 0);
33
+ return ignoredDirectories.some((entry) => {
34
+ const ignoredSegments = normalizeDirectoryPath(entry).split('/').filter((segment) => segment.length > 0);
35
+ if (ignoredSegments.length === 1) {
36
+ return segments.includes(ignoredSegments[0] ?? '');
37
+ }
38
+ return containsSegmentSequence(segments, ignoredSegments);
39
+ });
40
+ }
@@ -566,6 +566,22 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
566
566
  '--json',
567
567
  ],
568
568
  },
569
+ {
570
+ id: 'skill-import-report',
571
+ schemaFile: 'skill-import-report.schema.json',
572
+ producer: 'mf skill import <github-url> --json',
573
+ packaged: true,
574
+ documented: true,
575
+ installedCommand: [
576
+ 'mf',
577
+ 'skill',
578
+ 'import',
579
+ 'https://github.com/example/agent-skills/blob/main/security-review/SKILL.md',
580
+ '--dry-run',
581
+ '--json',
582
+ ],
583
+ expectedExitCodes: [0, 1],
584
+ },
569
585
  {
570
586
  id: 'route-fixture',
571
587
  schemaFile: 'route-fixture.schema.json',
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
4
5
  import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
5
6
  export const REFERENCE_DRIFT_PACK_ID = 'docs';
6
7
  export const REFERENCE_DRIFT_SCRIPT_ID = 'reference-drift';
@@ -16,7 +17,7 @@ const CHECKED_REFERENCE_KINDS = [
16
17
  'schema_file',
17
18
  'repo_path',
18
19
  ];
19
- const IGNORED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.astro']);
20
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
20
21
  const ERROR_CODES = new Set([
21
22
  'reference_drift_path_outside_root',
22
23
  'reference_drift_unreadable_path',
@@ -64,7 +65,7 @@ function addCandidate(candidates, findings, issues, policy, candidate) {
64
65
  }
65
66
  function collectDocumentsFromDirectory(projectRoot, absoluteDirectory, candidates, findings, issues, policy) {
66
67
  const relativeDirectory = normalizeRelativePath(path.relative(projectRoot, absoluteDirectory));
67
- if (IGNORED_DIRECTORIES.has(path.basename(relativeDirectory)) || [...IGNORED_DIRECTORIES].some((entry) => relativeDirectory.startsWith(`${entry}/`))) {
68
+ if (isIgnoredDirectoryPath(relativeDirectory, IGNORED_DIRECTORIES)) {
68
69
  return;
69
70
  }
70
71
  let entries;
@@ -318,6 +319,7 @@ export function checkReferenceDrift(projectRoot, options) {
318
319
  max_file_bytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES,
319
320
  default_paths: [...DEFAULT_PATHS],
320
321
  path_filters: [...PATH_FILTERS],
322
+ ignored_directories: [...IGNORED_DIRECTORIES],
321
323
  checked_reference_kinds: [...CHECKED_REFERENCE_KINDS],
322
324
  };
323
325
  const commandNames = new Set(options.commandNames);
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
4
5
  import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
5
6
  export const RELATED_FILES_PACK_ID = 'repo';
6
7
  export const RELATED_FILES_SCRIPT_ID = 'related-files';
@@ -12,17 +13,7 @@ const MAX_ISSUES = 50;
12
13
  const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
13
14
  const RESOLVE_EXTENSIONS = [...SOURCE_EXTENSIONS, '.json'];
14
15
  const RELATED_EXTENSIONS = [...RESOLVE_EXTENSIONS, '.d.ts', '.md', '.mdx', '.css', '.scss', '.sass', '.less'];
15
- const IGNORED_DIRECTORIES = [
16
- '.git',
17
- '.mustflow/cache',
18
- '.mustflow/state',
19
- 'node_modules',
20
- 'dist',
21
- 'build',
22
- 'coverage',
23
- '.next',
24
- '.turbo',
25
- ];
16
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
26
17
  const CONFIG_FILE_PATTERNS = [
27
18
  /^package\.json$/u,
28
19
  /^tsconfig(?:\.[^.]+)?\.json$/u,
@@ -71,8 +62,7 @@ function isSourceLanguage(language) {
71
62
  return language !== 'json' && language !== 'other';
72
63
  }
73
64
  function isIgnoredDirectory(relativePath) {
74
- const normalized = normalizeRelativePath(relativePath);
75
- return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
65
+ return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
76
66
  }
77
67
  function makeFinding(code, severity, pathValue, message) {
78
68
  return { code, severity, path: pathValue, message };
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { existsSync, lstatSync, readdirSync } from 'node:fs';
4
4
  import path from 'node:path';
5
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
5
6
  import { ensureInsideWithoutSymlinks, readUtf8FileInsideWithoutSymlinks } from './safe-filesystem.js';
6
7
  export const REPO_MERGE_CONFLICT_SCAN_PACK_ID = 'repo';
7
8
  export const REPO_MERGE_CONFLICT_SCAN_SCRIPT_ID = 'merge-conflict-scan';
@@ -15,15 +16,9 @@ const MARKER_PATTERNS = [
15
16
  { prefix: '>>>>>>>', marker: 'end' },
16
17
  ];
17
18
  const SKIPPED_DIRECTORY_NAMES = new Set([
18
- '.git',
19
- 'node_modules',
19
+ ...DEFAULT_IGNORED_DIRECTORIES,
20
20
  'vendor',
21
21
  'third_party',
22
- 'dist',
23
- 'build',
24
- 'coverage',
25
- '.mustflow/cache',
26
- '.mustflow/state',
27
22
  ]);
28
23
  function normalizeRelativePath(value) {
29
24
  return value.replace(/\\/gu, '/').replace(/^\.\/+/u, '').replace(/\/+$/u, '') || '.';
@@ -95,8 +90,7 @@ function collectGitChangedFiles(root, issues, findings) {
95
90
  }
96
91
  function shouldSkipDirectory(relativePath) {
97
92
  const normalized = normalizeRelativePath(relativePath);
98
- const firstSegment = normalized.split('/')[0] ?? normalized;
99
- return SKIPPED_DIRECTORY_NAMES.has(normalized) || SKIPPED_DIRECTORY_NAMES.has(firstSegment);
93
+ return isIgnoredDirectoryPath(normalized, [...SKIPPED_DIRECTORY_NAMES]);
100
94
  }
101
95
  function isLikelyTextFile(relativePath) {
102
96
  const extension = path.posix.extname(relativePath).toLowerCase();
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { lstatSync, readdirSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
4
5
  import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
5
6
  export const CODE_ROUTE_OUTLINE_SCRIPT_ID = 'route-outline';
6
7
  export const CODE_ROUTE_OUTLINE_SCRIPT_REF = `code/${CODE_ROUTE_OUTLINE_SCRIPT_ID}`;
@@ -39,17 +40,7 @@ const LIFECYCLE_METHODS = [
39
40
  'onAfterHandle',
40
41
  'onError',
41
42
  ];
42
- const IGNORED_DIRECTORIES = [
43
- '.git',
44
- '.mustflow/cache',
45
- '.mustflow/state',
46
- 'node_modules',
47
- 'dist',
48
- 'build',
49
- 'coverage',
50
- '.next',
51
- '.turbo',
52
- ];
43
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
53
44
  const ERROR_CODES = new Set([
54
45
  'code_route_outline_path_outside_root',
55
46
  'code_route_outline_unreadable_path',
@@ -85,8 +76,7 @@ function languageForPath(filePath) {
85
76
  }
86
77
  }
87
78
  function isIgnoredDirectory(relativePath) {
88
- const normalized = normalizeRelativePath(relativePath);
89
- return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
79
+ return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
90
80
  }
91
81
  function makeFinding(code, severity, pathValue, message) {
92
82
  return { code, severity, path: pathValue, message };
@@ -80,26 +80,37 @@ export function classifyScriptPackPathSurface(relativePath) {
80
80
  return surfaces.length > 0 ? uniqueSortedSurfaces(surfaces) : ['unknown'];
81
81
  }
82
82
  function readChangedPaths(mustflowRoot, issues) {
83
- const result = spawnSync('git', ['status', '--short'], {
83
+ const result = spawnSync('git', ['status', '--porcelain=v1', '-z', '--untracked-files=all'], {
84
84
  cwd: mustflowRoot,
85
85
  encoding: 'utf8',
86
86
  stdio: ['ignore', 'pipe', 'pipe'],
87
87
  windowsHide: true,
88
+ maxBuffer: 16 * 1024 * 1024,
88
89
  });
89
- if (result.status !== 0) {
90
- const detail = result.stderr.trim() || result.stdout.trim() || `git status exited with ${result.status}`;
90
+ if (result.error || result.status !== 0) {
91
+ const detail = result.error?.message ?? (result.stderr.trim() || result.stdout.trim() || `git status exited with ${result.status}`);
91
92
  issues.push(`Could not read changed paths: ${detail}`);
92
93
  return [];
93
94
  }
94
- return result.stdout
95
- .split(/\r?\n/u)
96
- .map((line) => line.trimEnd())
97
- .filter((line) => line.length > 0)
98
- .map((line) => {
99
- const renamed = /\s->\s(?<target>.+)$/u.exec(line);
100
- return renamed?.groups?.target ?? line.slice(3).trim();
101
- })
102
- .filter((entry) => entry.length > 0);
95
+ const paths = [];
96
+ const records = result.stdout.split('\0');
97
+ let index = 0;
98
+ while (index < records.length) {
99
+ const record = records[index] ?? '';
100
+ index += 1;
101
+ if (record.length === 0) {
102
+ continue;
103
+ }
104
+ const status = record.slice(0, 2);
105
+ const relativePath = record.slice(3);
106
+ if (relativePath.length > 0) {
107
+ paths.push(relativePath);
108
+ }
109
+ if (status.includes('R') || status.includes('C')) {
110
+ index += 1;
111
+ }
112
+ }
113
+ return paths;
103
114
  }
104
115
  function surfacesForScript(script) {
105
116
  const surfaces = new Set();
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
4
5
  import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
5
6
  export const SECRET_RISK_SCAN_PACK_ID = 'repo';
6
7
  export const SECRET_RISK_SCAN_SCRIPT_ID = 'secret-risk-scan';
@@ -35,17 +36,7 @@ const ENV_EXAMPLE_NAMES = [
35
36
  '.env.local.example',
36
37
  '.dev.vars.example',
37
38
  ];
38
- const IGNORED_DIRECTORIES = [
39
- '.git',
40
- '.mustflow/cache',
41
- '.mustflow/state',
42
- 'node_modules',
43
- 'dist',
44
- 'build',
45
- 'coverage',
46
- '.next',
47
- '.turbo',
48
- ];
39
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
49
40
  const ERROR_CODES = new Set([
50
41
  'secret_risk_path_outside_root',
51
42
  'secret_risk_unreadable_path',
@@ -68,8 +59,7 @@ function makeFinding(code, severity, pathValue, message, details = {}) {
68
59
  return { code, severity, path: pathValue, message, ...details };
69
60
  }
70
61
  function isIgnoredDirectory(relativePath) {
71
- const normalized = normalizeRelativePath(relativePath);
72
- return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
62
+ return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
73
63
  }
74
64
  function isSecretFile(relativePath) {
75
65
  return SECRET_FILE_NAMES.includes(path.basename(relativePath).toLowerCase());
@@ -7,6 +7,8 @@ const SKILL_INDEX_PATH = '.mustflow/skills/INDEX.md';
7
7
  const SKILL_ROUTER_PATH = '.mustflow/skills/router.toml';
8
8
  const SKILL_ROUTES_METADATA_PATH = '.mustflow/skills/routes.toml';
9
9
  const SKILL_FRONTMATTER_SOURCE = '.mustflow/skills/*/SKILL.md';
10
+ const EXTERNAL_SKILL_FRONTMATTER_SOURCE = '.mustflow/external-skills/*/SKILL.md';
11
+ const EXTERNAL_SKILL_PROVENANCE_FILE = 'mustflow-skill-source.json';
10
12
  const DEFAULT_MAX_CANDIDATES = 5;
11
13
  const DEFAULT_MAX_MAIN = 1;
12
14
  const DEFAULT_MAX_ADJUNCTS = 2;
@@ -36,13 +38,15 @@ const ROUTE_TYPE_WEIGHTS = {
36
38
  authoring: 16,
37
39
  adjunct: 8,
38
40
  event: 4,
41
+ external: 2,
39
42
  };
40
43
  function normalizeSkillPath(value) {
41
44
  return value.replace(/\\/gu, '/');
42
45
  }
43
46
  function skillNameFromPath(skillPath) {
44
47
  const match = /^\.mustflow\/skills\/([^/]+)\/SKILL\.md$/u.exec(skillPath);
45
- return match?.[1] ?? skillPath;
48
+ const externalMatch = /^\.mustflow\/external-skills\/([^/]+)\/SKILL\.md$/u.exec(skillPath);
49
+ return match?.[1] ?? externalMatch?.[1] ?? skillPath;
46
50
  }
47
51
  function tokenize(value) {
48
52
  return [
@@ -211,6 +215,58 @@ function readSkillFrontmatterRoutes(projectRoot) {
211
215
  }
212
216
  return routes;
213
217
  }
218
+ function hasValidExternalSkillProvenance(projectRoot, skillDirectory) {
219
+ const provenancePath = path.join(projectRoot, '.mustflow', 'external-skills', skillDirectory, EXTERNAL_SKILL_PROVENANCE_FILE);
220
+ if (!existsSync(provenancePath)) {
221
+ return false;
222
+ }
223
+ try {
224
+ const content = readUtf8FileInsideWithoutSymlinks(projectRoot, provenancePath, {
225
+ maxBytes: MUSTFLOW_TEXT_MAX_BYTES,
226
+ });
227
+ const parsed = JSON.parse(content);
228
+ return (isRecord(parsed) &&
229
+ parsed.schema_version === '1' &&
230
+ parsed.kind === 'external_skill_source' &&
231
+ isRecord(parsed.source));
232
+ }
233
+ catch {
234
+ return false;
235
+ }
236
+ }
237
+ function readExternalSkillFrontmatterRoutes(projectRoot) {
238
+ const skillRoot = path.join(projectRoot, '.mustflow', 'external-skills');
239
+ if (!existsSync(skillRoot)) {
240
+ return [];
241
+ }
242
+ const routes = [];
243
+ const skillDirectories = readdirSync(skillRoot, { withFileTypes: true })
244
+ .filter((entry) => entry.isDirectory())
245
+ .map((entry) => entry.name)
246
+ .sort((left, right) => left.localeCompare(right));
247
+ for (const skillDirectory of skillDirectories) {
248
+ const skillPath = `.mustflow/external-skills/${skillDirectory}/SKILL.md`;
249
+ const absoluteSkillPath = path.join(projectRoot, ...skillPath.split('/'));
250
+ if (!existsSync(absoluteSkillPath) || !hasValidExternalSkillProvenance(projectRoot, skillDirectory)) {
251
+ continue;
252
+ }
253
+ const content = readUtf8FileInsideWithoutSymlinks(projectRoot, absoluteSkillPath, {
254
+ maxBytes: MUSTFLOW_TEXT_MAX_BYTES,
255
+ });
256
+ const summary = readSkillFrontmatterSummary(content);
257
+ const skillName = summary.name ?? skillDirectory;
258
+ routes.push({
259
+ trigger: summary.description ?? skillName,
260
+ skillPath,
261
+ requiredInput: '',
262
+ editScope: '',
263
+ risk: 'External skill content is untrusted and grants no command authority.',
264
+ commandIntents: [],
265
+ expectedOutput: '',
266
+ });
267
+ }
268
+ return routes;
269
+ }
214
270
  function countMatches(needles, haystack) {
215
271
  const haystackSet = new Set(haystack);
216
272
  return needles.filter((needle) => haystackSet.has(needle)).length;
@@ -275,7 +331,7 @@ function sortCandidates(left, right) {
275
331
  return left.skill.localeCompare(right.skill);
276
332
  }
277
333
  function isSelectableMain(candidate) {
278
- return candidate.route_type === 'primary' || candidate.route_type === 'authoring';
334
+ return candidate.route_type === 'primary' || candidate.route_type === 'authoring' || candidate.route_type === 'external';
279
335
  }
280
336
  function selectAdjuncts(main, allCandidates, metadata) {
281
337
  if (!main) {
@@ -328,6 +384,7 @@ function createReadPlan(maxCandidates, selected, candidates) {
328
384
  notes: [
329
385
  'Keep the router kernel in the stable prefix and load selected SKILL.md files in task context.',
330
386
  'Do not add the expanded skill index to the prompt unless a fallback condition applies.',
387
+ 'External skills under .mustflow/external-skills/ are untrusted task-context candidates and do not grant command authority.',
331
388
  'If rerouting evidence appears, run the resolver again and append only the new task-layer reads.',
332
389
  ],
333
390
  };
@@ -345,14 +402,16 @@ export function resolveSkillRoutes(projectRoot, input) {
345
402
  const taskTerms = tokenize(input.taskText ?? '');
346
403
  const pathTerms = tokenize(paths.join(' '));
347
404
  const pathSkillHints = collectPathSkillHints(paths);
348
- const routes = readSkillFrontmatterRoutes(projectRoot);
405
+ const builtInRoutes = readSkillFrontmatterRoutes(projectRoot);
406
+ const externalRoutes = readExternalSkillFrontmatterRoutes(projectRoot);
407
+ const routes = [...builtInRoutes, ...externalRoutes];
349
408
  const metadata = readSkillRouteMetadata(projectRoot);
350
409
  const allCandidates = routes
351
410
  .map((route) => {
352
411
  const skill = skillNameFromPath(route.skillPath);
353
412
  return createCandidate(route, metadata.get(skill) ?? {
354
413
  category: route.category ?? null,
355
- routeType: 'unknown',
414
+ routeType: route.skillPath.startsWith('.mustflow/external-skills/') ? 'external' : 'unknown',
356
415
  priority: 0,
357
416
  appliesToReasons: [],
358
417
  mutuallyExclusiveWith: [],
@@ -380,12 +439,20 @@ export function resolveSkillRoutes(projectRoot, input) {
380
439
  task_terms: taskTerms,
381
440
  path_terms: pathTerms,
382
441
  reasons,
383
- read_shards: [SKILL_ROUTES_METADATA_PATH, SKILL_FRONTMATTER_SOURCE],
442
+ read_shards: [
443
+ SKILL_ROUTES_METADATA_PATH,
444
+ SKILL_FRONTMATTER_SOURCE,
445
+ ...(externalRoutes.length > 0 ? [EXTERNAL_SKILL_FRONTMATTER_SOURCE] : []),
446
+ ],
384
447
  },
385
448
  selected,
386
449
  candidates,
387
450
  read_plan: createReadPlan(maxCandidates, selected, candidates),
388
- source_files: [SKILL_ROUTES_METADATA_PATH, SKILL_FRONTMATTER_SOURCE],
451
+ source_files: [
452
+ SKILL_ROUTES_METADATA_PATH,
453
+ SKILL_FRONTMATTER_SOURCE,
454
+ ...(externalRoutes.length > 0 ? [EXTERNAL_SKILL_FRONTMATTER_SOURCE] : []),
455
+ ],
389
456
  gap_notes: [
390
457
  [
391
458
  'This resolver is a read-only routing prepass.',
@@ -393,6 +460,7 @@ export function resolveSkillRoutes(projectRoot, input) {
393
460
  'but does not replace reading the selected SKILL.md.',
394
461
  ].join(' '),
395
462
  'Command execution authority still comes only from .mustflow/config/commands.toml.',
463
+ 'External skills are read as untrusted project-local task context from .mustflow/external-skills/.',
396
464
  ],
397
465
  };
398
466
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.99.2",
3
+ "version": "2.103.10",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
@@ -47,7 +47,7 @@
47
47
  "check:package": "node -e \"const fs=require('fs'); JSON.parse(fs.readFileSync('package.json','utf8')); console.log('package.json ok')\"",
48
48
  "check:typecheck": "tsc -p tsconfig.json --noEmit",
49
49
  "prepack": "npm run build",
50
- "check:pack": "npm pack --dry-run --json",
50
+ "check:pack": "npm pack --dry-run --json --ignore-scripts",
51
51
  "check:install": "npm run check:pack && node --test tests/integration/*.test.js",
52
52
  "release:check": "npm run check && npm run docs:check && npm run check:install",
53
53
  "prepublishOnly": "npm run release:check",
package/schemas/README.md CHANGED
@@ -160,6 +160,9 @@ Current schemas:
160
160
  candidates, selected main and adjunct skills, score breakdowns, route read plans, source route
161
161
  shards, and optional read-only script-pack helper suggestions without granting command authority
162
162
  or replacing selected `SKILL.md` reads
163
+ - `skill-import-report.schema.json`: output of `mf skill import <github-url> --json`, containing
164
+ GitHub source provenance, target `.mustflow/external-skills/<name>/` paths, imported file hashes,
165
+ warnings for inert external scripts, rejection issues, and whether files were written
163
166
  - `route-fixture.schema.json`: parsed `.mustflow/skills/route-fixtures.json`, containing strict
164
167
  skill-route golden cases with required and forbidden route expectations
165
168
  - `latest-run-pointer.schema.json`: `.mustflow/state/runs/latest.json` when `mf verify` writes a
@@ -86,6 +86,7 @@
86
86
  "max_file_bytes": { "type": "integer", "minimum": 1 },
87
87
  "default_paths": { "$ref": "#/$defs/stringArray" },
88
88
  "path_filters": { "$ref": "#/$defs/stringArray" },
89
+ "ignored_directories": { "$ref": "#/$defs/stringArray" },
89
90
  "checked_link_kinds": {
90
91
  "type": "array",
91
92
  "items": { "$ref": "#/$defs/linkKind" }
@@ -86,6 +86,7 @@
86
86
  "max_file_bytes": { "type": "integer", "minimum": 1 },
87
87
  "default_paths": { "$ref": "#/$defs/stringArray" },
88
88
  "path_filters": { "$ref": "#/$defs/stringArray" },
89
+ "ignored_directories": { "$ref": "#/$defs/stringArray" },
89
90
  "checked_reference_kinds": {
90
91
  "type": "array",
91
92
  "items": { "$ref": "#/$defs/referenceKind" }