mustflow 2.22.47 → 2.22.49

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
@@ -253,8 +253,9 @@ mf run mustflow_update_apply
253
253
  | `mf api command-catalog --json` | Print command intent availability and safe `mf run` entrypoints without exposing raw command strings. |
254
254
  | `mf api verification-plan --changed --json` | Print a stable, read-only verification plan for changed files without executing commands. |
255
255
  | `mf api latest-evidence --json` | Print bounded latest run or verify evidence without raw command output. |
256
- | `mf api diff-risk --changed --json` | Print a compact changed-file risk and verification summary. |
256
+ | `mf api diff-risk --changed --json` | Print a compact changed-file risk, verification summary, and read-only residual correction signals. |
257
257
  | `mf api health --json` | Print a compact workspace health report for quick agent gating. |
258
+ | `mf api locks --json` | Print active `mf run` locks for multi-session coordination. |
258
259
  | `mf docs review list` | Show documents still waiting for prose review after agent edits. |
259
260
  | `mf docs review add <path>` | Add or refresh a document review queue entry. |
260
261
  | `mf docs review comment <path>` | Add multiline review guidance to an existing queue entry. |
@@ -264,6 +265,7 @@ mf run mustflow_update_apply
264
265
  | `mf map --stdout` | Print the current mustflow root map to stdout. |
265
266
  | `mf map --write` | Create or update `REPO_MAP.md`. |
266
267
  | `mf run <intent>` | Run an allowed one-shot command. |
268
+ | `mf run <intent> --wait` | Wait for conflicting active run locks before executing the command. |
267
269
  | `mf run <intent> --dry-run --json` | Preview whether an intent is runnable and what command metadata would be used, without executing it. |
268
270
  | `mf index` | Build a SQLite index for mustflow docs, skill routes, command rules, command-effect locks, and file fingerprints. Use `--incremental` to reuse a compatible fresh index without rewriting it. |
269
271
  | `mf search <query>` | Search docs, skills, skill routes, command rules, and command-effect locks in the SQLite index. |
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createClassifyOutput } from './classify.js';
4
+ import { listActiveRunLocks } from '../../core/active-run-locks.js';
4
5
  import { createChangeVerificationReport, } from '../../core/change-verification.js';
