mustflow 2.22.47 → 2.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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.23.0",
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
+ }
@@ -56,7 +56,7 @@ translations = {}
56
56
  [documents."skills.index"]
57
57
  source = "locales/en/.mustflow/skills/INDEX.md"
58
58
  source_locale = "en"
59
- revision = 81
59
+ revision = 82
60
60
  translations = {}
61
61
 
62
62
  [documents."skill.adapter-boundary"]
@@ -137,6 +137,12 @@ source_locale = "en"
137
137
  revision = 1
138
138
  translations = {}
139
139
 
140
+ [documents."skill.version-freshness-check"]
141
+ source = "locales/en/.mustflow/skills/version-freshness-check/SKILL.md"
142
+ source_locale = "en"
143
+ revision = 1
144
+ translations = {}
145
+
140
146
  [documents."skill.line-ending-hygiene"]
141
147
  source = "locales/en/.mustflow/skills/line-ending-hygiene/SKILL.md"
142
148
  source_locale = "en"
@@ -2,7 +2,7 @@
2
2
  mustflow_doc: skills.index
3
3
  locale: en
4
4
  canonical: true
5
- revision: 81
5
+ revision: 82
6
6
  authority: router
7
7
  lifecycle: mustflow-owned
8
8
  ---
@@ -151,6 +151,7 @@ routes. Event routes stay inactive until their event occurs.
151
151
  | Database migration files, schema migration history, ORM schema migrations, generated clients, schema dumps, SQL snapshots, backfills, rolling deploy compatibility, expand-and-contract changes, destructive database changes, migration rollback claims, or production database migration procedures are created, changed, reviewed, or reported | `.mustflow/skills/database-migration-change/SKILL.md` | Source schema, target schema, migration files, migration history, generated clients, schema dumps, SQL snapshots, affected queries, deployment shape, database engine, table size or lock assumptions, backfill plan, rollback type, validation query, and command contract entries | Migration files, ORM schemas, generated clients, schema dumps, SQL snapshots, backfill code, validation checks, seeds, fixtures, compatibility code, docs, tests, and directly synchronized examples | data loss, drop-plus-add rename, old/new app incompatibility, unsafe rolling deploy, unbounded backfill, production lock, generated-client drift, migration-history drift, false rollback claim, ORM autogenerate mistake, or destructive contract mixed with expand phase | `changes_status`, `changes_diff_summary`, `lint`, `build`, `test_related`, `test`, `docs_validate_fast`, `test_release`, `mustflow_check` | Migration phase, old/new schema compatibility, backfill and validation plan, rollback classification, ORM/generated/schema dump surfaces, dependent surfaces, verification, and remaining database-migration risk |
152
152
  | Dependency versions, lockfiles, package-manager metadata, workspace constraints, runtime engines, peer dependencies, optional dependencies, security advisory fixes, generated dependency output, framework plugins, CI actions, Docker base images, package manager behavior, or toolchain versions are upgraded, downgraded, pinned, widened, regenerated, reviewed, or reported | `.mustflow/skills/dependency-upgrade-review/SKILL.md` | Dependency name, old and new versions or ranges, direct or transitive path, ecosystem and package manager, declaration files, lockfiles, runtime or toolchain files, advisory or release-note evidence, generated outputs, callers, docs, package output, Docker or CI surfaces, and command contract entries | Package declarations, lockfiles, generated outputs, compatibility code, tests, docs, package metadata, Docker or CI files, and directly synchronized examples | lockfile churn, hidden transitive replacement, peer or engine break, module-format drift, native or optional package break, framework or generator output drift, unsafe broad security update, weakened tests, Docker or CI runtime drift, or unreviewed supply-chain change | `changes_status`, `changes_diff_summary`, `lint`, `build`, `test_related`, `test`, `docs_validate_fast`, `test_release`, `mustflow_check` | Upgrade reason, ecosystem surface, direct and transitive graph changes, compatibility classification, runtime/peer/engine/module/feature/platform/generated-output risks, synchronized surfaces, verification, and remaining dependency-upgrade risk |
