mustflow 2.21.1 → 2.21.2

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.
@@ -171,7 +171,9 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
171
171
  }
172
172
  const runReceiptPolicy = profiler.measure('retention_policy', () => resolveRunReceiptRetentionPolicy(readMustflowConfigIfExists(projectRoot)));
173
173
  const env = profiler.measure('environment', () => createCommandEnv(projectRoot, { policy: plan.envPolicy, allowlist: plan.envAllowlist }));
174
- const writeTracker = profiler.measure('write_drift_before', () => startRunWriteTracking(projectRoot, contract, intentName));
174
+ const writeTracker = profiler.measure('write_drift_before', () => startRunWriteTracking(projectRoot, contract, intentName, {
175
+ additionalDeclaredPaths: options.additionalDeclaredWritePaths,
176
+ }));
175
177
  const stdoutTailBytes = Math.min(runReceiptPolicy.stdoutTailBytes, plan.maxOutputBytes);
176
178
  const stderrTailBytes = Math.min(runReceiptPolicy.stderrTailBytes, plan.maxOutputBytes);
177
179
  let streamedOutput = false;
@@ -628,11 +628,12 @@ function testTargetsByScheduledIntent(report) {
628
628
  candidate.appliedTestTargets.length > 0)
629
629
  .map((candidate) => [candidate.intent, candidate.appliedTestTargets]));
630
630
  }
631
- async function runVerificationIntent(intent, lang, verificationPlanId, testTargets = []) {
631
+ async function runVerificationIntent(intent, lang, verificationPlanId, testTargets = [], additionalDeclaredWritePaths = []) {
632
632
  const output = createBufferedOutput();
633
633
  const exitCode = await runRun([intent, '--json'], output.reporter, lang, {
634
634
  writeLatestReceipt: false,
635
635
  testTargets,
636
+ additionalDeclaredWritePaths,
636
637
  });
637
638
  const rawStdout = output.stdout().trim();
638
639
  let receipt = null;
@@ -680,7 +681,12 @@ async function runVerificationEntriesInParallelChunks(entries, parallelism, lang
680
681
  const results = [];
681
682
  for (let index = 0; index < entries.length; index += parallelism) {
682
683
  const chunk = entries.slice(index, index + parallelism);
683
- results.push(...(await Promise.all(chunk.map((entry) => runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? [])))));
684
+ const batchDeclaredWritePaths = [
685
+ ...new Set(chunk.flatMap((entry) => entry.effects
686
+ .filter((effect) => effect.access === 'write' && typeof effect.path === 'string')
687
+ .map((effect) => effect.path))),
688
+ ].sort((left, right) => left.localeCompare(right));
689
+ results.push(...(await Promise.all(chunk.map((entry) => runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? [], batchDeclaredWritePaths)))));
684
690
  }
685
691
  return results;
686
692
  }
@@ -1,103 +1,10 @@
1
- import { closeSync, constants, copyFileSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, statSync, writeFileSync, } from 'node:fs';
1
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- const NOFOLLOW_FLAG = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
3
+ export { ensureFileTargetInsideWithoutSymlinks, ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks, readUtf8FileInsideWithoutSymlinks, writeFileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks, } from '../../core/safe-filesystem.js';
4
+ import { readFileInsideWithoutSymlinks, writeFileInsideWithoutSymlinks, } from '../../core/safe-filesystem.js';
4
5
  export function toPosixPath(value) {
5
6
  return value.split(path.sep).join('/');
6
7
  }
