mustflow 2.18.20 → 2.18.21

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.
@@ -16,7 +16,7 @@ import { readCommandContract } from '../../core/config-loading.js';
16
16
  import { DEFAULT_VERIFY_PARALLELISM, parseVerifyArgs } from './verify/args.js';
17
17
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
18
18
  import { t } from '../lib/i18n.js';
19
- import { readLocalCommandEffectGraph, readLocalPathSurfaces, readLocalSourceAnchorVerdictRisks, } from '../lib/local-index.js';
19
+ import { readLocalCommandEffectGraphs, readLocalPathSurfaces, readLocalSourceAnchorVerdictRisks, } from '../lib/local-index.js';
20
20
  import { resolveMustflowRoot } from '../lib/project-root.js';
21
21
  const VERIFY_SCHEMA_VERSION = '1';
22
22
  const RUN_STATE_DIR = path.join('.mustflow', 'state', 'runs');
@@ -578,6 +578,20 @@ function skippedResult(candidate) {
578
578
  receipt: null,
579
579
  };
580
580
  }
581
+ function stoppedAfterFailedBatchResult(entry, verificationPlanId) {
582
+ return {
583
+ intent: entry.intent,
584
+ status: 'skipped',
585
+ skipped: true,
586
+ reason: 'stopped_after_failed_batch',
587
+ detail: 'Skipped because an earlier verification batch failed and the schedule failure policy stops before the next batch.',
588
+ exit_code: null,
589
+ verification_plan_id: verificationPlanId,
590
+ receipt_path: null,
591
+ receipt_sha256: null,
592
+ receipt: null,
593
+ };
594
+ }
581
595
  function candidateResultKey(candidate) {
582
596
  return candidate.intent
583
597
  ? `intent:${candidate.intent}`
@@ -670,21 +684,40 @@ async function runVerificationEntriesInParallelChunks(entries, parallelism, lang
670
684
  }
671
685
  return results;
672
686
  }
