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
@@ -0,0 +1,455 @@
1
+ import { existsSync, renameSync, rmSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { ensureFileTargetInsideWithoutSymlinks, writeJsonFileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks, } from '../../core/safe-filesystem.js';
5
+ const EXTERNAL_SKILL_ROOT = '.mustflow/external-skills';
6
+ const PROVENANCE_FILE = 'mustflow-skill-source.json';
7
+ const DEFAULT_GITHUB_REF = 'HEAD';
8
+ const MAX_IMPORTED_FILES = 40;
9
+ const MAX_TOTAL_BYTES = 512 * 1024;
10
+ const MAX_FILE_BYTES = 256 * 1024;
11
+ const ALLOWED_SUPPORT_DIRECTORIES = new Set(['assets', 'references', 'scripts']);
12
+ const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
13
+ const GITHUB_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/u;
14
+ function normalizeRef(ref) {
15
+ return ref?.trim() || DEFAULT_GITHUB_REF;
16
+ }
17
+ function toPosixPath(value) {
18
+ return value.replace(/\\/gu, '/').replace(/^\/+/u, '').replace(/\/+$/u, '');
19
+ }
20
+ function encodeGitHubPath(value) {
21
+ return toPosixPath(value)
22
+ .split('/')
23
+ .filter(Boolean)
24
+ .map((segment) => encodeURIComponent(segment))
25
+ .join('/');
26
+ }
27
+ function validateGitHubName(value, label) {
28
+ if (!value || !GITHUB_NAME_PATTERN.test(value) || value === '.' || value === '..') {
29
+ throw new Error(`Invalid GitHub ${label}: ${value}`);
30
+ }
31
+ }
32
+ function validateRelativeImportPath(value) {
33
+ const normalized = toPosixPath(value);
34
+ if (!normalized || normalized.includes('\0')) {
35
+ throw new Error(`Invalid external skill path: ${value}`);
36
+ }
37
+ const segments = normalized.split('/');
38
+ if (segments.some((segment) => segment === '.' || segment === '..' || segment.length === 0)) {
39
+ throw new Error(`External skill path must not contain traversal segments: ${value}`);
40
+ }
41
+ return normalized;
42
+ }
43
+ function skillDirectoryPathForBlobPath(blobPath) {
44
+ const normalized = validateRelativeImportPath(blobPath);
45
+ if (!normalized.endsWith('SKILL.md')) {
46
+ throw new Error('Blob and raw GitHub URLs must point to a SKILL.md file.');
47
+ }
48
+ return normalized.split('/').slice(0, -1).join('/');
49
+ }
50
+ function parseGitHubUrl(inputUrl, refOverride) {
51
+ let url;
52
+ try {
53
+ url = new URL(inputUrl);
54
+ }
55
+ catch {
56
+ throw new Error(`Unsupported external skill URL: ${inputUrl}`);
57
+ }
58
+ if (url.protocol !== 'https:') {
59
+ throw new Error('External skill imports require an HTTPS GitHub URL.');
60
+ }
61
+ const pathParts = url.pathname.split('/').filter(Boolean);
62
+ if (url.hostname === 'github.com') {
63
+ const [owner, repo, mode, ref, ...rest] = pathParts;
64
+ const ownerName = owner ?? '';
65
+ const repoName = repo ?? '';
66
+ validateGitHubName(ownerName, 'owner');
67
+ validateGitHubName(repoName, 'repo');
68
+ if (!mode) {
69
+ const selectedRef = normalizeRef(refOverride);
70
+ return {
71
+ host: 'github.com',
72
+ owner: ownerName,
73
+ repo: repoName,
74
+ ref: selectedRef,
75
+ skillPath: '',
76
+ sourceUrl: `https://github.com/${ownerName}/${repoName}`,
77
+ };
78
+ }
79
+ if ((mode === 'tree' || mode === 'blob') && ref) {
80
+ const selectedRef = normalizeRef(refOverride ?? ref);
81
+ const sourcePath = rest.join('/');
82
+ const skillPath = mode === 'blob' ? skillDirectoryPathForBlobPath(sourcePath) : validateRelativeImportPath(sourcePath);
83
+ return {
84
+ host: 'github.com',
85
+ owner: ownerName,
86
+ repo: repoName,
87
+ ref: selectedRef,
88
+ skillPath,
89
+ sourceUrl: `https://github.com/${ownerName}/${repoName}/${mode}/${ref}/${sourcePath}`,
90
+ };
91
+ }
92
+ }
93
+ if (url.hostname === 'raw.githubusercontent.com') {
94
+ const [owner, repo, ref, ...rest] = pathParts;
95
+ const ownerName = owner ?? '';
96
+ const repoName = repo ?? '';
97
+ validateGitHubName(ownerName, 'owner');
98
+ validateGitHubName(repoName, 'repo');
99
+ if (!ref) {
100
+ throw new Error('Raw GitHub URLs must include a ref.');
101
+ }
102
+ const rawPath = rest.join('/');
103
+ const selectedRef = normalizeRef(refOverride ?? ref);
104
+ return {
105
+ host: 'raw.githubusercontent.com',
106
+ owner: ownerName,
107
+ repo: repoName,
108
+ ref: selectedRef,
109
+ skillPath: skillDirectoryPathForBlobPath(rawPath),
110
+ sourceUrl: `https://raw.githubusercontent.com/${ownerName}/${repoName}/${ref}/${rawPath}`,
111
+ };
112
+ }
113
+ throw new Error('Only github.com and raw.githubusercontent.com skill URLs are supported.');
114
+ }
115
+ function externalSkillSourceFromParsed(inputUrl, parsed) {
116
+ return {
117
+ input_url: inputUrl,
118
+ host: parsed.host,
119
+ owner: parsed.owner,
120
+ repo: parsed.repo,
121
+ ref: parsed.ref,
122
+ skill_path: parsed.skillPath,
123
+ source_url: parsed.sourceUrl,
124
+ };
125
+ }
126
+ function sanitizeSkillName(value) {
127
+ return value
128
+ .toLowerCase()
129
+ .replace(/[^a-z0-9]+/gu, '-')
130
+ .replace(/^-+|-+$/gu, '')
131
+ .replace(/-{2,}/gu, '-');
132
+ }
133
+ function readFrontmatterScalar(content, key) {
134
+ if (!content.startsWith('---')) {
135
+ return null;
136
+ }
137
+ const firstLineEnd = content.indexOf('\n');
138
+ const end = firstLineEnd >= 0 ? content.indexOf('\n---', firstLineEnd + 1) : -1;
139
+ if (firstLineEnd < 0 || end < 0) {
140
+ return null;
141
+ }
142
+ for (const line of content.slice(firstLineEnd + 1, end).split(/\r?\n/u)) {
143
+ const match = /^([a-zA-Z0-9_]+):\s*(.*)$/u.exec(line);
144
+ if (match?.[1] === key) {
145
+ return match[2].trim().replace(/^["']|["']$/gu, '') || null;
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ function targetNameForSkill(files, parsed, nameOverride) {
151
+ const candidate = nameOverride?.trim()
152
+ ?? readFrontmatterScalar(files.find((file) => file.relativePath === 'SKILL.md')?.content ?? '', 'name')
153
+ ?? parsed.skillPath.split('/').filter(Boolean).pop()
154
+ ?? parsed.repo;
155
+ const sanitized = sanitizeSkillName(candidate);
156
+ if (!SLUG_PATTERN.test(sanitized)) {
157
+ throw new Error(`External skill name cannot be converted to a safe slug: ${candidate}`);
158
+ }
159
+ return sanitized;
160
+ }
161
+ function kindForRelativePath(relativePath) {
162
+ if (relativePath === 'SKILL.md') {
163
+ return 'skill';
164
+ }
165
+ const [topLevel] = relativePath.split('/');
166
+ if (topLevel === 'scripts') {
167
+ return 'script';
168
+ }
169
+ if (topLevel === 'assets') {
170
+ return 'asset';
171
+ }
172
+ return 'reference';
173
+ }
174
+ function validateImportRelativeFilePath(relativePath) {
175
+ const normalized = validateRelativeImportPath(relativePath);
176
+ const [topLevel] = normalized.split('/');
177
+ if (normalized === 'SKILL.md') {
178
+ return normalized;
179
+ }
180
+ if (!ALLOWED_SUPPORT_DIRECTORIES.has(topLevel)) {
181
+ throw new Error(`External skill import rejected unsupported file path: ${relativePath}`);
182
+ }
183
+ return normalized;
184
+ }
185
+ function normalizeTextContent(content) {
186
+ return content.replace(/\r\n?/gu, '\n');
187
+ }
188
+ function readFrontmatterParts(content) {
189
+ if (!content.startsWith('---')) {
190
+ return {
191
+ name: null,
192
+ description: null,
193
+ body: content,
194
+ };
195
+ }
196
+ const firstLineEnd = content.indexOf('\n');
197
+ const end = firstLineEnd >= 0 ? content.indexOf('\n---', firstLineEnd + 1) : -1;
198
+ if (firstLineEnd < 0 || end < 0) {
199
+ return {
200
+ name: null,
201
+ description: null,
202
+ body: content,
203
+ };
204
+ }
205
+ const frontmatter = content.slice(firstLineEnd + 1, end).split(/\r?\n/u);
206
+ const bodyStart = content.indexOf('\n', end + 1);
207
+ return {
208
+ name: readFrontmatterScalar(content, 'name'),
209
+ description: readFrontmatterScalar(content, 'description'),
210
+ body: bodyStart >= 0 ? content.slice(bodyStart + 1) : '',
211
+ };
212
+ }
213
+ function renderYamlScalar(key, value) {
214
+ return `${key}: ${JSON.stringify(value)}`;
215
+ }
216
+ function sanitizeExternalSkillMarkdown(content) {
217
+ const frontmatter = readFrontmatterParts(content);
218
+ const lines = [
219
+ '---',
220
+ ...(frontmatter.name ? [renderYamlScalar('name', frontmatter.name)] : []),
221
+ ...(frontmatter.description ? [renderYamlScalar('description', frontmatter.description)] : []),
222
+ 'external_authority: untrusted',
223
+ '---',
224
+ frontmatter.body,
225
+ ];
226
+ return lines.join('\n');
227
+ }
228
+ function normalizeImportedSkillFiles(files) {
229
+ return files.map((file) => ({
230
+ ...file,
231
+ content: file.relativePath === 'SKILL.md' ? sanitizeExternalSkillMarkdown(file.content) : file.content,
232
+ }));
233
+ }
234
+ function hashContent(content) {
235
+ return `sha256:${createHash('sha256').update(content).digest('hex')}`;
236
+ }
237
+ function isGitHubNotFoundError(error) {
238
+ return error instanceof Error && /\(404\)/u.test(error.message);
239
+ }
240
+ async function readResponseText(response, url) {
241
+ if (!response.ok) {
242
+ throw new Error(`GitHub request failed (${response.status}) for ${url}`);
243
+ }
244
+ const text = await response.text();
245
+ if (Buffer.byteLength(text, 'utf8') > MAX_FILE_BYTES) {
246
+ throw new Error(`External skill file exceeds ${MAX_FILE_BYTES} bytes: ${url}`);
247
+ }
248
+ return normalizeTextContent(text);
249
+ }
250
+ async function fetchJson(fetchImpl, url) {
251
+ const response = await fetchImpl(url, {
252
+ headers: {
253
+ accept: 'application/vnd.github+json',
254
+ 'user-agent': 'mustflow-external-skill-import',
255
+ },
256
+ });
257
+ if (!response.ok) {
258
+ throw new Error(`GitHub request failed (${response.status}) for ${url}`);
259
+ }
260
+ return response.json();
261
+ }
262
+ function apiContentsUrl(parsed, relativePath) {
263
+ const fullPath = [parsed.skillPath, relativePath].filter(Boolean).join('/');
264
+ const encodedPath = encodeGitHubPath(fullPath);
265
+ const pathSuffix = encodedPath ? `/${encodedPath}` : '';
266
+ return `https://api.github.com/repos/${parsed.owner}/${parsed.repo}/contents${pathSuffix}?ref=${encodeURIComponent(parsed.ref)}`;
267
+ }
268
+ function decodeGitHubFileContent(entry) {
269
+ if (entry.encoding !== 'base64' || typeof entry.content !== 'string') {
270
+ throw new Error(`GitHub content API response did not include base64 content for ${entry.path}`);
271
+ }
272
+ return normalizeTextContent(Buffer.from(entry.content.replace(/\s+/gu, ''), 'base64').toString('utf8'));
273
+ }
274
+ function assertGitHubContentEntry(value) {
275
+ if (!value || typeof value !== 'object') {
276
+ throw new Error('Unexpected GitHub contents API response.');
277
+ }
278
+ const record = value;
279
+ if (typeof record.type !== 'string' || typeof record.name !== 'string' || typeof record.path !== 'string') {
280
+ throw new Error('Unexpected GitHub contents API entry shape.');
281
+ }
282
+ return record;
283
+ }
284
+ async function loadGitHubFile(fetchImpl, parsed, relativePath) {
285
+ const normalizedRelativePath = validateImportRelativeFilePath(relativePath);
286
+ const apiUrl = apiContentsUrl(parsed, normalizedRelativePath);
287
+ const entry = assertGitHubContentEntry(await fetchJson(fetchImpl, apiUrl));
288
+ if (entry.type !== 'file') {
289
+ throw new Error(`Expected a file in GitHub contents API response: ${normalizedRelativePath}`);
290
+ }
291
+ const content = entry.download_url
292
+ ? await readResponseText(await fetchImpl(entry.download_url), entry.download_url)
293
+ : decodeGitHubFileContent(entry);
294
+ return {
295
+ relativePath: normalizedRelativePath,
296
+ kind: kindForRelativePath(normalizedRelativePath),
297
+ content,
298
+ };
299
+ }
300
+ async function listGitHubDirectory(fetchImpl, parsed, relativePath) {
301
+ const apiUrl = apiContentsUrl(parsed, relativePath);
302
+ const value = await fetchJson(fetchImpl, apiUrl);
303
+ if (!Array.isArray(value)) {
304
+ throw new Error(`Expected a directory in GitHub contents API response: ${relativePath || '.'}`);
305
+ }
306
+ return value.map(assertGitHubContentEntry);
307
+ }
308
+ async function loadSupportDirectory(fetchImpl, parsed, directory) {
309
+ let entries;
310
+ try {
311
+ entries = await listGitHubDirectory(fetchImpl, parsed, directory);
312
+ }
313
+ catch (error) {
314
+ if (isGitHubNotFoundError(error)) {
315
+ return [];
316
+ }
317
+ throw error;
318
+ }
319
+ const files = [];
320
+ const pending = entries
321
+ .filter((entry) => entry.type === 'file')
322
+ .map((entry) => `${directory}/${entry.name}`)
323
+ .sort((left, right) => left.localeCompare(right));
324
+ for (const relativePath of pending) {
325
+ files.push(await loadGitHubFile(fetchImpl, parsed, relativePath));
326
+ }
327
+ return files;
328
+ }
329
+ async function loadExternalSkillFiles(fetchImpl, parsed) {
330
+ const files = [
331
+ await loadGitHubFile(fetchImpl, parsed, 'SKILL.md'),
332
+ ...(await loadSupportDirectory(fetchImpl, parsed, 'assets')),
333
+ ...(await loadSupportDirectory(fetchImpl, parsed, 'references')),
334
+ ...(await loadSupportDirectory(fetchImpl, parsed, 'scripts')),
335
+ ].sort((left, right) => left.relativePath.localeCompare(right.relativePath));
336
+ if (files.length > MAX_IMPORTED_FILES) {
337
+ throw new Error(`External skill import exceeds ${MAX_IMPORTED_FILES} files.`);
338
+ }
339
+ const totalBytes = files.reduce((total, file) => total + Buffer.byteLength(file.content, 'utf8'), 0);
340
+ if (totalBytes > MAX_TOTAL_BYTES) {
341
+ throw new Error(`External skill import exceeds ${MAX_TOTAL_BYTES} bytes.`);
342
+ }
343
+ return files;
344
+ }
345
+ function fileReports(files) {
346
+ return files.map((file) => ({
347
+ relative_path: file.relativePath,
348
+ kind: file.kind,
349
+ bytes: Buffer.byteLength(file.content, 'utf8'),
350
+ sha256: hashContent(file.content),
351
+ }));
352
+ }
353
+ function createTarget(skillName) {
354
+ const skillDir = `${EXTERNAL_SKILL_ROOT}/${skillName}`;
355
+ return {
356
+ root: EXTERNAL_SKILL_ROOT,
357
+ skill_name: skillName,
358
+ skill_dir: skillDir,
359
+ provenance_path: `${skillDir}/${PROVENANCE_FILE}`,
360
+ };
361
+ }
362
+ function writeImportedSkillFiles(projectRoot, target, source, files, fileReport, warnings) {
363
+ const targetPath = path.join(projectRoot, ...target.skill_dir.split('/'));
364
+ const skillPath = path.join(targetPath, 'SKILL.md');
365
+ if (existsSync(targetPath)) {
366
+ throw new Error(`External skill already exists: ${target.skill_dir}`);
367
+ }
368
+ ensureFileTargetInsideWithoutSymlinks(projectRoot, skillPath, { allowMissingLeaf: true });
369
+ const tempSkillDir = `${EXTERNAL_SKILL_ROOT}/.${target.skill_name}.tmp-${process.pid}-${Date.now()}`;
370
+ const tempTarget = {
371
+ ...target,
372
+ skill_dir: tempSkillDir,
373
+ provenance_path: `${tempSkillDir}/${PROVENANCE_FILE}`,
374
+ };
375
+ const tempPath = path.join(projectRoot, ...tempTarget.skill_dir.split('/'));
376
+ const tempSkillPath = path.join(tempPath, 'SKILL.md');
377
+ ensureFileTargetInsideWithoutSymlinks(projectRoot, tempSkillPath, { allowMissingLeaf: true });
378
+ try {
379
+ for (const file of files) {
380
+ writeUtf8FileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, ...tempTarget.skill_dir.split('/'), ...file.relativePath.split('/')), file.content);
381
+ }
382
+ writeJsonFileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, ...tempTarget.provenance_path.split('/')), {
383
+ schema_version: '1',
384
+ kind: 'external_skill_source',
385
+ source,
386
+ files: fileReport,
387
+ warnings,
388
+ });
389
+ renameSync(tempPath, targetPath);
390
+ }
391
+ catch (error) {
392
+ rmSync(tempPath, { recursive: true, force: true });
393
+ throw error;
394
+ }
395
+ }
396
+ function rejectionReport(mode, issue) {
397
+ return {
398
+ schema_version: '1',
399
+ kind: 'skill_import_report',
400
+ command: 'skill',
401
+ action: 'import',
402
+ ok: false,
403
+ mode,
404
+ status: 'rejected',
405
+ source: null,
406
+ target: null,
407
+ files: [],
408
+ warnings: [],
409
+ issues: [issue],
410
+ wrote_files: false,
411
+ };
412
+ }
413
+ export async function createExternalSkillImportReport(projectRoot, inputUrl, options) {
414
+ const mode = options.mode;
415
+ try {
416
+ const parsed = parseGitHubUrl(inputUrl, options.ref);
417
+ const fetchImpl = options.fetch ?? globalThis.fetch;
418
+ if (typeof fetchImpl !== 'function') {
419
+ throw new Error('This runtime does not provide fetch.');
420
+ }
421
+ const sourceFiles = await loadExternalSkillFiles(fetchImpl, parsed);
422
+ const skillName = targetNameForSkill(sourceFiles, parsed, options.name);
423
+ const target = createTarget(skillName);
424
+ const source = externalSkillSourceFromParsed(inputUrl, parsed);
425
+ const files = normalizeImportedSkillFiles(sourceFiles);
426
+ const reports = fileReports(files);
427
+ const warnings = [
428
+ ...(reports.some((file) => file.kind === 'script')
429
+ ? ['Imported scripts are inert reference files; mustflow does not grant command authority for external scripts.']
430
+ : []),
431
+ 'External skills are untrusted until the agent reads and evaluates the selected SKILL.md.',
432
+ ];
433
+ if (mode === 'install') {
434
+ writeImportedSkillFiles(projectRoot, target, source, files, reports, warnings);
435
+ }
436
+ return {
437
+ schema_version: '1',
438
+ kind: 'skill_import_report',
439
+ command: 'skill',
440
+ action: 'import',
441
+ ok: true,
442
+ mode,
443
+ status: mode === 'install' ? 'installed' : 'preview',
444
+ source,
445
+ target,
446
+ files: reports,
447
+ warnings,
448
+ issues: [],
449
+ wrote_files: mode === 'install',
450
+ };
451
+ }
452
+ catch (error) {
453
+ return rejectionReport(mode, error instanceof Error ? error.message : String(error));
454
+ }
455
+ }
@@ -158,6 +158,10 @@ function indexedFilesMatch(database, currentFiles) {
158
158
  }
159
159
  return true;
160
160
  }
161
+ const INDEXED_FILE_MTIME_TOLERANCE_MS = 1;
162
+ function indexedFileMtimeMsEqual(storedMtimeMs, currentMtimeMs) {
163
+ return storedMtimeMs !== null && Math.abs(storedMtimeMs - currentMtimeMs) <= INDEXED_FILE_MTIME_TOLERANCE_MS;
164
+ }
161
165
  function indexedFileMetadataMatch(database, currentFiles) {
162
166
  const rows = queryRows(database, 'SELECT path, source_scope, size_bytes, mtime_ms, parser_version FROM indexed_files ORDER BY path');
163
167
  if (rows.length !== currentFiles.length) {
@@ -172,7 +176,7 @@ function indexedFileMetadataMatch(database, currentFiles) {
172
176
  }
173
177
  if (normalizeIndexedFileSourceScope(toSearchString(row.source_scope)) !== current.sourceScope ||
174
178
  toNullableNumber(row.size_bytes) !== current.sizeBytes ||
175
- toNullableNumber(row.mtime_ms) !== current.mtimeMs ||
179
+ !indexedFileMtimeMsEqual(toNullableNumber(row.mtime_ms), current.mtimeMs) ||
176
180
  toSearchString(row.parser_version) !== LOCAL_INDEX_PARSER_VERSION) {
177
181
  return false;
178
182
  }
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
- export async function loadSqlJs() {
2
+ let sqlJsPromise = null;
3
+ async function initializeSqlJs() {
3
4
  const require = createRequire(import.meta.url);
4
5
  const wasmPath = require.resolve('sql.js/dist/sql-wasm.wasm');
5
6
  const sqlJsModule = (await import('sql.js'));
@@ -13,3 +14,10 @@ export async function loadSqlJs() {
13
14
  },
14
15
  });
15
16
  }
17
+ export async function loadSqlJs() {
18
+ sqlJsPromise ??= initializeSqlJs().catch((error) => {
19
+ sqlJsPromise = null;
20
+ throw error;
21
+ });
22
+ return sqlJsPromise;
23
+ }
@@ -7,6 +7,7 @@ import { evaluateCommandIntentEligibility, } from '../../core/command-intent-eli
7
7
  import { inspectActiveRunLocks, } from '../../core/active-run-locks.js';
8
8
  import { isRecord, readPositiveInteger, readString, readStringArray, } from '../../core/config-loading.js';
9
9
  import { DEFAULT_COMMAND_MAX_OUTPUT_BYTES, COMMAND_OUTPUT_LIMIT_SCOPE, } from '../../core/command-output-limits.js';
10
+ import { checkRepoApprovalGate } from '../../core/repo-approval-gate.js';
10
11
  import { normalizeSuccessExitCodes } from '../../core/success-exit-codes.js';
11
12
  import { normalizeSafeTestTargetPath, TEST_TARGET_PATH_ERROR } from '../../core/test-target-paths.js';
12
13
  import { evaluateCommandPreconditions, } from '../../core/command-preconditions.js';
@@ -124,6 +125,38 @@ function readRunIntentMetadata(contract, intent) {
124
125
  relatedOneshotChecks: readStringArray(intent, 'related_oneshot_checks') ?? [],
125
126
  };
126
127
  }
128
+ function createApprovalBlock(projectRoot, metadata) {
129
+ const actionTypes = [];
130
+ if (metadata.network === true) {
131
+ actionTypes.push('network_access');
132
+ }
133
+ if (metadata.destructive === true) {
134
+ actionTypes.push('destructive_command');
135
+ }
136
+ if (actionTypes.length === 0) {
137
+ return null;
138
+ }
139
+ const approvalReport = checkRepoApprovalGate(projectRoot, actionTypes);
140
+ if (approvalReport.issues.length > 0) {
141
+ return {
142
+ reasonCode: 'approval_policy_unreadable',
143
+ detail: `Could not evaluate ${approvalReport.input.policy_path}: ${approvalReport.issues.join(' ')}`,
144
+ };
145
+ }
146
+ const requiredActions = approvalReport.decisions
147
+ .filter((decision) => decision.approval_required)
148
+ .map((decision) => decision.action_type);
149
+ if (requiredActions.length === 0) {
150
+ return null;
151
+ }
152
+ const reasonCode = requiredActions.includes('destructive_command')
153
+ ? 'destructive_requires_approval'
154
+ : 'network_requires_approval';
155
+ return {
156
+ reasonCode,
157
+ detail: `Action ${requiredActions.map((action) => JSON.stringify(action)).join(', ')} requires explicit approval before mf run can execute this intent.`,
158
+ };
159
+ }
127
160
  function createBlockedRunPlan(contract, intentName, intent, eligibility, reasonCode, detail, preconditions = []) {
128
161
  const metadata = intent ? readRunIntentMetadata(contract, intent) : null;
129
162
  return {
@@ -177,6 +210,10 @@ export function createRunPlan(projectRoot, contract, intentName, options = {}) {
177
210
  return createBlockedRunPlan(contract, intentName, rawIntent, eligibility, eligibility.code, eligibility.detail, preconditions);
178
211
  }
179
212
  const metadata = readRunIntentMetadata(contract, rawIntent);
213
+ const approvalBlock = createApprovalBlock(projectRoot, metadata);
214
+ if (approvalBlock) {
215
+ return createBlockedRunPlan(contract, intentName, rawIntent, eligibility, approvalBlock.reasonCode, approvalBlock.detail, preconditions);
216
+ }
180
217
  const maxOutputBytesLimitDetail = getCommandMaxOutputBytesLimitDetail(contract, rawIntent);
181
218
  if (maxOutputBytesLimitDetail) {
182
219
  return createBlockedRunPlan(contract, intentName, rawIntent, eligibility, 'max_output_bytes_exceeds_limit', maxOutputBytesLimitDetail, preconditions);
@@ -195,6 +195,13 @@ function addDependencyImpacts(root, changedFiles, impacts, policy, findings, iss
195
195
  issues.push(`dependency-graph: ${issue}`);
196
196
  }
197
197
  }
198
+ for (const dependencyFinding of dependencyReport.findings) {
199
+ const mappedFinding = mapDependencyGraphTruncationFinding(dependencyFinding);
200
+ if (mappedFinding === null) {
201
+ continue;
202
+ }
203
+ findings.push(mappedFinding);
204
+ }
198
205
  const changedPathSet = new Set(sourcePaths);
199
206
  for (const edge of dependencyReport.edges) {
200
207
  if (!changedPathSet.has(edge.target_path) || changedPathSet.has(edge.source_path)) {
@@ -209,6 +216,15 @@ function addDependencyImpacts(root, changedFiles, impacts, policy, findings, iss
209
216
  }, policy, findings, issues);
210
217
  }
211
218
  }
219
+ function mapDependencyGraphTruncationFinding(finding) {
220
+ if (finding.code === 'dependency_graph_max_files_exceeded') {
221
+ return makeFinding('change_impact_max_files_exceeded', 'high', finding.path, `Dependency graph input was truncated while computing change impact: ${finding.message}`);
222
+ }
223
+ if (finding.code === 'dependency_graph_max_nodes_exceeded' || finding.code === 'dependency_graph_max_edges_exceeded') {
224
+ return makeFinding('change_impact_max_impacts_exceeded', 'high', finding.path, `Dependency graph impact expansion was truncated while computing change impact: ${finding.message}`);
225
+ }
226
+ return null;
227
+ }
212
228
  function createScriptHints(changedFiles) {
213
229
  const sourcePaths = changedFiles.filter((file) => file.surface === 'source').map((file) => file.path);
214
230
  const selectorRelevantFiles = changedFiles.filter((file) => ['source', 'test', 'schema', 'config', 'package', 'template', 'workflow', 'unknown'].includes(file.surface));
@@ -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
  import { listSourceAnchorFiles, parseSourceAnchorsInContent, sourceAnchorTextContainsSecretLike, splitSourceAnchorList, } from './source-anchors.js';
6
7
  export const CODE_PACK_ID = 'code';
@@ -19,17 +20,7 @@ const SVELTE_EXTENSIONS = ['.svelte'];
19
20
  const GO_EXTENSIONS = ['.go'];
20
21
  const RUST_EXTENSIONS = ['.rs'];
21
22
  const PYTHON_EXTENSIONS = ['.py'];
22
- const IGNORED_DIRECTORIES = [
23
- '.git',
24
- '.mustflow/cache',
25
- '.mustflow/state',
26
- 'node_modules',
27
- 'dist',
28
- 'build',
29
- 'coverage',
30
- '.next',
31
- '.turbo',
32
- ];
23
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
33
24
  const ERROR_OUTLINE_CODES = new Set([
34
25
  'code_outline_path_outside_root',
35
26
  'code_outline_unreadable_path',
@@ -97,8 +88,7 @@ export function languageForPath(filePath) {
97
88
  return languageAdapterForPath(filePath)?.languageForPath(filePath) ?? null;
98
89
  }
99
90
  function isIgnoredDirectory(relativePath) {
100
- const normalized = normalizeRelativePath(relativePath);
101
- return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
91
+ return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
102
92
  }
103
93
  function makeOutlineFinding(code, severity, pathValue, message) {
104
94
  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 CONFIG_CHAIN_PACK_ID = 'repo';
6
7
  export const CONFIG_CHAIN_SCRIPT_ID = 'config-chain';
@@ -37,17 +38,7 @@ const CONFIG_FILE_NAMES = [
37
38
  '.mustflow/config/commands.toml',
38
39
  '.mustflow/config/mustflow.toml',
39
40
  ];
40
- const IGNORED_DIRECTORIES = [
41
- '.git',
42
- '.mustflow/cache',
43
- '.mustflow/state',
44
- 'node_modules',
45
- 'dist',
46
- 'build',
47
- 'coverage',
48
- '.next',
49
- '.turbo',
50
- ];
41
+ const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
51
42
  const ERROR_CODES = new Set([
52
43
  'config_chain_path_outside_root',
53
44
  'config_chain_unreadable_path',
@@ -133,8 +124,7 @@ function isConfigFile(relativePath) {
133
124
  /^tsconfig(?:\..*)?\.json$/u.test(name));
134
125
  }
135
126
  function isIgnoredDirectory(relativePath) {
136
- const normalized = normalizeRelativePath(relativePath);
137
- return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
127
+ return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
138
128
  }
139
129
  function normalizeTargetPath(projectRoot, targetPath) {
140
130
  const absolutePath = path.resolve(process.cwd(), targetPath);