mustflow 2.27.0 → 2.28.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.
package/README.md CHANGED
@@ -307,9 +307,9 @@ Runnable work is declared in `.mustflow/config/commands.toml` so agents do not g
307
307
  - `run_policy = "agent_allowed"`
308
308
  - `stdin = "closed"`
309
309
 
310
- Development servers, watch modes, browser UIs, interactive commands, and background processes do not run directly. `mf run` also rejects obvious long-running `argv` shapes, such as shell-wrapper background payloads, interpreter loops, package-manager development scripts, watchers, and development servers declared as one-shot commands.
310
+ Development servers, watch modes, browser UIs, interactive commands, and background processes do not run directly. `mf run` also rejects obvious long-running `argv` shapes, such as shell-wrapper background payloads, interpreter loops, package-manager development scripts, watchers, and development servers declared as one-shot commands. If a bounded one-shot command has a name that matches a common long-running pattern, the intent can explicitly acknowledge that with `allow_long_running_command_patterns = true`; background shell patterns remain blocked.
311
311
 
312
- Command environments remove the project-local `node_modules/.bin` path from `PATH` by default. If an intent needs a project dependency binary such as `eslint`, `tsc`, or `vitest`, declare it through the package manager, for example `npm exec eslint -- ...`, `pnpm exec tsc -- --noEmit`, `bun x eslint ...`, or `yarn exec eslint ...`. `mf check --strict` warns when an agent-runnable intent uses a bare executable name that appears under the project-local `.bin` directory.
312
+ Command environments remove the project-local `node_modules/.bin` path from `PATH` by default. If an intent needs a project dependency binary such as `eslint`, `tsc`, or `vitest`, declare it through the package manager, for example `npm exec eslint -- ...`, `pnpm exec tsc -- --noEmit`, `bun x eslint ...`, or `yarn exec eslint ...`. `mf check --strict` warns when an agent-runnable intent uses a bare executable name that appears under the project-local `.bin` directory, except for names listed in `defaults.allow_project_local_bin_bare_executables`. `mf run` may resolve those allowed names directly from the local `.bin` directory without exposing every local binary through `PATH`. The installed template allows `mf` and `mustflow` by default. Intent-level `allow_env_inheritance_risks = true` is available when a command intentionally uses `env_policy = "inherit"`.
313
313
 
314
314
  Use `mf verify --reason <event> --plan-only --json` to inspect matching verification intents, command eligibility, remaining gaps, and missing runnable coverage without executing commands. Use `mf run <intent> --dry-run --json` to inspect one resolved command intent without spawning a process or writing a run receipt. Plan-only verification includes a `decision_graph` that connects changed surfaces, classification reasons, command candidates, eligibility checks, effects, and gaps. When `.mustflow/cache/mustflow.sqlite` is fresh, scheduled entries also include read-only `effectGraph` metadata for write locks and lock conflicts. These graph rows are marked `explanation_only` and never grant command authority; `.mustflow/config/commands.toml` remains the only runnable command source.
315
315
 
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { isMustflowBinName } from '../../core/command-classification.js';
3
3
  import { resolveSafeProjectCwd } from '../../core/command-cwd.js';
4
- import { resolveCommandEnv } from '../../core/command-env.js';
4
+ import { readProjectLocalBinBareExecutableAllowlist, resolveAllowedProjectLocalBinExecutable, resolveCommandEnv, } from '../../core/command-env.js';
5
5
  import { getCommandMaxOutputBytesLimitDetail, readEffectiveCommandCwd, readEffectiveCommandMaxOutputBytes, } from '../../core/command-run-constraints.js';
6
6
  import { evaluateCommandIntentEligibility, } from '../../core/command-intent-eligibility.js';
7
7
  import { inspectActiveRunLocks, } from '../../core/active-run-locks.js';
@@ -56,7 +56,7 @@ function resolveCurrentCliEntrypoint() {
56
56
  const entrypoint = process.argv[1];
57
57
  return entrypoint ? path.resolve(entrypoint) : undefined;
58
58
  }