5
6
  import { readUtf8FileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
6
7
  import { createVerificationPlanId } from '../../core/verification-plan-id.js';
@@ -18,8 +19,10 @@ const API_VERIFICATION_PLAN_SCHEMA_VERSION = '1';
18
19
  const API_LATEST_EVIDENCE_SCHEMA_VERSION = '1';
19
20
  const API_DIFF_RISK_SCHEMA_VERSION = '1';
20
21
  const API_HEALTH_SCHEMA_VERSION = '1';
22
+ const API_LOCKS_SCHEMA_VERSION = '1';
21
23
  const COMMANDS_RELATIVE_PATH = '.mustflow/config/commands.toml';
22
24
  const LATEST_RUN_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
25
+ const LOCKS_RELATIVE_PATH = '.mustflow/state/locks';
23
26
  const MUSTFLOW_JSON_MAX_BYTES = 1024 * 1024;
24
27
  export function getApiHelp(lang = 'en') {
25
28
  return renderHelp({
@@ -50,6 +53,10 @@ export function getApiHelp(lang = 'en') {
50
53
  label: 'health',
51
54
  description: t(lang, 'api.help.action.health'),
52
55
  },
56
+ {
57
+ label: 'locks',
58
+ description: t(lang, 'api.help.action.locks'),
59
+ },
53
60
  ],
54
61
  options: [
55
62
  { label: '--changed', description: t(lang, 'classify.help.option.changed') },
@@ -63,6 +70,7 @@ export function getApiHelp(lang = 'en') {
63
70
  'mf api latest-evidence --json',
64
71
  'mf api diff-risk --changed --json',
65
72
  'mf api health --json',
73
+ 'mf api locks --json',
66
74
  ],
67
75
  exitCodes: [
68
76
  { label: '0', description: t(lang, 'api.help.exit.ok') },
@@ -658,6 +666,150 @@ function getRiskLevel(classification, report) {
658
666
  }
659
667
  return 'low';
660
668
  }
669
+ function uniqueSortedStrings(values) {
670
+ return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))]
671
+ .sort((left, right) => left.localeCompare(right));
672
+ }
673
+ function readEvidenceRequirements(parsed) {
674
+ if (!isRecord(parsed) || !isRecord(parsed.evidence_model) || !Array.isArray(parsed.evidence_model.requirements)) {
675
+ return [];
676
+ }
677
+ return parsed.evidence_model.requirements.filter(isRecord);
678
+ }
679
+ function readEvidenceRemainingRisks(parsed) {
680
+ if (!isRecord(parsed) || !isRecord(parsed.evidence_model) || !Array.isArray(parsed.evidence_model.remaining_risks)) {
681
+ return [];
682
+ }
683
+ return parsed.evidence_model.remaining_risks.filter(isRecord);
684
+ }
685
+ function readEvidenceReceipts(parsed) {
686
+ if (!isRecord(parsed) || !isRecord(parsed.evidence_model) || !Array.isArray(parsed.evidence_model.receipts)) {
687
+ return [];
688
+ }
689
+ return parsed.evidence_model.receipts.filter(isRecord);
690
+ }
691
+ function affectedReasonsFromLatest(parsed) {
692
+ return uniqueSortedStrings(readEvidenceRequirements(parsed).map((requirement) => readString(requirement, 'reason')));
693
+ }
694
+ function affectedIntentsFromLatest(parsed) {
695
+ const requirementIntents = readEvidenceRequirements(parsed).flatMap((requirement) => [
696
+ ...(Array.isArray(requirement.selected_intents) ? requirement.selected_intents : []),
697
+ ...(Array.isArray(requirement.skipped_intents) ? requirement.skipped_intents : []),
698
+ ]);
699
+ const receiptIntents = readEvidenceReceipts(parsed).map((receipt) => readString(receipt, 'intent'));
700
+ return uniqueSortedStrings([...requirementIntents, ...receiptIntents]);
701
+ }
702
+ function repeatedFailureConfidence(seenCount, requiresNewEvidence) {
703
+ if (requiresNewEvidence || seenCount >= 3) {
704
+ return 'high';
705
+ }
706
+ return seenCount >= 2 ? 'medium' : 'low';
707
+ }
708
+ function createResidualPolicy() {
709
+ return {
710
+ single_observation_changes_plan: false,
711
+ minimum_repeated_samples_for_plan_change: 2,
712
+ automatic_intent_additions_enabled: false,
713
+ };
714
+ }
715
+ function createEmptyResidualCorrections(status, issues = []) {
716
+ return {
717
+ status,
718
+ mode: 'read_only',
719
+ grants_command_authority: false,
720
+ applies_to_current_plan: false,
721
+ selected_intent_additions: [],
722
+ suggested_intent_additions: [],
723
+ recommended_commands: [],
724
+ signals: [],
725
+ policy: createResidualPolicy(),
726
+ issues,
727
+ };
728
+ }
729
+ function createResidualCorrections(projectRoot, verificationPlanId) {
730
+ if (!verificationPlanId) {
731
+ return createEmptyResidualCorrections('not_enough_evidence');
732
+ }
733
+ let parsed;
734
+ try {
735
+ parsed = readJsonInsideRoot(projectRoot, LATEST_RUN_RELATIVE_PATH);
736
+ }
737
+ catch (error) {
738
+ const message = error instanceof Error ? error.message : String(error);
739
+ return message.includes('ENOENT') ? createEmptyResidualCorrections('not_enough_evidence') : createEmptyResidualCorrections('unavailable', [message]);
740
+ }
741
+ if (!isRecord(parsed) || parsed.command !== 'verify' || parsed.kind !== 'verify_run_summary') {
742
+ return createEmptyResidualCorrections('not_enough_evidence');
743
+ }
744
+ const latestPlanId = readString(parsed, 'verification_plan_id') ?? null;
745
+ const appliesToCurrentPlan = latestPlanId === verificationPlanId;
746
+ const affectedReasons = affectedReasonsFromLatest(parsed);
747
+ const affectedIntents = affectedIntentsFromLatest(parsed);
748
+ const completionVerdict = isRecord(parsed.completion_verdict) ? parsed.completion_verdict : null;
749
+ const completionStatus = completionVerdict ? readString(completionVerdict, 'status') : null;
750
+ const signals = [];
751
+ if (appliesToCurrentPlan && completionStatus && completionStatus !== 'verified') {
752
+ signals.push({
753
+ kind: 'latest_unresolved_plan',
754
+ confidence: 'low',
755
+ evidence_count: 1,
756
+ source: 'latest-evidence',
757
+ verification_plan_id: latestPlanId,
758
+ affected_reasons: affectedReasons,
759
+ affected_intents: affectedIntents,
760
+ detail: `Latest verify evidence for this plan ended with completion verdict ${completionStatus}; do not treat the base plan as complete without new evidence.`,
761
+ recommendation: 'provide_new_evidence',
762
+ });
763
+ }
764
+ const repeatedFailure = isRecord(parsed.repeated_failure_summary) ? parsed.repeated_failure_summary : null;
765
+ if (appliesToCurrentPlan && repeatedFailure) {
766
+ const seenCount = typeof repeatedFailure.seen_count === 'number' && Number.isFinite(repeatedFailure.seen_count)
767
+ ? Math.max(1, Math.floor(repeatedFailure.seen_count))
768
+ : 1;
769
+ const requiresNewEvidence = repeatedFailure.requires_new_evidence === true;
770
+ if (seenCount >= 2 || requiresNewEvidence) {
771
+ signals.push({
772
+ kind: 'repeated_failure',
773
+ confidence: repeatedFailureConfidence(seenCount, requiresNewEvidence),
774
+ evidence_count: seenCount,
775
+ source: 'latest-evidence',
776
+ verification_plan_id: latestPlanId,
777
+ affected_reasons: affectedReasons,
778
+ affected_intents: affectedIntents,
779
+ detail: 'Latest evidence reports repeated unresolved verification for this plan; collect new evidence or narrow the hypothesis before claiming completion.',
780
+ recommendation: 'provide_new_evidence',
781
+ });
782
+ }
783
+ }
784
+ if (appliesToCurrentPlan) {
785
+ for (const risk of readEvidenceRemainingRisks(parsed).slice(0, 5)) {
786
+ const detail = readString(risk, 'detail') ?? readString(risk, 'code') ?? 'Latest verify evidence has a remaining risk.';
787
+ signals.push({
788
+ kind: 'remaining_risk',
789
+ confidence: 'low',
790
+ evidence_count: 1,
791
+ source: 'latest-evidence',
792
+ verification_plan_id: latestPlanId,
793
+ affected_reasons: affectedReasons,
794
+ affected_intents: affectedIntents,
795
+ detail,
796
+ recommendation: 'review_remaining_risk',
797
+ });
798
+ }
799
+ }
800
+ return {
801
+ status: signals.length > 0 ? 'available' : 'not_enough_evidence',
802
+ mode: 'read_only',
803
+ grants_command_authority: false,
804
+ applies_to_current_plan: appliesToCurrentPlan,
805
+ selected_intent_additions: [],
806
+ suggested_intent_additions: [],
807
+ recommended_commands: signals.length > 0 ? ['mf api latest-evidence --json'] : [],
808
+ signals,
809
+ policy: createResidualPolicy(),
810
+ issues: [],
811
+ };
812
+ }
661
813
  function createDiffRiskOutput() {
662
814
  const mustflowRoot = resolveMustflowRoot();
663
815
  let classification = null;
@@ -680,21 +832,32 @@ function createDiffRiskOutput() {
680
832
  update_policies: [],
681
833
  drift_checks: [],
682
834
  required_verification: [],
835
+ residual_corrections: createResidualCorrections(mustflowRoot, null),
683
836
  gap_count: 0,
684
837
  gaps: [],
685
838
  recommended_commands: [],
686
839
  issues: [message],
687
840
  };
688
841
  }
689
- let report = null;
690
842
  const issues = [];
843
+ let contract = null;
691
844
  try {
692
- report = createChangeVerificationReport(classification, readCommandContract(mustflowRoot), mustflowRoot);
845
+ contract = readCommandContract(mustflowRoot);
693
846
  }
694
847
  catch (error) {
695
848
  issues.push(error instanceof Error ? error.message : String(error));
696
849
  }
850
+ let report = null;
851
+ if (contract) {
852
+ try {
853
+ report = createChangeVerificationReport(classification, contract, mustflowRoot);
854
+ }
855
+ catch (error) {
856
+ issues.push(error instanceof Error ? error.message : String(error));
857
+ }
858
+ }
697
859
  const requiredVerification = report ? report.schedule.entries.map((entry) => entry.intent) : [];
860
+ const verificationPlanId = report && contract ? createVerificationPlanId(report, contract) : null;
698
861
  return {
699
862
  schema_version: API_DIFF_RISK_SCHEMA_VERSION,
700
863
  command: 'api diff-risk',
@@ -709,6 +872,7 @@ function createDiffRiskOutput() {
709
872
  update_policies: classification.summary.updatePolicies,
710
873
  drift_checks: classification.summary.driftChecks,
711
874
  required_verification: requiredVerification,
875
+ residual_corrections: createResidualCorrections(mustflowRoot, verificationPlanId),
712
876
  gap_count: report?.gaps.length ?? 0,
713
877
  gaps: report?.gaps.map((gap) => ({
714
878
  reason: gap.reason,
@@ -757,6 +921,68 @@ function createHealthOutput() {
757
921
  recommended_next_commands: workspace.recommended_next_commands,
758
922
  };
759
923
  }
924
+ function createLocksOutput() {
925
+ const mustflowRoot = resolveMustflowRoot();
926
+ try {
927
+ const state = listActiveRunLocks(mustflowRoot);
928
+ const activeLocks = state.activeRecords.map((record) => ({
929
+ run_id: record.run_id,
930
+ intent: record.intent,
931
+ pid: record.pid,
932
+ started_at: record.started_at,
933
+ command_hash: record.command_hash,
934
+ effects: record.effects,
935
+ writes: record.writes,
936
+ }));
937
+ const staleLocks = state.staleRecords.map((record) => ({
938
+ run_id: record.runId,
939
+ intent: record.intent,
940
+ pid: record.pid,
941
+ reason: record.reason,
942
+ }));
943
+ const recommendedCommands = activeLocks.length > 0 ? ['mf api locks --json', 'mf run <intent> --wait'] : [];
944
+ return {
945
+ schema_version: API_LOCKS_SCHEMA_VERSION,
946
+ command: 'api locks',
947
+ mustflow_root: mustflowRoot,
948
+ status: activeLocks.length > 0 ? 'active' : staleLocks.length > 0 ? 'stale' : 'clear',
949
+ lock_root: LOCKS_RELATIVE_PATH,
950
+ active_count: activeLocks.length,
951
+ stale_count: staleLocks.length,
952
+ active_locks: activeLocks,
953
+ stale_locks: staleLocks,
954
+ policy: {
955
+ source: 'active-run-locks',
956
+ conflict_model: 'command_effects_and_writes',
957
+ direct_commands_bypass_locks: true,
958
+ wait_entrypoint: 'mf run <intent> --wait',
959
+ },
960
+ recommended_commands: recommendedCommands,
961
+ issues: [],
962
+ };
963
+ }
964
+ catch (error) {
965
+ return {
966
+ schema_version: API_LOCKS_SCHEMA_VERSION,
967
+ command: 'api locks',
968
+ mustflow_root: mustflowRoot,
969
+ status: 'unavailable',
970
+ lock_root: LOCKS_RELATIVE_PATH,
971
+ active_count: 0,
972
+ stale_count: 0,
973
+ active_locks: [],
974
+ stale_locks: [],
975
+ policy: {
976
+ source: 'active-run-locks',
977
+ conflict_model: 'command_effects_and_writes',
978
+ direct_commands_bypass_locks: true,
979
+ wait_entrypoint: 'mf run <intent> --wait',
980
+ },
981
+ recommended_commands: [],
982
+ issues: [error instanceof Error ? error.message : String(error)],
983
+ };
984
+ }
985
+ }
760
986
  function validateJsonOnlyAction(action, args, reporter, lang) {
761
987
  if (args.includes('--help') || args.includes('-h')) {
762
988
  reporter.stdout(getApiHelp(lang));
@@ -837,6 +1063,13 @@ function runHealth(args, reporter, lang) {
837
1063
  reporter.stdout(JSON.stringify(createHealthOutput(), null, 2));
838
1064
  return 0;
839
1065
  }
1066
+ function runLocks(args, reporter, lang) {
1067
+ if (!validateJsonOnlyAction('locks', args, reporter, lang)) {
1068
+ return args.includes('--help') || args.includes('-h') ? 0 : 1;
1069
+ }
1070
+ reporter.stdout(JSON.stringify(createLocksOutput(), null, 2));
1071
+ return 0;
1072
+ }
840
1073
  export function runApi(args, reporter, lang = 'en') {
841
1074
  if (args.includes('--help') || args.includes('-h')) {
842
1075
  reporter.stdout(getApiHelp(lang));
@@ -869,6 +1102,9 @@ export function runApi(args, reporter, lang = 'en') {
869
1102
  if (action === 'health') {
870
1103
  return runHealth(rest, reporter, lang);
871
1104
  }
1105
+ if (action === 'locks') {
1106
+ return runLocks(rest, reporter, lang);
1107
+ }
872
1108
  printUsageError(reporter, t(lang, 'api.error.unknownAction', { action }), 'mf api --help', getApiHelp(lang), lang);
873
1109
  return 1;
874
1110
  }
@@ -18,6 +18,13 @@ import { getRunStatus, runArgvCommandStreaming, runShellCommandStreaming } from
18
18
  import { emitOutput, isOutputLimitExceededError } from './run/output.js';
19
19
  import { createPendingTimeoutTermination, getKillMethod, terminateProcessTree } from './run/process-tree.js';
20
20
  import { assembleRunReceipt } from './run/receipt.js';
21
+ const DEFAULT_ACTIVE_LOCK_WAIT_TIMEOUT_SECONDS = 300;
22
+ const ACTIVE_LOCK_WAIT_POLL_MS = 1_000;
23
+ function delay(milliseconds) {
24
+ return new Promise((resolve) => {
25
+ setTimeout(resolve, milliseconds);
26
+ });
27
+ }
21
28
  function getRunPlanDetail(plan, lang, fallbackKey) {
22
29
  return plan.detail ?? t(lang, fallbackKey);
23
30
  }
@@ -117,6 +124,77 @@ function renderActiveLockConflictMessage(intentName, conflicts, lang) {
117
124
  : t(lang, 'run.error.activeLockConflictUnknown');
118
125
  return t(lang, 'run.error.activeLockConflict', { intent: intentName, detail });
119
126
  }
127
+ function parseRunArguments(args) {
128
+ const supportedBooleanOptions = new Set(['--json', '--dry-run', '--plan-only', '--wait', ALLOW_UNTRUSTED_ROOT_OPTION]);
129
+ const supportedValueOptions = new Set(['--wait-timeout']);
130
+ const positional = [];
131
+ const unsupported = [];
132
+ let waitTimeoutSeconds = DEFAULT_ACTIVE_LOCK_WAIT_TIMEOUT_SECONDS;
133
+ let invalidWaitTimeout = false;
134
+ for (let index = 0; index < args.length; index += 1) {
135
+ const arg = args[index];
136
+ if (supportedBooleanOptions.has(arg)) {
137
+ continue;
138
+ }
139
+ if (supportedValueOptions.has(arg)) {
140
+ const value = args[index + 1];
141
+ if (!value || value.startsWith('-')) {
142
+ invalidWaitTimeout = true;
143
+ continue;
144
+ }
145
+ const parsed = Number(value);
146
+ if (!Number.isInteger(parsed) || parsed <= 0) {
147
+ invalidWaitTimeout = true;
148
+ }
149
+ else {
150
+ waitTimeoutSeconds = parsed;
151
+ }
152
+ index += 1;
153
+ continue;
154
+ }
155
+ if (arg.startsWith('-')) {
156
+ unsupported.push(arg);
157
+ continue;
158
+ }
159
+ positional.push(arg);
160
+ }
161
+ const [intentName, ...extra] = positional;
162
+ return {
163
+ json: args.includes('--json'),
164
+ dryRun: args.includes('--dry-run'),
165
+ planOnly: args.includes('--plan-only'),
166
+ allowUntrustedRoot: args.includes(ALLOW_UNTRUSTED_ROOT_OPTION),
167
+ wait: args.includes('--wait'),
168
+ waitTimeoutSeconds,
169
+ intentName: intentName ?? null,
170
+ extra,
171
+ unsupported,
172
+ invalidWaitTimeout,
173
+ };
174
+ }
175
+ async function acquireActiveRunLockWithOptionalWait(input) {
176
+ const startedAt = Date.now();
177
+ let reportedWait = false;
178
+ while (true) {
179
+ const result = acquireActiveRunLock(input.projectRoot, input.contract, input.intentName, { commandHash: input.commandHash });
180
+ if (result.ok || !input.enabled || result.conflicts.length === 0) {
181
+ return result;
182
+ }
183
+ if (!input.json && !reportedWait) {
184
+ const [first] = result.conflicts;
185
+ input.reporter.stderr(t(input.lang, 'run.progress.waitingForActiveLock', {
186
+ intent: input.intentName,
187
+ activeIntent: first?.conflictsWithIntent ?? 'unknown',
188
+ seconds: input.waitTimeoutSeconds,
189
+ }));
190
+ reportedWait = true;
191
+ }
192
+ if (Date.now() - startedAt >= input.waitTimeoutSeconds * 1000) {
193
+ return result;
194
+ }
195
+ await delay(Math.min(ACTIVE_LOCK_WAIT_POLL_MS, Math.max(1, input.waitTimeoutSeconds * 1000 - (Date.now() - startedAt))));
196
+ }
197
+ }
120
198
  function createRunProgressReporter(input) {
121
199
  if (!input.enabled) {
122
200
  return () => undefined;
@@ -150,6 +228,8 @@ export function getRunHelp(lang = 'en') {
150
228
  { label: '--dry-run', description: t(lang, 'run.help.option.dryRun') },
151
229
  { label: '--plan-only', description: t(lang, 'run.help.option.planOnly') },
152
230
  { label: '--json', description: t(lang, 'run.help.option.json') },
231
+ { label: '--wait', description: t(lang, 'run.help.option.wait') },
232
+ { label: '--wait-timeout <seconds>', description: t(lang, 'run.help.option.waitTimeout') },
153
233
  { label: ALLOW_UNTRUSTED_ROOT_OPTION, description: t(lang, 'run.help.option.allowUntrustedRoot') },
154
234
  { label: '-h, --help', description: t(lang, 'cli.option.help') },
155
235
  ],
@@ -180,23 +260,31 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
180
260
  reporter.stdout(getRunHelp(lang));
181
261
  return 0;
182
262
  }
183
- const supportedOptions = new Set(['--json', '--dry-run', '--plan-only', ALLOW_UNTRUSTED_ROOT_OPTION]);
184
- const unsupported = args.filter((arg) => arg.startsWith('-') && !supportedOptions.has(arg));
263
+ const parsedArgs = parseRunArguments(args);
264
+ const unsupported = parsedArgs.unsupported;
185
265
  if (unsupported.length > 0) {
186
266
  printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf run --help', getRunHelp(lang), lang);
187
267
  return 1;
188
268
  }
189
- const json = args.includes('--json');
190
- const dryRun = args.includes('--dry-run');
191
- const planOnly = args.includes('--plan-only');
192
- const allowUntrustedRoot = args.includes(ALLOW_UNTRUSTED_ROOT_OPTION);
269
+ if (parsedArgs.invalidWaitTimeout) {
270
+ printUsageError(reporter, t(lang, 'run.error.invalidWaitTimeout'), 'mf run --help', getRunHelp(lang), lang);
271
+ return 1;
272
+ }
273
+ const json = parsedArgs.json;
274
+ const dryRun = parsedArgs.dryRun;
275
+ const planOnly = parsedArgs.planOnly;
276
+ const allowUntrustedRoot = parsedArgs.allowUntrustedRoot;
193
277
  const previewMode = dryRun ? 'dry-run' : planOnly ? 'plan-only' : null;
194
278
  if (dryRun && planOnly) {
195
279
  printUsageError(reporter, t(lang, 'run.error.conflictingPreviewModes'), 'mf run --help', getRunHelp(lang), lang);
196
280
  return 1;
197
281
  }
198
- const positional = args.filter((arg) => !supportedOptions.has(arg));
199
- const [intentName, ...extra] = positional;
282
+ if (parsedArgs.wait && previewMode) {
283
+ printUsageError(reporter, t(lang, 'run.error.waitRequiresExecution'), 'mf run --help', getRunHelp(lang), lang);
284
+ return 1;
285
+ }
286
+ const intentName = parsedArgs.intentName;
287
+ const extra = parsedArgs.extra;
200
288
  if (!intentName) {
201
289
  printUsageError(reporter, t(lang, 'run.error.missingIntent'), 'mf run --help', getRunHelp(lang), lang);
202
290
  return 1;
@@ -243,7 +331,17 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
243
331
  });
244
332
  return 1;
245
333
  }
246
- const activeRunLock = profiler.measure('active_lock_acquire', () => acquireActiveRunLock(projectRoot, contract, intentName, { commandHash: createPlanCommandHash(plan) }));
334
+ const activeRunLock = await profiler.measureAsync('active_lock_acquire', () => acquireActiveRunLockWithOptionalWait({
335
+ enabled: parsedArgs.wait,
336
+ waitTimeoutSeconds: parsedArgs.waitTimeoutSeconds,
337
+ projectRoot,
338
+ contract,
339
+ intentName,
340
+ commandHash: createPlanCommandHash(plan),
341
+ json,
342
+ reporter,
343
+ lang,
344
+ }));
247
345
  if (!activeRunLock.ok) {
248
346
  reporter.stderr(renderCliError(renderActiveLockConflictMessage(intentName, activeRunLock.conflicts, lang), 'mf run --dry-run --json', lang));
249
347
  writeLatestProfile(profiler, options, {
@@ -94,8 +94,9 @@ export const enMessages = {
94
94
  "api.help.action.latestEvidence": "Print bounded latest run or verify evidence for agents",
95
95
  "api.help.action.diffRisk": "Print a compact changed-file risk and verification summary",
96
96
  "api.help.action.health": "Print a compact workspace health summary",
97
+ "api.help.action.locks": "Print active mf run locks for multi-session coordination",
97
98
  "api.help.exit.ok": "The API report was inspected and printed",
98
- "api.error.missingAction": "Specify an api action: workspace-summary, command-catalog, verification-plan, latest-evidence, diff-risk, or health",
99
+ "api.error.missingAction": "Specify an api action: workspace-summary, command-catalog, verification-plan, latest-evidence, diff-risk, health, or locks",
99
100
  "api.error.unknownAction": "Unknown api action: {action}",
100
101
  "api.error.actionRequiresJson": "{action} requires --json",
101
102
  "api.error.actionRequiresChanged": "{action} currently requires --changed",
@@ -674,12 +675,15 @@ Read these files before working:
674
675
  "run.help.option.dryRun": "Print a non-executing command plan",
675
676
  "run.help.option.planOnly": "Alias for --dry-run",
676
677
  "run.help.option.json": "Print the run record or command plan as JSON",
678
+ "run.help.option.wait": "Wait for conflicting active run locks before executing",
679
+ "run.help.option.waitTimeout": "Maximum seconds to wait for active run locks. Default: 300",
677
680
  "run.help.option.allowUntrustedRoot": "Allow one execution from a root with a missing or invalid manifest lock after manual review",
678
681
  "run.help.exit.ok": "The command completed with an allowed exit code",
679
682
  "run.help.exit.fail": "The command was invalid, refused, timed out, or failed",
680
683
  "run.label.suggestedIntentSnippet": "Suggested command contract snippet",
681
684
  "run.progress.started": "Running {intent} (timeout: {seconds}s)...",
682
685
  "run.progress.timeoutWarning": "Still running {intent}... ({seconds}s elapsed, {percent}% of timeout)",
686
+ "run.progress.waitingForActiveLock": "Waiting to run {intent}; active intent {activeIntent} holds a conflicting lock (timeout: {seconds}s)",
683
687
  "run.error.missingIntent": "Missing command name",
684
688
  "run.error.unknownIntent": "Unknown command: {intent}",
685
689
  "run.error.statusNotConfigured": 'Command "{intent}" is {status}; only configured commands can be run',
@@ -702,6 +706,8 @@ Read these files before working:
702
706
  "run.error.maxOutputBytes": 'Command "{intent}" has invalid max_output_bytes. {detail}',
703
707
  "run.error.maxOutputBytesDetail": "The output limit must stay within the allowed maximum.",
704
708
  "run.error.conflictingPreviewModes": "Use either --dry-run or --plan-only, not both",
709
+ "run.error.invalidWaitTimeout": "--wait-timeout must be a positive integer",
710
+ "run.error.waitRequiresExecution": "--wait can only be used when executing a command, not with --dry-run or --plan-only",
705
711
  "run.error.untrustedRootMissing": "Refused to execute commands because {path} is missing. Run mf init/update to install the workflow, or pass --allow-untrusted-root after reviewing AGENTS.md and .mustflow/config/commands.toml.",
706
712
  "run.error.untrustedRootInvalid": "Refused to execute commands because the manifest lock is invalid: {detail}. Restore or regenerate it, or pass --allow-untrusted-root after reviewing AGENTS.md and .mustflow/config/commands.toml.",
707
713
  "run.error.timedOut": 'Command "{intent}" timed out after {seconds} seconds',
@@ -94,8 +94,9 @@ export const esMessages = {
94
94
  "api.help.action.latestEvidence": "Imprime evidencia bounded del último run o verify para agentes",
95
95
  "api.help.action.diffRisk": "Imprime un resumen compacto de riesgo y verificación para archivos cambiados",
96
96
  "api.help.action.health": "Imprime un resumen compacto de salud del workspace",
97
+ "api.help.action.locks": "Imprime bloqueos mf run activos para coordinar varias sesiones",
97
98
  "api.help.exit.ok": "El informe API se inspeccionó e imprimió",
98
- "api.error.missingAction": "Especifica una acción api: workspace-summary, command-catalog, verification-plan, latest-evidence, diff-risk o health",
99
+ "api.error.missingAction": "Especifica una acción api: workspace-summary, command-catalog, verification-plan, latest-evidence, diff-risk, health o locks",
99
100
  "api.error.unknownAction": "Acción api desconocida: {action}",
100
101
  "api.error.actionRequiresJson": "{action} requiere --json",
101
102
  "api.error.actionRequiresChanged": "{action} actualmente requiere --changed",
@@ -674,12 +675,15 @@ Lee estos archivos antes de trabajar:
674
675
  "run.help.option.dryRun": "Imprime un plan de comando sin ejecutarlo",
675
676
  "run.help.option.planOnly": "Alias de --dry-run",
676
677
  "run.help.option.json": "Imprime el registro de ejecución o el plan de comando como JSON",
678
+ "run.help.option.wait": "Espera bloqueos activos en conflicto antes de ejecutar",
679
+ "run.help.option.waitTimeout": "Segundos máximos para esperar bloqueos activos. Predeterminado: 300",
677
680
  "run.help.option.allowUntrustedRoot": "Permite una ejecución desde una raíz con bloqueo de manifiesto ausente o inválido tras revisión manual",
678
681
  "run.help.exit.ok": "El comando se completo con un codigo de salida permitido",
679
682
  "run.help.exit.fail": "El comando no era válido, fue rechazado, agotó el tiempo o falló",
680
683
  "run.label.suggestedIntentSnippet": "Snippet sugerido para el contrato de comandos",
681
684
  "run.progress.started": "Ejecutando {intent} (timeout: {seconds}s)...",
682
685
  "run.progress.timeoutWarning": "{intent} sigue ejecutándose... ({seconds}s transcurridos, {percent}% del timeout)",
686
+ "run.progress.waitingForActiveLock": "Esperando para ejecutar {intent}; el intent activo {activeIntent} mantiene un bloqueo en conflicto (timeout: {seconds}s)",
683
687
  "run.error.missingIntent": "Falta el nombre del comando",
684
688
  "run.error.unknownIntent": "Comando desconocido: {intent}",
685
689
  "run.error.statusNotConfigured": 'El comando "{intent}" está en estado {status}; sólo se pueden ejecutar comandos configurados',
@@ -702,6 +706,8 @@ Lee estos archivos antes de trabajar:
702
706
  "run.error.maxOutputBytes": 'El comando "{intent}" tiene max_output_bytes no válido. {detail}',
703
707
  "run.error.maxOutputBytesDetail": "El límite de salida debe permanecer dentro del máximo permitido.",
704
708
  "run.error.conflictingPreviewModes": "Usa --dry-run o --plan-only, no ambos",
709
+ "run.error.invalidWaitTimeout": "--wait-timeout debe ser un entero positivo",
710
+ "run.error.waitRequiresExecution": "--wait solo se puede usar al ejecutar un comando, no con --dry-run o --plan-only",
705
711
  "run.error.untrustedRootMissing": "Se rechazó ejecutar comandos porque falta {path}. Ejecuta mf init/update para instalar el flujo, o usa --allow-untrusted-root tras revisar AGENTS.md y .mustflow/config/commands.toml.",
706
712
  "run.error.untrustedRootInvalid": "Se rechazó ejecutar comandos porque el bloqueo de manifiesto no es válido: {detail}. Restáuralo o regenéralo, o usa --allow-untrusted-root tras revisar AGENTS.md y .mustflow/config/commands.toml.",
707
713
  "run.error.timedOut": 'El comando "{intent}" agotó el tiempo después de {seconds} segundos',
@@ -94,8 +94,9 @@ export const frMessages = {
94
94
  "api.help.action.latestEvidence": "Affiche les dernières preuves bounded de run ou verify pour les agents",
95
95
  "api.help.action.diffRisk": "Affiche un résumé compact du risque et de la vérification des fichiers modifiés",
96
96
  "api.help.action.health": "Affiche un résumé compact de santé du workspace",
97
+ "api.help.action.locks": "Affiche les verrous mf run actifs pour coordonner plusieurs sessions",
97
98
  "api.help.exit.ok": "Le rapport API a été inspecté et imprimé",
98
- "api.error.missingAction": "Indiquez une action api : workspace-summary, command-catalog, verification-plan, latest-evidence, diff-risk ou health",
99
+ "api.error.missingAction": "Indiquez une action api : workspace-summary, command-catalog, verification-plan, latest-evidence, diff-risk, health ou locks",
99
100
  "api.error.unknownAction": "Action api inconnue : {action}",
100
101
  "api.error.actionRequiresJson": "{action} exige --json",
101
102
  "api.error.actionRequiresChanged": "{action} exige actuellement --changed",
@@ -674,12 +675,15 @@ Lisez ces fichiers avant de travailler :
674
675
  "run.help.option.dryRun": "Imprime un plan de commande sans l'exécuter",
675
676
  "run.help.option.planOnly": "Alias de --dry-run",
676
677
  "run.help.option.json": "Imprime l'enregistrement d'exécution ou le plan de commande en JSON",
678
+ "run.help.option.wait": "Attend les verrous actifs en conflit avant d'exécuter",
679
+ "run.help.option.waitTimeout": "Nombre maximal de secondes d'attente des verrous actifs. Par défaut : 300",
677
680
  "run.help.option.allowUntrustedRoot": "Autorise une seule exécution depuis une racine sans verrou de manifeste valide après revue manuelle",
678
681
  "run.help.exit.ok": "La commande s'est terminée avec un code de sortie autorisé",
679
682
  "run.help.exit.fail": "La commande était non valide, refusée, expirée ou a échoué",
680
683
  "run.label.suggestedIntentSnippet": "Extrait suggéré de contrat de commande",
681
684
  "run.progress.started": "Exécution de {intent} (timeout : {seconds}s)...",
682
685
  "run.progress.timeoutWarning": "{intent} est toujours en cours... ({seconds}s écoulées, {percent}% du timeout)",
686
+ "run.progress.waitingForActiveLock": "Attente avant d'exécuter {intent} ; l'intention active {activeIntent} détient un verrou en conflit (timeout : {seconds}s)",
683
687
  "run.error.missingIntent": "Nom de commande manquant",
684
688
  "run.error.unknownIntent": "Commande inconnue : {intent}",
685
689
  "run.error.statusNotConfigured": 'La commande "{intent}" est {status} ; seules les commandes configurées peuvent être exécutées',
@@ -702,6 +706,8 @@ Lisez ces fichiers avant de travailler :
702
706
  "run.error.maxOutputBytes": 'La commande "{intent}" a une valeur max_output_bytes non valide. {detail}',
703
707
  "run.error.maxOutputBytesDetail": "La limite de sortie doit rester dans le maximum autorisé.",
704
708
  "run.error.conflictingPreviewModes": "Utilisez --dry-run ou --plan-only, pas les deux",
709
+ "run.error.invalidWaitTimeout": "--wait-timeout doit être un entier positif",
710
+ "run.error.waitRequiresExecution": "--wait ne peut être utilisé que lors de l'exécution d'une commande, pas avec --dry-run ou --plan-only",
705
711
  "run.error.untrustedRootMissing": "Exécution refusée car {path} est absent. Lancez mf init/update pour installer le workflow, ou ajoutez --allow-untrusted-root après avoir relu AGENTS.md et .mustflow/config/commands.toml.",
706
712
  "run.error.untrustedRootInvalid": "Exécution refusée car le verrou de manifeste est invalide : {detail}. Restaurez-le ou régénérez-le, ou ajoutez --allow-untrusted-root après avoir relu AGENTS.md et .mustflow/config/commands.toml.",
707
713
  "run.error.timedOut": 'La commande "{intent}" a expiré après {seconds} secondes',
@@ -94,8 +94,9 @@ export const hiMessages = {
94
94
  "api.help.action.latestEvidence": "agents के लिए bounded latest run या verify evidence प्रिंट करें",
95
95
  "api.help.action.diffRisk": "बदली गई files के लिए compact risk और verification summary प्रिंट करें",
96
96
  "api.help.action.health": "workspace health की compact summary प्रिंट करें",
97
+ "api.help.action.locks": "multi-session coordination के लिए active mf run locks प्रिंट करें",
97
98
  "api.help.exit.ok": "API रिपोर्ट जाँची और प्रिंट की गई",
98
- "api.error.missingAction": "api action बताएँ: workspace-summary, command-catalog, verification-plan, latest-evidence, diff-risk, या health",
99
+ "api.error.missingAction": "api action बताएँ: workspace-summary, command-catalog, verification-plan, latest-evidence, diff-risk, health, या locks",
99
100
  "api.error.unknownAction": "अज्ञात api action: {action}",
100
101
  "api.error.actionRequiresJson": "{action} के लिए --json चाहिए",
101
102
  "api.error.actionRequiresChanged": "{action} को अभी --changed चाहिए",
@@ -674,12 +675,15 @@ export const hiMessages = {
674
675
  "run.help.option.dryRun": "कमांड चलाए बिना उसका plan प्रिंट करें",
675
676
  "run.help.option.planOnly": "--dry-run का alias",
676
677
  "run.help.option.json": "Run record या command plan को JSON के रूप में प्रिंट करें",
678
+ "run.help.option.wait": "चलाने से पहले conflicting active run locks के लिए wait करें",
679
+ "run.help.option.waitTimeout": "active run locks के लिए wait करने की maximum seconds. Default: 300",
677
680
  "run.help.option.allowUntrustedRoot": "Manual review के बाद missing या invalid manifest lock वाली root से एक execution allow करें",
678
681
  "run.help.exit.ok": "कमांड अनुमत exit code के साथ पूरी हुई",
679
682
  "run.help.exit.fail": "कमांड अमान्य थी, अस्वीकार हुई, timed out हुई या विफल हुई",
680
683
  "run.label.suggestedIntentSnippet": "Suggested command contract snippet",
681
684
  "run.progress.started": "{intent} चल रहा है (timeout: {seconds}s)...",
682
685
  "run.progress.timeoutWarning": "{intent} अभी भी चल रहा है... ({seconds}s बीते, timeout का {percent}%)",
686
+ "run.progress.waitingForActiveLock": "{intent} चलाने के लिए wait कर रहे हैं; active intent {activeIntent} conflicting lock रखता है (timeout: {seconds}s)",
683
687
  "run.error.missingIntent": "कमांड नाम नहीं दिया गया",
684
688
  "run.error.unknownIntent": "अज्ञात कमांड: {intent}",
685
689
  "run.error.statusNotConfigured": 'कमांड "{intent}" {status} है; केवल configured कमांड चलाई जा सकती हैं',
@@ -702,6 +706,8 @@ export const hiMessages = {
702
706
  "run.error.maxOutputBytes": 'कमांड "{intent}" में max_output_bytes अमान्य है। {detail}',
703
707
  "run.error.maxOutputBytesDetail": "Output limit अनुमत maximum के अंदर रहनी चाहिए।",
704
708
  "run.error.conflictingPreviewModes": "--dry-run या --plan-only में से एक इस्तेमाल करें, दोनों नहीं",
709
+ "run.error.invalidWaitTimeout": "--wait-timeout positive integer होना चाहिए",
710
+ "run.error.waitRequiresExecution": "--wait केवल command execute करते समय इस्तेमाल हो सकता है, --dry-run या --plan-only के साथ नहीं",
705
711
  "run.error.untrustedRootMissing": "{path} missing है, इसलिए commands execute करने से मना किया गया। Workflow install करने के लिए mf init/update चलाएँ, या AGENTS.md और .mustflow/config/commands.toml review करने के बाद --allow-untrusted-root पास करें।",
706
712
  "run.error.untrustedRootInvalid": "Manifest lock invalid है, इसलिए commands execute करने से मना किया गया: {detail}. इसे restore या regenerate करें, या AGENTS.md और .mustflow/config/commands.toml review करने के बाद --allow-untrusted-root पास करें।",
707
713
  "run.error.timedOut": 'कमांड "{intent}" {seconds} सेकंड बाद time out हुई',
@@ -94,8 +94,9 @@ export const koMessages = {
94
94
  "api.help.action.latestEvidence": "에이전트용 bounded 최신 run 또는 verify evidence를 출력합니다",
95
95
  "api.help.action.diffRisk": "변경 파일 risk와 verification 요약을 작게 출력합니다",
96
96
  "api.help.action.health": "workspace health 요약을 작게 출력합니다",
97
+ "api.help.action.locks": "여러 세션 조율용 활성 mf run 잠금을 출력합니다",
97
98
  "api.help.exit.ok": "API 보고서를 확인하고 출력했습니다",
98
- "api.error.missingAction": "api 작업을 지정하세요: workspace-summary, command-catalog, verification-plan, latest-evidence, diff-risk 또는 health",
99
+ "api.error.missingAction": "api 작업을 지정하세요: workspace-summary, command-catalog, verification-plan, latest-evidence, diff-risk, health 또는 locks",
99
100
  "api.error.unknownAction": "알 수 없는 api 작업: {action}",
100
101
  "api.error.actionRequiresJson": "{action}에는 --json이 필요합니다",
101
102
  "api.error.actionRequiresChanged": "{action}은 현재 --changed가 필요합니다",
@@ -674,12 +675,15 @@ export const koMessages = {
674
675
  "run.help.option.dryRun": "실행하지 않고 명령 계획을 출력합니다",
675
676
  "run.help.option.planOnly": "--dry-run과 같은 동작입니다",
676
677
  "run.help.option.json": "실행 결과 또는 명령 계획을 JSON으로 출력합니다",
678
+ "run.help.option.wait": "충돌하는 활성 실행 잠금이 풀릴 때까지 기다린 뒤 실행합니다",
679
+ "run.help.option.waitTimeout": "활성 실행 잠금을 기다릴 최대 초입니다. 기본값: 300",
677
680
  "run.help.option.allowUntrustedRoot": "잠금 파일이 없거나 올바르지 않은 루트에서 수동 검토 후 이번 실행만 허용합니다",
678
681
  "run.help.exit.ok": "명령이 허용된 종료 코드로 완료되었습니다",
679
682
  "run.help.exit.fail": "명령이 잘못되었거나, 거부되었거나, 시간 초과되었거나, 실패했습니다",
680
683
  "run.label.suggestedIntentSnippet": "제안 명령 계약 조각",
681
684
  "run.progress.started": "{intent} 실행 중(timeout: {seconds}초)...",
682
685
  "run.progress.timeoutWarning": "{intent} 계속 실행 중... ({seconds}초 경과, timeout의 {percent}%)",
686
+ "run.progress.waitingForActiveLock": "{intent} 실행 대기 중; 활성 intent {activeIntent}가 충돌하는 잠금을 보유 중입니다(timeout: {seconds}초)",
683
687
  "run.error.missingIntent": "명령 이름이 없습니다",
684
688
  "run.error.unknownIntent": "알 수 없는 명령: {intent}",
685
689
  "run.error.statusNotConfigured": '명령 "{intent}"의 상태는 {status}입니다. 설정된 상태(configured)인 명령만 실행할 수 있습니다',
@@ -702,6 +706,8 @@ export const koMessages = {
702
706
  "run.error.maxOutputBytes": '명령 "{intent}"의 max_output_bytes 값이 올바르지 않습니다. {detail}',
703
707
  "run.error.maxOutputBytesDetail": "출력 상한은 허용된 최댓값 안에 있어야 합니다.",
704
708
  "run.error.conflictingPreviewModes": "--dry-run과 --plan-only 중 하나만 사용하세요",
709
+ "run.error.invalidWaitTimeout": "--wait-timeout은 양의 정수여야 합니다",
710
+ "run.error.waitRequiresExecution": "--wait는 --dry-run 또는 --plan-only가 아닌 실제 명령 실행에만 사용할 수 있습니다",
705
711
  "run.error.untrustedRootMissing": "{path}이 없어 명령 실행을 거부했습니다. mf init/update로 워크플로우를 설치하거나, AGENTS.md와 .mustflow/config/commands.toml을 검토한 뒤 --allow-untrusted-root를 붙이세요.",
706
712
  "run.error.untrustedRootInvalid": "잠금 파일이 올바르지 않아 명령 실행을 거부했습니다: {detail}. 파일을 복구하거나 다시 생성하거나, AGENTS.md와 .mustflow/config/commands.toml을 검토한 뒤 --allow-untrusted-root를 붙이세요.",
707
713
  "run.error.timedOut": '명령 "{intent}"가 {seconds}초 뒤 시간 초과되었습니다',
@@ -94,8 +94,9 @@ export const zhMessages = {
94
94
  "api.help.action.latestEvidence": "为代理输出 bounded 最新 run 或 verify evidence",
95
95
  "api.help.action.diffRisk": "为已变更文件输出紧凑 risk 和 verification 摘要",
96
96
  "api.help.action.health": "输出紧凑 workspace health 摘要",
97
+ "api.help.action.locks": "输出用于多会话协调的活动 mf run 锁",
97
98
  "api.help.exit.ok": "已检查并输出 API 报告",
98
- "api.error.missingAction": "请指定 api 操作:workspace-summary、command-catalog、verification-plan、latest-evidence、diff-risk 或 health",
99
+ "api.error.missingAction": "请指定 api 操作:workspace-summary、command-catalog、verification-plan、latest-evidence、diff-risk、healthlocks",
99
100
  "api.error.unknownAction": "未知 api 操作:{action}",
100
101
  "api.error.actionRequiresJson": "{action} 需要 --json",
101
102
  "api.error.actionRequiresChanged": "{action} 当前需要 --changed",
@@ -674,12 +675,15 @@ export const zhMessages = {
674
675
  "run.help.option.dryRun": "输出命令计划但不执行",
675
676
  "run.help.option.planOnly": "--dry-run 的别名",
676
677
  "run.help.option.json": "将运行记录或命令计划输出为 JSON",
678
+ "run.help.option.wait": "执行前等待冲突的活动运行锁释放",
679
+ "run.help.option.waitTimeout": "等待活动运行锁的最大秒数。默认值:300",
677
680
  "run.help.option.allowUntrustedRoot": "人工复核后,允许从缺失或无效清单锁的根目录执行一次命令",
678
681
  "run.help.exit.ok": "命令已以允许的退出码完成",
679
682
  "run.help.exit.fail": "命令无效、被拒绝、超时或失败",
680
683
  "run.label.suggestedIntentSnippet": "建议的命令契约片段",
681
684
  "run.progress.started": "正在运行 {intent}(超时:{seconds} 秒)...",
682
685
  "run.progress.timeoutWarning": "{intent} 仍在运行...(已用 {seconds} 秒,达到超时的 {percent}%)",
686
+ "run.progress.waitingForActiveLock": "正在等待运行 {intent};活动 intent {activeIntent} 持有冲突锁(超时:{seconds} 秒)",
683
687
  "run.error.missingIntent": "缺少命令名称",
684
688
  "run.error.unknownIntent": "未知命令:{intent}",
685
689
  "run.error.statusNotConfigured": '命令 "{intent}" 的状态为 {status};只能运行已配置的命令',
@@ -702,6 +706,8 @@ export const zhMessages = {
702
706
  "run.error.maxOutputBytes": '命令 "{intent}" 的 max_output_bytes 无效。{detail}',
703
707
  "run.error.maxOutputBytesDetail": "输出限制必须保持在允许的最大值内。",
704
708
  "run.error.conflictingPreviewModes": "只能使用 --dry-run 或 --plan-only,不能同时使用",
709
+ "run.error.invalidWaitTimeout": "--wait-timeout 必须是正整数",
710
+ "run.error.waitRequiresExecution": "--wait 只能用于实际执行命令,不能与 --dry-run 或 --plan-only 一起使用",
705
711
  "run.error.untrustedRootMissing": "已拒绝执行命令,因为缺少 {path}。请运行 mf init/update 安装工作流,或在检查 AGENTS.md 和 .mustflow/config/commands.toml 后传入 --allow-untrusted-root。",
706
712
  "run.error.untrustedRootInvalid": "已拒绝执行命令,因为清单锁无效:{detail}。请恢复或重新生成它,或在检查 AGENTS.md 和 .mustflow/config/commands.toml 后传入 --allow-untrusted-root。",
707
713
  "run.error.timedOut": '命令 "{intent}" 在 {seconds} 秒后超时',
@@ -268,6 +268,16 @@ export function inspectActiveRunLocks(projectRoot, contract, intentName) {
268
268
  staleRecords,
269
269
  };
270
270
  }
271
+ export function listActiveRunLocks(projectRoot) {
272
+ const records = readActiveRecords(projectRoot);
273
+ const staleRecords = records.map(staleRecordFor).filter((record) => record !== null);
274
+ const activeRecords = records.filter((record) => !staleRecords.some((stale) => stale.runId === record.run_id));
275
+ return {
276
+ records,
277
+ activeRecords,
278
+ staleRecords,
279
+ };
280
+ }
271
281
  export function acquireActiveRunLock(projectRoot, contract, intentName, options = {}) {
272
282
  const effects = normalizeCommandEffects(projectRoot, contract, intentName);
273
283
  if (effects.length === 0) {
@@ -71,6 +71,14 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
71
71
  documented: true,
72
72
  installedCommand: ['mf', 'api', 'health', '--json'],
73
73
  },
74
+ {
75
+ id: 'locks',
76
+ schemaFile: 'locks.schema.json',
77
+ producer: 'mf api locks --json',
78
+ packaged: true,
79
+ documented: true,
80
+ installedCommand: ['mf', 'api', 'locks', '--json'],
81
+ },
74
82
  {
75
83
  id: 'run-receipt',
76
84
  schemaFile: 'run-receipt.schema.json',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.22.47",
3
+ "version": "2.22.49",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
package/schemas/README.md CHANGED
@@ -14,6 +14,7 @@ Current schemas:
14
14
  - `latest-evidence.schema.json`: output of `mf api latest-evidence --json`
15
15
  - `diff-risk.schema.json`: output of `mf api diff-risk --changed --json`
16
16
  - `health.schema.json`: output of `mf api health --json`
17
+ - `locks.schema.json`: output of `mf api locks --json`
17
18
  - `run-receipt.schema.json`: output of `mf run <intent> --json` and `.mustflow/state/runs/latest.json`,
18
19
  including bounded declared-write drift metadata, a safe latest-run performance summary, and optional
19
20
  structured phase timings and selection summaries
@@ -37,6 +37,7 @@
37
37
  "update_policies": { "$ref": "#/$defs/stringArray" },
38
38
  "drift_checks": { "$ref": "#/$defs/stringArray" },
39
39
  "required_verification": { "$ref": "#/$defs/stringArray" },
40
+ "residual_corrections": { "$ref": "#/$defs/residualCorrections" },
40
41
  "gap_count": { "type": "integer", "minimum": 0 },
41
42
  "gaps": {
42
43
  "type": "array",
@@ -69,6 +70,80 @@
69
70
  "surfaces": { "$ref": "#/$defs/stringArray" },
70
71
  "detail": { "type": "string" }
71
72
  }
73
+ },
74
+ "residualCorrections": {
75
+ "type": "object",
76
+ "additionalProperties": false,
77
+ "required": [
78
+ "status",
79
+ "mode",
80
+ "grants_command_authority",
81
+ "applies_to_current_plan",
82
+ "selected_intent_additions",
83
+ "suggested_intent_additions",
84
+ "recommended_commands",
85
+ "signals",
86
+ "policy",
87
+ "issues"
88
+ ],
89
+ "properties": {
90
+ "status": { "enum": ["not_enough_evidence", "available", "unavailable"] },
91
+ "mode": { "const": "read_only" },
92
+ "grants_command_authority": { "const": false },
93
+ "applies_to_current_plan": { "type": "boolean" },
94
+ "selected_intent_additions": { "$ref": "#/$defs/stringArray" },
95
+ "suggested_intent_additions": { "$ref": "#/$defs/stringArray" },
96
+ "recommended_commands": { "$ref": "#/$defs/stringArray" },
97
+ "signals": {
98
+ "type": "array",
99
+ "items": { "$ref": "#/$defs/residualSignal" }
100
+ },
101
+ "policy": { "$ref": "#/$defs/residualPolicy" },
102
+ "issues": { "$ref": "#/$defs/stringArray" }
103
+ }
104
+ },
105
+ "residualPolicy": {
106
+ "type": "object",
107
+ "additionalProperties": false,
108
+ "required": [
109
+ "single_observation_changes_plan",
110
+ "minimum_repeated_samples_for_plan_change",
111
+ "automatic_intent_additions_enabled"
112
+ ],
113
+ "properties": {
114
+ "single_observation_changes_plan": { "const": false },
115
+ "minimum_repeated_samples_for_plan_change": { "const": 2 },
116
+ "automatic_intent_additions_enabled": { "const": false }
117
+ }
118
+ },
119
+ "residualSignal": {
120
+ "type": "object",
121
+ "additionalProperties": false,
122
+ "required": [
123
+ "kind",
124
+ "confidence",
125
+ "evidence_count",
126
+ "source",
127
+ "verification_plan_id",
128
+ "affected_reasons",
129
+ "affected_intents",
130
+ "detail",
131
+ "recommendation"
132
+ ],
133
+ "properties": {
134
+ "kind": { "enum": ["latest_unresolved_plan", "repeated_failure", "remaining_risk"] },
135
+ "confidence": { "enum": ["low", "medium", "high"] },
136
+ "evidence_count": { "type": "integer", "minimum": 1 },
137
+ "source": { "const": "latest-evidence" },
138
+ "verification_plan_id": {
139
+ "type": ["string", "null"],
140
+ "pattern": "^sha256:[0-9a-f]{64}$"
141
+ },
142
+ "affected_reasons": { "$ref": "#/$defs/stringArray" },
143
+ "affected_intents": { "$ref": "#/$defs/stringArray" },
144
+ "detail": { "type": "string" },
145
+ "recommendation": { "enum": ["provide_new_evidence", "review_remaining_risk"] }
146
+ }
72
147
  }
73
148
  }
74
149
  }
@@ -0,0 +1,102 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://mustflow.github.io/schemas/locks.schema.json",
4
+ "title": "mustflow active locks API report",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "command",
10
+ "mustflow_root",
11
+ "status",
12
+ "lock_root",
13
+ "active_count",
14
+ "stale_count",
15
+ "active_locks",
16
+ "stale_locks",
17
+ "policy",
18
+ "recommended_commands",
19
+ "issues"
20
+ ],
21
+ "properties": {
22
+ "schema_version": { "const": "1" },
23
+ "command": { "const": "api locks" },
24
+ "mustflow_root": { "type": "string" },
25
+ "status": { "enum": ["clear", "active", "stale", "unavailable"] },
26
+ "lock_root": { "const": ".mustflow/state/locks" },
27
+ "active_count": { "type": "integer", "minimum": 0 },
28
+ "stale_count": { "type": "integer", "minimum": 0 },
29
+ "active_locks": {
30
+ "type": "array",
31
+ "items": { "$ref": "#/$defs/activeLock" }
32
+ },
33
+ "stale_locks": {
34
+ "type": "array",
35
+ "items": { "$ref": "#/$defs/staleLock" }
36
+ },
37
+ "policy": { "$ref": "#/$defs/policy" },
38
+ "recommended_commands": { "$ref": "#/$defs/stringArray" },
39
+ "issues": { "$ref": "#/$defs/stringArray" }
40
+ },
41
+ "$defs": {
42
+ "stringArray": {
43
+ "type": "array",
44
+ "items": { "type": "string" }
45
+ },
46
+ "activeLock": {
47
+ "type": "object",
48
+ "additionalProperties": false,
49
+ "required": ["run_id", "intent", "pid", "started_at", "command_hash", "effects", "writes"],
50
+ "properties": {
51
+ "run_id": { "type": "string" },
52
+ "intent": { "type": "string" },
53
+ "pid": { "type": "integer" },
54
+ "started_at": { "type": "string" },
55
+ "command_hash": {
56
+ "type": ["string", "null"],
57
+ "pattern": "^sha256:[0-9a-f]{64}$"
58
+ },
59
+ "effects": {
60
+ "type": "array",
61
+ "items": { "$ref": "#/$defs/effect" }
62
+ },
63
+ "writes": { "$ref": "#/$defs/stringArray" }
64
+ }
65
+ },
66
+ "effect": {
67
+ "type": "object",
68
+ "additionalProperties": false,
69
+ "required": ["source", "access", "mode", "path", "lock", "concurrency"],
70
+ "properties": {
71
+ "source": { "type": "string" },
72
+ "access": { "type": "string" },
73
+ "mode": { "type": "string" },
74
+ "path": { "type": ["string", "null"] },
75
+ "lock": { "type": "string" },
76
+ "concurrency": { "type": "string" }
77
+ }
78
+ },
79
+ "staleLock": {
80
+ "type": "object",
81
+ "additionalProperties": false,
82
+ "required": ["run_id", "intent", "pid", "reason"],
83
+ "properties": {
84
+ "run_id": { "type": "string" },
85
+ "intent": { "type": "string" },
86
+ "pid": { "type": "integer" },
87
+ "reason": { "type": "string" }
88
+ }
89
+ },
90
+ "policy": {
91
+ "type": "object",
92
+ "additionalProperties": false,
93
+ "required": ["source", "conflict_model", "direct_commands_bypass_locks", "wait_entrypoint"],
94
+ "properties": {
95
+ "source": { "const": "active-run-locks" },
96
+ "conflict_model": { "const": "command_effects_and_writes" },
97
+ "direct_commands_bypass_locks": { "const": true },
98
+ "wait_entrypoint": { "const": "mf run <intent> --wait" }
99
+ }
100
+ }
101
+ }
102
+ }
@@ -1,6 +1,6 @@
1
1
  id = "default"
2
2
  name = "default"
3
- version = "2.22.47"
3
+ version = "2.22.49"
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"