153
153
  | Dependency, package, runtime, framework, tool, command, plugin, service, platform capability, supported-version policy, security patch path, ecosystem maturity claim, maintainer-risk assumption, runtime portability claim, edge or serverless compatibility claim, critical-path library choice, package script, lifecycle hook, binary download, lockfile, audit result, or supply-chain-sensitive dependency surface is assumed, added, removed, imported, invoked, installed, audited, or documented | `.mustflow/skills/dependency-reality-check/SKILL.md` | Assumed dependency or capability, declaration files, version or feature expectation, role criticality, supported-version or end-of-life evidence, patchability expectation, runtime compatibility boundary, maintainer and ecosystem evidence when available, lockfile entry, package script or lifecycle hook, audit or provenance evidence, and relevant command intents | Package metadata, lockfiles, imports, scripts, command contracts, docs, tests, runtime policy notes, portability notes, and reports | unavailable dependency, hallucinated or lookalike package, fragile single-maintainer core dependency, experimental technology in a survival path, unsupported runtime, unclear security patch path, runtime-specific API leakage into core logic, stale version claim, lifecycle script risk, audit suppression, lockfile drift, or install guidance mismatch | `changes_status`, `changes_diff_summary`, `build`, `test_release`, `mustflow_check` | Dependency checked, ecosystem and maintainer-risk boundary reviewed, supported-version, patchability, and runtime-portability boundary reviewed, supply-chain surface reviewed, declarations synchronized, verification, and remaining dependency risk |
154
+ | Generated or edited code, configuration, CI workflows, package metadata, install instructions, examples, Docker images, framework setup, runtime declarations, toolchain declarations, or migration-sensitive snippets introduce explicit external version references, action refs, package ranges, runtime versions, framework majors, Docker image tags, or scaffold commands that may be stale | `.mustflow/skills/version-freshness-check/SKILL.md` | Versioned reference, owning files, repository version policy, approved freshness source, compatibility context, migration risk, and command contract entries | Package metadata, lockfiles, CI workflows, Dockerfiles, runtime files, framework config, docs, examples, templates, tests, and version-decision reports | stale default version, false latest claim, accidental major migration, repository policy mismatch, unsupported generated example, floating-tag drift, or unverified security/support claim | `changes_status`, `changes_diff_summary`, `build`, `test_related`, `docs_validate_fast`, `test_release`, `mustflow_check` | Versioned surfaces checked, repository policy and freshness source, selected version track, compatibility classification, approval need, synchronized surfaces, verification, and remaining version-freshness risk |
154
155
  | External systems, protocols, SDKs, databases, webhooks, queues, files, object storage, signed upload or download URLs, caches, API response models, framework requests or responses, server actions, route handlers, edge functions, worker handlers, AI models, browser storage, search engines, analytics tools, email platforms, no-code tools, observability backends, trace or request context, or provider data cross the core boundary or need port/adapter translation, error mapping, timeout, retry, circuit-breaker, bulkhead, idempotency, reconciliation, security, core-state ownership, vendor portability, or observability handling | `.mustflow/skills/adapter-boundary/SKILL.md` | External system or protocol, inbound/outbound direction, delivery boundary, internal use case, local port/adapter patterns, provider risk, provider failure policy, core-state ownership risk, vendor portability risk, observability identifier policy, API contract risk, changed files, and command contract entries | Ports, adapters, mappers, controllers, workers, stores, gateways, response mappers, telemetry mappers, timeout and retry policies, circuit breakers, bulkhead boundaries, tests, fixtures, assembly wiring, and directly synchronized docs or templates | provider leakage, framework business-rule leakage, telemetry backend leakage, storage-key leakage, screen-shaped API coupling, pass-through wrapper, SaaS dashboard as truth source, search or analytics policy leakage, queue contract leakage, unclassified external failure, duplicate side effect, unsafe retry, missing timeout, missing circuit breaker, missing bulkhead, unresolved unknown provider outcome, broken identifier propagation, secret or personal-data leak, or untested integration drift | `changes_status`, `changes_diff_summary`, `test_related`, `test`, `lint`, `build`, `docs_validate_fast`, `test_release`, `mustflow_check` | Boundary classification, delivery adapter responsibility, internal port, provider containment, core-state ownership, vendor portability, validation and mapping, API response mapping, observability identifier flow, timeout/retry/circuit-breaker/bulkhead/idempotency handling, reconciliation behavior, security notes, verification, and remaining provider risk |
