mustflow 2.74.2 → 2.74.4

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 (37) hide show
  1. package/dist/cli/commands/script-pack.js +132 -3
  2. package/dist/cli/i18n/en.js +30 -3
  3. package/dist/cli/i18n/es.js +30 -3
  4. package/dist/cli/i18n/fr.js +30 -3
  5. package/dist/cli/i18n/hi.js +30 -3
  6. package/dist/cli/i18n/ko.js +30 -3
  7. package/dist/cli/i18n/zh.js +30 -3
  8. package/dist/cli/lib/script-pack-registry.js +53 -0
  9. package/dist/cli/script-packs/repo-generated-boundary.js +94 -0
  10. package/dist/core/generated-boundary.js +231 -0
  11. package/dist/core/public-json-contracts.js +35 -0
  12. package/dist/core/script-pack-suggestions.js +199 -0
  13. package/package.json +1 -1
  14. package/schemas/README.md +8 -1
  15. package/schemas/generated-boundary-report.schema.json +148 -0
  16. package/schemas/script-pack-catalog.schema.json +32 -0
  17. package/schemas/script-pack-suggestion-report.schema.json +159 -0
  18. package/templates/default/common/.mustflow/config/commands.toml +32 -0
  19. package/templates/default/i18n.toml +17 -17
  20. package/templates/default/locales/en/.mustflow/skills/INDEX.md +4 -4
  21. package/templates/default/locales/en/.mustflow/skills/astro-code-change/SKILL.md +5 -3
  22. package/templates/default/locales/en/.mustflow/skills/completion-evidence-gate/SKILL.md +4 -1
  23. package/templates/default/locales/en/.mustflow/skills/dependency-upgrade-review/SKILL.md +7 -7
  24. package/templates/default/locales/en/.mustflow/skills/elysia-code-change/SKILL.md +15 -8
  25. package/templates/default/locales/en/.mustflow/skills/external-skill-intake/SKILL.md +10 -5
  26. package/templates/default/locales/en/.mustflow/skills/public-json-contract-change/SKILL.md +5 -2
  27. package/templates/default/locales/en/.mustflow/skills/rate-limit-integrity-review/SKILL.md +7 -4
  28. package/templates/default/locales/en/.mustflow/skills/repo-improvement-loop/SKILL.md +6 -2
  29. package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +4 -3
  30. package/templates/default/locales/en/.mustflow/skills/skill-authoring/SKILL.md +3 -1
  31. package/templates/default/locales/en/.mustflow/skills/skill-refresh/SKILL.md +19 -2
  32. package/templates/default/locales/en/.mustflow/skills/tailwind-code-change/SKILL.md +12 -9
  33. package/templates/default/locales/en/.mustflow/skills/template-install-surface-sync/SKILL.md +6 -3
  34. package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +22 -17
  35. package/templates/default/locales/en/.mustflow/skills/unocss-code-change/SKILL.md +16 -5
  36. package/templates/default/locales/en/.mustflow/skills/version-freshness-check/SKILL.md +8 -8
  37. package/templates/default/manifest.toml +1 -1
@@ -13,11 +13,64 @@ export const SCRIPT_PACKS = [
13
13
  usage: 'mf script-pack run core/text-budget check <path...> [options]',
14
14
  summaryKey: 'scriptPack.script.textBudget.summary',
15
15
  actions: ['check'],
16
+ useWhen: [
17
+ 'Check exact text budgets for docs, package metadata, prompts, release notes, or user-facing copy.',
18
+ 'Inspect a JSON string field by JSON Pointer when a public description, label, or summary has a length contract.',
19
+ ],
20
+ phases: ['before_change', 'after_change', 'review'],
21
+ readOnly: true,
22
+ mutates: false,
23
+ network: false,
24
+ inputs: ['path', 'json_pointer', 'budget', 'unit'],
25
+ outputs: ['human_summary', 'json_report'],
26
+ relatedSkills: [
27
+ 'docs-prose-review',
28
+ 'public-json-contract-change',
29
+ 'readme-authoring',
30
+ 'release-notes-authoring',
31
+ ],
32
+ riskLevel: 'low',
33
+ cost: 'low',
16
34
  reportSchemaFile: 'text-budget-report.schema.json',
17
35
  loadRunner: async () => (await import('../script-packs/core-text-budget.js')).runCoreTextBudgetScript,
18
36
  },
19
37
  ],
20
38
  },
