mustflow 2.28.0 → 2.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/cli/commands/context.js +1 -0
  2. package/dist/cli/commands/help.js +55 -1
  3. package/dist/cli/commands/tech.js +346 -0
  4. package/dist/cli/i18n/en.js +1 -0
  5. package/dist/cli/i18n/es.js +1 -0
  6. package/dist/cli/i18n/fr.js +1 -0
  7. package/dist/cli/i18n/hi.js +1 -0
  8. package/dist/cli/i18n/ko.js +1 -0
  9. package/dist/cli/i18n/zh.js +1 -0
  10. package/dist/cli/index.js +1 -0
  11. package/dist/cli/lib/agent-context.js +16 -0
  12. package/dist/cli/lib/command-registry.js +6 -0
  13. package/dist/cli/lib/validation/index.js +11 -0
  14. package/dist/cli/lib/validation/primitives.js +5 -0
  15. package/dist/core/technology-preferences.js +189 -0
  16. package/package.json +1 -1
  17. package/schemas/context-report.schema.json +61 -0
  18. package/templates/default/common/.mustflow/config/mustflow.toml +8 -0
  19. package/templates/default/common/.mustflow/config/technology.toml +20 -0
  20. package/templates/default/i18n.toml +78 -12
  21. package/templates/default/locales/en/.mustflow/skills/INDEX.md +33 -1
  22. package/templates/default/locales/en/.mustflow/skills/code-review/SKILL.md +15 -5
  23. package/templates/default/locales/en/.mustflow/skills/codebase-orientation/SKILL.md +15 -8
  24. package/templates/default/locales/en/.mustflow/skills/command-intent-mapping-gate/SKILL.md +124 -0
  25. package/templates/default/locales/en/.mustflow/skills/completion-evidence-gate/SKILL.md +178 -0
  26. package/templates/default/locales/en/.mustflow/skills/contract-sync-check/SKILL.md +9 -3
  27. package/templates/default/locales/en/.mustflow/skills/dependency-reality-check/SKILL.md +6 -3
  28. package/templates/default/locales/en/.mustflow/skills/evidence-stall-breaker/SKILL.md +166 -0
  29. package/templates/default/locales/en/.mustflow/skills/external-prompt-injection-defense/SKILL.md +8 -6
  30. package/templates/default/locales/en/.mustflow/skills/provenance-license-gate/SKILL.md +131 -0
  31. package/templates/default/locales/en/.mustflow/skills/public-json-contract-change/SKILL.md +133 -0
  32. package/templates/default/locales/en/.mustflow/skills/restricted-handoff-resume/SKILL.md +122 -0
  33. package/templates/default/locales/en/.mustflow/skills/routes.toml +60 -0
  34. package/templates/default/locales/en/.mustflow/skills/runtime-target-selection/SKILL.md +203 -0
  35. package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +55 -18
  36. package/templates/default/locales/en/.mustflow/skills/secret-exposure-response/SKILL.md +125 -0
  37. package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +10 -1
  38. package/templates/default/locales/en/.mustflow/skills/skill-authoring/SKILL.md +9 -5
  39. package/templates/default/locales/en/.mustflow/skills/source-freshness-check/SKILL.md +3 -2
  40. package/templates/default/locales/en/.mustflow/skills/structure-first-engineering/SKILL.md +205 -0
  41. package/templates/default/locales/en/.mustflow/skills/template-install-surface-sync/SKILL.md +131 -0
  42. package/templates/default/locales/en/AGENTS.md +8 -7
  43. package/templates/default/locales/ko/AGENTS.md +8 -7
  44. package/templates/default/manifest.toml +66 -1
@@ -77,6 +77,7 @@ export async function runContext(args, reporter, lang = 'en') {
77
77
  reporter.stdout(`${t(lang, 'label.mustflowRoot')}: ${context.mustflow_root}`);
78
78
  reporter.stdout(`${t(lang, 'label.commandContract')}: ${context.command_contract.exists ? t(lang, 'value.present') : t(lang, 'value.missing')}`);
79
79
  reporter.stdout(`${t(lang, 'label.runnableIntents')}: ${context.command_contract.runnable_intents.length}`);
80
+ reporter.stdout(`Technology preferences: ${context.technology_preferences.exists ? t(lang, 'value.present') : t(lang, 'value.missing')} (${context.technology_preferences.count})`);
80
81
  reporter.stdout(`${t(lang, 'label.latestRun')}: ${context.latest_run.exists ? t(lang, 'value.present') : t(lang, 'value.missing')}`);
81
82
  return 0;
82
83
  }
@@ -4,6 +4,7 @@ import { t } from '../lib/i18n.js';
4
4
  import { readMustflowTextFileIfExists } from '../lib/mustflow-read.js';
5
5
  import { resolveMustflowRoot } from '../lib/project-root.js';
6
6
  import { readMustflowTomlFile } from '../lib/toml.js';
7
+ import { normalizeTechnologyPreferencesTable, TECHNOLOGY_CONFIG_RELATIVE_PATH, } from '../../core/technology-preferences.js';
7
8
  function readTextIfExists(projectRoot, relativePath) {
8
9
  return readMustflowTextFileIfExists(projectRoot, relativePath) ?? undefined;
9
10
  }
@@ -58,6 +59,51 @@ function renderPreferencesHelp(projectRoot, lang) {
58
59
  }
59
60
  return lines.join('\n').trimEnd();
60
61
  }