155
156
  | Tauri frontend invokes, Rust commands, capabilities, permissions, scopes, plugins, filesystem, dialog, shell, opener, updater, sidecar, or mobile native permissions are created or changed | `.mustflow/skills/tauri-code-change/SKILL.md` | Frontend call sites, Tauri config, Rust commands, capability and permission files, plugin config, changed files, and command contract entries | Tauri frontend, Rust commands, capabilities, permissions, scopes, plugins, tests, and docs | broad native permission, untrusted IPC input, filesystem escape, shell or updater risk, or WebView/native boundary drift | `changes_status`, `changes_diff_summary`, `lint`, `build`, `test_related`, `test`, `docs_validate_fast`, `mustflow_check` | IPC, permission, scope, filesystem, shell, updater, and native boundary checked, verification, and remaining Tauri risk |
156
157
  | File path handling, cross-platform path behavior, path helpers, safe filesystem wrappers, temp or cache paths, atomic writes, locks, archive extraction, uploads, downloads, scanners, CLI/API/schema path contracts, snapshots, generated outputs, or package artifact paths are created, changed, reviewed, or reported | `.mustflow/skills/file-path-cross-platform-change/SKILL.md` | Path ledger, trust classes, accepted path representation, base root, path helpers, safe filesystem wrappers, temp/cache helpers, lock policy, archive policy, upload/download policy, scanner policy, CLI/API/schema/snapshot/generated/package surfaces, platform expectations, and command contract entries | Path validators, helpers, wrappers, schemas, CLI/API parsing, snapshots, fixtures, docs, tests, generated-output paths, package artifact paths, archive extraction, scanner bounds, temp/cache handling, locks, and cleanup code | path traversal, base containment bypass, drive-relative path bug, reserved-name bug, case-collision bug, symlink or junction escape, unsafe archive extraction, non-atomic write claim, stale lock, scanner loop, cleanup data loss, path contract drift, or package artifact path drift | `changes_status`, `changes_diff_summary`, `lint`, `build`, `test_related`, `test`, `docs_validate_fast`, `test_release`, `mustflow_check` | Path contract, path ledger, trust classes, root policy, Windows/macOS/Linux/archive/upload/download/scanner/lock/temp/cache/atomic/cleanup decisions, synchronized contract surfaces, verification, and remaining path risk |
@@ -228,6 +228,12 @@ route_type = "adjunct"
228
228
  priority = 45
229
229
  applies_to_reasons = ["code_change", "docs_change", "security_change"]
230
230
 
231
+ [routes."version-freshness-check"]
232
+ category = "data_external"
233
+ route_type = "adjunct"
234
+ priority = 68
235
+ applies_to_reasons = ["code_change", "docs_change", "package_metadata_change", "mustflow_config_change", "release_risk"]
236
+
231
237
  [routes."file-path-cross-platform-change"]
232
238
  category = "data_external"
233
239
  route_type = "primary"