7
- export function ensureInside(parentPath, childPath) {
8
- const parent = path.resolve(parentPath);
9
- const child = path.resolve(childPath);
10
- const relative = path.relative(parent, child);
11
- if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) {
12
- return;
13
- }
14
- throw new Error(`Path escapes allowed directory: ${childPath}`);
15
- }
16
- function isMissingPathError(error) {
17
- return error instanceof Error && 'code' in error && error.code === 'ENOENT';
18
- }
19
- export function ensureInsideWithoutSymlinks(parentPath, childPath, options = {}) {
20
- ensureInside(parentPath, childPath);
21
- const parent = path.resolve(parentPath);
22
- const child = path.resolve(childPath);
23
- const relative = path.relative(parent, child);
24
- const segments = relative === '' ? [] : relative.split(path.sep).filter((segment) => segment.length > 0);
25
- let currentPath = parent;
26
- const parentStats = lstatSync(parent);
27
- if (parentStats.isSymbolicLink()) {
28
- throw new Error(`Path must not contain symlinks: ${childPath}`);
29
- }
30
- for (const [index, segment] of segments.entries()) {
31
- currentPath = path.join(currentPath, segment);
32
- const isLeaf = index === segments.length - 1;
33
- try {
34
- const stats = lstatSync(currentPath);
35
- if (stats.isSymbolicLink()) {
36
- throw new Error(`Path must not contain symlinks: ${childPath}`);
37
- }
38
- if (!isLeaf && !stats.isDirectory()) {
39
- throw new Error(`Path component is not a directory: ${currentPath}`);
40
- }
41
- }
42
- catch (error) {
43
- if (isMissingPathError(error) && options.allowMissingLeaf) {
44
- return;
45
- }
46
- throw error;
47
- }
48
- }
49
- }
50
- export function readUtf8FileInsideWithoutSymlinks(parentPath, childPath) {
51
- return readFileInsideWithoutSymlinks(parentPath, childPath).toString('utf8');
52
- }
53
- export function readFileInsideWithoutSymlinks(parentPath, childPath) {
54
- const absoluteChildPath = path.resolve(childPath);
55
- ensureInsideWithoutSymlinks(parentPath, absoluteChildPath);
56
- const fileDescriptor = openSync(absoluteChildPath, constants.O_RDONLY | NOFOLLOW_FLAG);
57
- try {
58
- return readFileSync(fileDescriptor);
59
- }
60
- finally {
61
- closeSync(fileDescriptor);
62
- }
63
- }
64
- export function ensureFileTargetInsideWithoutSymlinks(parentPath, childPath, options = {}) {
65
- const absoluteChildPath = path.resolve(childPath);
66
- ensureInside(parentPath, absoluteChildPath);
67
- ensureInsideWithoutSymlinks(parentPath, path.dirname(absoluteChildPath), { allowMissingLeaf: true });
68
- try {
69
- const stats = lstatSync(absoluteChildPath);
70
- if (stats.isSymbolicLink()) {
71
- throw new Error(`Path must not contain symlinks: ${childPath}`);
72
- }
73
- if (!stats.isFile()) {
74
- throw new Error(`Path must be a regular file: ${childPath}`);
75
- }
76
- }
77
- catch (error) {
78
- if (isMissingPathError(error) && options.allowMissingLeaf) {
79
- return;
80
- }
81
- throw error;
82
- }
83
- }
84
- export function writeUtf8FileInsideWithoutSymlinks(parentPath, childPath, content) {
85
- writeFileInsideWithoutSymlinks(parentPath, childPath, content);
86
- }
87
- export function writeFileInsideWithoutSymlinks(parentPath, childPath, content) {
88
- const absoluteChildPath = path.resolve(childPath);
89
- const directoryPath = path.dirname(absoluteChildPath);
90
- ensureInsideWithoutSymlinks(parentPath, directoryPath, { allowMissingLeaf: true });
91
- mkdirSync(directoryPath, { recursive: true });
92
- ensureFileTargetInsideWithoutSymlinks(parentPath, absoluteChildPath, { allowMissingLeaf: true });
93
- const fileDescriptor = openSync(absoluteChildPath, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | NOFOLLOW_FLAG);
94
- try {
95
- writeFileSync(fileDescriptor, content);
96
- }
97
- finally {
98
- closeSync(fileDescriptor);
99
- }
100
- }
101
8
  export function copyFileInsideWithoutSymlinks(sourceParentPath, sourcePath, targetParentPath, targetPath) {
102
9
  const content = readFileInsideWithoutSymlinks(sourceParentPath, sourcePath);
103
10
  writeFileInsideWithoutSymlinks(targetParentPath, targetPath, content);
@@ -2245,7 +2245,7 @@ export async function createLocalIndex(projectRoot, options = {}) {
2245
2245
  max_snippet_bytes_per_document: MAX_SNIPPET_BYTES_PER_DOCUMENT,
2246
2246
  excluded_raw_data_kinds: [...LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS],
2247
2247
  indexed_file_count: indexedFiles.length,
2248
- indexed_paths: documents.map((document) => document.path),
2248
+ indexed_paths: indexedFiles.map((file) => file.path),
2249
2249
  };
2250
2250
  }