687
+ function verificationResultFailed(result) {
688
+ return (!result.skipped &&
689
+ (result.status === 'failed' ||
690
+ result.status === 'timed_out' ||
691
+ result.status === 'start_failed' ||
692
+ result.status === 'output_limit_exceeded'));
693
+ }
673
694
  async function runScheduledVerificationIntents(report, lang, verificationPlanId, scheduledTestTargets, parallelism) {
674
- if (parallelism <= DEFAULT_VERIFY_PARALLELISM) {
675
- return runVerificationEntriesSequentially(report.schedule.entries, lang, verificationPlanId, scheduledTestTargets);
676
- }
677
695
  const results = [];
678
- for (const batch of report.schedule.batches) {
696
+ for (let batchIndex = 0; batchIndex < report.schedule.batches.length; batchIndex += 1) {
697
+ const batch = report.schedule.batches[batchIndex];
679
698
  const entries = entriesForScheduleBatch(report.schedule.entries, batch);
680
699
  if (entries.length === 0) {
681
700
  continue;
682
701
  }
702
+ let batchResults;
683
703
  if (entries.length > 1 && entries.every((entry) => entry.parallelEligible)) {
684
- results.push(...(await runVerificationEntriesInParallelChunks(entries, parallelism, lang, verificationPlanId, scheduledTestTargets)));
704
+ batchResults =
705
+ parallelism > DEFAULT_VERIFY_PARALLELISM
706
+ ? await runVerificationEntriesInParallelChunks(entries, parallelism, lang, verificationPlanId, scheduledTestTargets)
707
+ : await runVerificationEntriesSequentially(entries, lang, verificationPlanId, scheduledTestTargets);
708
+ }
709
+ else {
710
+ batchResults = await runVerificationEntriesSequentially(entries, lang, verificationPlanId, scheduledTestTargets);
711
+ }
712
+ results.push(...batchResults);
713
+ if (!batchResults.some(verificationResultFailed)) {
685
714
  continue;
686
715
  }
687
- results.push(...(await runVerificationEntriesSequentially(entries, lang, verificationPlanId, scheduledTestTargets)));
716
+ const remainingEntries = report.schedule.batches
717
+ .slice(batchIndex + 1)
718
+ .flatMap((remainingBatch) => entriesForScheduleBatch(report.schedule.entries, remainingBatch));
719
+ results.push(...remainingEntries.map((entry) => stoppedAfterFailedBatchResult(entry, verificationPlanId)));
720
+ break;
688
721
  }
689
722
  return results;
690
723
  }
@@ -1276,14 +1309,11 @@ async function createPlanOnlyOutput(input, projectRoot) {
1276
1309
  if (!firstEntry) {
1277
1310
  return { ...report, verification_plan_id: verificationPlanId, requirements };
1278
1311
  }
1279
- const firstGraph = await readLocalCommandEffectGraph(projectRoot, firstEntry.intent);
1280
- const graphsByIntent = new Map([[firstEntry.intent, firstGraph]]);
1281
- if (firstGraph.status === 'fresh') {
1282
- for (const entry of report.schedule.entries.slice(1)) {
1283
- if (!graphsByIntent.has(entry.intent)) {
1284
- graphsByIntent.set(entry.intent, await readLocalCommandEffectGraph(projectRoot, entry.intent));
1285
- }
1286
- }
1312
+ const scheduledIntents = Array.from(new Set(report.schedule.entries.map((entry) => entry.intent)));
1313
+ const graphsByIntent = await readLocalCommandEffectGraphs(projectRoot, scheduledIntents);
1314
+ const firstGraph = graphsByIntent.get(firstEntry.intent);
1315
+ if (!firstGraph) {
1316
+ return { ...report, verification_plan_id: verificationPlanId, requirements };
1287
1317
  }
1288
1318
  return {
1289
1319
  ...report,
@@ -1,5 +1,7 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { parseGitStatusOutput } from '../../core/change-classification.js';
3
+ const GIT_STATUS_TIMEOUT_MS = 10_000;
4
+ const GIT_STATUS_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
3
5
  export class GitChangedFilesError extends Error {
4
6
  result;
5
7
  constructor(result) {
@@ -9,9 +11,13 @@ export class GitChangedFilesError extends Error {
9
11
  }
10
12
  }
11
13
  export function readGitChangedFiles(projectRoot) {
12
- const result = spawnSync('git', ['status', '--short', '--untracked-files=all'], {
14
+ const result = spawnSync('git', ['status', '--porcelain=v1', '-z', '--untracked-files=all'], {
13
15
  cwd: projectRoot,
14
16
  encoding: 'utf8',
17
+ input: '',
18
+ maxBuffer: GIT_STATUS_MAX_BUFFER_BYTES,
19
+ stdio: ['ignore', 'pipe', 'pipe'],
20
+ timeout: GIT_STATUS_TIMEOUT_MS,
15
21
  windowsHide: true,
16
22
  });
17
23
  if (result.status !== 0 || typeof result.stdout !== 'string') {
@@ -331,16 +331,16 @@ function collectSourceAnchorCandidatePaths(projectRoot, sourceConfig) {
331
331
  excludeGeneratedOrVendor: true,
332
332
  });
333
333
  }
334
- function collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource, sourceConfig) {
334
+ function collectFastPreflightIndexedFileRecords(projectRoot, includeSource, sourceConfig) {
335
335
  const records = new Map();
336
336
  for (const relativePath of getExistingIndexablePaths(projectRoot)) {
337
- records.set(relativePath, readIndexedFileMetadataRecord(projectRoot, relativePath, 'workflow'));
337
+ records.set(relativePath, readIndexedFileRecord(projectRoot, relativePath, 'workflow'));
338
338
  }
339
339
  if (includeSource) {
340
340
  try {
341
341
  for (const sourcePath of collectSourceAnchorCandidatePaths(projectRoot, sourceConfig)) {
342
342
  if (!records.has(sourcePath)) {
343
- records.set(sourcePath, readIndexedFileMetadataRecord(projectRoot, sourcePath, 'source_anchor'));
343
+ records.set(sourcePath, readIndexedFileRecord(projectRoot, sourcePath, 'source_anchor'));
344
344
  }
345
345
  }
346
346
  }
@@ -349,7 +349,7 @@ function collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSour
349
349
  }
350
350
  }
351
351
  if (existsSync(path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/')))) {
352
- records.set(LATEST_RUN_STATE_RELATIVE_PATH, readIndexedFileMetadataRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
352
+ records.set(LATEST_RUN_STATE_RELATIVE_PATH, readIndexedFileRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
353
353
  }
354
354
  return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
355
355
  }
@@ -2053,27 +2053,6 @@ function createStoredLocalIndexResult(projectRoot, databasePath, dryRun, indexMo
2053
2053
  indexed_paths: readStoredIndexedPaths(database),
2054
2054
  };
2055
2055
  }
2056
- function indexedFileMetadataMatch(database, currentFiles) {
2057
- const rows = queryRows(database, 'SELECT path, source_scope, size_bytes, mtime_ms, parser_version FROM indexed_files ORDER BY path');
2058
- if (rows.length !== currentFiles.length) {
2059
- return false;
2060
- }
2061
- const currentByPath = new Map(currentFiles.map((file) => [file.path, file]));
2062
- for (const row of rows) {
2063
- const storedPath = toSearchString(row.path);
2064
- const current = currentByPath.get(storedPath);
2065
- if (!current) {
2066
- return false;
2067
- }
2068
- if (normalizeIndexedFileSourceScope(row.source_scope) !== current.sourceScope ||
2069
- row.size_bytes !== current.sizeBytes ||
2070
- row.mtime_ms !== current.mtimeMs ||
2071
- toSearchString(row.parser_version) !== LOCAL_INDEX_PARSER_VERSION) {
2072
- return false;
2073
- }
2074
- }
2075
- return true;
2076
- }
2077
2056
  function indexedFilesMatch(database, currentFiles) {
2078
2057
  const rows = queryRows(database, 'SELECT path, source_scope, content_hash, parser_version FROM indexed_files ORDER BY path');
2079
2058
  if (rows.length !== currentFiles.length) {
@@ -2116,7 +2095,7 @@ async function readIncrementalPreflightReuse(SQL, databasePath, projectRoot, cur
2116
2095
  if (!hasTable(database, 'indexed_files')) {
2117
2096
  return { result: null, rebuildReason: 'indexed_files_missing' };
2118
2097
  }
2119
- if (!indexedFileMetadataMatch(database, currentFiles)) {
2098
+ if (!indexedFilesMatch(database, currentFiles)) {
2120
2099
  return { result: null, rebuildReason: 'file_fingerprint_mismatch' };
2121
2100
  }
2122
2101
  const capabilities = readStoredSearchCapabilities(database);
@@ -2190,7 +2169,7 @@ export async function createLocalIndex(projectRoot, options = {}) {
2190
2169
  capabilities = detectLocalSearchCapabilities(capabilityDatabase);
2191
2170
  capabilityDatabase.close();
2192
2171
  if (incremental) {
2193
- const preflightFiles = collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource, sourceConfig);
2172
+ const preflightFiles = collectFastPreflightIndexedFileRecords(projectRoot, includeSource, sourceConfig);
2194
2173
  const preflightReuse = await readIncrementalPreflightReuse(SQL, databasePath, projectRoot, preflightFiles, sourceScopeHash, dryRun, indexMode);
2195
2174
  if (preflightReuse.result) {
2196
2175
  return preflightReuse.result;
@@ -46,14 +46,36 @@ function uniqueSorted(values) {
46
46
  return [...new Set(values)].sort((left, right) => left.localeCompare(right));
47
47
  }
48
48
  function toPosixPath(value) {
49
- return value.trim().replaceAll('\\', '/');
49
+ return value.replaceAll('\\', '/');
50
50
  }
51
51
  export function normalizeStatusPath(value) {
52
- const pathText = toPosixPath(value);
52
+ const pathText = toPosixPath(value.trim());
53
53
  const renameTarget = pathText.includes(' -> ') ? (pathText.split(' -> ').pop() ?? pathText) : pathText;
54
54
  return renameTarget.replace(/^"|"$/gu, '');
55
55
  }
56
+ function normalizePorcelainStatusPath(value) {
57
+ return toPosixPath(value);
58
+ }
59
+ function parseGitPorcelainStatusOutput(output) {
60
+ const paths = [];
61
+ const parts = output.split('\0').filter((part) => part.length > 0);
62
+ for (let index = 0; index < parts.length; index += 1) {
63
+ const entry = parts[index] ?? '';
64
+ const status = entry.slice(0, 2);
65
+ const filePath = normalizePorcelainStatusPath(entry.slice(3));
66
+ if (filePath.length > 0) {
67
+ paths.push(filePath);
68
+ }
69
+ if (status.includes('R') || status.includes('C')) {
70
+ index += 1;
71
+ }
72
+ }
73
+ return uniqueSorted(paths);
74
+ }
56
75
  export function parseGitStatusOutput(output) {
76
+ if (output.includes('\0')) {
77
+ return parseGitPorcelainStatusOutput(output);
78
+ }
57
79
  const paths = output
58
80
  .split(/\r?\n/u)
59
81
  .map((line) => line.slice(3))
@@ -125,6 +125,12 @@ export function commandIntentBlockedCommandPattern(intent) {
125
125
  detail: 'Shell command contains a blocked long-running or background pattern.',
126
126
  };
127
127
  }
128
+ if (intent.mode === 'shell' && typeof intent.cmd === 'string' && commandTextHasLongRunningPattern(intent.cmd)) {
129
+ return {
130
+ code: 'long_running_command_pattern',
131
+ detail: `Shell command contains a blocked long-running pattern: ${intent.cmd}.`,
132
+ };
133
+ }
128
134
  const argv = readStringArray(intent, 'argv');
129
135
  if (!argv) {
130
136
  return null;
@@ -130,6 +130,10 @@ function listObservedChangedPaths(before, after) {
130
130
  function declaredPathCoversObservedPath(declaredPath, observedPath) {
131
131
  const declaredKey = pathKey(declaredPath);
132
132
  const observedKey = pathKey(observedPath);
133
+ if (declaredKey.endsWith('/**')) {
134
+ const baseKey = declaredKey.slice(0, -3) || '.';
135
+ return baseKey === '.' || observedKey === baseKey || observedKey.startsWith(`${baseKey}/`);
136
+ }
133
137
  return declaredKey === '.' || observedKey === declaredKey || observedKey.startsWith(`${declaredKey}/`);
134
138
  }
135
139
  function truncatePaths(paths) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.18.20",
3
+ "version": "2.18.21",
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.18.20"
3
+ version = "2.18.21"
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"