@@ -0,0 +1,141 @@
1
+ ---
2
+ mustflow_doc: skill.version-freshness-check
3
+ locale: en
4
+ canonical: true
5
+ revision: 1
6
+ lifecycle: mustflow-owned
7
+ authority: procedure
8
+ name: version-freshness-check
9
+ description: Apply this skill when generated or edited code, configuration, CI workflows, package metadata, install instructions, examples, Docker images, framework setup, runtime declarations, toolchain declarations, or migration-sensitive snippets introduce explicit external version references that may be stale.
10
+ metadata:
11
+ mustflow_schema: "1"
12
+ mustflow_kind: procedure
13
+ pack_id: mustflow.core
14
+ skill_id: mustflow.core.version-freshness-check
15
+ command_intents:
16
+ - changes_status
17
+ - changes_diff_summary
18
+ - build
19
+ - test_related
20
+ - docs_validate_fast
21
+ - test_release
22
+ - mustflow_check
23
+ ---
24
+
25
+ # Version Freshness Check
26
+
27
+ <!-- mustflow-section: purpose -->
28
+ ## Purpose
29
+
30
+ Prevent agents from writing stale external version references from memory, while avoiding blind upgrades that ignore repository policy, compatibility, or migration cost.
31
+
32
+ <!-- mustflow-section: use-when -->
33
+ ## Use When
34
+
35
+ - Generated or edited files introduce explicit external version references, action refs, package ranges, runtime versions, framework majors, Docker image tags, toolchain versions, setup actions, scaffold commands, install commands, or migration examples.
36
+ - CI workflows, release workflows, Dockerfiles, package metadata, lockfiles, runtime files, framework configuration, README examples, docs, tests, fixtures, or templates mention external versions such as GitHub Actions refs, Node, Bun, Deno, Python, Rust, Tauri, Astro, Next, SvelteKit, Electron, Docker images, package managers, SDKs, plugins, or generators.
37
+ - An agent proposes a versioned dependency, tool, framework, action, image, or runtime based on memory, copied snippets, older project examples, or user-provided text that may be stale.
38
+ - The task asks whether a newer stable, recommended, LTS, or security-patched version should replace a version the agent was about to write.
39
+ - A patch claims a version is latest, current, recommended, stable, LTS, supported, deprecated, end-of-life, or migration-safe.
40
+
41
+ <!-- mustflow-section: do-not-use-when -->
42
+ ## Do Not Use When
43
+
44
+ - The version reference is purely repository-local, such as an internal schema revision, fixture id, or package version already handled by `date-number-audit`.
45
+ - The task only preserves an existing pinned external version without touching code, docs, examples, package metadata, CI, Docker, runtime declarations, or compatibility claims.
46
+ - The task is a deliberate dependency upgrade, downgrade, lockfile refresh, or security advisory fix; use `dependency-upgrade-review` as the main skill and this skill only for freshness-specific claims if needed.
47
+ - The task only checks whether a dependency exists or whether a package name is real; use `dependency-reality-check` first.
48
+ - The user explicitly requests an offline-only draft and accepts that version freshness will be reported as unverified.
49
+
50
+ <!-- mustflow-section: required-inputs -->
51
+ ## Required Inputs
52
+
53
+ - The versioned external reference being introduced, changed, preserved, or reported.
54
+ - Files that own or repeat the version: package metadata, lockfiles, workflow files, Dockerfiles, runtime files, framework config, docs, examples, templates, fixtures, and tests.
55
+ - Repository version policy if present: pinned ranges, lockfile expectations, LTS policy, security patch policy, supported runtime matrix, migration notes, downgrade constraints, or organization rules.
56
+ - Approved freshness evidence when available: official docs, upstream repository releases, package registry metadata, image registry metadata, official migration notes, security advisory ranges, or existing repository-maintained snapshots.
57
+ - Compatibility context: new project or existing project, patch/minor/major difference, framework adapter/plugin compatibility, runtime engine support, generated output, migration burden, rollback path, and whether the version touches a survival path.
58
+ - Relevant command-intent contract entries for build, tests, docs, packaging, or mustflow validation.
59
+
60
+ <!-- mustflow-section: preconditions -->
61
+ ## Preconditions
62
+
63
+ - The task matches the Use When conditions and does not match the Do Not Use When exclusions.
64
+ - Higher-priority instructions and `.mustflow/config/commands.toml` have been checked for the current scope.
65
+ - Freshness evidence can be gathered from allowed local files, configured tooling, approved connectors, official sources, package metadata, registry metadata, or the user-provided source text. If none is available, the check must be reported as unverified rather than guessed.
66
+
67
+ <!-- mustflow-section: allowed-edits -->
68
+ ## Allowed Edits
69
+
70
+ - Align versioned references across package metadata, workflow files, runtime declarations, templates, docs, examples, and tests when the repository policy and compatibility classification support the change.
71
+ - Replace stale generated defaults with a verified stable, recommended, LTS, or repository-pinned value when the change is compatible and within scope.
72
+ - Add conservative wording when a version was not refreshed or when multiple legitimate version tracks exist.
73
+ - Do not force the newest major version, floating tag, or broad range when the repository pins a different supported track.
74
+ - Do not claim a version is current, latest, stable, recommended, LTS, deprecated, or secure unless the claim was refreshed or clearly marked as snapshot-only.
75
+ - Do not add package-manager, registry, browser, or network commands to the skill. Use configured command intents or report missing verification.
76
+
77
+ <!-- mustflow-section: procedure -->
78
+ ## Procedure
79
+
80
+ 1. Build a version ledger before editing: each external version reference, where it appears, whether it is new or existing, and whether it is code, config, CI, Docker, package metadata, docs, template, fixture, or test data.
81
+ 2. Check repository policy before upstream freshness: package and lock metadata, runtime files, CI matrices, Docker tags, supported-version docs, migration notes, existing examples, and command contracts.
82
+ 3. Identify the intended track for each reference: repository-pinned, lockfile-resolved, latest stable, recommended major, LTS, security-patched minimum, compatibility range, floating tag, digest-pinned image, or snapshot-only example.
83
+ 4. Refresh stale-sensitive external facts with the highest-authority allowed source available. Prefer official docs, upstream releases, package registry metadata, official migration notes, official image metadata, or user-provided current evidence over secondary summaries.
84
+ 5. If freshness cannot be checked with the available tools or permissions, keep the version conservative, avoid current-version claims, and report the unchecked source boundary.
85
+ 6. Compare the proposed value, repository policy, and upstream evidence. Classify the difference as `same`, `patch`, `minor`, `major`, `migration-required`, `security-minimum`, `policy-pinned`, `floating`, or `unknown`.
86
+ 7. Treat major, migration-required, pre-1.0, framework, runtime-engine, CI-action, Docker-image, generator, native, security-sensitive, and survival-path changes as higher risk even when the version number looks small.
87
+ 8. For new projects or new examples, prefer the verified stable or officially recommended track unless the repository policy pins another track.
88
+ 9. For existing projects, do not cross a major, migration-required, engine, framework, CI-image, or generated-output boundary without user approval or explicit repository policy.
89
+ 10. For patch, security-minimum, and low-risk minor differences, update only when the declaration, examples, lockfile policy, and verification surface can stay aligned. Otherwise report the proposed change and leave the pinned value unchanged.
90
+ 11. For GitHub Actions and CI tools, review the action source, major tag policy, runtime support, cache behavior, permissions, and organization pinning rule. Do not assume a newer major is safe only because it exists.
91
+ 12. For framework and runtime majors such as Astro, Tauri, Electron, Next, SvelteKit, Node, Bun, Deno, Python, Rust, or Java, check migration notes, config schema, plugin and adapter compatibility, generated files, security model, deployment target, and rollback path before recommending a major change.
92
+ 13. For Docker images, decide whether the project prefers semver tags, distro tags, LTS tags, date tags, or digests. Do not replace a digest or pinned base image with a floating tag unless the repository policy says so.
93
+ 14. Synchronize every accepted version decision across package metadata, lockfiles when intentionally updated, CI, Docker, runtime files, docs, examples, templates, tests, and release notes.
94
+ 15. Run the narrowest configured verification that covers the changed versioned surface. Use broader verification for major, migration-required, runtime, framework, generated-output, package-publish, Docker, CI, or security-sensitive changes.
95
+
96
+ <!-- mustflow-section: postconditions -->
97
+ ## Postconditions
98
+
99
+ - Every touched external version reference has a ledger entry with repository policy, freshness evidence, compatibility classification, and final decision.
100
+ - Stale model defaults are not silently written as if they were current.
101
+ - Repository-pinned versions are preserved unless the task, policy, and compatibility classification support changing them.
102
+ - Major or migration-required changes are either explicitly approved, deferred with a recommendation, or left unchanged with the risk reported.
103
+ - Docs and examples do not make unverifiable current-version claims.
104
+
105
+ <!-- mustflow-section: verification -->
106
+ ## Verification
107
+
108
+ Use configured oneshot command intents when available:
109
+
110
+ - `changes_status`
111
+ - `changes_diff_summary`
112
+ - `build`
113
+ - `test_related`
114
+ - `docs_validate_fast`
115
+ - `test_release`
116
+ - `mustflow_check`
117
+
118
+ Choose the narrowest configured intent that proves the changed versioned surface. Report missing dependency, package, docs, Docker, CI, or release verification instead of inventing commands.
119
+
120
+ <!-- mustflow-section: failure-handling -->
121
+ ## Failure Handling
122
+
123
+ - If repository policy and upstream evidence disagree, preserve the repository policy unless the user explicitly chooses a migration or the existing version is outside a required security or support range.
124
+ - If official sources conflict, prefer the source that owns the artifact being referenced and report the conflict.
125
+ - If a freshness check requires network, credentials, or a connector that is not available, report the boundary and avoid current-version claims.
126
+ - If a proposed major or migration-required version is better for greenfield work but risky for the existing project, present both choices and ask before changing the project.
127
+ - If verification fails after a freshness update, do not weaken tests, lower type checks, delete lockfiles, or widen ranges to make the update pass. Revert or narrow the version decision unless the behavior change is intentional.
128
+
129
+ <!-- mustflow-section: output-format -->
130
+ ## Output Format
131
+
132
+ - Versioned surfaces checked
133
+ - Repository version policy found or missing
134
+ - Freshness source checked or unavailable
135
+ - Proposed and selected version track
136
+ - Compatibility classification: `same`, `patch`, `minor`, `major`, `migration-required`, `security-minimum`, `policy-pinned`, `floating`, or `unknown`
137
+ - User approval needed or not, with reason
138
+ - Surfaces synchronized
139
+ - Command intents run
140
+ - Skipped freshness or verification checks and reasons
141
+ - Remaining version freshness risk
@@ -1,6 +1,6 @@
1
1
  id = "default"
