mustflow 2.115.15 → 2.115.16

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.
@@ -2,8 +2,8 @@ import { renderHelp } from '../../lib/cli-output.js';
2
2
  import { t } from '../../lib/i18n.js';
3
3
  import { getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../../lib/option-parser.js';
4
4
  import { ALLOW_UNTRUSTED_ROOT_OPTION } from '../../lib/run-root-trust.js';
5
+ import { APPROVAL_ACTION_TYPE_SET, APPROVAL_ACTION_TYPES } from '../../../core/approval-actions.js';
5
6
  const DEFAULT_ACTIVE_LOCK_WAIT_TIMEOUT_SECONDS = 300;
6
- const SUPPORTED_RUN_APPROVAL_ACTIONS = new Set(['network_access', 'destructive_command']);
7
7
  const RUN_OPTIONS = [
8
8
  { name: '--json', kind: 'boolean' },
9
9
  { name: '--dry-run', kind: 'boolean' },
@@ -17,7 +17,7 @@ export function hasRunHelpOption(args) {
17
17
  return hasCliOptionToken(args, '--help', ['-h']);
18
18
  }
19
19
  export function getSupportedRunApprovalActions() {
20
- return [...SUPPORTED_RUN_APPROVAL_ACTIONS].sort((left, right) => left.localeCompare(right));
20
+ return [...APPROVAL_ACTION_TYPES].sort((left, right) => left.localeCompare(right));
21
21
  }
22
22
  function getAllowApprovalValues(parsed) {
23
23
  const values = parsed.occurrences
@@ -27,7 +27,7 @@ function getAllowApprovalValues(parsed) {
27
27
  return [...new Set(values)];
28
28
  }
29
29
  function findInvalidApprovalAction(values) {
30
- return values.find((value) => !SUPPORTED_RUN_APPROVAL_ACTIONS.has(value)) ?? null;
30
+ return values.find((value) => !APPROVAL_ACTION_TYPE_SET.has(value)) ?? null;
31
31
  }
32
32
  export function parseRunArguments(args) {
33
33
  const parsed = parseCliOptions(args, RUN_OPTIONS, { allowPositionals: true });
@@ -93,7 +93,11 @@ export function getRunHelp(lang = 'en') {
93
93
  { label: ALLOW_UNTRUSTED_ROOT_OPTION, description: t(lang, 'run.help.option.allowUntrustedRoot') },
94
94
  { label: '-h, --help', description: t(lang, 'cli.option.help') },
95
95
  ],
96
- examples: ['mf run test', 'mf run lint --json', 'mf run release_npm_version_available --allow-approval network_access --json'],
96
+ examples: [
97
+ 'mf run test',
98
+ 'mf run lint --json',
99
+ 'mf run release_npm_publish --allow-approval release --allow-approval network_access --json',
100
+ ],
97
101
  exitCodes: [
98
102
  {
99
103
  label: '0',
@@ -72,6 +72,7 @@ function reportRunPlanFailure(plan, reporter, lang) {
72
72
  break;
73
73
  case 'network_requires_approval':
74
74
  case 'destructive_requires_approval':
75
+ case 'explicit_approval_required':
75
76
  case 'approval_policy_unreadable':
76
77
  message = t(lang, 'run.error.approvalRequired', {
77
78
  intent: plan.intentName,
@@ -10,7 +10,10 @@ export function executeRunPreviewCommand(input, reporter, lang, options) {
10
10
  const profiler = new RunProfiler();
11
11
  const projectRoot = profiler.measure('root_detection', () => resolveMustflowRoot());
12
12
  const contract = profiler.measure('command_contract', () => readCommandContract(projectRoot));
13
- const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, input.intentName, { testTargets: options.testTargets }));
13
+ const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, input.intentName, {
14
+ testTargets: options.testTargets,
15
+ approvedActions: input.allowApprovals,
16
+ }));
14
17
  profiler.measure('preview_render', () => {
15
18
  if (input.json) {
16
19
  reporter.stdout(JSON.stringify(createRunPreview(plan, input.previewMode), null, 2));
@@ -72,5 +72,5 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
72
72
  }, reporter, lang, options);
73
73
  return result.exitCode;
74
74
  }
75
- return executeRunPreviewCommand({ intentName, json, previewMode }, reporter, lang, options);
75
+ return executeRunPreviewCommand({ intentName, json, previewMode, allowApprovals: parsedArgs.allowApprovals }, reporter, lang, options);
76
76
  }
@@ -123,6 +123,7 @@ function readRunIntentMetadata(contract, intent) {
123
123
  effects: readArray(intent, 'effects'),
124
124
  network: readBoolean(intent, 'network'),
125
125
  destructive: readBoolean(intent, 'destructive'),
126
+ approvalActions: readStringArray(intent, 'approval_actions') ?? [],
126
127
  envPolicy: env.policy,
127
128
  envAllowlist: env.allowlist,
128
129
  testTargets: [],
@@ -133,17 +134,18 @@ function readRunIntentMetadata(contract, intent) {
133
134
  };
134
135
  }
135
136
  function createApprovalBlock(projectRoot, metadata, approvedActions) {
136
- const actionTypes = [];
137
+ const actionTypes = [...metadata.approvalActions];
137
138
  if (metadata.network === true) {
138
139
  actionTypes.push('network_access');
139
140
  }
140
141
  if (metadata.destructive === true) {
141
142
  actionTypes.push('destructive_command');
142
143
  }
143
- if (actionTypes.length === 0) {
144
+ const uniqueActionTypes = [...new Set(actionTypes)];
145
+ if (uniqueActionTypes.length === 0) {
144
146
  return null;
145
147
  }
146
- const approvalReport = checkRepoApprovalGate(projectRoot, actionTypes);
148
+ const approvalReport = checkRepoApprovalGate(projectRoot, uniqueActionTypes);
147
149
  if (approvalReport.issues.length > 0) {
148
150
  return {
149
151
  reasonCode: 'approval_policy_unreadable',
@@ -159,7 +161,9 @@ function createApprovalBlock(projectRoot, metadata, approvedActions) {
159
161
  }
160
162
  const reasonCode = missingActions.includes('destructive_command')
161
163
  ? 'destructive_requires_approval'
162
- : 'network_requires_approval';
164
+ : missingActions.includes('network_access')
165
+ ? 'network_requires_approval'
166
+ : 'explicit_approval_required';
163
167
  return {
164
168
  reasonCode,
165
169
  detail: `Action ${missingActions.map((action) => JSON.stringify(action)).join(', ')} requires explicit approval before mf run can execute this intent.`,
@@ -193,6 +197,7 @@ function createBlockedRunPlan(contract, intentName, intent, eligibility, reasonC
193
197
  effects: metadata?.effects,
194
198
  network: metadata?.network,
195
199
  destructive: metadata?.destructive,
200
+ approvalActions: metadata?.approvalActions ?? [],
196
201
  envPolicy: metadata?.envPolicy ?? null,
197
202
  envAllowlist: metadata?.envAllowlist ?? [],
198
203
  testTargets: [],
@@ -277,6 +282,7 @@ export function createRunPlan(projectRoot, contract, intentName, options = {}) {
277
282
  effects: metadata.effects,
278
283
  network: metadata.network,
279
284
  destructive: metadata.destructive,
285
+ approvalActions: metadata.approvalActions,
280
286
  envPolicy: metadata.envPolicy,
281
287
  envAllowlist: metadata.envAllowlist,
282
288
  testTargets,
@@ -360,6 +366,7 @@ export function createRunPreview(plan, previewMode) {
360
366
  effects: plan.effects,
361
367
  network: plan.network,
362
368
  destructive: plan.destructive,
369
+ approval_actions: plan.approvalActions,
363
370
  env_policy: plan.envPolicy,
364
371
  env_allowlist: plan.envAllowlist,
365
372
  test_targets: plan.testTargets,
@@ -72,6 +72,9 @@ function shouldIncludeTemplatePath(relativePath, selectedSkills) {
72
72
  return selectedSkills.includes(skillName);
73
73
  }
74
74
  const SKILL_INDEX_SKILL_PATH_PATTERN = /`\.mustflow\/skills\/([^/]+)\/SKILL\.md`/u;
75
+ const SKILL_NAME_REFERENCE_PATTERN = /`([a-z][a-z0-9-]+)`/gu;
76
+ const SKILL_PATH_REFERENCE_PATTERN = /`\.mustflow\/skills\/([^/]+)\/SKILL\.md`/gu;
77
+ const UNAVAILABLE_SKILL_REFERENCE_FALLBACK = 'the closest installed route for this scope';
75
78
  const SKILL_INDEX_HEADING_PATTERN = /^(#{2,3})\s+(.+?)\s*$/u;
76
79
  const SKILL_INDEX_ROUTE_CATEGORY_NAMES = [
77
80
  'Bug and Failure',
@@ -190,6 +193,16 @@ function filterSkillIndexContent(content, selectedSkills) {
190
193
  }
191
194
  return filteredLines.join('\n').replace(/\n{3,}/gu, '\n\n');
192
195
  }
196
+ function filterUnavailableSkillReferences(content, selectedSkills, knownSkills) {
197
+ const selectedSkillSet = new Set(selectedSkills);
198
+ const knownSkillSet = new Set(knownSkills);
199
+ const replaceUnavailableReference = (reference, skillName) => knownSkillSet.has(skillName) && !selectedSkillSet.has(skillName)
200
+ ? UNAVAILABLE_SKILL_REFERENCE_FALLBACK
201
+ : reference;
202
+ return content
203
+ .replace(SKILL_PATH_REFERENCE_PATTERN, replaceUnavailableReference)
204
+ .replace(SKILL_NAME_REFERENCE_PATTERN, replaceUnavailableReference);
205
+ }
193
206
  function filterSkillRouteMetadataContent(content, selectedSkills) {
194
207
  const selectedSkillSet = new Set(selectedSkills);
195
208
  let keepCurrentRoute = true;
@@ -298,6 +311,7 @@ export function getTemplateFiles(template, locale = template.manifest.defaultLoc
298
311
  ? path.join(template.templateRoot, template.manifest.localesRoot, template.manifest.defaultLocale)
299
312
  : undefined;
300
313
  const selectedSkills = selectedSkillNames(template.manifest, profile, options);
314
+ const knownSkills = templateSkillNames(template.manifest.creates);
301
315
  return template.manifest.creates.filter((relativePath) => shouldIncludeTemplatePath(relativePath, selectedSkills)).map((relativePath) => {
302
316
  const localePath = localeRoot ? path.join(localeRoot, ...relativePath.split('/')) : undefined;
303
317
  const sourceLocalePath = sourceLocaleRoot ? path.join(sourceLocaleRoot, ...relativePath.split('/')) : undefined;
@@ -306,11 +320,16 @@ export function getTemplateFiles(template, locale = template.manifest.defaultLoc
306
320
  const fallbackLocalePath = sourceLocalePath && existsSync(sourceLocalePath) ? sourceLocalePath : undefined;
307
321
  const commonSourcePath = existsSync(commonPath) ? commonPath : undefined;
308
322
  const selectedSourcePath = localizedPath ?? fallbackLocalePath ?? commonSourcePath;
309
- const content = selectedSourcePath && relativePath === '.mustflow/skills/INDEX.md'
323
+ const selectedContent = selectedSourcePath && relativePath === '.mustflow/skills/INDEX.md'
310
324
  ? filterSkillIndexContent(readFileSync(selectedSourcePath, 'utf8'), selectedSkills)
311
325
  : selectedSourcePath && relativePath === '.mustflow/skills/routes.toml'
312
326
  ? filterSkillRouteMetadataContent(readFileSync(selectedSourcePath, 'utf8'), selectedSkills)
313
327
  : undefined;
328
+ const content = selectedSourcePath && relativePath === '.mustflow/skills/INDEX.md'
329
+ ? filterUnavailableSkillReferences(selectedContent ?? '', selectedSkills, knownSkills)
330
+ : selectedSourcePath && /^\.mustflow\/skills\/[^/]+\/SKILL\.md$/u.test(relativePath)
331
+ ? filterUnavailableSkillReferences(readFileSync(selectedSourcePath, 'utf8'), selectedSkills, knownSkills)
332
+ : selectedContent;
314
333
  if (localizedPath) {
315
334
  return {
316
335
  relativePath,
@@ -130,18 +130,7 @@ export const ALLOWED_PROMPT_CACHE_STRATEGIES = new Set(['stable_prefix']);
130
130
  export const ALLOWED_PROMPT_CACHE_STABLE_PREFIX_POLICIES = new Set(['hash_verified']);
131
131
  export const ALLOWED_PROMPT_CACHE_TASK_READ_POLICIES = new Set(['task_relevant_only']);
132
132
  export const ALLOWED_BUDGET_LIMIT_ACTIONS = new Set(['stop_and_handoff', 'stop_and_report']);
133
- export const ALLOWED_APPROVAL_GATES = new Set([
134
- 'git_commit',
135
- 'git_push',
136
- 'dependency_install',
137
- 'dependency_upgrade',
138
- 'network_access',
139
- 'database_migration',
140
- 'destructive_command',
141
- 'secret_access',
142
- 'release',
143
- 'cross_repository_change',
144
- ]);
133
+ export { APPROVAL_ACTION_TYPE_SET as ALLOWED_APPROVAL_GATES } from '../../../core/approval-actions.js';
145
134
  export const ALLOWED_APPROVAL_ACTIONS = new Set(['stop_and_request_approval']);
146
135
  export const ALLOWED_ISOLATION_PREFERENCES = new Set(['none', 'git_worktree', 'sandbox']);
147
136
  export const ALLOWED_REFRESH_MODES = new Set(['checkpoint']);
@@ -1066,7 +1066,7 @@ function readSkillRouteMetadataFromTomlContent(content, sourceLabel, issues) {
1066
1066
  }
1067
1067
  return metadata;
1068
1068
  }
1069
- function validateTemplateProfileSkillRoutes(profile, files, issues) {
1069
+ function validateTemplateProfileSkillRoutes(profile, files, knownSkillNames, issues) {
1070
1070
  const contentByPath = new Map(files.map((file) => [file.relativePath, templateFileContent(file)]));
1071
1071
  const indexContent = contentByPath.get(SKILL_INDEX_PATH);
1072
1072
  const routesContent = contentByPath.get(SKILL_ROUTES_METADATA_PATH);
@@ -1104,6 +1104,17 @@ function validateTemplateProfileSkillRoutes(profile, files, issues) {
1104
1104
  selectedSkillContents.set(skillName, content);
1105
1105
  }
1106
1106
  }
1107
+ for (const [relativePath, content] of contentByPath.entries()) {
1108
+ if (relativePath !== SKILL_INDEX_PATH && !skillRouteName(relativePath)) {
1109
+ continue;
1110
+ }
1111
+ for (const match of content.matchAll(/`([a-z][a-z0-9-]+)`/gu)) {
1112
+ const referencedSkill = match[1];
1113
+ if (referencedSkill && knownSkillNames.has(referencedSkill) && !selectedSkillContents.has(referencedSkill)) {
1114
+ pushStrictIssue(issues, `template profile "${profile}" ${relativePath} references skill "${referencedSkill}" not installed by that profile`);
1115
+ }
1116
+ }
1117
+ }
1107
1118
  for (const route of skillRoutes) {
1108
1119
  const routeSkillName = skillRouteName(route.skillPath);
1109
1120
  if (!routeSkillName) {
@@ -1178,8 +1189,11 @@ function validateStrictTemplateSkillProfiles(issues) {
1178
1189
  pushStrictIssue(issues, `default template skill profiles could not be loaded: ${message}`);
1179
1190
  return;
1180
1191
  }
1192
+ const knownSkillNames = new Set(template.manifest.creates
1193
+ .map((relativePath) => skillRouteName(relativePath))
1194
+ .filter((skillName) => Boolean(skillName)));
1181
1195
  for (const profile of template.manifest.profiles) {
1182
- validateTemplateProfileSkillRoutes(profile, getTemplateFiles(template, template.manifest.defaultLocale, profile), issues);
1196
+ validateTemplateProfileSkillRoutes(profile, getTemplateFiles(template, template.manifest.defaultLocale, profile), knownSkillNames, issues);
1183
1197
  }
1184
1198
  }
1185
1199
  function listManagedMarkdownDocuments(projectRoot) {
@@ -0,0 +1,13 @@
1
+ export const APPROVAL_ACTION_TYPES = [
2
+ 'git_commit',
3
+ 'git_push',
4
+ 'dependency_install',
5
+ 'dependency_upgrade',
6
+ 'network_access',
7
+ 'database_migration',
8
+ 'destructive_command',
9
+ 'secret_access',
10
+ 'release',
11
+ 'cross_repository_change',
12
+ ];
13
+ export const APPROVAL_ACTION_TYPE_SET = new Set(APPROVAL_ACTION_TYPES);
@@ -6,6 +6,7 @@ import { COMMAND_PRECONDITION_KINDS } from './command-preconditions.js';
6
6
  import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
7
7
  import { MAX_COMMAND_OUTPUT_BYTES, commandMaxOutputBytesLimitMessage } from './command-output-limits.js';
8
8
  import { SUCCESS_EXIT_CODES_CONTRACT_DESCRIPTION, successExitCodesAreValid } from './success-exit-codes.js';
9
+ import { APPROVAL_ACTION_TYPE_SET } from './approval-actions.js';
9
10
  const COMMAND_INPUT_TYPES = new Set(['path', 'enum', 'boolean', 'integer', 'literal']);
10
11
  const COMMAND_INPUT_NAME_PATTERN = /^[a-z][a-z0-9_]*$/u;
11
12
  const COMMAND_ARGV_PLACEHOLDER_PATTERN = /^\{([a-z][a-z0-9_]*)\}$/u;
@@ -59,6 +60,57 @@ function validateStringArrayField(table, key, label, issues) {
59
60
  issues.push(commandContractIssue(`${label} must be a string array`));
60
61
  }
61
62
  }
63
+ function validateApprovalActions(intentName, intent, issues) {
64
+ validateStringArrayField(intent, 'approval_actions', `[commands.intents.${intentName}].approval_actions`, issues);
65
+ const approvalActions = readStringArray(intent, 'approval_actions');
66
+ if (!approvalActions) {
67
+ return;
68
+ }
69
+ const unsupported = [...new Set(approvalActions.filter((action) => !APPROVAL_ACTION_TYPE_SET.has(action)))];
70
+ if (unsupported.length > 0) {
71
+ issues.push(commandContractIssue(`[commands.intents.${intentName}].approval_actions contains unsupported action types: ${unsupported.join(', ')}`));
72
+ }
73
+ }
74
+ function readGitSubcommand(argv) {
75
+ if (normalizeCommandExecutableName(argv[0] ?? '') !== 'git') {
76
+ return null;
77
+ }
78
+ const optionsWithSeparateValue = new Set(['-C', '-c', '--exec-path', '--git-dir', '--work-tree', '--namespace']);
79
+ for (let index = 1; index < argv.length; index += 1) {
80
+ const argument = argv[index] ?? '';
81
+ if (optionsWithSeparateValue.has(argument)) {
82
+ index += 1;
83
+ continue;
84
+ }
85
+ if (argument.startsWith('-')) {
86
+ continue;
87
+ }
88
+ return argument;
89
+ }
90
+ return null;
91
+ }
92
+ function validateSensitiveGitApprovalActions(intentName, intent, issues) {
93
+ if (intent.status !== 'configured' || intent.run_policy !== 'agent_allowed') {
94
+ return;
95
+ }
96
+ const argv = readStringArray(intent, 'argv');
97
+ if (!argv) {
98
+ return;
99
+ }
100
+ const gitSubcommand = readGitSubcommand(argv);
101
+ const requiredAction = gitSubcommand === 'add' || gitSubcommand === 'commit'
102
+ ? 'git_commit'
103
+ : gitSubcommand === 'push'
104
+ ? 'git_push'
105
+ : null;
106
+ if (!requiredAction) {
107
+ return;
108
+ }
109
+ const approvalActions = readStringArray(intent, 'approval_actions') ?? [];
110
+ if (!approvalActions.includes(requiredAction)) {
111
+ issues.push(commandContractIssue(`Agent-runnable git ${gitSubcommand} intent ${intentName} must include approval_actions = ["${requiredAction}"]`));
112
+ }
113
+ }
62
114
  function validatePositiveIntegerField(table, key, label, issues) {
63
115
  if (hasOwn(table, key) && !isPositiveInteger(table[key])) {
64
116
  issues.push(commandContractIssue(`${label} must be a positive integer`));
@@ -339,6 +391,8 @@ function validateCommandIntent(intentName, intent, allIntents, issues) {
339
391
  validateAllowedStringField(intent, 'env_policy', `[commands.intents.${intentName}].env_policy`, COMMAND_ENV_POLICIES, issues);
340
392
  validateBooleanField(intent, 'allow_shell', `[commands.intents.${intentName}].allow_shell`, issues);
341
393
  validateStringArrayField(intent, 'env_allowlist', `[commands.intents.${intentName}].env_allowlist`, issues);
394
+ validateApprovalActions(intentName, intent, issues);
395
+ validateSensitiveGitApprovalActions(intentName, intent, issues);
342
396
  validateBooleanField(intent, ALLOW_ENV_INHERITANCE_RISKS_KEY, `[commands.intents.${intentName}].${ALLOW_ENV_INHERITANCE_RISKS_KEY}`, issues);
343
397
  validateBooleanField(intent, ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY, `[commands.intents.${intentName}].${ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY}`, issues);
344
398
  validateMaxOutputBytesField(intent, 'max_output_bytes', `[commands.intents.${intentName}].max_output_bytes`, issues);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.115.15",
3
+ "version": "2.115.16",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
@@ -210,6 +210,24 @@
210
210
  },
211
211
  "network": { "type": "boolean" },
212
212
  "destructive": { "type": "boolean" },
213
+ "approval_actions": {
214
+ "type": "array",
215
+ "uniqueItems": true,
216
+ "items": {
217
+ "enum": [
218
+ "git_commit",
219
+ "git_push",
220
+ "dependency_install",
221
+ "dependency_upgrade",
222
+ "network_access",
223
+ "database_migration",
224
+ "destructive_command",
225
+ "secret_access",
226
+ "release",
227
+ "cross_repository_change"
228
+ ]
229
+ }
230
+ },
213
231
  "required_after": {
214
232
  "type": "array",
215
233
  "items": { "type": "string" }
@@ -379,6 +379,14 @@ required_after = ["before_final_report", "commit_message_suggestion"]
379
379
 
380
380
  [intents.git_commit]
381
381
  status = "manual_only"
382
- description = "Create a Git commit."
383
- reason = "Creating a commit requires explicit user approval."
384
- agent_action = "do_not_commit_report_suggestion_only"
382
+ description = "Create an unscoped Git commit."
383
+ reason = "A generic commit cannot safely choose reviewed paths or a commit message."
384
+ agent_action = "author_bounded_repo_specific_stage_and_commit_intents_after_explicit_user_request"
385
+ approval_actions = ["git_commit"]
386
+
387
+ [intents.git_push]
388
+ status = "manual_only"
389
+ description = "Push an unspecified Git ref."
390
+ reason = "A generic push cannot safely choose the remote and ref."
391
+ agent_action = "author_bounded_repo_specific_push_intent_after_explicit_user_request"
392
+ approval_actions = ["git_push"]
@@ -1,6 +1,6 @@
1
1
  id = "default"
2
2
  name = "default"
3
- version = "2.115.15"
3
+ version = "2.115.16"
4
4
  description = "Minimal workflow for LLM agents to read, edit, and verify their work in a repository."
5
5
  common_root = "common"
6
6
  locales_root = "locales"