39
+ {
40
+ id: 'repo',
41
+ summaryKey: 'scriptPack.pack.repo.summary',
42
+ scripts: [
43
+ {
44
+ packId: 'repo',
45
+ id: 'generated-boundary',
46
+ ref: scriptRef('repo', 'generated-boundary'),
47
+ usage: 'mf script-pack run repo/generated-boundary check <path...> [options]',
48
+ summaryKey: 'scriptPack.script.generatedBoundary.summary',
49
+ actions: ['check'],
50
+ useWhen: [
51
+ 'Check candidate edit paths before changing files that may be generated, ignored, protected, vendor, or cache output.',
52
+ 'Review changed paths after implementation when generated or protected-file drift would make completion evidence misleading.',
53
+ ],
54
+ phases: ['before_change', 'after_change', 'review'],
55
+ readOnly: true,
56
+ mutates: false,
57
+ network: false,
58
+ inputs: ['path'],
59
+ outputs: ['human_summary', 'json_report'],
60
+ relatedSkills: [
61
+ 'completion-evidence-gate',
62
+ 'proactive-risk-surfacing',
63
+ 'quality-gaming-guard',
64
+ 'repo-improvement-loop',
65
+ 'template-install-surface-sync',
66
+ ],
67
+ riskLevel: 'low',
68
+ cost: 'low',
69
+ reportSchemaFile: 'generated-boundary-report.schema.json',
70
+ loadRunner: async () => (await import('../script-packs/repo-generated-boundary.js')).runRepoGeneratedBoundaryScript,
71
+ },
72
+ ],
73
+ },
21
74
  ];
