mustflow 2.22.13 → 2.22.14
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 +1 -1
- package/dist/cli/commands/explain-verify.js +8 -1
- package/dist/cli/commands/explain.js +269 -2
- package/dist/cli/commands/run.js +109 -77
- package/dist/cli/commands/verify.js +16 -1
- package/dist/cli/lib/run-plan.js +31 -5
- package/dist/core/active-run-locks.js +294 -0
- package/dist/core/check-issues.js +8 -0
- package/dist/core/command-contract-validation.js +179 -2
- package/dist/core/command-explanation.js +5 -3
- package/dist/core/command-preconditions.js +261 -0
- package/dist/core/skill-route-explanation.js +115 -9
- package/package.json +1 -1
- package/schemas/README.md +6 -3
- package/schemas/change-verification-report.schema.json +52 -0
- package/schemas/commands.schema.json +41 -0
- package/schemas/explain-report.schema.json +152 -4
- package/templates/default/manifest.toml +1 -1
package/README.md
CHANGED
|
@@ -275,7 +275,7 @@ mf run mustflow_update_apply
|
|
|
275
275
|
| `mf verify --reason <event>` | Run configured verification intents selected by `required_after` metadata. |
|
|
276
276
|
| `mf verify --reason <event> --plan-only --json` | Print the required verification plan without running commands. |
|
|
277
277
|
| `mf explain authority [path]` | Explain managed Markdown authority decisions without modifying files. |
|
|
278
|
-
| `mf explain skill <skill_id>` | Explain the trigger, scope, risk, checks,
|
|
278
|
+
| `mf explain skill <skill_id>` | Explain the trigger, scope, risk, checks, output contract, and selection evidence for one skill route. |
|
|
279
279
|
| `mf explain skills` | Explain the strict skill index/body alignment summary used by `mf doctor --strict`. |
|
|
280
280
|
| `mf explain surface [path]` | Explain how a path maps to public-surface and validation categories. |
|
|
281
281
|
|
|
@@ -107,7 +107,7 @@ export async function getVerifyExplainOutput(schemaVersion, projectRoot, reasons
|
|
|
107
107
|
const readModel = await readLatestLocalVerificationReadModelQueries(projectRoot);
|
|
108
108
|
const requirements = plans.map((plan) => {
|
|
109
109
|
const candidates = plan.candidates.map((candidate) => {
|
|
110
|
-
const command = candidate.intent ? explainCommandIntent(contract, candidate.intent).intent : null;
|
|
110
|
+
const command = candidate.intent ? explainCommandIntent(contract, candidate.intent, { projectRoot }).intent : null;
|
|
111
111
|
return {
|
|
112
112
|
intent: candidate.intent.length > 0 ? candidate.intent : null,
|
|
113
113
|
status: candidate.status,
|
|
@@ -211,6 +211,13 @@ export function renderVerifyExplainDecision(decision, lang) {
|
|
|
211
211
|
if (candidate.requiredAfter.length > 0) {
|
|
212
212
|
lines.push(` required_after: ${candidate.requiredAfter.join(', ')}`);
|
|
213
213
|
}
|
|
214
|
+
if (candidate.command && candidate.command.preconditions.length > 0) {
|
|
215
|
+
lines.push(` preconditions: ${candidate.command.preconditions.length}`);
|
|
216
|
+
for (const precondition of candidate.command.preconditions) {
|
|
217
|
+
const target = precondition.path ?? precondition.artifact ?? precondition.label ?? precondition.kind;
|
|
218
|
+
lines.push(` - ${precondition.kind} ${target}: ${precondition.status}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
214
221
|
if (candidate.effectGraph) {
|
|
215
222
|
lines.push(` effect_graph: ${candidate.effectGraph.status}`, ` index_fresh: ${candidate.effectGraph.indexFresh ? t(lang, 'value.yes') : t(lang, 'value.no')}`);
|
|
216
223
|
if (candidate.effectGraph.refreshHint) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
2
4
|
import { t } from '../lib/i18n.js';
|
|
3
5
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
@@ -13,9 +15,10 @@ import { checkMustflowProject } from '../lib/validation.js';
|
|
|
13
15
|
import { readLocalCommandEffectGraph, readLocalPathSurface, } from '../lib/local-index.js';
|
|
14
16
|
import { explainVerifyArgErrorMessage, explainVerifyPlanErrorMessage, getVerifyExplainOutput, parseExplainVerifyArgs, readExplainVerifyPlanReasons, renderVerifyExplainDecision, } from './explain-verify.js';
|
|
15
17
|
const EXPLAIN_SCHEMA_VERSION = '1';
|
|
18
|
+
const LATEST_RUN_RECEIPT_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
|
|
16
19
|
export function getExplainHelp(lang = 'en') {
|
|
17
20
|
return renderHelp({
|
|
18
|
-
usage: 'mf explain <topic> [target] [options] | mf explain verify --reason <event> [options] | mf explain
|
|
21
|
+
usage: 'mf explain <topic> [target] [options] | mf explain verify --reason <event> [options] | mf explain why <target> [options]',
|
|
19
22
|
summary: t(lang, 'explain.help.summary'),
|
|
20
23
|
options: [
|
|
21
24
|
{ label: '--json', description: t(lang, 'cli.option.json') },
|
|
@@ -34,6 +37,9 @@ export function getExplainHelp(lang = 'en') {
|
|
|
34
37
|
'mf explain retention --json',
|
|
35
38
|
'mf explain verify --reason code_change',
|
|
36
39
|
'mf explain verify --from-plan verify-plan.json --json',
|
|
40
|
+
'mf explain why command test --json',
|
|
41
|
+
'mf explain why verify --reason code_change --json',
|
|
42
|
+
'mf explain why latest-failure --json',
|
|
37
43
|
'mf explain skill code-review',
|
|
38
44
|
'mf explain skill mustflow.core.code-review --json',
|
|
39
45
|
'mf explain skills',
|
|
@@ -76,7 +82,7 @@ function getAnchorExplainOutput(projectRoot, anchorId) {
|
|
|
76
82
|
};
|
|
77
83
|
}
|
|
78
84
|
async function getCommandExplainOutput(projectRoot, commandName) {
|
|
79
|
-
const decision = explainCommandIntent(readCommandContract(projectRoot), commandName);
|
|
85
|
+
const decision = explainCommandIntent(readCommandContract(projectRoot), commandName, { projectRoot });
|
|
80
86
|
const effectGraph = decision.intent ? await readLocalCommandEffectGraph(projectRoot, commandName) : undefined;
|
|
81
87
|
return {
|
|
82
88
|
schema_version: EXPLAIN_SCHEMA_VERSION,
|
|
@@ -124,6 +130,237 @@ async function getSurfaceExplainOutput(projectRoot, pathArg) {
|
|
|
124
130
|
decision: { ...decision, readModel },
|
|
125
131
|
};
|
|
126
132
|
}
|
|
133
|
+
function isJsonRecord(value) {
|
|
134
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
135
|
+
}
|
|
136
|
+
function stringField(record, key) {
|
|
137
|
+
const value = record[key];
|
|
138
|
+
return typeof value === 'string' ? value : null;
|
|
139
|
+
}
|
|
140
|
+
function integerField(record, key) {
|
|
141
|
+
const value = record[key];
|
|
142
|
+
return typeof value === 'number' && Number.isInteger(value) ? value : null;
|
|
143
|
+
}
|
|
144
|
+
function recordField(record, key) {
|
|
145
|
+
const value = record[key];
|
|
146
|
+
return isJsonRecord(value) ? value : null;
|
|
147
|
+
}
|
|
148
|
+
function latestFailureDecision(latestFailure, decision, reason, effectiveAction) {
|
|
149
|
+
return {
|
|
150
|
+
kind: 'latest_failure',
|
|
151
|
+
decision,
|
|
152
|
+
reason,
|
|
153
|
+
effectiveAction,
|
|
154
|
+
countsAsMustflowVerification: false,
|
|
155
|
+
sourceFiles: [
|
|
156
|
+
LATEST_RUN_RECEIPT_RELATIVE_PATH,
|
|
157
|
+
'.mustflow/config/mustflow.toml',
|
|
158
|
+
'.mustflow/docs/agent-workflow.md',
|
|
159
|
+
],
|
|
160
|
+
latestFailure,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function getLatestFailureExplainOutput(projectRoot) {
|
|
164
|
+
const latestPath = path.join(projectRoot, ...LATEST_RUN_RECEIPT_RELATIVE_PATH.split('/'));
|
|
165
|
+
const sourcePath = LATEST_RUN_RECEIPT_RELATIVE_PATH;
|
|
166
|
+
if (!existsSync(latestPath)) {
|
|
167
|
+
return {
|
|
168
|
+
schema_version: EXPLAIN_SCHEMA_VERSION,
|
|
169
|
+
command: 'explain',
|
|
170
|
+
topic: 'why',
|
|
171
|
+
mustflow_root: projectRoot,
|
|
172
|
+
decision: latestFailureDecision({
|
|
173
|
+
path: sourcePath,
|
|
174
|
+
present: false,
|
|
175
|
+
valid: false,
|
|
176
|
+
failed: false,
|
|
177
|
+
status: null,
|
|
178
|
+
intent: null,
|
|
179
|
+
exitCode: null,
|
|
180
|
+
errorKind: null,
|
|
181
|
+
durationMs: null,
|
|
182
|
+
summary: 'No latest mf run receipt is available.',
|
|
183
|
+
}, 'latest run receipt is missing', 'mustflow has no bounded latest run metadata to explain.', 'Run a configured mf run or mf verify command before treating latest-run state as evidence.'),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
let parsed;
|
|
187
|
+
try {
|
|
188
|
+
parsed = JSON.parse(readFileSync(latestPath, 'utf8'));
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return {
|
|
192
|
+
schema_version: EXPLAIN_SCHEMA_VERSION,
|
|
193
|
+
command: 'explain',
|
|
194
|
+
topic: 'why',
|
|
195
|
+
mustflow_root: projectRoot,
|
|
196
|
+
decision: latestFailureDecision({
|
|
197
|
+
path: sourcePath,
|
|
198
|
+
present: true,
|
|
199
|
+
valid: false,
|
|
200
|
+
failed: false,
|
|
201
|
+
status: null,
|
|
202
|
+
intent: null,
|
|
203
|
+
exitCode: null,
|
|
204
|
+
errorKind: null,
|
|
205
|
+
durationMs: null,
|
|
206
|
+
summary: 'The latest mf run receipt is not valid JSON.',
|
|
207
|
+
}, 'latest run receipt is invalid', 'mustflow could not read bounded latest run metadata safely.', 'Ignore the latest-run state and rerun the intended configured command to create a fresh receipt.'),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (!isJsonRecord(parsed)) {
|
|
211
|
+
return {
|
|
212
|
+
schema_version: EXPLAIN_SCHEMA_VERSION,
|
|
213
|
+
command: 'explain',
|
|
214
|
+
topic: 'why',
|
|
215
|
+
mustflow_root: projectRoot,
|
|
216
|
+
decision: latestFailureDecision({
|
|
217
|
+
path: sourcePath,
|
|
218
|
+
present: true,
|
|
219
|
+
valid: false,
|
|
220
|
+
failed: false,
|
|
221
|
+
status: null,
|
|
222
|
+
intent: null,
|
|
223
|
+
exitCode: null,
|
|
224
|
+
errorKind: null,
|
|
225
|
+
durationMs: null,
|
|
226
|
+
summary: 'The latest mf run receipt is not a JSON object.',
|
|
227
|
+
}, 'latest run receipt is invalid', 'mustflow could not read bounded latest run metadata safely.', 'Ignore the latest-run state and rerun the intended configured command to create a fresh receipt.'),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const status = stringField(parsed, 'status');
|
|
231
|
+
const intent = stringField(parsed, 'intent');
|
|
232
|
+
const exitCode = integerField(parsed, 'exit_code');
|
|
233
|
+
const performance = recordField(parsed, 'performance');
|
|
234
|
+
const resultSummary = performance ? recordField(performance, 'result_summary') : null;
|
|
235
|
+
const errorKind = resultSummary ? stringField(resultSummary, 'error_kind') : null;
|
|
236
|
+
const durationMs = integerField(parsed, 'duration_ms') ?? (performance ? integerField(performance, 'duration_ms') : null);
|
|
237
|
+
if (!status) {
|
|
238
|
+
return {
|
|
239
|
+
schema_version: EXPLAIN_SCHEMA_VERSION,
|
|
240
|
+
command: 'explain',
|
|
241
|
+
topic: 'why',
|
|
242
|
+
mustflow_root: projectRoot,
|
|
243
|
+
decision: latestFailureDecision({
|
|
244
|
+
path: sourcePath,
|
|
245
|
+
present: true,
|
|
246
|
+
valid: false,
|
|
247
|
+
failed: false,
|
|
248
|
+
status: null,
|
|
249
|
+
intent,
|
|
250
|
+
exitCode,
|
|
251
|
+
errorKind,
|
|
252
|
+
durationMs,
|
|
253
|
+
summary: 'The latest mf run receipt does not include a status field.',
|
|
254
|
+
}, 'latest run receipt is invalid', 'mustflow could not read bounded latest run metadata safely.', 'Ignore the latest-run state and rerun the intended configured command to create a fresh receipt.'),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const failed = status !== 'passed';
|
|
258
|
+
return {
|
|
259
|
+
schema_version: EXPLAIN_SCHEMA_VERSION,
|
|
260
|
+
command: 'explain',
|
|
261
|
+
topic: 'why',
|
|
262
|
+
mustflow_root: projectRoot,
|
|
263
|
+
decision: latestFailureDecision({
|
|
264
|
+
path: sourcePath,
|
|
265
|
+
present: true,
|
|
266
|
+
valid: true,
|
|
267
|
+
failed,
|
|
268
|
+
status,
|
|
269
|
+
intent,
|
|
270
|
+
exitCode,
|
|
271
|
+
errorKind,
|
|
272
|
+
durationMs,
|
|
273
|
+
summary: `Latest bounded run receipt status is ${status}.`,
|
|
274
|
+
}, failed ? 'latest run did not pass' : 'latest run is not a failure', failed
|
|
275
|
+
? 'the latest bounded run receipt reports a non-passing status without exposing stdout or stderr tails.'
|
|
276
|
+
: 'the latest bounded run receipt does not report a failure status.', failed
|
|
277
|
+
? 'Inspect the configured command result and rerun the narrowest relevant intent after fixing the cause.'
|
|
278
|
+
: 'Do not treat this as a failure explanation; choose the next verification from current changes and command contracts.'),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function withWhyTopic(output) {
|
|
282
|
+
return { ...output, topic: 'why' };
|
|
283
|
+
}
|
|
284
|
+
async function getWhyExplainOutput(projectRoot, targetArg, rest, lang, reporter) {
|
|
285
|
+
switch (targetArg) {
|
|
286
|
+
case 'command':
|
|
287
|
+
case 'intent': {
|
|
288
|
+
const [commandName, ...extra] = rest;
|
|
289
|
+
if (!commandName) {
|
|
290
|
+
printUsageError(reporter, t(lang, 'explain.error.missingCommand'), 'mf explain --help', getExplainHelp(lang), lang);
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
if (extra.length > 0) {
|
|
294
|
+
printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: extra[0] }), 'mf explain --help', getExplainHelp(lang), lang);
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
return withWhyTopic(await getCommandExplainOutput(projectRoot, commandName));
|
|
298
|
+
}
|
|
299
|
+
case 'verify': {
|
|
300
|
+
const parsed = parseExplainVerifyArgs([...rest]);
|
|
301
|
+
if (parsed.error) {
|
|
302
|
+
printUsageError(reporter, explainVerifyArgErrorMessage(parsed.error, lang), 'mf explain --help', getExplainHelp(lang), lang);
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
const selectedInputCount = [parsed.reason, parsed.fromPlan].filter(Boolean).length;
|
|
306
|
+
if (selectedInputCount > 1) {
|
|
307
|
+
printUsageError(reporter, t(lang, 'verify.error.conflictingInputs'), 'mf explain --help', getExplainHelp(lang), lang);
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
if (selectedInputCount === 0) {
|
|
311
|
+
printUsageError(reporter, t(lang, 'verify.error.missingReason'), 'mf explain --help', getExplainHelp(lang), lang);
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
if (parsed.fromPlan) {
|
|
316
|
+
const reasons = readExplainVerifyPlanReasons(projectRoot, parsed.fromPlan);
|
|
317
|
+
return withWhyTopic(await getVerifyExplainOutput(EXPLAIN_SCHEMA_VERSION, projectRoot, reasons, null, parsed.fromPlan));
|
|
318
|
+
}
|
|
319
|
+
return withWhyTopic(await getVerifyExplainOutput(EXPLAIN_SCHEMA_VERSION, projectRoot, [parsed.reason], parsed.reason, null));
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
printUsageError(reporter, explainVerifyPlanErrorMessage(error, lang), 'mf explain --help', getExplainHelp(lang), lang);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
case 'skill': {
|
|
327
|
+
const [skillName, ...extra] = rest;
|
|
328
|
+
if (!skillName) {
|
|
329
|
+
printUsageError(reporter, t(lang, 'explain.error.missingSkill'), 'mf explain --help', getExplainHelp(lang), lang);
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
if (extra.length > 0) {
|
|
333
|
+
printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: extra[0] }), 'mf explain --help', getExplainHelp(lang), lang);
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
return withWhyTopic(getSkillExplainOutput(projectRoot, skillName));
|
|
337
|
+
}
|
|
338
|
+
case 'skills':
|
|
339
|
+
if (rest.length > 0) {
|
|
340
|
+
printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: rest[0] }), 'mf explain --help', getExplainHelp(lang), lang);
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
return withWhyTopic(getSkillsExplainOutput(projectRoot));
|
|
344
|
+
case 'surface': {
|
|
345
|
+
const [pathArg, ...extra] = rest;
|
|
346
|
+
if (extra.length > 0) {
|
|
347
|
+
printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: extra[0] }), 'mf explain --help', getExplainHelp(lang), lang);
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
return withWhyTopic(await getSurfaceExplainOutput(projectRoot, pathArg));
|
|
351
|
+
}
|
|
352
|
+
case 'latest-failure':
|
|
353
|
+
case 'latest-run':
|
|
354
|
+
if (rest.length > 0) {
|
|
355
|
+
printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: rest[0] }), 'mf explain --help', getExplainHelp(lang), lang);
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
return getLatestFailureExplainOutput(projectRoot);
|
|
359
|
+
default:
|
|
360
|
+
printUsageError(reporter, t(lang, targetArg ? 'explain.error.unknownTopic' : 'explain.error.missingTopic', { topic: targetArg ?? '' }), 'mf explain --help', getExplainHelp(lang), lang);
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
127
364
|
function formatNullable(value, lang) {
|
|
128
365
|
if (value === null) {
|
|
129
366
|
return t(lang, 'value.none');
|
|
@@ -152,6 +389,10 @@ function renderExplainDecision(output, lang) {
|
|
|
152
389
|
if ('verification' in output.decision) {
|
|
153
390
|
lines.push(...renderVerifyExplainDecision(output.decision, lang));
|
|
154
391
|
}
|
|
392
|
+
if ('latestFailure' in output.decision) {
|
|
393
|
+
const latest = output.decision.latestFailure;
|
|
394
|
+
lines.push('', 'Latest run failure', `- path: ${latest.path}`, `- present: ${latest.present ? t(lang, 'value.yes') : t(lang, 'value.no')}`, `- valid: ${latest.valid ? t(lang, 'value.yes') : t(lang, 'value.no')}`, `- failed: ${latest.failed ? t(lang, 'value.yes') : t(lang, 'value.no')}`, `- status: ${latest.status ?? t(lang, 'value.none')}`, `- intent: ${latest.intent ?? t(lang, 'value.none')}`, `- exit_code: ${latest.exitCode ?? t(lang, 'value.none')}`, `- error_kind: ${latest.errorKind ?? t(lang, 'value.none')}`, `- duration_ms: ${latest.durationMs ?? t(lang, 'value.none')}`, `- summary: ${latest.summary}`);
|
|
395
|
+
}
|
|
155
396
|
if ('boundary' in output.decision) {
|
|
156
397
|
lines.push('', t(lang, 'explain.label.authorityBoundary'), `- role: ${output.decision.boundary.role}`);
|
|
157
398
|
if (output.decision.boundary.canDefine.length > 0) {
|
|
@@ -170,6 +411,17 @@ function renderExplainDecision(output, lang) {
|
|
|
170
411
|
if ('intent' in output.decision && output.decision.intent) {
|
|
171
412
|
const intent = output.decision.intent;
|
|
172
413
|
lines.push('', t(lang, 'explain.label.commandIntent'), `- ${t(lang, 'explain.label.commandName')}: ${intent.name}`, `- status: ${intent.status ?? t(lang, 'value.none')}`, `- lifecycle: ${intent.lifecycle ?? t(lang, 'value.none')}`, `- run_policy: ${intent.runPolicy ?? t(lang, 'value.none')}`, `- stdin: ${intent.stdin ?? t(lang, 'value.none')}`, `- timeout_seconds: ${intent.timeoutSeconds ?? t(lang, 'value.none')}`, `- mode: ${intent.mode}`, `- cwd: ${intent.cwd ?? t(lang, 'value.none')}`, `- writes: ${intent.writes.join(', ') || t(lang, 'value.none')}`, `- network: ${formatNullable(intent.network, lang)}`, `- destructive: ${formatNullable(intent.destructive, lang)}`, `- success_exit_codes: ${intent.successExitCodes.join(', ') || t(lang, 'value.none')}`, `- required_after: ${intent.requiredAfter.join(', ') || t(lang, 'value.none')}`);
|
|
414
|
+
if (intent.preconditions.length > 0) {
|
|
415
|
+
lines.push('- preconditions:');
|
|
416
|
+
for (const precondition of intent.preconditions) {
|
|
417
|
+
const target = precondition.path ?? precondition.artifact ?? precondition.label ?? precondition.kind;
|
|
418
|
+
lines.push(` - ${precondition.kind} ${target}: ${precondition.status}`);
|
|
419
|
+
lines.push(` detail: ${precondition.detail}`);
|
|
420
|
+
if (precondition.satisfyIntent) {
|
|
421
|
+
lines.push(` satisfy_intent: ${precondition.satisfyIntent.intent} (${precondition.satisfyIntent.runnable ? 'runnable' : 'not_runnable'})`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
173
425
|
}
|
|
174
426
|
if ('effectGraph' in output.decision && output.decision.effectGraph) {
|
|
175
427
|
const graph = output.decision.effectGraph;
|
|
@@ -216,6 +468,8 @@ function renderExplainDecision(output, lang) {
|
|
|
216
468
|
else {
|
|
217
469
|
lines.push(`- skill: ${route.skill}`, `- path: ${route.skillPath}`, `- trigger: ${route.trigger}`, `- required_input: ${route.requiredInput}`, `- edit_scope: ${route.editScope}`, `- risk: ${route.risk}`, `- verification_intents: ${route.verificationIntents.join(', ') || t(lang, 'value.none')}`, `- declared_command_intents: ${route.declaredCommandIntents.join(', ') || t(lang, 'value.none')}`, `- expected_output: ${route.expectedOutput}`);
|
|
218
470
|
}
|
|
471
|
+
const evidence = output.decision.selectionEvidence;
|
|
472
|
+
lines.push('', 'Skill selection evidence', `- matched_by: ${evidence.matchedBy.join(', ') || t(lang, 'value.none')}`, `- required_inputs: ${evidence.requiredInputs.join(', ') || t(lang, 'value.none')}`, `- missing_inputs: ${evidence.missingInputs.join(', ') || t(lang, 'value.none')}`, `- candidate_adjuncts: ${evidence.candidateAdjuncts.join(', ') || t(lang, 'value.none')}`, `- unmatched_paths: ${evidence.unmatchedPaths.join(', ') || t(lang, 'value.none')}`, `- gap_notes: ${evidence.gapNotes.join(' | ') || t(lang, 'value.none')}`);
|
|
219
473
|
}
|
|
220
474
|
if ('surface' in output.decision) {
|
|
221
475
|
const surface = output.decision.surface;
|
|
@@ -254,6 +508,19 @@ export async function runExplain(args, reporter, lang = 'en') {
|
|
|
254
508
|
const json = args.includes('--json');
|
|
255
509
|
const positional = args.filter((arg) => arg !== '--json');
|
|
256
510
|
const [topic, targetArg, ...rest] = positional;
|
|
511
|
+
if (topic === 'why') {
|
|
512
|
+
const projectRoot = resolveMustflowRoot();
|
|
513
|
+
const output = await getWhyExplainOutput(projectRoot, targetArg, rest, lang, reporter);
|
|
514
|
+
if (!output) {
|
|
515
|
+
return 1;
|
|
516
|
+
}
|
|
517
|
+
if (json) {
|
|
518
|
+
reporter.stdout(JSON.stringify(output, null, 2));
|
|
519
|
+
return 0;
|
|
520
|
+
}
|
|
521
|
+
reporter.stdout(renderExplainDecision(output, lang));
|
|
522
|
+
return 0;
|
|
523
|
+
}
|
|
257
524
|
if (topic === 'verify') {
|
|
258
525
|
const verifyArgs = targetArg === undefined ? rest : [targetArg, ...rest];
|
|
259
526
|
const parsed = parseExplainVerifyArgs(verifyArgs);
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { performance } from 'node:perf_hooks';
|
|
3
|
+
import { acquireActiveRunLock } from '../../core/active-run-locks.js';
|
|
2
4
|
import { createCommandEnv } from '../../core/command-env.js';
|
|
3
5
|
import { createCorrelationId } from '../../core/correlation-id.js';
|
|
4
6
|
import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.js';
|
|
@@ -86,6 +88,20 @@ function writeLatestProfile(profiler, options, input) {
|
|
|
86
88
|
}
|
|
87
89
|
profiler.writeLatest(input);
|
|
88
90
|
}
|
|
91
|
+
function createPlanCommandHash(plan) {
|
|
92
|
+
const payload = {
|
|
93
|
+
mode: plan.mode,
|
|
94
|
+
cwd: plan.relativeCwd,
|
|
95
|
+
argv: plan.commandArgv ?? null,
|
|
96
|
+
cmd: plan.shellCommand ?? null,
|
|
97
|
+
};
|
|
98
|
+
return `sha256:${createHash('sha256').update(JSON.stringify(payload)).digest('hex')}`;
|
|
99
|
+
}
|
|
100
|
+
function renderActiveLockConflictMessage(intentName, conflicts) {
|
|
101
|
+
const [first] = conflicts;
|
|
102
|
+
const detail = first ? `${first.lock} conflicts with active intent ${first.conflictsWithIntent} (pid ${first.conflictsWithPid})` : 'unknown active lock conflict';
|
|
103
|
+
return `mf run ${intentName} is blocked by an active run lock: ${detail}`;
|
|
104
|
+
}
|
|
89
105
|
export function getRunHelp(lang = 'en') {
|
|
90
106
|
return renderHelp({
|
|
91
107
|
usage: 'mf run <intent> [options]',
|
|
@@ -187,88 +203,104 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
187
203
|
});
|
|
188
204
|
return 1;
|
|
189
205
|
}
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
206
|
+
const activeRunLock = profiler.measure('active_lock_acquire', () => acquireActiveRunLock(projectRoot, contract, intentName, { commandHash: createPlanCommandHash(plan) }));
|
|
207
|
+
if (!activeRunLock.ok) {
|
|
208
|
+
reporter.stderr(renderCliError(renderActiveLockConflictMessage(intentName, activeRunLock.conflicts), 'mf run --dry-run --json', lang));
|
|
209
|
+
writeLatestProfile(profiler, options, {
|
|
210
|
+
projectRoot,
|
|
211
|
+
intent: intentName,
|
|
212
|
+
status: 'blocked',
|
|
213
|
+
previewMode: null,
|
|
214
|
+
});
|
|
215
|
+
return 1;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const runReceiptPolicy = profiler.measure('retention_policy', () => resolveRunReceiptRetentionPolicy(readMustflowConfigIfExists(projectRoot)));
|
|
219
|
+
const env = profiler.measure('environment', () => createCommandEnv(projectRoot, { policy: plan.envPolicy, allowlist: plan.envAllowlist }));
|
|
220
|
+
const writeTracker = profiler.measure('write_drift_before', () => startRunWriteTracking(projectRoot, contract, intentName, {
|
|
221
|
+
additionalDeclaredPaths: options.additionalDeclaredWritePaths,
|
|
222
|
+
}));
|
|
223
|
+
const stdoutTailBytes = Math.min(runReceiptPolicy.stdoutTailBytes, plan.maxOutputBytes);
|
|
224
|
+
const stderrTailBytes = Math.min(runReceiptPolicy.stderrTailBytes, plan.maxOutputBytes);
|
|
225
|
+
let streamedOutput = false;
|
|
226
|
+
const childStartedAtMs = performance.now();
|
|
227
|
+
const startedAt = new Date();
|
|
228
|
+
const result = await profiler.measureAsync('child_command', async () => {
|
|
229
|
+
if (plan.commandArgv) {
|
|
230
|
+
streamedOutput = !json;
|
|
231
|
+
return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
|
|
232
|
+
}
|
|
202
233
|
streamedOutput = !json;
|
|
203
|
-
return
|
|
234
|
+
return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
|
|
235
|
+
});
|
|
236
|
+
const childDurationMs = performance.now() - childStartedAtMs;
|
|
237
|
+
const finishedAt = new Date();
|
|
238
|
+
const writeDrift = profiler.measure('write_drift_after', () => finishRunWriteTracking(writeTracker));
|
|
239
|
+
const exitCode = typeof result.status === 'number' ? result.status : null;
|
|
240
|
+
const runStatus = getRunStatus(result.error, exitCode, plan.successExitCodes);
|
|
241
|
+
let killMethod = null;
|
|
242
|
+
let termination = null;
|
|
243
|
+
if (runStatus === 'timed_out') {
|
|
244
|
+
termination = result.termination ?? createPendingTimeoutTermination(getKillMethod());
|
|
245
|
+
killMethod = termination.method;
|
|
246
|
+
if (!result.termination && result.pid) {
|
|
247
|
+
terminateProcessTree(result.pid);
|
|
248
|
+
}
|
|
204
249
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
250
|
+
const receipt = profiler.measure('receipt_create', () => assembleRunReceipt({
|
|
251
|
+
correlationId: options.correlationId ?? createCorrelationId('run'),
|
|
252
|
+
intentName,
|
|
253
|
+
runStatus,
|
|
254
|
+
startedAt,
|
|
255
|
+
finishedAt,
|
|
256
|
+
projectRoot,
|
|
257
|
+
plan,
|
|
258
|
+
result,
|
|
259
|
+
exitCode,
|
|
260
|
+
killMethod,
|
|
261
|
+
termination,
|
|
262
|
+
writeDrift,
|
|
263
|
+
executorOverheadMs: Math.max(0, Math.round((performance.now() - executorStartedAtMs - childDurationMs) * 1000) / 1000),
|
|
264
|
+
phaseTimings: profiler.getReceiptPhases(),
|
|
265
|
+
stdoutTailBytes: runReceiptPolicy.stdoutTailBytes,
|
|
266
|
+
stderrTailBytes: runReceiptPolicy.stderrTailBytes,
|
|
267
|
+
}));
|
|
268
|
+
if (options.writeLatestReceipt !== false) {
|
|
269
|
+
profiler.measure('receipt_write', () => writeRunReceipt(projectRoot, receipt));
|
|
220
270
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
termination,
|
|
234
|
-
writeDrift,
|
|
235
|
-
executorOverheadMs: Math.max(0, Math.round((performance.now() - executorStartedAtMs - childDurationMs) * 1000) / 1000),
|
|
236
|
-
phaseTimings: profiler.getReceiptPhases(),
|
|
237
|
-
stdoutTailBytes: runReceiptPolicy.stdoutTailBytes,
|
|
238
|
-
stderrTailBytes: runReceiptPolicy.stderrTailBytes,
|
|
239
|
-
}));
|
|
240
|
-
if (options.writeLatestReceipt !== false) {
|
|
241
|
-
profiler.measure('receipt_write', () => writeRunReceipt(projectRoot, receipt));
|
|
242
|
-
}
|
|
243
|
-
if (options.recordPerformanceHistory !== false) {
|
|
244
|
-
profiler.measure('performance_history_write', () => recordRunPerformanceHistory(projectRoot, receipt));
|
|
245
|
-
}
|
|
246
|
-
writeLatestProfile(profiler, options, {
|
|
247
|
-
projectRoot,
|
|
248
|
-
intent: intentName,
|
|
249
|
-
status: runStatus,
|
|
250
|
-
previewMode: null,
|
|
251
|
-
});
|
|
252
|
-
if (json) {
|
|
253
|
-
reporter.stdout(JSON.stringify(receipt, null, 2));
|
|
254
|
-
return runStatus === 'passed' ? 0 : 1;
|
|
255
|
-
}
|
|
256
|
-
if (!streamedOutput) {
|
|
257
|
-
emitOutput(reporter, result.stdout, 'stdout');
|
|
258
|
-
emitOutput(reporter, result.stderr, 'stderr');
|
|
259
|
-
}
|
|
260
|
-
if (result.error) {
|
|
261
|
-
const errorWithCode = result.error;
|
|
262
|
-
if (errorWithCode.code === 'ETIMEDOUT') {
|
|
263
|
-
reporter.stderr(t(lang, 'run.error.timedOut', { intent: intentName, seconds: plan.timeoutSeconds }));
|
|
264
|
-
return 1;
|
|
271
|
+
if (options.recordPerformanceHistory !== false) {
|
|
272
|
+
profiler.measure('performance_history_write', () => recordRunPerformanceHistory(projectRoot, receipt));
|
|
273
|
+
}
|
|
274
|
+
writeLatestProfile(profiler, options, {
|
|
275
|
+
projectRoot,
|
|
276
|
+
intent: intentName,
|
|
277
|
+
status: runStatus,
|
|
278
|
+
previewMode: null,
|
|
279
|
+
});
|
|
280
|
+
if (json) {
|
|
281
|
+
reporter.stdout(JSON.stringify(receipt, null, 2));
|
|
282
|
+
return runStatus === 'passed' ? 0 : 1;
|
|
265
283
|
}
|
|
266
|
-
if (
|
|
267
|
-
reporter.
|
|
284
|
+
if (!streamedOutput) {
|
|
285
|
+
emitOutput(reporter, result.stdout, 'stdout');
|
|
286
|
+
emitOutput(reporter, result.stderr, 'stderr');
|
|
287
|
+
}
|
|
288
|
+
if (result.error) {
|
|
289
|
+
const errorWithCode = result.error;
|
|
290
|
+
if (errorWithCode.code === 'ETIMEDOUT') {
|
|
291
|
+
reporter.stderr(t(lang, 'run.error.timedOut', { intent: intentName, seconds: plan.timeoutSeconds }));
|
|
292
|
+
return 1;
|
|
293
|
+
}
|
|
294
|
+
if (isOutputLimitExceededError(result.error)) {
|
|
295
|
+
reporter.stderr(t(lang, 'run.error.outputLimitExceeded', { intent: intentName, message: result.error.message }));
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
reporter.stderr(t(lang, 'run.error.startFailed', { intent: intentName, message: result.error.message }));
|
|
268
299
|
return 1;
|
|
269
300
|
}
|
|
270
|
-
|
|
271
|
-
|
|
301
|
+
return runStatus === 'passed' ? 0 : 1;
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
activeRunLock.handle.release();
|
|
272
305
|
}
|
|
273
|
-
return runStatus === 'passed' ? 0 : 1;
|
|
274
306
|
}
|
|
@@ -12,6 +12,7 @@ import { createScopeDiffRisks } from '../../core/scope-risk.js';
|
|
|
12
12
|
import { countValidationRatchetVerdictEffects, createValidationRatchetRisks, } from '../../core/validation-ratchet.js';
|
|
13
13
|
import { finishRunWriteBatchTracking, startRunWriteBatchTracking, } from '../../core/run-write-drift.js';
|
|
14
14
|
import { readCommandContract } from '../../core/config-loading.js';
|
|
15
|
+
import { evaluateCommandPreconditions, } from '../../core/command-preconditions.js';
|
|
15
16
|
import { DEFAULT_VERIFY_PARALLELISM, parseVerifyArgs, resolveVerifyParallelism, } from './verify/args.js';
|
|
16
17
|
import { createInputFromChanged, createSyntheticClassificationReport, planErrorMessageKey, readInputFromClassificationReport, resolveVerifyInputPath, writeChangedPlan, } from './verify/input.js';
|
|
17
18
|
import { readExternalEvidenceFile, readReproEvidenceFile } from './verify/evidence-input.js';
|
|
@@ -879,9 +880,22 @@ async function createPlanOnlyOutput(input, projectRoot) {
|
|
|
879
880
|
}
|
|
880
881
|
const scheduledIntents = Array.from(new Set(report.schedule.entries.map((entry) => entry.intent)));
|
|
881
882
|
const graphsByIntent = await readLocalCommandEffectGraphs(projectRoot, scheduledIntents);
|
|
883
|
+
const preconditionsByIntent = new Map(scheduledIntents.map((intent) => [intent, evaluateCommandPreconditions(projectRoot, contract, intent)]));
|
|
882
884
|
const firstGraph = graphsByIntent.get(firstEntry.intent);
|
|
883
885
|
if (!firstGraph) {
|
|
884
|
-
return {
|
|
886
|
+
return {
|
|
887
|
+
...report,
|
|
888
|
+
correlation_id: input.correlationId,
|
|
889
|
+
verification_plan_id: verificationPlanId,
|
|
890
|
+
requirements,
|
|
891
|
+
schedule: {
|
|
892
|
+
...report.schedule,
|
|
893
|
+
entries: report.schedule.entries.map((entry) => ({
|
|
894
|
+
...entry,
|
|
895
|
+
preconditions: preconditionsByIntent.get(entry.intent) ?? [],
|
|
896
|
+
})),
|
|
897
|
+
},
|
|
898
|
+
};
|
|
885
899
|
}
|
|
886
900
|
return {
|
|
887
901
|
...report,
|
|
@@ -893,6 +907,7 @@ async function createPlanOnlyOutput(input, projectRoot) {
|
|
|
893
907
|
entries: report.schedule.entries.map((entry) => ({
|
|
894
908
|
...entry,
|
|
895
909
|
effectGraph: graphsByIntent.get(entry.intent) ?? firstGraph,
|
|
910
|
+
preconditions: preconditionsByIntent.get(entry.intent) ?? [],
|
|
896
911
|
})),
|
|
897
912
|
},
|
|
898
913
|
};
|