mustflow 2.74.0 → 2.74.1

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
@@ -334,7 +334,7 @@ Command environments remove the project-local `node_modules/.bin` path from `PAT
334
334
 
335
335
  Use `mf verify --reason <event> --plan-only --json` to inspect matching verification intents, command eligibility, risk-priced evidence requirements, remaining gaps, and missing runnable coverage without executing commands. Use `mf run <intent> --dry-run --json` to inspect one resolved command intent without spawning a process or writing a run receipt. Plan-only verification includes a `decision_graph` that connects changed surfaces, classification reasons, command candidates, eligibility checks, effects, and gaps. When `.mustflow/cache/mustflow.sqlite` is fresh, scheduled entries also include read-only `effectGraph` metadata for write locks and lock conflicts. These graph rows are marked `explanation_only` and never grant command authority; `.mustflow/config/commands.toml` remains the only runnable command source.
336
336
 
337
- Each executed command run writes a run record under `.mustflow/state/runs/run-*` and atomically updates `.mustflow/state/runs/latest.json`. The record includes the intent name, working directory, timeout, exit code, timeout status, and the tail of stdout and stderr.
337
+ Each executed command run writes a run record under `.mustflow/state/runs/run-*` and atomically updates `.mustflow/state/runs/latest.json`. The record includes the intent name, working directory, timeout, exit code, timeout status, and the tail of stdout and stderr. `latest.json` is a root-scoped convenience pointer, not session-scoped proof; in multi-agent or multi-terminal workflows, use the per-run `receipt_path` or `mf run <intent> --json` output as the evidence for a specific run.
338
338
 
339
339
  ## Language and profiles
340
340
 
@@ -1,4 +1,5 @@
1
1
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
+ import { acquireActiveCommandLock, GENERATED_SURFACE_READ_EFFECTS, reportActiveCommandLockConflict, } from '../lib/active-command-lock.js';
2
3
  import { t } from '../lib/i18n.js';
3
4
  import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
5
  import { resolveMustflowRoot } from '../lib/project-root.js';
@@ -43,36 +44,47 @@ export function runCheck(args, reporter, lang = 'en') {
43
44
  return 1;
44
45
  }
45
46
  const strict = hasParsedCliOption(options, '--strict');
46
- const report = checkMustflowProjectReport(resolveMustflowRoot(), { strict });
47
- const issues = report.issues;
48
- const warnings = report.warnings;
49
- const ok = issues.length === 0;
50
- if (hasParsedCliOption(options, '--json')) {
51
- reporter.stdout(JSON.stringify({
52
- ok,
53
- strict,
54
- issueCount: issues.length,
55
- issues,
56
- warningCount: warnings.length,
57
- warnings,
58
- issueDetails: report.issueDetails,
59
- }, null, 2));
60
- return ok ? 0 : 1;
47
+ const projectRoot = resolveMustflowRoot();
48
+ const activeLock = acquireActiveCommandLock(projectRoot, 'mf check', GENERATED_SURFACE_READ_EFFECTS);
49
+ if (!activeLock.ok) {
50
+ reportActiveCommandLockConflict(reporter, 'mf check', activeLock.conflicts, 'mf check --help', lang);
51
+ return 1;
61
52
  }
62
- if (ok) {
63
- for (const warning of warnings) {
64
- reporter.stderr(warning);
53
+ try {
54
+ const report = checkMustflowProjectReport(projectRoot, { strict });
55
+ const issues = report.issues;
56
+ const warnings = report.warnings;
57
+ const ok = issues.length === 0;
58
+ if (hasParsedCliOption(options, '--json')) {
59
+ reporter.stdout(JSON.stringify({
60
+ ok,
61
+ strict,
62
+ issueCount: issues.length,
63
+ issues,
64
+ warningCount: warnings.length,
65
+ warnings,
66
+ issueDetails: report.issueDetails,
67
+ }, null, 2));
68
+ return ok ? 0 : 1;
65
69
  }
66
- if (strict) {
67
- reporter.stdout(t(lang, 'check.result.strictPassed'));
70
+ if (ok) {
71
+ for (const warning of warnings) {
72
+ reporter.stderr(warning);
73
+ }
74
+ if (strict) {
75
+ reporter.stdout(t(lang, 'check.result.strictPassed'));
76
+ return 0;
77
+ }
78
+ reporter.stdout(t(lang, 'check.result.passed'));
68
79
  return 0;
69
80
  }
70
- reporter.stdout(t(lang, 'check.result.passed'));
71
- return 0;
81
+ for (const issue of issues) {
82
+ reporter.stderr(issue);
83
+ }
84
+ reporter.stderr(t(lang, 'check.result.failed', { count: issues.length }));
85
+ return 1;
72
86
  }
73
- for (const issue of issues) {
74
- reporter.stderr(issue);
87
+ finally {
88
+ activeLock.handle.release();
75
89
  }
76
- reporter.stderr(t(lang, 'check.result.failed', { count: issues.length }));
77
- return 1;
78
90
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
4
+ import { acquireActiveCommandLock, GENERATED_SURFACE_READ_EFFECTS, reportActiveCommandLockConflict, } from '../lib/active-command-lock.js';
4
5
  import { getAgentContext, } from '../lib/agent-context.js';
5
6
  import { t } from '../lib/i18n.js';
6
7
  import { getLocalIndexDatabasePath } from '../lib/local-index.js';
@@ -278,11 +279,22 @@ export function runDoctor(args, reporter, lang = 'en') {
278
279
  printUsageError(reporter, formatCliOptionParseError(options.error, lang), 'mf doctor --help', getDoctorHelp(lang), lang);
279
280
  return 1;
280
281
  }
281
- const output = createDoctorOutput(hasParsedCliOption(options, '--strict'));
282
- if (hasParsedCliOption(options, '--json')) {
283
- reporter.stdout(JSON.stringify(output, null, 2));
282
+ const projectRoot = resolveMustflowRoot();
283
+ const activeLock = acquireActiveCommandLock(projectRoot, 'mf doctor', GENERATED_SURFACE_READ_EFFECTS);
284
+ if (!activeLock.ok) {
285
+ reportActiveCommandLockConflict(reporter, 'mf doctor', activeLock.conflicts, 'mf doctor --help', lang);
286
+ return 1;
287
+ }
288
+ try {
289
+ const output = createDoctorOutput(hasParsedCliOption(options, '--strict'));
290
+ if (hasParsedCliOption(options, '--json')) {
291
+ reporter.stdout(JSON.stringify(output, null, 2));
292
+ return output.ok ? 0 : 1;
293
+ }
294
+ reporter.stdout(renderDoctorOutput(output, lang));
284
295
  return output.ok ? 0 : 1;
285
296
  }
286
- reporter.stdout(renderDoctorOutput(output, lang));
287
- return output.ok ? 0 : 1;
297
+ finally {
298
+ activeLock.handle.release();
299
+ }
288
300
  }
@@ -1,4 +1,5 @@
1
1
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
+ import { acquireActiveCommandLock, LOCAL_INDEX_WRITE_EFFECTS, reportActiveCommandLockConflict, } from '../lib/active-command-lock.js';
2
3
  import { t } from '../lib/i18n.js';
3
4
  import { createLocalIndex } from '../lib/local-index.js';
4
5
  import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
@@ -83,15 +84,29 @@ export async function runIndex(args, reporter, lang = 'en') {
83
84
  printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf index --help', getIndexHelp(lang), lang);
84
85
  return 1;
85
86
  }
86
- const result = await createLocalIndex(resolveMustflowRoot(), {
87
- dryRun: hasParsedCliOption(parsed, '--dry-run'),
88
- includeSource: hasParsedCliOption(parsed, '--source'),
89
- incremental: hasParsedCliOption(parsed, '--incremental'),
90
- });
91
- if (hasParsedCliOption(parsed, '--json')) {
92
- reporter.stdout(JSON.stringify(result, null, 2));
87
+ const dryRun = hasParsedCliOption(parsed, '--dry-run');
88
+ const projectRoot = resolveMustflowRoot();
89
+ const activeLock = dryRun ? null : acquireActiveCommandLock(projectRoot, 'mf index', LOCAL_INDEX_WRITE_EFFECTS);
90
+ if (activeLock && !activeLock.ok) {
91
+ reportActiveCommandLockConflict(reporter, 'mf index', activeLock.conflicts, 'mf index --help', lang);
92
+ return 1;
93
+ }
94
+ try {
95
+ const result = await createLocalIndex(projectRoot, {
96
+ dryRun,
97
+ includeSource: hasParsedCliOption(parsed, '--source'),
98
+ incremental: hasParsedCliOption(parsed, '--incremental'),
99
+ });
100
+ if (hasParsedCliOption(parsed, '--json')) {
101
+ reporter.stdout(JSON.stringify(result, null, 2));
102
+ return 0;
103
+ }
104
+ reporter.stdout(renderIndexSummary(result, lang));
93
105
  return 0;
94
106
  }
95
- reporter.stdout(renderIndexSummary(result, lang));
96
- return 0;
107
+ finally {
108
+ if (activeLock?.ok) {
109
+ activeLock.handle.release();
110
+ }
111
+ }
97
112
  }
@@ -1,5 +1,6 @@
1
1
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
2
  import { t } from '../lib/i18n.js';
3
+ import { acquireActiveCommandLock, REPO_MAP_WRITE_EFFECTS, reportActiveCommandLockConflict, } from '../lib/active-command-lock.js';
3
4
  import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
5
  import { resolveMustflowRoot } from '../lib/project-root.js';
5
6
  import { generateRepoMap, writeRepoMap } from '../lib/repo-map.js';
@@ -71,13 +72,25 @@ export function runMap(args, reporter, lang = 'en') {
71
72
  shouldPrint = true;
72
73
  }
73
74
  const projectRoot = resolveMustflowRoot();
74
- const content = generateRepoMap(projectRoot, { depth, includeNested });
75
- if (shouldWrite) {
76
- writeRepoMap(projectRoot, content);
77
- reporter.stdout(t(lang, 'map.wrote'));
75
+ const activeLock = shouldWrite ? acquireActiveCommandLock(projectRoot, 'mf map --write', REPO_MAP_WRITE_EFFECTS) : null;
76
+ if (activeLock && !activeLock.ok) {
77
+ reportActiveCommandLockConflict(reporter, 'mf map --write', activeLock.conflicts, 'mf map --help', lang);
78
+ return 1;
79
+ }
80
+ try {
81
+ const content = generateRepoMap(projectRoot, { depth, includeNested });
82
+ if (shouldWrite) {
83
+ writeRepoMap(projectRoot, content);
84
+ reporter.stdout(t(lang, 'map.wrote'));
85
+ }
86
+ if (shouldPrint) {
87
+ reporter.stdout(content);
88
+ }
89
+ return 0;
78
90
  }
79
- if (shouldPrint) {
80
- reporter.stdout(content);
91
+ finally {
92
+ if (activeLock?.ok) {
93
+ activeLock.handle.release();
94
+ }
81
95
  }
82
- return 0;
83
96
  }
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { performance } from 'node:perf_hooks';
3
- import { acquireActiveRunLock } from '../../core/active-run-locks.js';
3
+ import { ACTIVE_RUN_LOCK_ID_ENV, acquireActiveRunLock } from '../../core/active-run-locks.js';
4
4
  import { createCommandEnv } from '../../core/command-env.js';
5
5
  import { createCorrelationId } from '../../core/correlation-id.js';
6
6
  import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.js';
@@ -360,6 +360,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
360
360
  try {
361
361
  const runReceiptPolicy = profiler.measure('retention_policy', () => resolveRunReceiptRetentionPolicy(readMustflowConfigIfExists(projectRoot)));
362
362
  const env = profiler.measure('environment', () => createCommandEnv(projectRoot, { policy: plan.envPolicy, allowlist: plan.envAllowlist }));
363
+ env[ACTIVE_RUN_LOCK_ID_ENV] = activeRunLock.handle.record.run_id;
363
364
  const writeTracker = profiler.measure('write_drift_before', () => startRunWriteTracking(projectRoot, contract, intentName, {
364
365
  additionalDeclaredPaths: options.additionalDeclaredWritePaths,
365
366
  env,
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { copyFileInsideWithoutSymlinks, ensureFileTargetInsideWithoutSymlinks, ensureInside, writeUtf8FileInsideWithoutSymlinks, } from '../lib/filesystem.js';
5
5
  import { MANIFEST_LOCK_RELATIVE_PATH, readManifestLock, sha256File } from '../lib/manifest-lock.js';
6
6
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
7
+ import { acquireActiveCommandLock, MUSTFLOW_UPDATE_APPLY_EFFECTS, reportActiveCommandLockConflict, } from '../lib/active-command-lock.js';
7
8
  import { t } from '../lib/i18n.js';
8
9
  import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
9
10
  import { resolveMustflowRoot } from '../lib/project-root.js';
@@ -430,53 +431,65 @@ export function runUpdate(args, reporter, lang = 'en') {
430
431
  return 1;
431
432
  }
432
433
  const projectRoot = resolveMustflowRoot();
433
- const plan = createUpdatePlan(projectRoot);
434
- if (plan.error) {
435
- if (wantsJson) {
436
- reporter.stdout(JSON.stringify(withMode(planOutput(publicPlanItems(plan.items), plan.error, false), requestedMode), null, 2));
437
- return 1;
438
- }
439
- reporter.stderr(plan.error);
434
+ const activeLock = wantsApply ? acquireActiveCommandLock(projectRoot, 'mf update --apply', MUSTFLOW_UPDATE_APPLY_EFFECTS) : null;
435
+ if (activeLock && !activeLock.ok) {
436
+ reportActiveCommandLockConflict(reporter, 'mf update --apply', activeLock.conflicts, 'mf update --help', lang);
440
437
  return 1;
441
438
  }
442
- const outputItems = wantsDiff ? withDiffPreviews(projectRoot, plan.items) : publicPlanItems(plan.items);
443
- const dryRunOutput = withMode(planOutput(outputItems, undefined, false), requestedMode);
444
- if (wantsDryRun) {
445
- if (wantsJson) {
446
- reporter.stdout(JSON.stringify(dryRunOutput, null, 2));
439
+ try {
440
+ const plan = createUpdatePlan(projectRoot);
441
+ if (plan.error) {
442
+ if (wantsJson) {
443
+ reporter.stdout(JSON.stringify(withMode(planOutput(publicPlanItems(plan.items), plan.error, false), requestedMode), null, 2));
444
+ return 1;
445
+ }
446
+ reporter.stderr(plan.error);
447
+ return 1;
448
+ }
449
+ const outputItems = wantsDiff ? withDiffPreviews(projectRoot, plan.items) : publicPlanItems(plan.items);
450
+ const dryRunOutput = withMode(planOutput(outputItems, undefined, false), requestedMode);
451
+ if (wantsDryRun) {
452
+ if (wantsJson) {
453
+ reporter.stdout(JSON.stringify(dryRunOutput, null, 2));
454
+ return dryRunOutput.ok ? 0 : 1;
455
+ }
456
+ printPlan(dryRunOutput, reporter, lang);
457
+ if (wantsDiff) {
458
+ printDiffPreviews(dryRunOutput.items, reporter, lang);
459
+ }
460
+ reporter.stdout(t(lang, 'update.plan.noFilesWritten'));
447
461
  return dryRunOutput.ok ? 0 : 1;
448
462
  }
449
- printPlan(dryRunOutput, reporter, lang);
450
- if (wantsDiff) {
451
- printDiffPreviews(dryRunOutput.items, reporter, lang);
463
+ if (!dryRunOutput.ok) {
464
+ if (wantsJson) {
465
+ reporter.stdout(JSON.stringify(dryRunOutput, null, 2));
466
+ return 1;
467
+ }
468
+ printPlan(dryRunOutput, reporter, lang);
469
+ reporter.stdout(t(lang, 'update.plan.noFilesWritten'));
470
+ return 1;
452
471
  }
453
- reporter.stdout(t(lang, 'update.plan.noFilesWritten'));
454
- return dryRunOutput.ok ? 0 : 1;
455
- }
456
- if (!dryRunOutput.ok) {
457
472
  if (wantsJson) {
458
- reporter.stdout(JSON.stringify(dryRunOutput, null, 2));
459
- return 1;
473
+ const applicableItems = plan.items.filter((item) => item.action === 'create' || item.action === 'update');
474
+ const applyResult = applyUpdate(projectRoot, applicableItems, {
475
+ stdout: () => undefined,
476
+ stderr: (message) => reporter.stderr(message),
477
+ }, lang);
478
+ reporter.stdout(JSON.stringify(withMode(planOutput(publicPlanItems(plan.items), undefined, applyResult.wroteFiles), 'apply'), null, 2));
479
+ return 0;
460
480
  }
461
- printPlan(dryRunOutput, reporter, lang);
462
- reporter.stdout(t(lang, 'update.plan.noFilesWritten'));
463
- return 1;
464
- }
465
- if (wantsJson) {
466
- const applicableItems = plan.items.filter((item) => item.action === 'create' || item.action === 'update');
467
- const applyResult = applyUpdate(projectRoot, applicableItems, {
468
- stdout: () => undefined,
469
- stderr: (message) => reporter.stderr(message),
470
- }, lang);
471
- reporter.stdout(JSON.stringify(withMode(planOutput(publicPlanItems(plan.items), undefined, applyResult.wroteFiles), 'apply'), null, 2));
481
+ const applyResult = applyUpdate(projectRoot, plan.items, reporter, lang);
482
+ if (!applyResult.wroteFiles) {
483
+ reporter.stdout(t(lang, 'update.plan.noUpdates'));
484
+ reporter.stdout(t(lang, 'update.plan.noFilesWritten'));
485
+ return 0;
486
+ }
487
+ reporter.stdout(t(lang, 'update.complete', { updated: applyResult.updated, created: applyResult.created }));
472
488
  return 0;
473
489
  }
474
- const applyResult = applyUpdate(projectRoot, plan.items, reporter, lang);
475
- if (!applyResult.wroteFiles) {
476
- reporter.stdout(t(lang, 'update.plan.noUpdates'));
477
- reporter.stdout(t(lang, 'update.plan.noFilesWritten'));
478
- return 0;
490
+ finally {
491
+ if (activeLock?.ok) {
492
+ activeLock.handle.release();
493
+ }
479
494
  }
480
- reporter.stdout(t(lang, 'update.complete', { updated: applyResult.updated, created: applyResult.created }));
481
- return 0;
482
495
  }
@@ -0,0 +1,96 @@
1
+ import { ACTIVE_RUN_LOCK_ID_ENV, acquireActiveRunLock, } from '../../core/active-run-locks.js';
2
+ import { renderCliError } from './cli-output.js';
3
+ import { t } from './i18n.js';
4
+ export const REPO_MAP_WRITE_EFFECTS = [
5
+ { type: 'write', mode: 'replace', path: 'REPO_MAP.md', concurrency: 'exclusive' },
6
+ ];
7
+ export const LOCAL_INDEX_WRITE_EFFECTS = [
8
+ {
9
+ type: 'write',
10
+ mode: 'replace',
11
+ path: '.mustflow/cache/**',
12
+ lock: 'local_index_cache',
13
+ concurrency: 'exclusive',
14
+ },
15
+ ];
16
+ export const MUSTFLOW_UPDATE_APPLY_EFFECTS = [
17
+ { type: 'write', mode: 'replace', path: 'AGENTS.md', concurrency: 'exclusive' },
18
+ { type: 'write', mode: 'replace', path: '.mustflow/config/manifest.lock.toml', concurrency: 'exclusive' },
19
+ { type: 'write', mode: 'replace', path: '.mustflow/config/commands.toml', concurrency: 'exclusive' },
20
+ { type: 'write', mode: 'replace', path: '.mustflow/config/mustflow.toml', concurrency: 'exclusive' },
21
+ { type: 'write', mode: 'replace', path: '.mustflow/config/preferences.toml', concurrency: 'exclusive' },
22
+ { type: 'write', mode: 'replace', path: '.mustflow/context/**', concurrency: 'exclusive' },
23
+ { type: 'write', mode: 'replace', path: '.mustflow/docs/**', concurrency: 'exclusive' },
24
+ { type: 'write', mode: 'replace', path: '.mustflow/skills/**', concurrency: 'exclusive' },
25
+ { type: 'write', mode: 'replace', path: '.mustflow/backups/**', concurrency: 'exclusive' },
26
+ ];
27
+ export const GENERATED_SURFACE_READ_EFFECTS = [
28
+ { type: 'read', mode: 'read', path: 'REPO_MAP.md', concurrency: 'shared' },
29
+ { type: 'read', mode: 'read', path: '.mustflow/config/manifest.lock.toml', concurrency: 'shared' },
30
+ {
31
+ type: 'read',
32
+ mode: 'read',
33
+ path: '.mustflow/cache/**',
34
+ lock: 'local_index_cache',
35
+ concurrency: 'shared',
36
+ },
37
+ ];
38
+ function effectToToml(effect) {
39
+ const output = {
40
+ type: effect.type,
41
+ };
42
+ if (effect.mode) {
43
+ output.mode = effect.mode;
44
+ }
45
+ if (effect.path) {
46
+ output.path = effect.path;
47
+ }
48
+ if (effect.paths) {
49
+ output.paths = [...effect.paths];
50
+ }
51
+ if (effect.lock) {
52
+ output.lock = effect.lock;
53
+ }
54
+ if (effect.concurrency) {
55
+ output.concurrency = effect.concurrency;
56
+ }
57
+ return output;
58
+ }
59
+ function createSyntheticCommandContract(intentName, effects) {
60
+ return {
61
+ defaults: { default_cwd: '.' },
62
+ resources: {},
63
+ intents: {
64
+ [intentName]: {
65
+ cwd: '.',
66
+ writes: [],
67
+ effects: effects.map(effectToToml),
68
+ },
69
+ },
70
+ };
71
+ }
72
+ function parentRunId() {
73
+ const value = process.env[ACTIVE_RUN_LOCK_ID_ENV]?.trim();
74
+ return value && value.length > 0 ? value : null;
75
+ }
76
+ export function acquireActiveCommandLock(projectRoot, displayName, effects) {
77
+ return acquireActiveRunLock(projectRoot, createSyntheticCommandContract(displayName, effects), displayName, {
78
+ commandHash: null,
79
+ ignoreRunId: parentRunId(),
80
+ ignorePid: process.ppid,
81
+ });
82
+ }
83
+ export function renderActiveCommandLockConflictMessage(displayName, conflicts, lang) {
84
+ const [first] = conflicts;
85
+ const detail = first
86
+ ? t(lang, 'run.error.activeLockConflictDetail', {
87
+ lock: first.lock,
88
+ intent: first.conflictsWithIntent,
89
+ pid: first.conflictsWithPid,
90
+ })
91
+ : t(lang, 'run.error.activeLockConflictUnknown');
92
+ return t(lang, 'run.error.activeLockConflict', { intent: displayName, detail });
93
+ }
94
+ export function reportActiveCommandLockConflict(reporter, displayName, conflicts, helpCommand, lang) {
95
+ reporter.stderr(renderCliError(renderActiveCommandLockConflictMessage(displayName, conflicts, lang), helpCommand, lang));
96
+ }
@@ -6,6 +6,7 @@ import { readUtf8FileInsideWithoutSymlinks, writeJsonFileInsideWithoutSymlinks,
6
6
  const ACTIVE_LOCK_SCHEMA_VERSION = '1';
7
7
  const ACTIVE_LOCK_KIND = 'active_run_lock';
8
8
  const LOCK_ROOT_RELATIVE_PATH = '.mustflow/state/locks';
9
+ export const ACTIVE_RUN_LOCK_ID_ENV = 'MUSTFLOW_ACTIVE_RUN_LOCK_ID';
9
10
  const LOCK_MUTEX_STALE_MS = 30_000;
10
11
  const LOCK_MUTEX_WAIT_MS = 1_000;
11
12
  const LOCK_MUTEX_SLEEP_MS = 25;
@@ -462,7 +463,12 @@ export function acquireActiveRunLock(projectRoot, contract, intentName, options
462
463
  }
463
464
  }
464
465
  const staleRecordIds = new Set(staleRecords.map((stale) => stale.runId));
465
- const liveRecords = records.filter((record) => !staleRecordIds.has(record.run_id));
466
+ const liveRecords = records.filter((record) => {
467
+ if (staleRecordIds.has(record.run_id)) {
468
+ return false;
469
+ }
470
+ return record.run_id !== options.ignoreRunId || record.pid !== options.ignorePid;
471
+ });
466
472
  const conflicts = findConflicts(intentName, effects, liveRecords);
467
473
  if (conflicts.length > 0) {
468
474
  return { ok: false, conflicts, recoveredStaleRecords: staleRecords };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.74.0",
3
+ "version": "2.74.1",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
@@ -1,6 +1,6 @@
1
1
  id = "default"
2
2
  name = "default"
3
- version = "2.74.0"
3
+ version = "2.74.1"
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"