22
75
  export function listScriptPackScripts() {
23
76
  return SCRIPT_PACKS.flatMap((pack) => pack.scripts);
@@ -0,0 +1,94 @@
1
+ import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
+ import { t } from '../lib/i18n.js';
3
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
+ import { resolveMustflowRoot } from '../lib/project-root.js';
5
+ import { GENERATED_BOUNDARY_SCRIPT_REF, inspectGeneratedBoundary, } from '../../core/generated-boundary.js';
6
+ const GENERATED_BOUNDARY_OPTIONS = [{ name: '--json', kind: 'boolean' }];
7
+ export function getRepoGeneratedBoundaryHelp(lang = 'en') {
8
+ return renderHelp({
9
+ usage: 'mf script-pack run repo/generated-boundary check <path...> [options]',
10
+ summary: t(lang, 'generatedBoundary.help.summary'),
11
+ options: [
12
+ { label: '--json', description: t(lang, 'cli.option.json') },
13
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
14
+ ],
15
+ examples: [
16
+ 'mf script-pack run repo/generated-boundary check src/cli/index.ts',
17
+ 'mf script-pack run repo/generated-boundary check dist/cli/index.js --json',
18
+ 'mf script-pack run repo/generated-boundary check AGENTS.md .mustflow/config/manifest.lock.toml',
19
+ ],
20
+ exitCodes: [
21
+ { label: '0', description: t(lang, 'generatedBoundary.help.exit.ok') },
22
+ { label: '1', description: t(lang, 'generatedBoundary.help.exit.fail') },
23
+ ],
24
+ }, lang);
25
+ }
26
+ function parseGeneratedBoundaryOptions(args, lang) {
27
+ const [action, ...rest] = args;
28
+ const parsed = parseCliOptions(rest, GENERATED_BOUNDARY_OPTIONS, { allowPositionals: true });
29
+ const json = hasParsedCliOption(parsed, '--json');
30
+ if (action !== 'check') {
31
+ return {
32
+ action: 'check',
33
+ json,
34
+ paths: parsed.positionals,
35
+ error: action
36
+ ? t(lang, 'generatedBoundary.error.unknownAction', { action })
37
+ : t(lang, 'generatedBoundary.error.missingAction'),
38
+ };
39
+ }
40
+ if (parsed.error) {
41
+ return { action, json, paths: parsed.positionals, error: formatCliOptionParseError(parsed.error, lang) };
42
+ }
43
+ if (parsed.positionals.length === 0) {
44
+ return { action, json, paths: parsed.positionals, error: t(lang, 'generatedBoundary.error.missingPath') };
45
+ }
46
+ return { action, json, paths: parsed.positionals };
47
+ }
48
+ function renderGeneratedBoundarySummary(report, lang) {
49
+ const lines = [
50
+ t(lang, 'generatedBoundary.title'),
51
+ `${t(lang, 'scriptPack.label.script')}: ${GENERATED_BOUNDARY_SCRIPT_REF}`,
52
+ `${t(lang, 'label.status')}: ${report.status}`,
53
+ `${t(lang, 'generatedBoundary.label.checkedTargets')}: ${report.targets.length}`,
54
+ `${t(lang, 'generatedBoundary.label.findings')}: ${report.findings.length}`,
55
+ ];
56
+ if (report.targets.length > 0) {
57
+ lines.push(t(lang, 'generatedBoundary.label.targets'));
58
+ for (const target of report.targets) {
59
+ const boundaries = target.matched_boundaries.length === 0 ? t(lang, 'value.none') : target.matched_boundaries.join(', ');
60
+ lines.push(`- ${target.path}: ${target.kind}, ${t(lang, 'generatedBoundary.label.boundaries')}: ${boundaries}`);
61
+ }
62
+ }
63
+ if (report.findings.length > 0) {
64
+ lines.push(t(lang, 'generatedBoundary.label.findings'));
65
+ for (const finding of report.findings) {
66
+ lines.push(`- ${finding.path}: ${finding.code} (${finding.message})`);
67
+ }
68
+ }
69
+ if (report.issues.length > 0) {
70
+ lines.push(t(lang, 'generatedBoundary.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
71
+ }
72
+ if (report.findings.length === 0 && report.issues.length === 0) {
73
+ lines.push(t(lang, 'generatedBoundary.clean'));
74
+ }
75
+ return lines.join('\n');
76
+ }
77
+ export function runRepoGeneratedBoundaryScript(args, reporter, lang = 'en') {
78
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
79
+ reporter.stdout(getRepoGeneratedBoundaryHelp(lang));
80
+ return 0;
81
+ }
82
+ const options = parseGeneratedBoundaryOptions(args, lang);
83
+ if (options.error) {
84
+ printUsageError(reporter, options.error, 'mf script-pack run repo/generated-boundary --help', getRepoGeneratedBoundaryHelp(lang), lang);
85
+ return 1;
86
+ }
87
+ const report = inspectGeneratedBoundary(resolveMustflowRoot(), { paths: options.paths });
88
+ if (options.json) {
89
+ reporter.stdout(JSON.stringify(report, null, 2));
90
+ return report.ok ? 0 : 1;
91
+ }
92
+ reporter.stdout(renderGeneratedBoundarySummary(report, lang));
93
+ return report.ok ? 0 : 1;
94
+ }
@@ -0,0 +1,231 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, lstatSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { isRecord, readMustflowConfigIfExists, readStringArray } from './config-loading.js';
5
+ import { ensureInside } from './safe-filesystem.js';
6
+ export const GENERATED_BOUNDARY_PACK_ID = 'repo';
7
+ export const GENERATED_BOUNDARY_SCRIPT_ID = 'generated-boundary';
8
+ export const GENERATED_BOUNDARY_SCRIPT_REF = `${GENERATED_BOUNDARY_PACK_ID}/${GENERATED_BOUNDARY_SCRIPT_ID}`;
9
+ const BUILTIN_GENERATED_PATTERNS = [
10
+ 'REPO_MAP.md',
11
+ '.mustflow/config/manifest.lock.toml',
12
+ '.mustflow/state/**',
13
+ '.mustflow/cache/**',
14
+ 'dist/**',
15
+ 'build/**',
16
+ 'coverage/**',
17
+ '.next/**',
18
+ '.turbo/**',
19
+ ];
20
+ const BUILTIN_VENDOR_PATTERNS = ['node_modules/**', 'vendor/**', 'third_party/**'];
21
+ const BUILTIN_CACHE_PATTERNS = ['.cache/**', '.parcel-cache/**', '.pytest_cache/**'];
22
+ const BUILTIN_PROTECTED_PATTERNS = ['.git/**', '**/*.pem', '**/*.key', '**/.env'];
23
+ const CATEGORY_FINDING = {
24
+ generated: { code: 'generated_boundary_generated_path', severity: 'medium' },
25
+ ignored: { code: 'generated_boundary_ignored_path', severity: 'medium' },
26
+ protected: { code: 'generated_boundary_protected_path', severity: 'critical' },
27
+ vendor: { code: 'generated_boundary_vendor_path', severity: 'high' },
28
+ cache: { code: 'generated_boundary_cache_path', severity: 'medium' },
29
+ outside_root: { code: 'generated_boundary_path_outside_root', severity: 'high' },
30
+ };
31
+ function toPosixPath(value) {
32
+ return value.replace(/\\/gu, '/');
33
+ }
34
+ function normalizeRelativePath(value) {
35
+ return toPosixPath(value).replace(/^\.\/+/u, '').replace(/\/+$/u, '') || '.';
36
+ }
37
+ function uniqueSorted(values) {
38
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right));
39
+ }
40
+ function escapeRegExp(value) {
41
+ return value.replace(/[\\^$+?.()|[\]{}]/gu, '\\$&');
42
+ }
43
+ function globToRegExp(pattern) {
44
+ const normalized = normalizeRelativePath(pattern).replace(/^\/+/u, '');
45
+ let source = '^';
46
+ for (let index = 0; index < normalized.length;) {
47
+ if (normalized.startsWith('**', index)) {
48
+ source += '.*';
49
+ index += 2;
50
+ continue;
51
+ }
52
+ const character = normalized[index];
53
+ if (character === '*') {
54
+ source += '[^/]*';
55
+ index += 1;
56
+ continue;
57
+ }
58
+ if (character === '?') {
59
+ source += '[^/]';
60
+ index += 1;
61
+ continue;
62
+ }
63
+ source += escapeRegExp(character ?? '');
64
+ index += 1;
65
+ }
66
+ return new RegExp(`${source}$`, 'u');
67
+ }
68
+ function patternMatches(pattern, relativePath) {
69
+ const normalizedPattern = normalizeRelativePath(pattern);
70
+ const normalizedPath = normalizeRelativePath(relativePath);
71
+ if (normalizedPattern.endsWith('/**')) {
72
+ const base = normalizedPattern.slice(0, -3);
73
+ if (normalizedPath === base || normalizedPath.startsWith(`${base}/`)) {
74
+ return true;
75
+ }
76
+ }
77
+ if (globToRegExp(normalizedPattern).test(normalizedPath)) {
78
+ return true;
79
+ }
80
+ return normalizedPattern.startsWith('**/') && globToRegExp(normalizedPattern.slice(3)).test(normalizedPath);
81
+ }
82
+ function readPatternTable(config, tableName, key) {
83
+ const table = config?.[tableName];
84
+ return isRecord(table) ? (readStringArray(table, key) ?? []) : [];
85
+ }
86
+ function createPolicy(projectRoot, issues) {
87
+ let config;
88
+ try {
89
+ config = readMustflowConfigIfExists(projectRoot);
90
+ }
91
+ catch (error) {
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ issues.push(`Could not read .mustflow/config/mustflow.toml: ${message}`);
94
+ }
95
+ return {
96
+ config_path: '.mustflow/config/mustflow.toml',
97
+ config_loaded: config !== undefined,
98
+ generated_patterns: readPatternTable(config, 'document_roots', 'generated'),
99
+ ignored_patterns: readPatternTable(config, 'document_roots', 'ignored'),
100
+ protected_patterns: readPatternTable(config, 'edit_policy', 'protected'),
101
+ builtin_generated_patterns: [...BUILTIN_GENERATED_PATTERNS],
102
+ builtin_vendor_patterns: [...BUILTIN_VENDOR_PATTERNS],
103
+ builtin_cache_patterns: [...BUILTIN_CACHE_PATTERNS],
104
+ builtin_protected_patterns: [...BUILTIN_PROTECTED_PATTERNS],
105
+ };
106
+ }
107
+ function createBoundaryPatterns(policy) {
108
+ return [
109
+ ...policy.generated_patterns.map((pattern) => ({ category: 'generated', pattern, source: 'mustflow_config' })),
110
+ ...policy.ignored_patterns.map((pattern) => ({ category: 'ignored', pattern, source: 'mustflow_config' })),
111
+ ...policy.protected_patterns.map((pattern) => ({ category: 'protected', pattern, source: 'mustflow_config' })),
112
+ ...policy.builtin_generated_patterns.map((pattern) => ({ category: 'generated', pattern, source: 'builtin' })),
113
+ ...policy.builtin_vendor_patterns.map((pattern) => ({ category: 'vendor', pattern, source: 'builtin' })),
114
+ ...policy.builtin_cache_patterns.map((pattern) => ({ category: 'cache', pattern, source: 'builtin' })),
115
+ ...policy.builtin_protected_patterns.map((pattern) => ({ category: 'protected', pattern, source: 'builtin' })),
116
+ ];
117
+ }
118
+ function targetKind(absolutePath) {
119
+ if (!existsSync(absolutePath)) {
120
+ return { exists: false, kind: 'missing' };
121
+ }
122
+ const stats = lstatSync(absolutePath);
123
+ if (stats.isFile()) {
124
+ return { exists: true, kind: 'file' };
125
+ }
126
+ if (stats.isDirectory()) {
127
+ return { exists: true, kind: 'directory' };
128
+ }
129
+ return { exists: true, kind: 'other' };
130
+ }
131
+ function makeFinding(code, severity, targetPath, boundary, message, pattern, source) {
132
+ return { code, severity, message, path: targetPath, boundary, pattern, source };
133
+ }
134
+ function createInputHash(policy, targets, issues) {
135
+ const inputState = {
136
+ policy,
137
+ targets: targets.map((target) => ({
138
+ input: target.input,
139
+ path: target.path,
140
+ exists: target.exists,
141
+ kind: target.kind,
142
+ matched_patterns: target.matched_patterns,
143
+ })),
144
+ issues,
145
+ };
146
+ return `sha256:${createHash('sha256').update(JSON.stringify(inputState)).digest('hex')}`;
147
+ }
148
+ export function inspectGeneratedBoundary(projectRoot, options) {
149
+ const root = path.resolve(projectRoot);
150
+ const issues = [];
151
+ const policy = createPolicy(root, issues);
152
+ const patterns = createBoundaryPatterns(policy);
153
+ const targets = [];
154
+ const findings = [];
155
+ for (const targetPath of options.paths) {
156
+ let relativePath = targetPath;
157
+ let absolutePath;
158
+ try {
159
+ absolutePath = path.resolve(process.cwd(), targetPath);
160
+ ensureInside(root, absolutePath);
161
+ relativePath = normalizeRelativePath(path.relative(root, absolutePath));
162
+ }
163
+ catch (error) {
164
+ const message = error instanceof Error ? error.message : String(error);
165
+ issues.push(message);
166
+ targets.push({
167
+ input: targetPath,
168
+ path: targetPath,
169
+ exists: null,
170
+ kind: 'unknown',
171
+ matched_boundaries: ['outside_root'],
172
+ matched_patterns: [],
173
+ });
174
+ findings.push(makeFinding('generated_boundary_path_outside_root', 'high', targetPath, 'outside_root', message, null, null));
175
+ continue;
176
+ }
177
+ let existence;
178
+ try {
179
+ existence = targetKind(absolutePath);
180
+ }
181
+ catch (error) {
182
+ const message = error instanceof Error ? error.message : String(error);
183
+ issues.push(`${relativePath}: ${message}`);
184
+ targets.push({
185
+ input: targetPath,
186
+ path: relativePath,
187
+ exists: null,
188
+ kind: 'unknown',
189
+ matched_boundaries: [],
190
+ matched_patterns: [],
191
+ });
192
+ findings.push(makeFinding('generated_boundary_unreadable_path', 'high', relativePath, 'protected', message, null, null));
193
+ continue;
194
+ }
195
+ const matchedPatterns = patterns.filter((candidate) => patternMatches(candidate.pattern, relativePath));
196
+ const matchedBoundaries = uniqueSorted(matchedPatterns.map((candidate) => candidate.category));
197
+ targets.push({
198
+ input: targetPath,
199
+ path: relativePath,
200
+ exists: existence.exists,
201
+ kind: existence.kind,
202
+ matched_boundaries: matchedBoundaries,
203
+ matched_patterns: matchedPatterns,
204
+ });
205
+ for (const match of matchedPatterns) {
206
+ const finding = CATEGORY_FINDING[match.category];
207
+ findings.push(makeFinding(finding.code, finding.severity, relativePath, match.category, `${relativePath} matches ${match.category} boundary pattern ${match.pattern}.`, match.pattern, match.source));
208
+ }
209
+ }
210
+ const status = findings.some((finding) => ['generated_boundary_path_outside_root', 'generated_boundary_unreadable_path'].includes(finding.code))
211
+ ? 'error'
212
+ : findings.length > 0 || issues.length > 0
213
+ ? 'failed'
214
+ : 'passed';
215
+ return {
216
+ schema_version: '1',
217
+ command: 'script-pack',
218
+ pack_id: GENERATED_BOUNDARY_PACK_ID,
219
+ script_id: GENERATED_BOUNDARY_SCRIPT_ID,
220
+ script_ref: GENERATED_BOUNDARY_SCRIPT_REF,
221
+ action: 'check',
222
+ status,
223
+ ok: status === 'passed',
224
+ mustflow_root: root,
225
+ policy,
226
+ input_hash: createInputHash(policy, targets, issues),
227
+ targets,
228
+ findings,
229
+ issues,
230
+ };
231
+ }
@@ -215,6 +215,23 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
215
215
  documented: true,
