mustflow 2.22.1 → 2.22.3
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/dist/cli/commands/run/process-tree.js +1 -1
- package/dist/cli/commands/run.js +12 -4
- package/dist/cli/commands/verify.js +2 -0
- package/dist/cli/lib/run-plan.js +2 -5
- package/dist/core/check-issues.js +4 -1
- package/dist/core/command-contract-rules.js +126 -13
- package/dist/core/command-contract-validation.js +3 -3
- package/dist/core/command-effects.js +9 -6
- package/dist/core/contract-lint.js +3 -2
- package/dist/core/run-write-drift.js +2 -2
- package/dist/core/success-exit-codes.js +18 -0
- package/package.json +1 -1
- package/templates/default/manifest.toml +1 -1
|
@@ -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' ? '
|
|
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 {
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
225
|
-
|
|
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
|
});
|
package/dist/cli/lib/run-plan.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
[
|
|
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],
|
|
@@ -10,10 +10,17 @@ const INTERPRETER_EVALUATION_FLAGS = new Map([
|
|
|
10
10
|
['py', new Set(['-c'])],
|
|
11
11
|
['ruby', new Set(['-e'])],
|
|
12
12
|
['perl', new Set(['-e'])],
|
|
13
|
+
['julia', new Set(['-e', '--eval'])],
|
|
14
|
+
]);
|
|
15
|
+
const INTERPRETER_LONG_RUNNING_MODULES = new Map([
|
|
16
|
+
['python', new Set(['http.server', 'simplehttpserver'])],
|
|
17
|
+
['python3', new Set(['http.server', 'simplehttpserver'])],
|
|
18
|
+
['py', new Set(['http.server', 'simplehttpserver'])],
|
|
13
19
|
]);
|
|
14
20
|
const PACKAGE_SCRIPT_RUNNERS = new Set(['bun', 'npm', 'pnpm', 'yarn']);
|
|
15
21
|
const LONG_RUNNING_PACKAGE_SCRIPTS = new Set(['dev', 'start', 'serve', 'watch', 'preview']);
|
|
16
22
|
const LONG_RUNNING_EXECUTABLES = new Set(['nodemon', 'pm2', 'serve', 'http-server', 'live-server', 'webpack-dev-server']);
|
|
23
|
+
const PACKAGE_EXEC_RUNNERS = new Set(['npx', 'bunx']);
|
|
17
24
|
const ATTACHED_EVALUATION_FLAGS = new Set(['-command', '-commandwithargs']);
|
|
18
25
|
export const BACKGROUND_SHELL_PATTERNS = [
|
|
19
26
|
/(?:^|[^&])&(?!&)\s*$/u,
|
|
@@ -28,6 +35,20 @@ export const BACKGROUND_SHELL_PATTERNS = [
|
|
|
28
35
|
];
|
|
29
36
|
export const LONG_RUNNING_COMMAND_TEXT_PATTERNS = [
|
|
30
37
|
/\b(?:npm|pnpm|bun|yarn)\s+(?:run\s+)?(?:dev|start|serve|watch|preview)\b/iu,
|
|
38
|
+
/\b(?:npx|bunx)\s+(?:-[^\s]+\s+)*(?:vite|nodemon|pm2|serve|http-server|live-server|webpack-dev-server)\b/iu,
|
|
39
|
+
/\b(?:npm|pnpm|yarn)\s+(?:exec|x|dlx)\s+(?:-[^\s]+\s+)*(?:vite|nodemon|pm2|serve|http-server|live-server|webpack-dev-server)\b/iu,
|
|
40
|
+
/\bnext\s+(?:dev|start)\b/iu,
|
|
41
|
+
/\bturbo\s+dev\b/iu,
|
|
42
|
+
/\btsx\s+(?:watch|--watch|-w)\b/iu,
|
|
43
|
+
/\bcargo\s+(?:watch|tauri\s+dev)\b/iu,
|
|
44
|
+
/\bzig\s+build\b(?=.*(?:--watch|-w|\bwatch\b))/iu,
|
|
45
|
+
/\btauri\s+dev\b/iu,
|
|
46
|
+
/\bgh\s+(?:run\s+watch|codespace\s+ssh|codespace\s+logs\b.*(?:--follow|-f))\b/iu,
|
|
47
|
+
/\bdeno\s+(?:serve|task\s+(?:dev|start|serve|watch|preview)|run\b.*(?:--watch|-w))\b/iu,
|
|
48
|
+
/\bflutter\s+(?:run|attach|logs)\b/iu,
|
|
49
|
+
/\bdart\s+run\s+build_runner\s+watch\b/iu,
|
|
50
|
+
/\bgo\s+run\s+(?:(?:github\.com\/(?:air-verse|cosmtrek)\/air)(?:\/v\d+)?|.*\bair\b)/iu,
|
|
51
|
+
/\b(?:python|python3|py)\s+-m\s+(?:http\.server|SimpleHTTPServer)\b/u,
|
|
31
52
|
/\b(?:nohup|disown)\b/iu,
|
|
32
53
|
/(?:^|[^&])&(?!&)\s*$/u,
|
|
33
54
|
/\bsetInterval\s*\(/u,
|
|
@@ -85,22 +106,38 @@ function readPackageScriptName(command, args) {
|
|
|
85
106
|
}
|
|
86
107
|
return null;
|
|
87
108
|
}
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
109
|
+
function findFirstNonOptionArgument(args) {
|
|
110
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
111
|
+
const argument = args[index] ?? '';
|
|
112
|
+
if (argument === '--') {
|
|
113
|
+
const value = args[index + 1];
|
|
114
|
+
return value ? { value, index: index + 1 } : null;
|
|
115
|
+
}
|
|
116
|
+
if (argument.length > 0 && !argument.startsWith('-')) {
|
|
117
|
+
return { value: argument, index };
|
|
118
|
+
}
|
|
94
119
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function readPackageExecCommand(command, args) {
|
|
123
|
+
if (PACKAGE_EXEC_RUNNERS.has(command)) {
|
|
124
|
+
const target = findFirstNonOptionArgument(args);
|
|
125
|
+
return target ? { command: normalizeExecutableName(target.value), args: args.slice(target.index + 1) } : null;
|
|
99
126
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return
|
|
127
|
+
if (command === 'npm' && ['exec', 'x'].includes(args[0] ?? '')) {
|
|
128
|
+
const target = findFirstNonOptionArgument(args.slice(1));
|
|
129
|
+
return target ? { command: normalizeExecutableName(target.value), args: args.slice(target.index + 2) } : null;
|
|
130
|
+
}
|
|
131
|
+
if ((command === 'pnpm' || command === 'yarn') && ['exec', 'dlx'].includes(args[0] ?? '')) {
|
|
132
|
+
const target = findFirstNonOptionArgument(args.slice(1));
|
|
133
|
+
return target ? { command: normalizeExecutableName(target.value), args: args.slice(target.index + 2) } : null;
|
|
103
134
|
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
function hasWatchFlag(args) {
|
|
138
|
+
return args.some((arg) => arg === '--watch' || arg === '-w' || arg === 'watch' || arg.startsWith('--watch='));
|
|
139
|
+
}
|
|
140
|
+
function longRunningExecutableDetail(command, args) {
|
|
104
141
|
if (LONG_RUNNING_EXECUTABLES.has(command)) {
|
|
105
142
|
return `executable "${command}" is commonly long-running`;
|
|
106
143
|
}
|
|
@@ -116,8 +153,84 @@ function argvHasBlockedLongRunningPattern(argv) {
|
|
|
116
153
|
if (command === 'tsc' && (args.includes('--watch') || args.includes('-w'))) {
|
|
117
154
|
return 'tsc watch mode is long-running';
|
|
118
155
|
}
|
|
156
|
+
if (command === 'tsx' && ['watch', '--watch', '-w'].includes(args[0] ?? '')) {
|
|
157
|
+
return `tsx ${args[0]} is commonly long-running`;
|
|
158
|
+
}
|
|
159
|
+
if (command === 'turbo' && args[0] === 'dev') {
|
|
160
|
+
return 'turbo dev is commonly long-running';
|
|
161
|
+
}
|
|
162
|
+
if (command === 'cargo' && args[0] === 'watch') {
|
|
163
|
+
return 'cargo watch is commonly long-running';
|
|
164
|
+
}
|
|
165
|
+
if (command === 'cargo' && args[0] === 'tauri' && args[1] === 'dev') {
|
|
166
|
+
return 'cargo tauri dev is commonly long-running';
|
|
167
|
+
}
|
|
168
|
+
if (command === 'zig' && args[0] === 'build' && hasWatchFlag(args.slice(1))) {
|
|
169
|
+
return 'zig build watch mode is commonly long-running';
|
|
170
|
+
}
|
|
171
|
+
if (command === 'tauri' && args[0] === 'dev') {
|
|
172
|
+
return 'tauri dev is commonly long-running';
|
|
173
|
+
}
|
|
174
|
+
if (command === 'gh' && args[0] === 'run' && args[1] === 'watch') {
|
|
175
|
+
return 'gh run watch is commonly long-running';
|
|
176
|
+
}
|
|
177
|
+
if (command === 'gh' && args[0] === 'codespace' && args[1] === 'ssh') {
|
|
178
|
+
return 'gh codespace ssh is interactive and commonly long-running';
|
|
179
|
+
}
|
|
180
|
+
if (command === 'gh' && args[0] === 'codespace' && args[1] === 'logs' && (args.includes('--follow') || args.includes('-f'))) {
|
|
181
|
+
return 'gh codespace logs follow mode is commonly long-running';
|
|
182
|
+
}
|
|
183
|
+
if (command === 'deno' && args[0] === 'task' && args[1] && LONG_RUNNING_PACKAGE_SCRIPTS.has(args[1])) {
|
|
184
|
+
return `deno task ${args[1]} is commonly long-running`;
|
|
185
|
+
}
|
|
186
|
+
if (command === 'deno' && args[0] === 'serve') {
|
|
187
|
+
return 'deno serve is commonly long-running';
|
|
188
|
+
}
|
|
189
|
+
if (command === 'deno' && args[0] === 'run' && hasWatchFlag(args.slice(1))) {
|
|
190
|
+
return 'deno run watch mode is commonly long-running';
|
|
191
|
+
}
|
|
192
|
+
if (command === 'flutter' && ['run', 'attach', 'logs'].includes(args[0] ?? '')) {
|
|
193
|
+
return `flutter ${args[0]} is commonly long-running`;
|
|
194
|
+
}
|
|
195
|
+
if (command === 'dart' && args[0] === 'run' && args[1] === 'build_runner' && args[2] === 'watch') {
|
|
196
|
+
return 'dart build_runner watch is commonly long-running';
|
|
197
|
+
}
|
|
198
|
+
if (command === 'go' && args[0] === 'run' && args.some((arg) => /(?:^|\/)(?:air)(?:$|@)/iu.test(arg))) {
|
|
199
|
+
return 'go run air is commonly long-running';
|
|
200
|
+
}
|
|
119
201
|
return null;
|
|
120
202
|
}
|
|
203
|
+
function argvHasBlockedLongRunningPattern(argv) {
|
|
204
|
+
const [rawCommand = '', ...args] = argv;
|
|
205
|
+
const command = normalizeExecutableName(rawCommand);
|
|
206
|
+
const shellPayload = SHELL_WRAPPER_COMMANDS.has(command) ? findFlagPayload(argv, SHELL_EVALUATION_FLAGS) : null;
|
|
207
|
+
if (shellPayload && (shellCommandHasBlockedBackgroundPattern(shellPayload) || commandTextHasLongRunningPattern(shellPayload))) {
|
|
208
|
+
return `shell wrapper payload contains a blocked long-running or background pattern: ${shellPayload}`;
|
|
209
|
+
}
|
|
210
|
+
const interpreterFlags = INTERPRETER_EVALUATION_FLAGS.get(command);
|
|
211
|
+
const interpreterPayload = interpreterFlags ? findFlagPayload(argv, interpreterFlags) : null;
|
|
212
|
+
if (interpreterPayload && commandTextHasLongRunningPattern(interpreterPayload)) {
|
|
213
|
+
return `interpreter evaluation payload contains a blocked long-running pattern: ${interpreterPayload}`;
|
|
214
|
+
}
|
|
215
|
+
const interpreterModules = INTERPRETER_LONG_RUNNING_MODULES.get(command);
|
|
216
|
+
const moduleFlagIndex = args.indexOf('-m');
|
|
217
|
+
const moduleName = moduleFlagIndex >= 0 ? args[moduleFlagIndex + 1]?.toLowerCase() : null;
|
|
218
|
+
if (moduleName && interpreterModules?.has(moduleName)) {
|
|
219
|
+
return `interpreter module "${moduleName}" is commonly long-running`;
|
|
220
|
+
}
|
|
221
|
+
const packageScriptName = readPackageScriptName(command, args);
|
|
222
|
+
if (packageScriptName && LONG_RUNNING_PACKAGE_SCRIPTS.has(packageScriptName)) {
|
|
223
|
+
return `package-manager script "${packageScriptName}" is commonly long-running`;
|
|
224
|
+
}
|
|
225
|
+
const packageExecCommand = readPackageExecCommand(command, args);
|
|
226
|
+
if (packageExecCommand) {
|
|
227
|
+
const detail = longRunningExecutableDetail(packageExecCommand.command, packageExecCommand.args);
|
|
228
|
+
if (detail) {
|
|
229
|
+
return `package-manager exec target ${detail}`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return longRunningExecutableDetail(command, args);
|
|
233
|
+
}
|
|
121
234
|
export function commandIntentBlockedCommandPattern(intent) {
|
|
122
235
|
if (intent.mode === 'shell' && typeof intent.cmd === 'string' && shellCommandHasBlockedBackgroundPattern(intent.cmd)) {
|
|
123
236
|
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
|
-
|
|
201
|
-
|
|
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
|
|
17
|
-
|
|
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 || (
|
|
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
|
|
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 = '
|
|
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: '
|
|
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