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 +1 -1
- package/dist/cli/commands/check.js +38 -26
- package/dist/cli/commands/doctor.js +17 -5
- package/dist/cli/commands/index.js +24 -9
- package/dist/cli/commands/map.js +20 -7
- package/dist/cli/commands/run.js +2 -1
- package/dist/cli/commands/update.js +52 -39
- package/dist/cli/lib/active-command-lock.js +96 -0
- package/dist/core/active-run-locks.js +7 -1
- package/package.json +1 -1
- package/templates/default/manifest.toml +1 -1
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
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 (
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
287
|
-
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
107
|
+
finally {
|
|
108
|
+
if (activeLock?.ok) {
|
|
109
|
+
activeLock.handle.release();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
97
112
|
}
|
package/dist/cli/commands/map.js
CHANGED
|
@@ -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
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
91
|
+
finally {
|
|
92
|
+
if (activeLock?.ok) {
|
|
93
|
+
activeLock.handle.release();
|
|
94
|
+
}
|
|
81
95
|
}
|
|
82
|
-
return 0;
|
|
83
96
|
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -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
|
|
434
|
-
if (
|
|
435
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
459
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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) =>
|
|
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