2
2
  name = "default"
3
- version = "2.22.47"
3
+ version = "2.23.0"
4
4
  description = "Minimal workflow for LLM agents to read, edit, and verify their work in a repository."
5
5
  common_root = "common"
6
6
  locales_root = "locales"
@@ -48,6 +48,7 @@ creates = [
48
48
  ".mustflow/skills/dependency-injection/SKILL.md",
49
49
  ".mustflow/skills/dependency-reality-check/SKILL.md",
50
50
  ".mustflow/skills/dependency-upgrade-review/SKILL.md",
51
+ ".mustflow/skills/version-freshness-check/SKILL.md",
51
52
  ".mustflow/skills/diff-risk-review/SKILL.md",
52
53
  ".mustflow/skills/pure-core-imperative-shell/SKILL.md",
53
54
  ".mustflow/skills/result-option/SKILL.md",
@@ -147,6 +148,7 @@ minimal = [
147
148
  "database-migration-change",
148
149
  "dependency-reality-check",
149
150
  "dependency-upgrade-review",
151
+ "version-freshness-check",
150
152
  "diff-risk-review",
151
153
  "docs-update",
152
154
  "external-prompt-injection-defense",
@@ -200,6 +202,7 @@ patterns = [
200
202
  "dependency-injection",
201
203
  "dependency-reality-check",
202
204
  "dependency-upgrade-review",
205
+ "version-freshness-check",
203
206
  "diff-risk-review",
204
207
  "docs-update",
205
208
  "external-prompt-injection-defense",
@@ -263,6 +266,7 @@ oss = [
263
266
  "dependency-injection",
264
267
  "dependency-reality-check",
265
268
  "dependency-upgrade-review",
269
+ "version-freshness-check",
266
270
  "diff-risk-review",
267
271
  "docs-prose-review",
268
272
  "docs-update",
@@ -335,6 +339,7 @@ team = [
335
339
  "dependency-injection",
336
340
  "dependency-reality-check",
337
341
  "dependency-upgrade-review",
342
+ "version-freshness-check",
338
343
  "diff-risk-review",
339
344
  "docs-update",
340
345
  "external-prompt-injection-defense",
@@ -395,6 +400,7 @@ product = [
395
400
  "dependency-injection",
396
401
  "dependency-reality-check",
397
402
  "dependency-upgrade-review",
403
+ "version-freshness-check",
398
404
  "diff-risk-review",
399
405
  "docs-update",
400
406
  "external-prompt-injection-defense",
@@ -463,6 +469,7 @@ library = [
463
469
  "dependency-injection",
464
470
  "dependency-reality-check",
465
471
  "dependency-upgrade-review",
472
+ "version-freshness-check",
466
473
  "diff-risk-review",
467
474
  "docs-prose-review",
468
475
  "docs-update",