2251
2251
  function readStoredSchemaVersion(database) {
@@ -17,12 +17,11 @@ function validateEffectPath(projectRoot, intent, rawPath) {
17
17
  const cwd = resolveSafeProjectCwd(projectRoot, readString(intent, 'cwd'));
18
18
  const resolved = path.resolve(cwd, rawPath);
19
19
  const root = path.resolve(projectRoot);
20
- const resolvedLower = resolved.toLowerCase();
21
- const rootLower = root.toLowerCase();
22
- if (resolvedLower !== rootLower && !resolvedLower.startsWith(`${rootLower}${path.sep}`)) {
20
+ const relative = path.relative(root, resolved);
21
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
23
22
  throw new Error(`Command effect path must stay inside the current root: ${rawPath}`);
24
23
  }
25
- return normalizeRelativePath(path.relative(projectRoot, resolved));
24
+ return normalizeRelativePath(relative);
26
25
  }
27
26
  function readResourcePaths(commandContract, lock) {
28
27
  const resource = commandContract.resources[lock];
@@ -11,7 +11,7 @@ const GIT_STATUS_UNTRACKED_MODE = 'normal';
11
11
  const MAX_HASH_BYTES = 5 * 1024 * 1024;
12
12
  const RECURSIVE_SNAPSHOT_ENV = 'MUSTFLOW_WRITE_DRIFT_SNAPSHOT';
13
13
  const EXCLUDED_DIRECTORY_NAMES = new Set(['.git', 'node_modules']);
14
- const EXCLUDED_RELATIVE_DIRECTORY_PATHS = new Set(['.mustflow/state/runs']);
14
+ const EXCLUDED_RELATIVE_DIRECTORY_PATHS = new Set(['.mustflow/state/perf', '.mustflow/state/runs']);
15
15
  function isRecursiveSnapshotEnabled() {
16
16
  const value = process.env[RECURSIVE_SNAPSHOT_ENV];
17
17
  return value === '1' || value?.toLowerCase() === 'true';
@@ -186,10 +186,14 @@ function truncatePaths(paths) {
186
186
  }
187
187
  return { paths: paths.slice(0, MAX_REPORTED_PATHS), truncated: true };
188
188
  }
189
- export function startRunWriteTracking(projectRoot, contract, intentName) {
189
+ export function startRunWriteTracking(projectRoot, contract, intentName, options = {}) {
190
+ const declaredPaths = [
191
+ ...listDeclaredWritePaths(projectRoot, contract, intentName),
192
+ ...(options.additionalDeclaredPaths ?? []).map(normalizeRelativePath),
193
+ ];
190
194
  return {
191
195
  projectRoot,
192
- declaredPaths: listDeclaredWritePaths(projectRoot, contract, intentName),
196
+ declaredPaths: [...new Set(declaredPaths)].sort((left, right) => left.localeCompare(right)),
193
197
  before: captureSnapshot(projectRoot),
194
198
  };
195
199
  }
@@ -114,6 +114,9 @@ export function readFileInsideWithoutSymlinks(parentPath, childPath) {
114
114
  closeSync(fileDescriptor);
115
115
  }
116
116
  }
117
+ export function readUtf8FileInsideWithoutSymlinks(parentPath, childPath) {
118
+ return readFileInsideWithoutSymlinks(parentPath, childPath).toString('utf8');
119
+ }
117
120
  export function writeFileInsideWithoutSymlinks(parentPath, childPath, content) {
118
121
  const absoluteChildPath = path.resolve(childPath);
119
122
  const directoryPath = path.dirname(absoluteChildPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.21.1",
3
+ "version": "2.21.2",
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.21.1"
3
+ version = "2.21.2"
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"