62
+ function renderTechnologyPreference(preference) {
63
+ const details = [
64
+ preference.scope.length > 0 ? `scope: ${preference.scope.join(', ')}` : null,
65
+ preference.ecosystem ? `ecosystem: ${preference.ecosystem}` : null,
66
+ preference.packages.length > 0 ? `packages: ${preference.packages.join(', ')}` : null,
67
+ ].filter((value) => value !== null);
68
+ const lines = [`- [${preference.status}] ${preference.kind} ${preference.name} (${preference.id})`];
69
+ if (details.length > 0) {
70
+ lines.push(` ${details.join('; ')}`);
71
+ }
72
+ if (preference.rationale) {
73
+ lines.push(` why: ${preference.rationale}`);
74
+ }
75
+ for (const constraint of preference.constraints) {
76
+ lines.push(` constraint: ${constraint}`);
77
+ }
78
+ return lines;
79
+ }
80
+ function renderTechnologyHelp(projectRoot, lang) {
81
+ const technology = readTomlIfExists(projectRoot, TECHNOLOGY_CONFIG_RELATIVE_PATH);
82
+ if (!technology) {
83
+ return `Technology Preferences\n\n${renderMissing(TECHNOLOGY_CONFIG_RELATIVE_PATH, lang)}`;
84
+ }
85
+ const file = normalizeTechnologyPreferencesTable(technology, true);
86
+ const lines = [
87
+ 'Technology Preferences',
88
+ '',
89
+ `Path: ${file.path}`,
90
+ `Authority: ${file.authority} (preferences only)`,
91
+ 'Guidance:',
92
+ ...file.guidance.map((item) => `- ${item}`),
93
+ '',
94
+ `Preferences: ${file.preferences.length}`,
95
+ ];
96
+ if (file.preferences.length === 0) {
97
+ lines.push('No technology preferences recorded.');
98
+ }
99
+ for (const preference of file.preferences) {
100
+ lines.push(...renderTechnologyPreference(preference));
101
+ }
102
+ if (file.issues.length > 0) {
103
+ lines.push('', 'Issues:', ...file.issues.map((issue) => `- ${issue}`));
104
+ }
105
+ return lines.join('\n');
106
+ }
61
107
  function renderPreferenceSection(lines, sectionName, section) {
62
108
  const nestedSections = [];
63
109
  const scalarLines = [];
@@ -97,9 +143,13 @@ export function getHelpHelp(lang = 'en') {
97
143
  label: 'preferences',
98
144
  description: t(lang, 'help.topic.preferences'),
99
145
  },
146
+ {
147
+ label: 'technology',
148
+ description: 'Show framework, library, runtime, and tool preferences',
149
+ },
100
150
  ],
101
151
  options: [{ label: '-h, --help', description: t(lang, 'cli.option.help') }],
