mustflow 2.22.1 → 2.22.2

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.
@@ -76,7 +76,7 @@ export function forceTerminateProcessTreeNonBlocking(pid) {
76
76
  signalProcessTreeNonBlocking(pid, 'SIGKILL');
77
77
  }
78
78
  export function getKillMethod() {
79
- return process.platform === 'win32' ? 'taskkill_process_tree' : 'process_group_sigterm';
79
+ return process.platform === 'win32' ? 'taskkill_process_tree_forced' : 'process_group_sigterm';
80
80
  }
81
81
  export function createPendingTimeoutTermination(method, forcedKillAttempted = false) {
82
82
  return {
@@ -78,6 +78,12 @@ function reportRunPlanFailure(plan, reporter, lang) {
78
78
  }
79
79
  reporter.stderr(renderCliError(message, 'mf help commands', lang));
80
80
  }
81
+ function writeLatestProfile(profiler, options, input) {
82
+ if (options.writeLatestProfile === false) {
83
+ return;
84
+ }
85
+ profiler.writeLatest(input);
86
+ }
81
87
  export function getRunHelp(lang = 'en') {
82
88
  return renderHelp({
83
89
  usage: 'mf run <intent> [options]',
@@ -151,7 +157,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
151
157
  reporter.stdout(renderRunPreviewText(plan, previewMode, lang));
152
158
  }
153
159
  });
154
- profiler.writeLatest({
160
+ writeLatestProfile(profiler, options, {
155
161
  projectRoot,
156
162
  intent: intentName,
157
163
  status: plan.ok ? 'previewed' : 'blocked',
@@ -161,7 +167,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
161
167
  }
162
168
  if (!plan.ok) {
163
169
  reportRunPlanFailure(plan, reporter, lang);
164
- profiler.writeLatest({
170
+ writeLatestProfile(profiler, options, {
165
171
  projectRoot,
166
172
  intent: intentName,
167
173
  status: 'blocked',
@@ -221,8 +227,10 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
221
227
  if (options.writeLatestReceipt !== false) {
222
228
  profiler.measure('receipt_write', () => writeRunReceipt(projectRoot, receipt));
223
229
  }
224
- profiler.measure('performance_history_write', () => recordRunPerformanceHistory(projectRoot, receipt));
225
- profiler.writeLatest({
230
+ if (options.recordPerformanceHistory !== false) {
231
+ profiler.measure('performance_history_write', () => recordRunPerformanceHistory(projectRoot, receipt));
232
+ }
233
+ writeLatestProfile(profiler, options, {
226
234
  projectRoot,
227
235
  intent: intentName,
228
236
  status: runStatus,
@@ -632,6 +632,8 @@ async function runVerificationIntent(intent, lang, verificationPlanId, testTarge
632
632
  const output = createBufferedOutput();
633
633
  const exitCode = await runRun([intent, '--json'], output.reporter, lang, {
634
634
  writeLatestReceipt: false,
635
+ writeLatestProfile: false,
636
+ recordPerformanceHistory: false,
635
637
  testTargets,
636
638
  additionalDeclaredWritePaths,
637
639
  });
@@ -5,13 +5,10 @@ import { resolveCommandEnv } from '../../core/command-env.js';
5
5
  import { evaluateCommandIntentEligibility, } from '../../core/command-intent-eligibility.js';
6
6
  import { isRecord, readPositiveInteger, readString, readStringArray, } from '../../core/config-loading.js';
7
7
  import { DEFAULT_COMMAND_MAX_OUTPUT_BYTES, COMMAND_OUTPUT_LIMIT_SCOPE, MAX_COMMAND_OUTPUT_BYTES, commandMaxOutputBytesLimitMessage, } from '../../core/command-output-limits.js';
8
+ import { normalizeSuccessExitCodes } from '../../core/success-exit-codes.js';
8
9
  import { t } from './i18n.js';
9
10
  function getSuccessExitCodes(intent) {
10
- const value = intent.success_exit_codes;
11
- if (!Array.isArray(value) || value.length === 0 || value.some((entry) => !Number.isInteger(entry))) {
12
- return [0];
13
- }
14
- return value.map(Number);
11
+ return normalizeSuccessExitCodes(intent.success_exit_codes);
15
12
  }
16
13
  function readBoolean(intent, key) {
17
14
  const value = intent[key];
@@ -10,7 +10,10 @@ const CHECK_ISSUE_ID_RULES = [
10
10
  ['mustflow.command_contract.executable_source_missing', /^Configured intent [^\s]+ must define argv or mode = "shell" with cmd$/u],
11
11
  ['mustflow.command_contract.shell_background_pattern', /^Shell intent [^\s]+ contains a blocked long-running or background pattern$/u],
12
12
  ['mustflow.command_contract.long_running_command_pattern', /^Intent [^\s]+ contains a blocked long-running or background command pattern$/u],
13
- ['mustflow.command_contract.success_exit_codes_invalid', /^\[commands\.intents\.[^\]]+\]\.success_exit_codes must be an integer array$/u],
13
+ [
14
+ 'mustflow.command_contract.success_exit_codes_invalid',
15
+ /^\[commands\.intents\.[^\]]+\]\.success_exit_codes must be a non-empty integer array with values from 0 through 255$/u,
16
+ ],
14
17
  ['mustflow.command_contract.effects_invalid', /^(?:Strict: )?(?:\[commands\.(?:resources|intents\.[^\]]+\.effects)[^\]]*\]|Command effect for intent [^\s]+ must define path, paths, or lock)/u],
15
18
  ['mustflow.command_contract.effect_path_escape', /^Strict: Command effect path must stay inside the current root:/u],
16
19
  ['mustflow.command_contract.shared_writes_without_effects', /^Strict warning: configured agent-runnable intents .+ share path:.+ through writes without explicit effects or resource locks$/u],
@@ -11,9 +11,15 @@ const INTERPRETER_EVALUATION_FLAGS = new Map([
11
11
  ['ruby', new Set(['-e'])],
12
12
  ['perl', new Set(['-e'])],
13
13
  ]);
14
+ const INTERPRETER_LONG_RUNNING_MODULES = new Map([
15
+ ['python', new Set(['http.server', 'simplehttpserver'])],
16
+ ['python3', new Set(['http.server', 'simplehttpserver'])],
17
+ ['py', new Set(['http.server', 'simplehttpserver'])],
18
+ ]);
14
19
  const PACKAGE_SCRIPT_RUNNERS = new Set(['bun', 'npm', 'pnpm', 'yarn']);
15
20
  const LONG_RUNNING_PACKAGE_SCRIPTS = new Set(['dev', 'start', 'serve', 'watch', 'preview']);
16
21
  const LONG_RUNNING_EXECUTABLES = new Set(['nodemon', 'pm2', 'serve', 'http-server', 'live-server', 'webpack-dev-server']);
22
+ const PACKAGE_EXEC_RUNNERS = new Set(['npx', 'bunx']);
17
23
  const ATTACHED_EVALUATION_FLAGS = new Set(['-command', '-commandwithargs']);
18
24
  export const BACKGROUND_SHELL_PATTERNS = [
19
25
  /(?:^|[^&])&(?!&)\s*$/u,
@@ -28,6 +34,12 @@ export const BACKGROUND_SHELL_PATTERNS = [
28
34
  ];
29
35
  export const LONG_RUNNING_COMMAND_TEXT_PATTERNS = [
30
36
  /\b(?:npm|pnpm|bun|yarn)\s+(?:run\s+)?(?:dev|start|serve|watch|preview)\b/iu,
37
+ /\b(?:npx|bunx)\s+(?:-[^\s]+\s+)*(?:vite|nodemon|pm2|serve|http-server|live-server|webpack-dev-server)\b/iu,
38
+ /\b(?:npm|pnpm|yarn)\s+(?:exec|x|dlx)\s+(?:-[^\s]+\s+)*(?:vite|nodemon|pm2|serve|http-server|live-server|webpack-dev-server)\b/iu,
39
+ /\bnext\s+(?:dev|start)\b/iu,
40
+ /\bturbo\s+dev\b/iu,
41
+ /\btsx\s+(?:watch|--watch|-w)\b/iu,
42
+ /\b(?:python|python3|py)\s+-m\s+(?:http\.server|SimpleHTTPServer)\b/u,
31
43
  /\b(?:nohup|disown)\b/iu,
32
44
  /(?:^|[^&])&(?!&)\s*$/u,
33
45
  /\bsetInterval\s*\(/u,
@@ -85,22 +97,35 @@ function readPackageScriptName(command, args) {
85
97
  }
86
98
  return null;
87
99
  }
88
- function argvHasBlockedLongRunningPattern(argv) {
89
- const [rawCommand = '', ...args] = argv;
90
- const command = normalizeExecutableName(rawCommand);
91
- const shellPayload = SHELL_WRAPPER_COMMANDS.has(command) ? findFlagPayload(argv, SHELL_EVALUATION_FLAGS) : null;
92
- if (shellPayload && (shellCommandHasBlockedBackgroundPattern(shellPayload) || commandTextHasLongRunningPattern(shellPayload))) {
93
- return `shell wrapper payload contains a blocked long-running or background pattern: ${shellPayload}`;
100
+ function findFirstNonOptionArgument(args) {
101
+ for (let index = 0; index < args.length; index += 1) {
102
+ const argument = args[index] ?? '';
103
+ if (argument === '--') {
104
+ const value = args[index + 1];
105
+ return value ? { value, index: index + 1 } : null;
106
+ }
107
+ if (argument.length > 0 && !argument.startsWith('-')) {
108
+ return { value: argument, index };
109
+ }
94
110
  }
95
- const interpreterFlags = INTERPRETER_EVALUATION_FLAGS.get(command);
96
- const interpreterPayload = interpreterFlags ? findFlagPayload(argv, interpreterFlags) : null;
97
- if (interpreterPayload && commandTextHasLongRunningPattern(interpreterPayload)) {
98
- return `interpreter evaluation payload contains a blocked long-running pattern: ${interpreterPayload}`;
111
+ return null;
112
+ }
113
+ function readPackageExecCommand(command, args) {
114
+ if (PACKAGE_EXEC_RUNNERS.has(command)) {
115
+ const target = findFirstNonOptionArgument(args);
116
+ return target ? { command: normalizeExecutableName(target.value), args: args.slice(target.index + 1) } : null;
99
117
  }
100
- const packageScriptName = readPackageScriptName(command, args);
101
- if (packageScriptName && LONG_RUNNING_PACKAGE_SCRIPTS.has(packageScriptName)) {
102
- return `package-manager script "${packageScriptName}" is commonly long-running`;
118
+ if (command === 'npm' && ['exec', 'x'].includes(args[0] ?? '')) {
119
+ const target = findFirstNonOptionArgument(args.slice(1));
120
+ return target ? { command: normalizeExecutableName(target.value), args: args.slice(target.index + 2) } : null;
121
+ }
122
+ if ((command === 'pnpm' || command === 'yarn') && ['exec', 'dlx'].includes(args[0] ?? '')) {
123
+ const target = findFirstNonOptionArgument(args.slice(1));
124
+ return target ? { command: normalizeExecutableName(target.value), args: args.slice(target.index + 2) } : null;
103
125
  }
126
+ return null;
127
+ }
128
+ function longRunningExecutableDetail(command, args) {
104
129
  if (LONG_RUNNING_EXECUTABLES.has(command)) {
105
130
  return `executable "${command}" is commonly long-running`;
106
131
  }
@@ -116,8 +141,45 @@ function argvHasBlockedLongRunningPattern(argv) {
116
141
  if (command === 'tsc' && (args.includes('--watch') || args.includes('-w'))) {
117
142
  return 'tsc watch mode is long-running';
118
143
  }
144
+ if (command === 'tsx' && ['watch', '--watch', '-w'].includes(args[0] ?? '')) {
145
+ return `tsx ${args[0]} is commonly long-running`;
146
+ }
147
+ if (command === 'turbo' && args[0] === 'dev') {
148
+ return 'turbo dev is commonly long-running';
149
+ }
119
150
  return null;
120
151
  }
152
+ function argvHasBlockedLongRunningPattern(argv) {
153
+ const [rawCommand = '', ...args] = argv;
154
+ const command = normalizeExecutableName(rawCommand);
155
+ const shellPayload = SHELL_WRAPPER_COMMANDS.has(command) ? findFlagPayload(argv, SHELL_EVALUATION_FLAGS) : null;
156
+ if (shellPayload && (shellCommandHasBlockedBackgroundPattern(shellPayload) || commandTextHasLongRunningPattern(shellPayload))) {
157
+ return `shell wrapper payload contains a blocked long-running or background pattern: ${shellPayload}`;
158
+ }
159
+ const interpreterFlags = INTERPRETER_EVALUATION_FLAGS.get(command);
160
+ const interpreterPayload = interpreterFlags ? findFlagPayload(argv, interpreterFlags) : null;
161
+ if (interpreterPayload && commandTextHasLongRunningPattern(interpreterPayload)) {
162
+ return `interpreter evaluation payload contains a blocked long-running pattern: ${interpreterPayload}`;
163
+ }
164
+ const interpreterModules = INTERPRETER_LONG_RUNNING_MODULES.get(command);
165
+ const moduleFlagIndex = args.indexOf('-m');
166
+ const moduleName = moduleFlagIndex >= 0 ? args[moduleFlagIndex + 1]?.toLowerCase() : null;
167
+ if (moduleName && interpreterModules?.has(moduleName)) {
168
+ return `interpreter module "${moduleName}" is commonly long-running`;
169
+ }
170
+ const packageScriptName = readPackageScriptName(command, args);
171
+ if (packageScriptName && LONG_RUNNING_PACKAGE_SCRIPTS.has(packageScriptName)) {
172
+ return `package-manager script "${packageScriptName}" is commonly long-running`;
173
+ }
174
+ const packageExecCommand = readPackageExecCommand(command, args);
175
+ if (packageExecCommand) {
176
+ const detail = longRunningExecutableDetail(packageExecCommand.command, packageExecCommand.args);
177
+ if (detail) {
178
+ return `package-manager exec target ${detail}`;
179
+ }
180
+ }
181
+ return longRunningExecutableDetail(command, args);
182
+ }
121
183
  export function commandIntentBlockedCommandPattern(intent) {
122
184
  if (intent.mode === 'shell' && typeof intent.cmd === 'string' && shellCommandHasBlockedBackgroundPattern(intent.cmd)) {
123
185
  return {
@@ -5,6 +5,7 @@ import { COMMAND_ENV_POLICIES, DEFAULT_COMMAND_ENV_POLICY } from './command-env.
5
5
  import { COMMAND_EFFECT_CONCURRENCY, COMMAND_EFFECT_MODES, COMMAND_EFFECT_TYPES, validateCommandEffectLockWarnings, validateCommandEffects, } from './command-effects.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
+ import { SUCCESS_EXIT_CODES_CONTRACT_DESCRIPTION, successExitCodesAreValid } from './success-exit-codes.js';
8
9
  function commandContractIssue(message) {
9
10
  return { message };
10
11
  }
@@ -197,9 +198,8 @@ function validateCommandIntent(intentName, intent, issues) {
197
198
  issues.push(commandContractIssue(`Intent ${intentName} contains a blocked long-running or background command pattern`));
198
199
  }
199
200
  if (hasOwn(intent, 'success_exit_codes')) {
200
- const value = intent.success_exit_codes;
201
- if (!Array.isArray(value) || value.length === 0 || value.some((entry) => !Number.isInteger(entry))) {
202
- issues.push(commandContractIssue(`[commands.intents.${intentName}].success_exit_codes must be an integer array`));
201
+ if (!successExitCodesAreValid(intent.success_exit_codes)) {
202
+ issues.push(commandContractIssue(`[commands.intents.${intentName}].success_exit_codes must be ${SUCCESS_EXIT_CODES_CONTRACT_DESCRIPTION}`));
203
203
  }
204
204
  }
205
205
  validateCommandIntentEffects(intentName, intent, issues);
@@ -13,8 +13,11 @@ function normalizeRelativePath(rawPath) {
13
13
  function pathLockKey(relativePath) {
14
14
  return `path:${normalizeRelativePath(relativePath)}`;
15
15
  }
16
- function validateEffectPath(projectRoot, intent, rawPath) {
17
- const cwd = resolveSafeProjectCwd(projectRoot, readString(intent, 'cwd'));
16
+ function readEffectiveCommandCwd(commandContract, intent) {
17
+ return readString(intent, 'cwd') ?? readString(commandContract.defaults, 'default_cwd') ?? '.';
18
+ }
19
+ function validateEffectPath(projectRoot, commandContract, intent, rawPath) {
20
+ const cwd = resolveSafeProjectCwd(projectRoot, readEffectiveCommandCwd(commandContract, intent));
18
21
  const resolved = path.resolve(cwd, rawPath);
19
22
  const root = path.resolve(projectRoot);
20
23
  const relative = path.relative(root, resolved);
@@ -83,7 +86,7 @@ function normalizeDeclaredEffect(projectRoot, commandContract, intentName, inten
83
86
  ];
84
87
  }
85
88
  return paths.map((rawPath) => {
86
- const normalizedPath = validateEffectPath(projectRoot, intent, rawPath);
89
+ const normalizedPath = validateEffectPath(projectRoot, commandContract, intent, rawPath);
87
90
  return {
88
91
  intent: intentName,
89
92
  source: 'effects',
@@ -95,10 +98,10 @@ function normalizeDeclaredEffect(projectRoot, commandContract, intentName, inten
95
98
  };
96
99
  });
97
100
  }
98
- function normalizeWritesEffect(projectRoot, intentName, intent) {
101
+ function normalizeWritesEffect(projectRoot, commandContract, intentName, intent) {
99
102
  const writes = readStringArray(intent, 'writes') ?? [];
100
103
  return writes.map((rawPath) => {
101
- const normalizedPath = validateEffectPath(projectRoot, intent, rawPath);
104
+ const normalizedPath = validateEffectPath(projectRoot, commandContract, intent, rawPath);
102
105
  return {
103
106
  intent: intentName,
104
107
  source: 'writes',
@@ -119,7 +122,7 @@ export function normalizeCommandEffects(projectRoot, commandContract, intentName
119
122
  if (Array.isArray(rawEffects) && rawEffects.length > 0) {
120
123
  return rawEffects.flatMap((effect) => isRecord(effect) ? normalizeDeclaredEffect(projectRoot, commandContract, intentName, intent, effect) : []);
121
124
  }
122
- return normalizeWritesEffect(projectRoot, intentName, intent);
125
+ return normalizeWritesEffect(projectRoot, commandContract, intentName, intent);
123
126
  }
124
127
  export function commandEffectsConflict(left, right) {
125
128
  if (left.lock !== right.lock) {
@@ -7,6 +7,7 @@ import { MAX_COMMAND_OUTPUT_BYTES } from './command-output-limits.js';
7
7
  import { commandEffectsConflict, normalizeCommandEffects } from './command-effects.js';
8
8
  import { listChangeClassificationValidationReasons } from './change-classification.js';
9
9
  import { parseSkillIndexRoutes } from './skill-route-alignment.js';
10
+ import { SUCCESS_EXIT_CODES_CONTRACT_DESCRIPTION, successExitCodesAreValid as successExitCodeValuesAreValid, } from './success-exit-codes.js';
10
11
  const CONTRACT_LINT_SOURCE_FILES = [
11
12
  '.mustflow/config/commands.toml',
12
13
  '.mustflow/docs/agent-workflow.md',
@@ -65,7 +66,7 @@ function readBoolean(intent, key) {
65
66
  }
66
67
  function successExitCodesAreValid(intent) {
67
68
  const value = intent.success_exit_codes;
68
- return value === undefined || (Array.isArray(value) && value.every((entry) => Number.isInteger(entry)));
69
+ return value === undefined || successExitCodeValuesAreValid(value);
69
70
  }
70
71
  function writesAreValid(intent) {
71
72
  const value = intent.writes;
@@ -331,7 +332,7 @@ function lintIntent(name, value, issues) {
331
332
  pushIssue(issues, 'error', 'long_running_command_pattern', name, `Intent ${name} contains a blocked long-running or background command pattern.`);
332
333
  }
333
334
  if (!successExitCodesAreValid(value)) {
334
- pushIssue(issues, 'error', 'invalid_success_exit_codes', name, `Intent ${name} success_exit_codes must be an integer array.`);
335
+ pushIssue(issues, 'error', 'invalid_success_exit_codes', name, `Intent ${name} success_exit_codes must be ${SUCCESS_EXIT_CODES_CONTRACT_DESCRIPTION}.`);
335
336
  }
336
337
  if (!writesAreValid(value)) {
337
338
  pushIssue(issues, 'error', 'invalid_writes', name, `Intent ${name} writes must be a string array.`);
@@ -7,7 +7,7 @@ const MAX_SNAPSHOT_FILES = 20_000;
7
7
  const MAX_REPORTED_PATHS = 200;
8
8
  const GIT_STATUS_TIMEOUT_MS = 10_000;
9
9
  const GIT_STATUS_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
10
- const GIT_STATUS_UNTRACKED_MODE = 'normal';
10
+ const GIT_STATUS_UNTRACKED_MODE = 'all';
11
11
  const MAX_HASH_BYTES = 5 * 1024 * 1024;
12
12
  const RECURSIVE_SNAPSHOT_ENV = 'MUSTFLOW_WRITE_DRIFT_SNAPSHOT';
13
13
  const EXCLUDED_DIRECTORY_NAMES = new Set(['.git', 'node_modules']);
@@ -150,7 +150,7 @@ function captureGitStatusSnapshot(projectRoot) {
150
150
  return {
151
151
  status: 'partial',
152
152
  entries,
153
- reason: 'git_status_untracked_files_normal',
153
+ reason: 'git_status_untracked_files_all',
154
154
  source: 'git_status',
155
155
  };
156
156
  }
@@ -0,0 +1,18 @@
1
+ export const MIN_SUCCESS_EXIT_CODE = 0;
2
+ export const MAX_SUCCESS_EXIT_CODE = 255;
3
+ export const SUCCESS_EXIT_CODES_CONTRACT_DESCRIPTION = `a non-empty integer array with values from ${MIN_SUCCESS_EXIT_CODE} through ${MAX_SUCCESS_EXIT_CODE}`;
4
+ export function successExitCodeIsValid(value) {
5
+ return (typeof value === 'number' &&
6
+ Number.isInteger(value) &&
7
+ value >= MIN_SUCCESS_EXIT_CODE &&
8
+ value <= MAX_SUCCESS_EXIT_CODE);
9
+ }
10
+ export function successExitCodesAreValid(value) {
11
+ return Array.isArray(value) && value.length > 0 && value.every(successExitCodeIsValid);
12
+ }
13
+ export function normalizeSuccessExitCodes(value) {
14
+ if (!successExitCodesAreValid(value)) {
15
+ return [0];
16
+ }
17
+ return [...new Set(value.map(Number))].sort((left, right) => left - right);
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.22.1",
3
+ "version": "2.22.2",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
@@ -1,6 +1,6 @@
1
1
  id = "default"
2
2
  name = "default"
3
- version = "2.22.1"
3
+ version = "2.22.2"
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"