59
- function resolveArgvCommand(intent, commandArgv) {
59
+ function resolveArgvCommand(projectRoot, contract, intent, commandArgv) {
60
60
  const [command = '', ...args] = commandArgv;
61
61
  if (isMustflowBuiltinIntent(intent) && isMustflowBinName(command)) {
62
62
  const entrypoint = resolveCurrentCliEntrypoint();
@@ -68,6 +68,14 @@ function resolveArgvCommand(intent, commandArgv) {
68
68
  };
69
69
  }
70
70
  }
71
+ const localBinExecutable = resolveAllowedProjectLocalBinExecutable(projectRoot, command, readProjectLocalBinBareExecutableAllowlist(contract));
72
+ if (localBinExecutable) {
73
+ return {
74
+ executable: localBinExecutable,
75
+ args,
76
+ shell: shouldUseShellForArgvExecutable(localBinExecutable),
77
+ };
78
+ }
71
79
  return {
72
80
  executable: command,
73
81
  args,
@@ -212,7 +220,7 @@ export function createRunPlan(projectRoot, contract, intentName, options = {}) {
212
220
  commandArgv,
213
221
  shellCommand: metadata.shellCommand,
214
222
  mode: metadata.mode,
215
- argvCommand: commandArgv ? resolveArgvCommand(rawIntent, commandArgv) : undefined,
223
+ argvCommand: commandArgv ? resolveArgvCommand(projectRoot, contract, rawIntent, commandArgv) : undefined,
216
224
  writes: metadata.writes,
217
225
  effects: metadata.effects,
218
226
  network: metadata.network,
@@ -1,7 +1,6 @@
1
- import { existsSync } from 'node:fs';
2
1
  import path from 'node:path';
3
2
  import { COMMAND_LIFECYCLES, COMMAND_RUN_POLICIES, LONG_RUNNING_LIFECYCLES, isRecord, readStringArray, } from './config-loading.js';
4
- import { COMMAND_ENV_POLICIES, DEFAULT_COMMAND_ENV_POLICY } from './command-env.js';
3
+ import { COMMAND_ENV_POLICIES, DEFAULT_COMMAND_ENV_POLICY, PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST_KEY, normalizeCommandExecutableName, readProjectLocalBinBareExecutableAllowlist, resolveAllowedProjectLocalBinExecutable, } from './command-env.js';
5
4
  import { COMMAND_EFFECT_CONCURRENCY, COMMAND_EFFECT_MODES, COMMAND_EFFECT_TYPES, validateCommandEffectLockWarnings, validateCommandEffects, } from './command-effects.js';
6
5
  import { COMMAND_PRECONDITION_KINDS } from './command-preconditions.js';
7
6
  import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
@@ -13,6 +12,8 @@ const COMMAND_ARGV_PLACEHOLDER_PATTERN = /^\{([a-z][a-z0-9_]*)\}$/u;
13
12
  const COMMAND_ARGV_MIXED_PLACEHOLDER_PATTERN = /\{([a-z][a-z0-9_]*)\}/u;
14
13
  const WINDOWS_RESERVED_PATH_SEGMENTS = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu;
15
14
  const SAFE_PATH_EXTENSION_PATTERN = /^\.[A-Za-z0-9][A-Za-z0-9._-]*$/u;
15
+ const ALLOW_ENV_INHERITANCE_RISKS_KEY = 'allow_env_inheritance_risks';
16
+ const ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY = 'allow_long_running_command_patterns';
16
17
  function commandContractIssue(message, id) {
17
18
  return id ? { id, message } : { message };
18
19
  }
@@ -257,6 +258,7 @@ function validateCommandDefaults(commandsToml, issues) {
257
258
  validateStringField(defaults, 'on_timeout', '[commands.defaults].on_timeout', issues);
258
259
  validateAllowedStringField(defaults, 'env_policy', '[commands.defaults].env_policy', COMMAND_ENV_POLICIES, issues);
259
260
  validateStringArrayField(defaults, 'env_allowlist', '[commands.defaults].env_allowlist', issues);
261
+ validateStringArrayField(defaults, PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST_KEY, `[commands.defaults].${PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST_KEY}`, issues);
260
262
  validatePositiveIntegerField(defaults, 'default_timeout_seconds', '[commands.defaults].default_timeout_seconds', issues);
261
263
  validateMaxOutputBytesField(defaults, 'max_output_bytes', '[commands.defaults].max_output_bytes', issues);
262
264
  validatePositiveIntegerField(defaults, 'kill_after_seconds', '[commands.defaults].kill_after_seconds', issues);
@@ -337,6 +339,8 @@ function validateCommandIntent(intentName, intent, allIntents, issues) {
337
339
  validateAllowedStringField(intent, 'env_policy', `[commands.intents.${intentName}].env_policy`, COMMAND_ENV_POLICIES, issues);
338
340
  validateBooleanField(intent, 'allow_shell', `[commands.intents.${intentName}].allow_shell`, issues);
339
341
  validateStringArrayField(intent, 'env_allowlist', `[commands.intents.${intentName}].env_allowlist`, issues);
342
+ validateBooleanField(intent, ALLOW_ENV_INHERITANCE_RISKS_KEY, `[commands.intents.${intentName}].${ALLOW_ENV_INHERITANCE_RISKS_KEY}`, issues);
343
+ validateBooleanField(intent, ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY, `[commands.intents.${intentName}].${ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY}`, issues);
340
344
  validateMaxOutputBytesField(intent, 'max_output_bytes', `[commands.intents.${intentName}].max_output_bytes`, issues);
341
345
  validatePositiveIntegerField(intent, 'kill_after_seconds', `[commands.intents.${intentName}].kill_after_seconds`, issues);
342
346
  validateCommandIntentSelection(intentName, intent, issues);
@@ -375,7 +379,7 @@ function validateCommandIntent(intentName, intent, allIntents, issues) {
375
379
  if (blockedCommandPattern?.code === 'shell_background_pattern') {
376
380
  issues.push(commandContractIssue(`Shell intent ${intentName} contains a blocked long-running or background pattern`, 'mustflow.command_contract.shell_background_pattern'));
377
381
  }
378
- if (blockedCommandPattern?.code === 'long_running_command_pattern') {
382
+ if (blockedCommandPattern?.code === 'long_running_command_pattern' && intent[ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY] !== true) {
379
383
  issues.push(commandContractIssue(`Intent ${intentName} contains a blocked long-running or background command pattern`, 'mustflow.command_contract.long_running_command_pattern'));
380
384
  }
381
385
  if (hasOwn(intent, 'success_exit_codes')) {
@@ -419,6 +423,9 @@ export function findCommandEnvInheritanceWarnings(commandsToml) {
419
423
  if (envPolicy.policy !== 'inherit') {
420
424
  continue;
421
425
  }
426
+ if (intent[ALLOW_ENV_INHERITANCE_RISKS_KEY] === true) {
427
+ continue;
428
+ }
422
429
  const reasons = readCommandEnvInheritanceRiskReasons(intent);
423
430
  warnings.push({
424
431
  intentName,
@@ -465,21 +472,14 @@ function validateCommandEnvInheritanceWarnings(commandsToml) {
465
472
  return issues;
466
473
  }
467
474
  function projectLocalBinExecutableExists(projectRoot, executable) {
468
- const localBinPath = path.join(projectRoot, 'node_modules', '.bin');
469
- const executableName = path.basename(executable).replace(/\.(?:cmd|exe|ps1)$/iu, '');
470
- const candidates = [
471
- executableName,
472
- `${executableName}.cmd`,
473
- `${executableName}.exe`,
474
- `${executableName}.ps1`,
475
- ];
476
- return candidates.some((candidate) => existsSync(path.join(localBinPath, candidate)));
475
+ return resolveAllowedProjectLocalBinExecutable(projectRoot, executable, new Set([normalizeCommandExecutableName(executable)])) !== null;
477
476
  }
478
477
  function validateProjectLocalBinWarnings(projectRoot, commandsToml) {
479
478
  const issues = [];
480
479
  if (!isRecord(commandsToml?.intents)) {
481
480
  return issues;
482
481
  }
482
+ const allowedBareExecutables = readProjectLocalBinBareExecutableAllowlist(commandsToml);
483
483
  for (const [intentName, intent] of Object.entries(commandsToml.intents)) {
484
484
  if (!isRecord(intent)) {
485
485
  continue;
@@ -492,6 +492,9 @@ function validateProjectLocalBinWarnings(projectRoot, commandsToml) {
492
492
  if (!executable || executable.includes('/') || executable.includes('\\')) {
493
493
  continue;
494
494
  }
495
+ if (allowedBareExecutables.has(normalizeCommandExecutableName(executable))) {
496
+ continue;
497
+ }
495
498
  if (!projectLocalBinExecutableExists(projectRoot, executable)) {
496
499
  continue;
497
500
  }
@@ -1,3 +1,4 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import path from 'node:path';
2
3
  import { readString, readStringArray } from './config-loading.js';
3
4
  export const COMMAND_ENV_POLICIES = new Set(['inherit', 'minimal', 'allowlist']);
@@ -25,6 +26,8 @@ const BASE_MINIMAL_ENV_KEYS = [
25
26
  'WINDIR',
26
27
  'windir',
27
28
  ];
29
+ export const PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST_KEY = 'allow_project_local_bin_bare_executables';
30
+ export const DEFAULT_PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST = new Set(['mf', 'mustflow']);
28
31
  function getPathEnvKey(env) {
29
32
  return Object.keys(env).find((key) => key.toLowerCase() === 'path') ?? 'PATH';
30
33
  }
@@ -48,6 +51,46 @@ function readEnvPolicy(table) {
48
51
  function readEnvAllowlist(table) {
49
52
  return table ? (readStringArray(table, 'env_allowlist') ?? []) : [];
50
53
  }
54
+ export function normalizeCommandExecutableName(executable) {
55
+ return path.basename(executable).replace(/\.(?:cmd|exe|ps1)$/iu, '').toLowerCase();
56
+ }
57
+ export function readProjectLocalBinBareExecutableAllowlist(contractOrCommands) {
58
+ const defaults = contractOrCommands && typeof contractOrCommands === 'object' && 'defaults' in contractOrCommands
59
+ ? contractOrCommands.defaults
60
+ : undefined;
61
+ const configuredAllowlist = defaults && typeof defaults === 'object'
62
+ ? defaults[PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST_KEY]
63
+ : undefined;
64
+ if (!Array.isArray(configuredAllowlist)) {
65
+ return DEFAULT_PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST;
66
+ }
67
+ return new Set(configuredAllowlist
68
+ .filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
69
+ .map((entry) => normalizeCommandExecutableName(entry)));
70
+ }
71
+ export function resolveAllowedProjectLocalBinExecutable(projectRoot, executable, allowlist) {
72
+ if (executable.includes('/') || executable.includes('\\')) {
73
+ return null;
74
+ }
75
+ const executableName = normalizeCommandExecutableName(executable);
76
+ if (!allowlist.has(executableName)) {
77
+ return null;
78
+ }
79
+ const localBinPath = path.join(projectRoot, 'node_modules', '.bin');
80
+ const candidates = [
81
+ executableName,
82
+ `${executableName}.cmd`,
83
+ `${executableName}.exe`,
84
+ `${executableName}.ps1`,
85
+ ];
86
+ for (const candidate of candidates) {
87
+ const candidatePath = path.join(localBinPath, candidate);
88
+ if (existsSync(candidatePath)) {
89
+ return candidatePath;
90
+ }
91
+ }
92
+ return null;
93
+ }
51
94
  function pickEnv(source, names) {
52
95
  const output = {};
53
96
  for (const name of names) {
@@ -1,5 +1,6 @@
1
1
  import { isRecord, readString } from './config-loading.js';
2
2
  import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
3
+ const ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY = 'allow_long_running_command_patterns';
3
4
  export const COMMAND_INTENT_INELIGIBILITY_CODES = [
4
5
  'intent_not_table',
5
6
  'status_not_configured',
@@ -83,7 +84,7 @@ export function evaluateCommandIntentEligibility(intentName, rawIntent) {
83
84
  detail: blockedPattern.detail,
84
85
  };
85
86
  }
86
- if (blockedPattern?.code === 'long_running_command_pattern') {
87
+ if (blockedPattern?.code === 'long_running_command_pattern' && rawIntent[ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY] !== true) {
87
88
  return {
88
89
  ok: false,
89
90
  code: 'blocked_long_running_command_pattern',
@@ -58,6 +58,7 @@ const AGENT_WORKFLOW_PATH = '.mustflow/docs/agent-workflow.md';
58
58
  const PACKAGE_SCRIPT_RUNNERS = new Set(['bun', 'npm', 'pnpm', 'yarn']);
59
59
  const MAKEFILE_CANDIDATES = ['Makefile', 'makefile'];
60
60
  const JUSTFILE_CANDIDATES = ['justfile', 'Justfile'];
61
+ const ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY = 'allow_long_running_command_patterns';
61
62
  function uniqueSorted(values) {
62
63
  return [...new Set(values)].sort((left, right) => left.localeCompare(right));
63
64
  }
@@ -333,7 +334,7 @@ function lintIntent(name, value, issues) {
333
334
  if (blockedCommandPattern?.code === 'shell_background_pattern') {
334
335
  pushIssue(issues, 'error', 'shell_background_pattern', name, `Shell intent ${name} contains a blocked long-running or background pattern.`);
335
336
  }
336
- if (blockedCommandPattern?.code === 'long_running_command_pattern') {
337
+ if (blockedCommandPattern?.code === 'long_running_command_pattern' && value[ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY] !== true) {
337
338
  pushIssue(issues, 'error', 'long_running_command_pattern', name, `Intent ${name} contains a blocked long-running or background command pattern.`);
338
339
  }
339
340
  if (!successExitCodesAreValid(value)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.27.0",
3
+ "version": "2.28.0",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
@@ -36,7 +36,8 @@
36
36
  "on_timeout": { "type": "string" },
37
37
  "kill_after_seconds": { "type": "integer" },
38
38
  "env_policy": { "$ref": "#/$defs/envPolicy" },
39
- "env_allowlist": { "$ref": "#/$defs/stringArray" }
39
+ "env_allowlist": { "$ref": "#/$defs/stringArray" },
40
+ "allow_project_local_bin_bare_executables": { "$ref": "#/$defs/stringArray" }
40
41
  }
41
42
  },
42
43
  "intents": {
@@ -172,6 +173,7 @@
172
173
  },
173
174
  "cmd": { "type": "string" },
174
175
  "allow_shell": { "type": "boolean" },
176
+ "allow_long_running_command_patterns": { "type": "boolean" },
175
177
  "cwd": { "type": "string" },
176
178
  "timeout_seconds": { "type": "integer" },
177
179
  "kill_after_seconds": { "type": "integer" },
@@ -185,6 +187,7 @@
185
187
  },
186
188
  "env_policy": { "$ref": "#/$defs/envPolicy" },
187
189
  "env_allowlist": { "$ref": "#/$defs/stringArray" },
190
+ "allow_env_inheritance_risks": { "type": "boolean" },
188
191
  "manual_start_hint": { "type": "string" },
189
192
  "health_check_url": { "type": "string" },
190
193
  "stop_instruction": { "type": "string" },
@@ -18,6 +18,7 @@ on_timeout = "terminate_process_tree"
18
18
  kill_after_seconds = 5
19
19
  env_policy = "minimal"
20
20
  env_allowlist = []
21
+ allow_project_local_bin_bare_executables = ["mf", "mustflow"]
21
22
 
22
23
  [resources.local_index_cache]
23
24
  description = "Generated mustflow SQLite local index under .mustflow/cache/."
@@ -1,6 +1,6 @@
1
1
  id = "default"
2
2
  name = "default"
3
- version = "2.27.0"
3
+ version = "2.28.0"
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"