102
- examples: ['mf help workflow', 'mf help skills', 'mf help preferences'],
152
+ examples: ['mf help workflow', 'mf help skills', 'mf help preferences', 'mf help technology'],
103
153
  exitCodes: [
104
154
  {
105
155
  label: '0',
@@ -139,6 +189,10 @@ export function runHelp(args, reporter, lang = 'en') {
139
189
  reporter.stdout(renderPreferencesHelp(projectRoot, lang));
140
190
  return 0;
141
191
  }
192
+ if (topic === 'technology') {
193
+ reporter.stdout(renderTechnologyHelp(projectRoot, lang));
194
+ return 0;
195
+ }
142
196
  reporter.stderr(renderCliError(t(lang, 'help.error.unknownTopic', { topic }), 'mf help --help', lang));
143
197
  return 1;
144
198
  }
@@ -0,0 +1,346 @@
1
+ import path from 'node:path';
2
+ import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.js';
3
+ import { writeUtf8FileInsideWithoutSymlinks } from '../lib/filesystem.js';
4
+ import { isRecord } from '../lib/command-contract.js';
5
+ import { t } from '../lib/i18n.js';
6
+ import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
7
+ import { resolveMustflowRoot } from '../lib/project-root.js';
8
+ import { readMustflowTomlFile } from '../lib/toml.js';
9
+ import { createTechnologyPreferenceId, normalizeTechnologyKey, normalizeTechnologyName, normalizeTechnologyPreferencesTable, serializeTechnologyPreferences, technologyConfigExists, technologyPreferenceMatches, TECHNOLOGY_AUTHORITY, TECHNOLOGY_CONFIG_RELATIVE_PATH, TECHNOLOGY_GUIDANCE, TECHNOLOGY_KINDS, TECHNOLOGY_STATUSES, } from '../../core/technology-preferences.js';
10
+ const TECH_OPTIONS = [
11
+ { name: '--json', kind: 'boolean' },
12
+ { name: '--scope', kind: 'string' },
13
+ { name: '--kind', kind: 'string' },
14
+ { name: '--status', kind: 'string' },
15
+ { name: '--ecosystem', kind: 'string' },
16
+ { name: '--package', kind: 'string' },
17
+ { name: '--why', kind: 'string' },
18
+ { name: '--constraint', kind: 'string' },
19
+ ];
20
+ function getRepeatedStringOptions(occurrences, name) {
21
+ return occurrences
22
+ .filter((occurrence) => occurrence.name === name && typeof occurrence.value === 'string')
23
+ .map((occurrence) => String(occurrence.value).trim())
24
+ .filter((value) => value.length > 0);
25
+ }
26
+ function isTechnologyKind(value) {
27
+ return value !== null && TECHNOLOGY_KINDS.includes(value);
28
+ }
29
+ function isTechnologyStatus(value) {
30
+ return value !== null && TECHNOLOGY_STATUSES.includes(value);
31
+ }
32
+ export function getTechHelp(lang = 'en') {
33
+ return renderHelp({
34
+ usage: 'mf tech <list|add|remove|suggest> [options]',
35
+ summary: 'Manage low-authority technology preferences for agents.',
36
+ options: [
37
+ { label: '--json', description: t(lang, 'cli.option.json') },
38
+ { label: '--scope <scope>', description: 'Filter or tag a project area such as frontend, backend, ui, data, or cli' },
39
+ { label: '--kind <kind>', description: `Technology kind: ${TECHNOLOGY_KINDS.join(', ')}` },
40
+ { label: '--status <status>', description: `Preference status: ${TECHNOLOGY_STATUSES.join(', ')}` },
41
+ { label: '--ecosystem <ecosystem>', description: 'Package ecosystem or platform, such as npm, cargo, pip, go, or deno' },
42
+ { label: '--package <package>', description: 'Package name to associate with the preference. Repeatable.' },
43
+ { label: '--why <text>', description: 'Short rationale for the preference' },
44
+ { label: '--constraint <text>', description: 'Guardrail agents must keep in mind. Repeatable.' },
45
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
46
+ ],
47
+ examples: [
48
+ 'mf tech list',
49
+ 'mf tech add framework nextjs --scope frontend --ecosystem npm --package next --package react --why "Preferred React app framework"',
50
+ 'mf tech add language rust --scope backend --status preferred --why "Use for correctness-critical engines"',
51
+ 'mf tech add library jquery --scope frontend --status avoid --why "Avoid new usage"',
52
+ 'mf tech suggest --scope frontend',
53
+ 'mf tech remove framework.frontend.nextjs',
54
+ ],
55
+ exitCodes: [
56
+ { label: '0', description: 'Technology preferences were inspected or updated' },
57
+ { label: '1', description: t(lang, 'cli.common.invalidInput') },
58
+ ],
59
+ }, lang);
60
+ }
61
+ function parseTechOptions(args, lang) {
62
+ const [actionToken, ...rest] = args;
63
+ const action = actionToken;
64
+ if (action !== 'list' && action !== 'add' && action !== 'remove' && action !== 'suggest') {
65
+ return {
66
+ action: 'list',
67
+ positionals: [],
68
+ json: false,
69
+ scope: [],
70
+ kind: null,
71
+ status: null,
72
+ ecosystem: null,
73
+ packages: [],
74
+ why: null,
75
+ constraints: [],
76
+ error: actionToken ? `Unknown tech action: ${actionToken}` : 'Specify a tech action: list, add, remove, or suggest',
77
+ };
78
+ }
79
+ const parsed = parseCliOptions(rest, TECH_OPTIONS, { allowPositionals: true });
80
+ if (parsed.error) {
81
+ return {
82
+ action,
83
+ positionals: parsed.positionals,
84
+ json: hasParsedCliOption(parsed, '--json'),
85
+ scope: [],
86
+ kind: null,
87
+ status: null,
88
+ ecosystem: null,
89
+ packages: [],
90
+ why: null,
91
+ constraints: [],
92
+ error: formatCliOptionParseError(parsed.error, lang),
93
+ };
94
+ }
95
+ const kind = getParsedCliStringOption(parsed, '--kind');
96
+ const status = getParsedCliStringOption(parsed, '--status');
97
+ if (kind !== null && !isTechnologyKind(kind)) {
98
+ return invalidParsed(action, parsed.positionals, hasParsedCliOption(parsed, '--json'), `Unsupported technology kind: ${kind}`);
99
+ }
100
+ if (status !== null && !isTechnologyStatus(status)) {
101
+ return invalidParsed(action, parsed.positionals, hasParsedCliOption(parsed, '--json'), `Unsupported technology status: ${status}`);
102
+ }
103
+ return {
104
+ action,
105
+ positionals: parsed.positionals,
106
+ json: hasParsedCliOption(parsed, '--json'),
107
+ scope: getRepeatedStringOptions(parsed.occurrences, '--scope'),
108
+ kind,
109
+ status,
110
+ ecosystem: getParsedCliStringOption(parsed, '--ecosystem'),
111
+ packages: getRepeatedStringOptions(parsed.occurrences, '--package'),
112
+ why: getParsedCliStringOption(parsed, '--why'),
113
+ constraints: getRepeatedStringOptions(parsed.occurrences, '--constraint'),
114
+ };
115
+ }
116
+ function invalidParsed(action, positionals, json, error) {
117
+ return {
118
+ action,
119
+ positionals,
120
+ json,
121
+ scope: [],
122
+ kind: null,
123
+ status: null,
124
+ ecosystem: null,
125
+ packages: [],
126
+ why: null,
127
+ constraints: [],
128
+ error,
129
+ };
130
+ }
131
+ function readTechnologyPreferences(projectRoot) {
132
+ if (!technologyConfigExists(projectRoot)) {
133
+ return normalizeTechnologyPreferencesTable(undefined, false);
134
+ }
135
+ const parsed = readMustflowTomlFile(projectRoot, TECHNOLOGY_CONFIG_RELATIVE_PATH);
136
+ if (!isRecord(parsed)) {
137
+ return {
138
+ ...normalizeTechnologyPreferencesTable(undefined, true),
139
+ issues: [`${TECHNOLOGY_CONFIG_RELATIVE_PATH} must contain a TOML table`],
140
+ };
141
+ }
142
+ return normalizeTechnologyPreferencesTable(parsed, true);
143
+ }
144
+ function writeTechnologyPreferences(projectRoot, preferences) {
145
+ const targetPath = path.join(projectRoot, ...TECHNOLOGY_CONFIG_RELATIVE_PATH.split('/'));
146
+ writeUtf8FileInsideWithoutSymlinks(projectRoot, targetPath, serializeTechnologyPreferences(preferences));
147
+ }
148
+ function renderPreference(preference) {
149
+ const details = [
150
+ preference.scope.length > 0 ? `scope: ${preference.scope.join(', ')}` : null,
151
+ preference.ecosystem ? `ecosystem: ${preference.ecosystem}` : null,
152
+ preference.packages.length > 0 ? `packages: ${preference.packages.join(', ')}` : null,
153
+ ].filter((value) => value !== null);
154
+ const lines = [`- [${preference.status}] ${preference.kind} ${preference.name} (${preference.id})`];
155
+ if (details.length > 0) {
156
+ lines.push(` ${details.join('; ')}`);
157
+ }
158
+ if (preference.rationale) {
159
+ lines.push(` why: ${preference.rationale}`);
160
+ }
161
+ for (const constraint of preference.constraints) {
162
+ lines.push(` constraint: ${constraint}`);
163
+ }
164
+ return lines;
165
+ }
166
+ function renderList(file, preferences) {
167
+ const lines = [
168
+ 'mustflow technology preferences',
169
+ `Path: ${file.path}`,
170
+ `Authority: ${file.authority} (preferences only)`,
171
+ `Preferences: ${preferences.length}`,
172
+ ];
173
+ if (file.issues.length > 0) {
174
+ lines.push('Issues:', ...file.issues.map((issue) => `- ${issue}`));
175
+ }
176
+ if (preferences.length === 0) {
177
+ lines.push('No technology preferences recorded.');
178
+ return lines.join('\n');
179
+ }
180
+ for (const preference of preferences) {
181
+ lines.push(...renderPreference(preference));
182
+ }
183
+ return lines.join('\n');
184
+ }
185
+ function filterPreferences(file, options) {
186
+ return file.preferences.filter((preference) => technologyPreferenceMatches(preference, {
187
+ scope: options.scope[0] ?? null,
188
+ kind: options.kind,
189
+ status: options.status,
190
+ }));
191
+ }
192
+ function runList(projectRoot, options, reporter) {
193
+ const file = readTechnologyPreferences(projectRoot);
194
+ const preferences = filterPreferences(file, options);
195
+ if (options.json) {
196
+ reporter.stdout(JSON.stringify({ ...file, preferences }, null, 2));
197
+ return file.issues.length === 0 ? 0 : 1;
198
+ }
199
+ reporter.stdout(renderList(file, preferences));
200
+ return file.issues.length === 0 ? 0 : 1;
201
+ }
202
+ function runSuggest(projectRoot, options, reporter) {
203
+ const file = readTechnologyPreferences(projectRoot);
204
+ const filtered = filterPreferences(file, {
205
+ ...options,
206
+ status: null,
207
+ });
208
+ const preferred = filtered.filter((preference) => preference.status === 'preferred' || preference.status === 'allowed');
209
+ const avoid = filtered.filter((preference) => preference.status === 'avoid');
210
+ const payload = {
211
+ path: file.path,
212
+ authority: TECHNOLOGY_AUTHORITY,
213
+ guidance: TECHNOLOGY_GUIDANCE,
214
+ preferred,
215
+ avoid,
216
+ issues: file.issues,
217
+ };
218
+ if (options.json) {
219
+ reporter.stdout(JSON.stringify(payload, null, 2));
220
+ return file.issues.length === 0 ? 0 : 1;
221
+ }
222
+ const lines = ['mustflow technology suggestions', `Path: ${file.path}`, `Authority: ${TECHNOLOGY_AUTHORITY}`];
223
+ lines.push('Guidance:', ...TECHNOLOGY_GUIDANCE.map((item) => `- ${item}`));
224
+ lines.push('Preferred or allowed:');
225
+ lines.push(...(preferred.length === 0 ? ['- none'] : preferred.flatMap(renderPreference)));
226
+ lines.push('Avoid:');
227
+ lines.push(...(avoid.length === 0 ? ['- none'] : avoid.flatMap(renderPreference)));
228
+ if (file.issues.length > 0) {
229
+ lines.push('Issues:', ...file.issues.map((issue) => `- ${issue}`));
230
+ }
231
+ reporter.stdout(lines.join('\n'));
232
+ return file.issues.length === 0 ? 0 : 1;
233
+ }
234
+ function findExistingIndex(preferences, candidate) {
235
+ return preferences.findIndex((preference) => {
236
+ if (preference.id === candidate.id) {
237
+ return true;
238
+ }
239
+ return preference.kind === candidate.kind && normalizeTechnologyKey(preference.name) === normalizeTechnologyKey(candidate.name);
240
+ });
241
+ }
242
+ function runAdd(projectRoot, options, reporter) {
243
+ const [kindToken, nameToken] = options.positionals;
244
+ if (!isTechnologyKind(kindToken ?? null)) {
245
+ reporter.stderr(renderCliError('Missing or unsupported technology kind', 'mf tech --help'));
246
+ return 1;
247
+ }
248
+ if (!nameToken) {
249
+ reporter.stderr(renderCliError('Missing technology name', 'mf tech --help'));
250
+ return 1;
251
+ }
252
+ const kind = kindToken;
253
+ if (options.status !== null && !isTechnologyStatus(options.status)) {
254
+ reporter.stderr(renderCliError(`Unsupported technology status: ${options.status}`, 'mf tech --help'));
255
+ return 1;
256
+ }
257
+ const file = readTechnologyPreferences(projectRoot);
258
+ if (file.issues.length > 0) {
259
+ reporter.stderr(renderCliError(`Cannot update invalid ${TECHNOLOGY_CONFIG_RELATIVE_PATH}`, 'mf tech list'));
260
+ return 1;
261
+ }
262
+ const status = options.status ?? 'preferred';
263
+ const scope = options.scope.length > 0 ? options.scope : [];
264
+ const name = normalizeTechnologyName(nameToken);
265
+ const candidate = {
266
+ id: createTechnologyPreferenceId(kind, name, scope),
267
+ kind,
268
+ name,
269
+ status,
270
+ authority: TECHNOLOGY_AUTHORITY,
271
+ scope,
272
+ ecosystem: options.ecosystem,
273
+ packages: options.packages,
274
+ rationale: options.why,
275
+ constraints: options.constraints,
276
+ };
277
+ const preferences = [...file.preferences];
278
+ const existingIndex = findExistingIndex(preferences, candidate);
279
+ const action = existingIndex === -1 ? 'created' : 'updated';
280
+ if (existingIndex === -1) {
281
+ preferences.push(candidate);
282
+ }
283
+ else {
284
+ preferences[existingIndex] = candidate;
285
+ }
286
+ writeTechnologyPreferences(projectRoot, preferences);
287
+ if (options.json) {
288
+ reporter.stdout(JSON.stringify({ action, preference: candidate, path: TECHNOLOGY_CONFIG_RELATIVE_PATH }, null, 2));
289
+ return 0;
290
+ }
291
+ reporter.stdout(`${action === 'created' ? 'Created' : 'Updated'} ${candidate.id} in ${TECHNOLOGY_CONFIG_RELATIVE_PATH}`);
292
+ return 0;
293
+ }
294
+ function runRemove(projectRoot, options, reporter) {
295
+ const [target] = options.positionals;
296
+ if (!target) {
297
+ reporter.stderr(renderCliError('Missing technology id or name', 'mf tech --help'));
298
+ return 1;
299
+ }
300
+ const file = readTechnologyPreferences(projectRoot);
301
+ if (file.issues.length > 0) {
302
+ reporter.stderr(renderCliError(`Cannot update invalid ${TECHNOLOGY_CONFIG_RELATIVE_PATH}`, 'mf tech list'));
303
+ return 1;
304
+ }
305
+ const key = normalizeTechnologyKey(target);
306
+ const matches = file.preferences.filter((preference) => preference.id === target || normalizeTechnologyKey(preference.name) === key);
307
+ if (matches.length === 0) {
308
+ reporter.stderr(renderCliError(`No technology preference matched ${target}`, 'mf tech list'));
309
+ return 1;
310
+ }
311
+ if (matches.length > 1) {
312
+ reporter.stderr(renderCliError(`Multiple technology preferences matched ${target}; remove by id`, 'mf tech list'));
313
+ return 1;
314
+ }
315
+ const removed = matches[0];
316
+ const preferences = file.preferences.filter((preference) => preference.id !== removed.id);
317
+ writeTechnologyPreferences(projectRoot, preferences);
318
+ if (options.json) {
319
+ reporter.stdout(JSON.stringify({ action: 'removed', preference: removed, path: TECHNOLOGY_CONFIG_RELATIVE_PATH }, null, 2));
320
+ return 0;
321
+ }
322
+ reporter.stdout(`Removed ${removed.id} from ${TECHNOLOGY_CONFIG_RELATIVE_PATH}`);
323
+ return 0;
324
+ }
325
+ export function runTech(args, reporter, lang = 'en') {
326
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
327
+ reporter.stdout(getTechHelp(lang));
328
+ return 0;
329
+ }
330
+ const options = parseTechOptions(args, lang);
331
+ if (options.error) {
332
+ printUsageError(reporter, options.error, 'mf tech --help', getTechHelp(lang), lang);
333
+ return 1;
334
+ }
335
+ const projectRoot = resolveMustflowRoot();
336
+ if (options.action === 'list') {
337
+ return runList(projectRoot, options, reporter);
338
+ }
339
+ if (options.action === 'suggest') {
340
+ return runSuggest(projectRoot, options, reporter);
341
+ }
342
+ if (options.action === 'add') {
343
+ return runAdd(projectRoot, options, reporter);
344
+ }
345
+ return runRemove(projectRoot, options, reporter);
346
+ }
@@ -41,6 +41,7 @@ export const enMessages = {
41
41
  "command.lineEndings.summary": "Inspect and normalize line-ending policy",
42
42
  "command.run.summary": "Run a configured oneshot command",
43
43
  "command.context.summary": "Print machine-readable agent context",
44
+ "command.tech.summary": "Manage technology preferences for agents",
44
45
  "command.doctor.summary": "Inspect mustflow health and next steps",
45
46
  "command.docs.summary": "Track documentation review queue entries",
46
47
  "command.handoff.summary": "Validate restricted work-item and handoff records",
@@ -41,6 +41,7 @@ export const esMessages = {
41
41
  "command.lineEndings.summary": "Inspecciona y normaliza la política de finales de línea",
42
42
  "command.run.summary": "Ejecuta un comando configurado de una sola ejecución",
43
43
  "command.context.summary": "Imprime contexto de agente legible por máquinas",
44
+ "command.tech.summary": "Gestiona preferencias tecnológicas para agentes",
44
45
  "command.doctor.summary": "Inspecciona la salud de mustflow y los siguientes pasos",
45
46
  "command.docs.summary": "Rastrea la cola de revisión de documentación",
46
47
  "command.handoff.summary": "Valida registros restringidos de trabajo y handoff",
@@ -41,6 +41,7 @@ export const frMessages = {
41
41
  "command.lineEndings.summary": "Inspecte et normalise la politique de fins de ligne",
42
42
  "command.run.summary": "Exécute une commande configurée à exécution unique",
43
43
  "command.context.summary": "Imprime le contexte d'agent lisible par machine",
44
+ "command.tech.summary": "Gère les préférences technologiques pour les agents",
44
45
  "command.doctor.summary": "Inspecte l'état de mustflow et les prochaines étapes",
45
46
  "command.docs.summary": "Suit la file de révision de documentation",
46
47
  "command.handoff.summary": "Valide les enregistrements restreints de travail et de handoff",
@@ -41,6 +41,7 @@ export const hiMessages = {
41
41
  "command.lineEndings.summary": "लाइन-एंडिंग नीति की जाँच और सामान्यीकरण करें",
42
42
  "command.run.summary": "कॉन्फ़िगर की गई एक-बार चलने वाली कमांड चलाएँ",
43
43
  "command.context.summary": "मशीन-पठनीय एजेंट संदर्भ प्रिंट करें",
44
+ "command.tech.summary": "एजेंटों के लिए technology preferences प्रबंधित करें",
44
45
  "command.doctor.summary": "mustflow स्वास्थ्य और अगले कदम जाँचें",
45
46
  "command.docs.summary": "दस्तावेज़ review queue entries track करें",
46
47
  "command.handoff.summary": "Restricted work-item और handoff records validate करें",
@@ -41,6 +41,7 @@ export const koMessages = {
41
41
  "command.lineEndings.summary": "줄바꿈 정책을 검사하고 정규화합니다",
42
42
  "command.run.summary": "설정된 일회성 명령을 실행합니다",
43
43
  "command.context.summary": "에이전트 작업 맥락을 출력합니다",
44
+ "command.tech.summary": "에이전트용 기술 선호를 관리합니다",
44
45
  "command.doctor.summary": "mustflow 상태를 점검하고 후속 조치를 안내합니다",
45
46
  "command.docs.summary": "문서 검수 대기열 항목을 추적합니다",
46
47
  "command.handoff.summary": "제한된 작업 항목과 인수인계 기록을 검증합니다",
@@ -41,6 +41,7 @@ export const zhMessages = {
41
41
  "command.lineEndings.summary": "检查并规范化换行符策略",
42
42
  "command.run.summary": "运行已配置的一次性命令",
43
43
  "command.context.summary": "输出机器可读的代理上下文",
44
+ "command.tech.summary": "管理代理使用的技术偏好",
44
45
  "command.doctor.summary": "检查 mustflow 健康状态和后续步骤",
45
46
  "command.docs.summary": "跟踪文档审阅队列条目",
46
47
  "command.handoff.summary": "验证受限的工作项和交接记录",
package/dist/cli/index.js CHANGED
@@ -43,6 +43,7 @@ function getTopLevelHelp(lang) {
43
43
  'mf workspace status --json',
44
44
  'mf workspace verify --changed --plan-only --json',
45
45
  'mf context --json',
46
+ 'mf tech suggest --scope frontend',
46
47
  'mf map --write',
47
48
  'mf search mustflow_check',
48
49
  'mf explain authority AGENTS.md',
@@ -9,10 +9,12 @@ import { inspectManifestLock } from './manifest-lock.js';
9
9
  import { MUSTFLOW_JSON_MAX_BYTES, readMustflowTextFile, readMustflowTextFileIfExists, } from './mustflow-read.js';
10
10
  import { createRunPlan } from './run-plan.js';
11
11
  import { readMustflowTomlFile } from './toml.js';
12
+ import { normalizeTechnologyPreferencesTable, TECHNOLOGY_CONFIG_RELATIVE_PATH, } from '../../core/technology-preferences.js';
12
13
  const CONTEXT_SCHEMA_VERSION = '1';
13
14
  const COMMANDS_RELATIVE_PATH = '.mustflow/config/commands.toml';
14
15
  const MUSTFLOW_RELATIVE_PATH = '.mustflow/config/mustflow.toml';
15
16
  const PREFERENCES_RELATIVE_PATH = '.mustflow/config/preferences.toml';
17
+ const TECHNOLOGY_RELATIVE_PATH = TECHNOLOGY_CONFIG_RELATIVE_PATH;
16
18
  const LATEST_RUN_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
17
19
  const CACHE_RELATIVE_PATH = '.mustflow/cache/';
18
20
  const STATE_RELATIVE_PATH = '.mustflow/state/';
@@ -148,6 +150,19 @@ function readCommandContractContext(projectRoot) {
148
150
  runnable_intents: runnableIntents,
149
151
  };
150
152
  }
153
+ function readTechnologyPreferencesContext(projectRoot) {
154
+ const technology = readTomlTableIfExists(projectRoot, TECHNOLOGY_RELATIVE_PATH);
155
+ const file = normalizeTechnologyPreferencesTable(technology, safeExists(projectRoot, TECHNOLOGY_RELATIVE_PATH));
156
+ return {
157
+ path: file.path,
158
+ exists: file.exists,
159
+ authority: file.authority,
160
+ guidance: file.guidance,
161
+ count: file.preferences.length,
162
+ preferences: file.preferences,
163
+ issues: file.issues,
164
+ };
165
+ }
151
166
  function readEffectivePolicyContext(mustflow, preferences) {
152
167
  const verification = readNestedTable(mustflow, 'verification');
153
168
  const retention = readNestedTable(mustflow, 'retention');
@@ -342,6 +357,7 @@ export function getAgentContext(projectRoot) {
342
357
  read_order: readPathContext(projectRoot, readOrder),
343
358
  optional_read_order: readPathContext(projectRoot, optionalReadOrder),
344
359
  command_contract: readCommandContractContext(projectRoot),
360
+ technology_preferences: readTechnologyPreferencesContext(projectRoot),
345
361
  effective_policy: readEffectivePolicyContext(mustflow, preferences),
346
362
  state_policy: readStatePolicyContext(),
347
363
  blocked_actions: BLOCKED_ACTIONS,
@@ -101,6 +101,12 @@ export const COMMAND_DEFINITIONS = [
101
101
  summaryKey: 'command.context.summary',
102
102
  loadRunner: async () => (await import('../commands/context.js')).runContext,
103
103
  },
104
+ {
105
+ id: 'tech',
106
+ usage: 'mf tech',
107
+ summaryKey: 'command.tech.summary',
108
+ loadRunner: async () => (await import('../commands/tech.js')).runTech,
109
+ },
104
110
  {
105
111
  id: 'doctor',
106
112
  usage: 'mf doctor',
@@ -16,6 +16,7 @@ import { parseTomlText, readMustflowTomlFile } from '../toml.js';
16
16
  import { MUSTFLOW_JSON_MAX_BYTES, readMustflowTextFileResult, } from '../mustflow-read.js';
17
17
  import { getContractModelDefinitions, validateCandidateContractModelConfig, } from '../../../core/contract-models.js';
18
18
  import { VERSIONING_CONFIG_PATH, detectVersionSourcePaths, readDeclaredVersionSources, releaseVersioningIsEnabled, } from '../../../core/version-sources.js';
19
+ import { normalizeTechnologyPreferencesTable, TECHNOLOGY_CONFIG_RELATIVE_PATH, } from '../../../core/technology-preferences.js';
19
20
  import { ALLOWED_APPROVAL_ACTIONS, ALLOWED_APPROVAL_GATES, ALLOWED_BUDGET_LIMIT_ACTIONS, ALLOWED_CAPABILITY_STATES, ALLOWED_COMMIT_MESSAGE_STYLES, ALLOWED_COMPACTION_CATEGORIES, ALLOWED_COMPACTION_LONG_LIMIT_ACTIONS, ALLOWED_COMPACTION_RAW_LIMIT_ACTIONS, ALLOWED_COMPACTION_STATE_STORES, ALLOWED_COMPACTION_STRATEGIES, ALLOWED_CONTEXT_AUTHORITIES, ALLOWED_CONTEXT_DOCUMENT_AUTHORITIES, ALLOWED_CONTEXT_READ_POLICIES, ALLOWED_HANDOFF_MODES, ALLOWED_HARNESS_FRESH_CONTEXT_MODES, ALLOWED_HARNESS_MODES, ALLOWED_HARNESS_PHASES, ALLOWED_ISOLATION_PREFERENCES, ALLOWED_MAP_MODES, ALLOWED_MAP_PRIVACY_LEVELS, ALLOWED_PROJECT_PROFILES, ALLOWED_REPO_MAP_DEGRADED_VALUES, ALLOWED_REPO_MAP_GIT_LS_FILES_STATUSES, ALLOWED_PROMPT_CACHE_STABLE_PREFIX_POLICIES, ALLOWED_PROMPT_CACHE_STRATEGIES, ALLOWED_PROMPT_CACHE_TASK_READ_POLICIES, ALLOWED_REFRESH_CHECKPOINTS, ALLOWED_REFRESH_METHODS, ALLOWED_REFRESH_MODES, ALLOWED_REFRESH_STATE_STORES, ALLOWED_SKILL_RESOURCE_TYPES, ALLOWED_SKILL_ROUTE_CATEGORIES, ALLOWED_SKILL_ROUTE_PROFILES, ALLOWED_SKILL_ROUTE_TYPES, ALLOWED_STALE_TEST_ACTIONS, ALLOWED_TEST_AUTHORING_POLICIES, ALLOWED_TEST_DELETION_REASONS, ALLOWED_TESTING_POLICIES, ALLOWED_TRANSLATION_POLICIES, ALLOWED_VERIFICATION_SELECTION_STRATEGIES, ALLOWED_VERSION_SOURCE_AUTHORITIES, ALLOWED_VERSION_SOURCE_KINDS, CAPABILITY_BOOLEAN_FIELDS, CAPABILITY_STATE_FIELDS, CONTEXT_AUTHORITY_DRIFT_PATTERNS, DESIGN_TOKEN_DEFINITION_PATTERNS, FORBIDDEN_RELEASE_VERSIONING_CONTRACT_FIELDS, FORBIDDEN_TEST_DELETION_REASONS, FORBIDDEN_VERIFICATION_SELECTION_AUTHORITY_FIELDS, LOCAL_ABSOLUTE_PATH_PATTERNS, LOCAL_TASK_STATE_ROOTS, RAW_COMMAND_FENCE_PATTERN, RELEASE_VERSIONING_BOOLEAN_FIELDS, REPO_MAP_DOC_ID, REPO_MAP_GENERATOR, REPO_MAP_LIFECYCLE, REPO_MAP_PRIVACY_MODE, REPO_MAP_RELATIVE_ROOT, REPO_MAP_REMOTE_OR_BRANCH_PATTERNS, REPO_MAP_SOURCE_FINGERPRINT_PATTERN, REPO_MAP_SOURCE_POLICY, REQUIRED_AGENT_LOOP_PHASES, REQUIRED_SKILL_SCRIPT_RUN_POLICY, REQUIRED_SKILL_SECTION_IDS, ROUTER_INDEX_FILES, ROUTER_INDEX_PROCEDURE_SECTION_PATTERN, SECRET_LIKE_CONTEXT_PATTERNS, SKILL_COMMAND_PERMISSION_CLAIM_PATTERNS, SKILL_INDEX_PATH, SKILL_PACK_ID_PATTERN, SKILL_RESOURCE_MANIFEST, SKILL_RESOURCE_ROOTS, SKILL_RESOURCE_TYPE_BY_ROOT, SKILL_ROUTE_CATEGORY_LABELS, SKILL_ROUTES_METADATA_PATH, SKILL_SECTION_MARKER_PATTERN, SUPPORTED_SKILL_SCHEMA_VERSION, TEST_AUTHORING_BOOLEAN_FIELDS, VERIFICATION_SELECTION_BOOLEAN_FIELDS, VOLATILE_REPO_MAP_PATTERNS } from './constants.js';
20
21
  import { hasOwn, isPositiveInteger, isSafeRelativePath, pushStrictIssue, pushStrictWarning, validateAllowedStringField, validateBooleanField, validateExactStringArrayField, validateNestedTable, validatePathArrayField, validatePathField, validatePositiveIntegerField, validateRequiredFiles, validateRequiredPathField, validateRequiredStringField, validateStringArrayField, validateStringArrayMembers, validateStringField, validateTable, validateToml, validateWorkspaceRoots } from './primitives.js';
21
22
  import { isConfiguredCommandIntent, isDeclaredCommandIntent } from './command-intents.js';
@@ -419,6 +420,15 @@ function validatePreferencesConfig(preferencesToml, issues) {
419
420
  validateStringArrayField(productI18n, 'do_not_translate', '[preferences.product_i18n].do_not_translate', issues);
420
421
  }
421
422
  }
423
+ function validateTechnologyConfig(technologyToml, issues) {
424
+ if (!technologyToml) {
425
+ return;
426
+ }
427
+ const technology = normalizeTechnologyPreferencesTable(technologyToml, true);
428
+ for (const issue of technology.issues) {
429
+ issues.push({ message: `${TECHNOLOGY_CONFIG_RELATIVE_PATH}: ${issue}` });
430
+ }
431
+ }
422
432
  function validateVersioningConfig(versioningToml, issues) {
423
433
  if (!versioningToml) {
424
434
  return;
@@ -1634,6 +1644,7 @@ function collectCheckIssues(projectRoot, options = {}) {
1634
1644
  const parsed = validateToml(projectRoot, issues);
1635
1645
  validateMustflowConfig(parsed.mustflowToml, issues);
1636
1646
  validatePreferencesConfig(parsed.preferencesToml, issues);
1647
+ validateTechnologyConfig(parsed.technologyToml, issues);
1637
1648
  validateVersioningConfig(parsed.versioningToml, issues);
1638
1649
  validateCommandIntents(parsed.commandsToml, issues);
1639
1650
  validateSkills(projectRoot, issues);
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { isRecord } from '../command-contract.js';
4
4
  import { readMustflowTomlFile } from '../toml.js';
5
5
  import { REQUIRED_FILES, } from './constants.js';
6
+ import { TECHNOLOGY_CONFIG_RELATIVE_PATH } from '../../../core/technology-preferences.js';
6
7
  import { VERSIONING_CONFIG_PATH } from '../../../core/version-sources.js';
7
8
  export function hasOwn(table, key) {
8
9
  return Object.prototype.hasOwnProperty.call(table, key);
@@ -34,6 +35,7 @@ export function validateToml(projectRoot, issues) {
34
35
  '.mustflow/config/mustflow.toml',
35
36
  '.mustflow/config/commands.toml',
36
37
  '.mustflow/config/preferences.toml',
38
+ TECHNOLOGY_CONFIG_RELATIVE_PATH,
37
39
  VERSIONING_CONFIG_PATH,
38
40
  ]) {
39
41
  const filePath = path.join(projectRoot, relativePath);
@@ -55,6 +57,9 @@ export function validateToml(projectRoot, issues) {
55
57
  if (relativePath.endsWith('preferences.toml')) {
56
58
  parsedFiles.preferencesToml = parsed;
57
59
  }
60
+ if (relativePath.endsWith('technology.toml')) {
61
+ parsedFiles.technologyToml = parsed;
62
+ }
58
63
  if (relativePath.endsWith('versioning.toml')) {
59
64
  parsedFiles.versioningToml = parsed;
60
65
  }