216
216
  installedCommand: ['mf', 'script-pack', 'list', '--json'],
217
217
  },
218
+ {
219
+ id: 'script-pack-suggestion-report',
220
+ schemaFile: 'script-pack-suggestion-report.schema.json',
221
+ producer: 'mf script-pack suggest --json',
222
+ packaged: true,
223
+ documented: true,
224
+ installedCommand: [
225
+ 'mf',
226
+ 'script-pack',
227
+ 'suggest',
228
+ '--path',
229
+ 'AGENTS.md',
230
+ '--phase',
231
+ 'before_change',
232
+ '--json',
233
+ ],
234
+ },
218
235
  {
219
236
  id: 'text-budget-report',
220
237
  schemaFile: 'text-budget-report.schema.json',
@@ -234,6 +251,24 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
234
251
  ],
235
252
  expectedExitCodes: [0, 1],
236
253
  },
254
+ {
255
+ id: 'generated-boundary-report',
256
+ schemaFile: 'generated-boundary-report.schema.json',
257
+ producer: 'mf script-pack run repo/generated-boundary check <path...> --json',
258
+ packaged: true,
259
+ documented: true,
260
+ installedCommand: [
261
+ 'mf',
262
+ 'script-pack',
263
+ 'run',
264
+ 'repo/generated-boundary',
265
+ 'check',
266
+ 'AGENTS.md',
267
+ '.mustflow/config/manifest.lock.toml',
268
+ '--json',
269
+ ],
270
+ expectedExitCodes: [0, 1],
271
+ },
237
272
  {
238
273
  id: 'skill-route-report',
239
274
  schemaFile: 'skill-route-report.schema.json',
@@ -0,0 +1,199 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+ export function isScriptPackSuggestionPhase(value) {
4
+ return ['before_change', 'during_change', 'after_change', 'review'].includes(value);
5
+ }
6
+ function uniqueSortedStrings(values) {
7
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right));
8
+ }
9
+ function uniqueSortedPhases(values) {
10
+ return uniqueSortedStrings(values);
11
+ }
12
+ function uniqueSortedSurfaces(values) {
13
+ return uniqueSortedStrings(values);
14
+ }
15
+ function normalizeReportPath(mustflowRoot, value) {
16
+ const absolute = path.resolve(mustflowRoot, value);
17
+ const relative = path.relative(mustflowRoot, absolute);
18
+ return relative.startsWith('..') || path.isAbsolute(relative)
19
+ ? value.replace(/\\/gu, '/')
20
+ : (relative.replace(/\\/gu, '/') || '.');
21
+ }
22
+ export function classifyScriptPackPathSurface(relativePath) {
23
+ const normalized = relativePath.replace(/\\/gu, '/').replace(/^\.\/+/u, '');
24
+ const surfaces = [];
25
+ if (normalized === 'REPO_MAP.md' ||
26
+ normalized === '.mustflow/config/manifest.lock.toml' ||
27
+ normalized.startsWith('dist/') ||
28
+ normalized.startsWith('build/') ||
29
+ normalized.startsWith('coverage/') ||
30
+ normalized.startsWith('.mustflow/cache/') ||
31
+ normalized.startsWith('.mustflow/state/')) {
32
+ surfaces.push('generated');
33
+ }
34
+ if (normalized.startsWith('.mustflow/config/') || normalized.startsWith('config/')) {
35
+ surfaces.push('config');
36
+ }
37
+ if (normalized.startsWith('.mustflow/skills/') || normalized.includes('/.mustflow/skills/')) {
38
+ surfaces.push('skill');
39
+ }
40
+ if (normalized.startsWith('templates/')) {
41
+ surfaces.push('template');
42
+ }
43
+ if (normalized.startsWith('schemas/') || normalized.endsWith('.schema.json')) {
44
+ surfaces.push('schema');
45
+ }
46
+ if (normalized === 'README.md' ||
47
+ normalized === 'CHANGELOG.md' ||
48
+ normalized.endsWith('.md') ||
49
+ normalized.startsWith('docs/') ||
50
+ normalized.startsWith('docs-site/')) {
51
+ surfaces.push('docs');
52
+ }
53
+ if (normalized.startsWith('tests/') || normalized.endsWith('.test.js') || normalized.endsWith('.test.ts')) {
54
+ surfaces.push('test');
55
+ }
56
+ if (normalized === 'package.json' ||
57
+ normalized === 'bun.lock' ||
58
+ normalized === 'package-lock.json' ||
59
+ normalized.startsWith('.github/workflows/')) {
60
+ surfaces.push('package');
61
+ }
62
+ if (normalized.startsWith('src/') || normalized.endsWith('.ts') || normalized.endsWith('.tsx')) {
63
+ surfaces.push('source');
64
+ }
65
+ return surfaces.length > 0 ? uniqueSortedSurfaces(surfaces) : ['unknown'];
66
+ }
67
+ function readChangedPaths(mustflowRoot, issues) {
68
+ const result = spawnSync('git', ['status', '--short'], {
69
+ cwd: mustflowRoot,
70
+ encoding: 'utf8',
71
+ stdio: ['ignore', 'pipe', 'pipe'],
72
+ windowsHide: true,
73
+ });
74
+ if (result.status !== 0) {
75
+ const detail = result.stderr.trim() || result.stdout.trim() || `git status exited with ${result.status}`;
76
+ issues.push(`Could not read changed paths: ${detail}`);
77
+ return [];
78
+ }
79
+ return result.stdout
80
+ .split(/\r?\n/u)
81
+ .map((line) => line.trimEnd())
82
+ .filter((line) => line.length > 0)
83
+ .map((line) => {
84
+ const renamed = /\s->\s(?<target>.+)$/u.exec(line);
85
+ return renamed?.groups?.target ?? line.slice(3).trim();
86
+ })
87
+ .filter((entry) => entry.length > 0);
88
+ }
89
+ function surfacesForScript(script) {
90
+ const surfaces = new Set();
91
+ const searchable = [
92
+ script.ref,
93
+ script.usage,
94
+ ...script.useWhen,
95
+ ...script.inputs,
96
+ ...script.outputs,
97
+ ...script.relatedSkills,
98
+ ].join(' ');
99
+ const addIf = (surface, pattern) => {
100
+ if (pattern.test(searchable)) {
101
+ surfaces.add(surface);
102
+ }
103
+ };
104
+ addIf('docs', /docs|readme|release|copy|text|prompt/u);
105
+ addIf('schema', /json|schema|contract/u);
106
+ addIf('template', /template|install|manifest/u);
107
+ addIf('skill', /skill|workflow/u);
108
+ addIf('generated', /generated|protected|vendor|cache|boundary/u);
109
+ addIf('config', /config|command/u);
110
+ addIf('package', /package|release/u);
111
+ addIf('source', /code|source|path/u);
112
+ return uniqueSortedSurfaces(surfaces);
113
+ }
114
+ function confidenceForScore(score) {
115
+ if (score >= 7) {
116
+ return 'high';
117
+ }
118
+ if (score >= 4) {
119
+ return 'medium';
120
+ }
121
+ return 'low';
122
+ }
123
+ export function createScriptPackSuggestionReport(mustflowRoot, options) {
124
+ const issues = [];
125
+ const changedPaths = options.changed ? readChangedPaths(mustflowRoot, issues) : [];
126
+ const inputPaths = uniqueSortedStrings([...options.paths, ...changedPaths].map((value) => normalizeReportPath(mustflowRoot, value)));
127
+ const analyzedPaths = inputPaths.map((entry) => ({ path: entry, surfaces: classifyScriptPackPathSurface(entry) }));
128
+ const requestedSurfaces = new Set(analyzedPaths.flatMap((entry) => entry.surfaces));
129
+ const suggestions = options.scripts
130
+ .map((script) => {
131
+ let score = 0;
132
+ const reasons = [];
133
+ const matchedPhases = options.phases.filter((phase) => script.phases.includes(phase));
134
+ if (matchedPhases.length > 0) {
135
+ score += matchedPhases.length * 3;
136
+ reasons.push(`Matches requested phase: ${matchedPhases.join(', ')}`);
137
+ }
138
+ const matchedSkills = options.skills.filter((skill) => script.relatedSkills.includes(skill));
139
+ if (matchedSkills.length > 0) {
140
+ score += matchedSkills.length * 4;
141
+ reasons.push(`Matches related skill: ${matchedSkills.join(', ')}`);
142
+ }
143
+ const scriptSurfaces = surfacesForScript(script);
144
+ const matchedSurfaces = scriptSurfaces.filter((surface) => requestedSurfaces.has(surface));
145
+ if (matchedSurfaces.length > 0) {
146
+ score += matchedSurfaces.length * 2;
147
+ reasons.push(`Matches changed surface: ${matchedSurfaces.join(', ')}`);
148
+ }
149
+ if (inputPaths.length > 0 && script.inputs.includes('path')) {
150
+ score += 1;
151
+ reasons.push('Accepts explicit path inputs.');
152
+ }
153
+ if (script.readOnly && !script.mutates && !script.network) {
154
+ score += 1;
155
+ reasons.push('Read-only, non-mutating, offline helper.');
156
+ }
157
+ if (score === 0) {
158
+ return null;
159
+ }
160
+ return {
161
+ script_ref: script.ref,
162
+ score,
163
+ confidence: confidenceForScore(score),
164
+ usage: script.usage,
165
+ phases: script.phases,
166
+ matched_phases: uniqueSortedPhases(matchedPhases),
167
+ matched_skills: uniqueSortedStrings(matchedSkills),
168
+ matched_surfaces: uniqueSortedSurfaces(matchedSurfaces),
169
+ reasons,
170
+ read_only: script.readOnly,
171
+ mutates: script.mutates,
172
+ network: script.network,
173
+ risk_level: script.riskLevel,
174
+ cost: script.cost,
175
+ report_schema_file: script.reportSchemaFile,
176
+ run_hint: script.usage,
177
+ };
178
+ })
179
+ .filter((suggestion) => suggestion !== null)
180
+ .sort((left, right) => right.score - left.score || left.script_ref.localeCompare(right.script_ref));
181
+ const status = issues.length > 0 ? 'partial' : suggestions.length > 0 ? 'suggested' : 'empty';
182
+ return {
183
+ schema_version: '1',
184
+ command: 'script-pack',
185
+ action: 'suggest',
186
+ status,
187
+ ok: true,
188
+ mustflow_root: mustflowRoot,
189
+ input: {
190
+ phases: uniqueSortedPhases(options.phases),
191
+ skills: uniqueSortedStrings(options.skills),
192
+ paths: inputPaths,
193
+ changed: options.changed,
194
+ },
195
+ analyzed_paths: analyzedPaths,
196
+ suggestions,
197
+ issues,
198
+ };
199
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.74.2",
3
+ "version": "2.74.4",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
package/schemas/README.md CHANGED
@@ -57,11 +57,18 @@ Current schemas:
57
57
  quality-gaming risks such as line stuffing, validation suppressions, test bypass markers, type
58
58
  escapes, generated/vendor logic, empty catch swallowing, and placeholder implementations
59
59
  - `script-pack-catalog.schema.json`: output of `mf script-pack list --json`, containing bundled
60
- script-pack ids, script refs, action names, usage strings, and associated report schemas
60
+ script-pack ids, script refs, action names, usage strings, workflow phases, read-only and
61
+ side-effect flags, input and output capability labels, related skill names, cost and risk hints,
62
+ and associated report schemas
63
+ - `script-pack-suggestion-report.schema.json`: output of `mf script-pack suggest --json`, containing
64
+ path, skill, phase, and changed-file evidence used to recommend optional script-pack helpers
61
65
  - `text-budget-report.schema.json`: output of
62
66
  `mf script-pack run core/text-budget check <path...> --json`, containing
63
67
  exact text-budget metrics, input content hashes, policy metadata, findings, and JSON Pointer field
64
68
  checks for bounded user-facing strings and generated text
69
+ - `generated-boundary-report.schema.json`: output of
70
+ `mf script-pack run repo/generated-boundary check <path...> --json`, containing candidate path
71
+ classifications for generated, ignored, protected, vendor, and cache boundaries before or after edits
65
72
  - `skill-route-report.schema.json`: output of `mf skill route --json`, containing compact route
66
73
  candidates, selected main and adjunct skills, score breakdowns, route read plans, and source
67
74
  route shards without granting command authority or replacing selected `SKILL.md` reads