mustflow 2.39.1 → 2.58.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.
Files changed (94) hide show
  1. package/dist/cli/commands/run/executor.js +16 -0
  2. package/dist/cli/commands/run/process-tree.js +6 -3
  3. package/dist/cli/commands/tech.js +60 -4
  4. package/dist/cli/commands/verify.js +3 -1
  5. package/dist/cli/lib/git-changes.js +11 -1
  6. package/dist/cli/lib/i18n.js +1 -1
  7. package/dist/cli/lib/local-index/index.js +8 -4
  8. package/dist/cli/lib/local-index/populate.js +17 -3
  9. package/dist/cli/lib/local-index/search-read-model.js +9 -7
  10. package/dist/cli/lib/local-index/search-text.js +2 -2
  11. package/dist/cli/lib/local-index/workflow-documents.js +17 -2
  12. package/dist/cli/lib/mustflow-read.js +14 -2
  13. package/dist/cli/lib/npm-version-check.js +36 -0
  14. package/dist/cli/lib/repo-map.js +16 -3
  15. package/dist/cli/lib/templates.js +8 -7
  16. package/dist/cli/lib/validation/constants.js +1 -1
  17. package/dist/core/active-run-locks.js +78 -20
  18. package/dist/core/change-classification.js +4 -0
  19. package/dist/core/command-contract-rules.js +1 -1
  20. package/dist/core/command-contract-validation.js +1 -1
  21. package/dist/core/command-cwd.js +13 -2
  22. package/dist/core/command-effects.js +22 -4
  23. package/dist/core/command-env.js +8 -6
  24. package/dist/core/command-preconditions.js +28 -2
  25. package/dist/core/completion-verdict.js +1 -1
  26. package/dist/core/line-endings.js +8 -4
  27. package/dist/core/safe-filesystem.js +9 -1
  28. package/dist/core/source-anchor-validation.js +7 -1
  29. package/dist/core/source-anchors.js +8 -2
  30. package/dist/core/verification-scheduler.js +8 -2
  31. package/package.json +1 -1
  32. package/templates/default/i18n.toml +330 -1
  33. package/templates/default/locales/en/.mustflow/skills/INDEX.md +302 -5
  34. package/templates/default/locales/en/.mustflow/skills/agent-eval-integrity-review/SKILL.md +160 -0
  35. package/templates/default/locales/en/.mustflow/skills/agent-execution-control-review/SKILL.md +163 -0
  36. package/templates/default/locales/en/.mustflow/skills/ai-generated-code-hardening/SKILL.md +49 -13
  37. package/templates/default/locales/en/.mustflow/skills/api-access-control-review/SKILL.md +298 -0
  38. package/templates/default/locales/en/.mustflow/skills/api-misuse-resistance-review/SKILL.md +297 -0
  39. package/templates/default/locales/en/.mustflow/skills/api-request-performance-review/SKILL.md +189 -0
  40. package/templates/default/locales/en/.mustflow/skills/app-startup-performance-review/SKILL.md +309 -0
  41. package/templates/default/locales/en/.mustflow/skills/backend-log-evidence-review/SKILL.md +213 -0
  42. package/templates/default/locales/en/.mustflow/skills/business-rule-leakage-review/SKILL.md +295 -0
  43. package/templates/default/locales/en/.mustflow/skills/cache-integrity-review/SKILL.md +291 -0
  44. package/templates/default/locales/en/.mustflow/skills/change-blast-radius-review/SKILL.md +297 -0
  45. package/templates/default/locales/en/.mustflow/skills/client-bundle-pruning-review/SKILL.md +160 -0
  46. package/templates/default/locales/en/.mustflow/skills/cloud-cost-guardrail-review/SKILL.md +321 -0
  47. package/templates/default/locales/en/.mustflow/skills/concurrency-invariant-review/SKILL.md +193 -0
  48. package/templates/default/locales/en/.mustflow/skills/core-web-vitals-field-review/SKILL.md +161 -0
  49. package/templates/default/locales/en/.mustflow/skills/credit-ledger-integrity-review/SKILL.md +156 -0
  50. package/templates/default/locales/en/.mustflow/skills/database-json-modeling-review/SKILL.md +171 -0
  51. package/templates/default/locales/en/.mustflow/skills/database-lock-contention-review/SKILL.md +192 -0
  52. package/templates/default/locales/en/.mustflow/skills/database-migration-change/SKILL.md +76 -34
  53. package/templates/default/locales/en/.mustflow/skills/database-query-bottleneck-review/SKILL.md +194 -0
  54. package/templates/default/locales/en/.mustflow/skills/deletion-lifecycle-review/SKILL.md +171 -0
  55. package/templates/default/locales/en/.mustflow/skills/deployment-rollout-safety-review/SKILL.md +321 -0
  56. package/templates/default/locales/en/.mustflow/skills/desktop-auto-update-safety-review/SKILL.md +265 -0
  57. package/templates/default/locales/en/.mustflow/skills/desktop-background-process-stability-review/SKILL.md +318 -0
  58. package/templates/default/locales/en/.mustflow/skills/desktop-memory-footprint-review/SKILL.md +318 -0
  59. package/templates/default/locales/en/.mustflow/skills/error-message-integrity-review/SKILL.md +283 -0
  60. package/templates/default/locales/en/.mustflow/skills/failure-integrity-review/SKILL.md +193 -0
  61. package/templates/default/locales/en/.mustflow/skills/file-upload-security-review/SKILL.md +305 -0
  62. package/templates/default/locales/en/.mustflow/skills/frame-render-performance-review/SKILL.md +159 -0
  63. package/templates/default/locales/en/.mustflow/skills/frontend-accessibility-tree-review/SKILL.md +202 -0
  64. package/templates/default/locales/en/.mustflow/skills/frontend-localization-review/SKILL.md +202 -0
  65. package/templates/default/locales/en/.mustflow/skills/frontend-state-ownership-review/SKILL.md +183 -0
  66. package/templates/default/locales/en/.mustflow/skills/frontend-stress-layout-review/SKILL.md +193 -0
  67. package/templates/default/locales/en/.mustflow/skills/hot-path-performance-review/SKILL.md +159 -0
  68. package/templates/default/locales/en/.mustflow/skills/idempotency-integrity-review/SKILL.md +195 -0
  69. package/templates/default/locales/en/.mustflow/skills/image-delivery-performance-review/SKILL.md +161 -0
  70. package/templates/default/locales/en/.mustflow/skills/incident-triage-review/SKILL.md +185 -0
  71. package/templates/default/locales/en/.mustflow/skills/llm-hallucination-control-review/SKILL.md +155 -0
  72. package/templates/default/locales/en/.mustflow/skills/llm-response-latency-review/SKILL.md +155 -0
  73. package/templates/default/locales/en/.mustflow/skills/llm-service-ux-review/SKILL.md +2 -0
  74. package/templates/default/locales/en/.mustflow/skills/llm-token-cost-control-review/SKILL.md +155 -0
  75. package/templates/default/locales/en/.mustflow/skills/low-end-device-support-review/SKILL.md +340 -0
  76. package/templates/default/locales/en/.mustflow/skills/memory-lifetime-review/SKILL.md +169 -0
  77. package/templates/default/locales/en/.mustflow/skills/mobile-energy-efficiency-review/SKILL.md +329 -0
  78. package/templates/default/locales/en/.mustflow/skills/module-boundary-review/SKILL.md +278 -0
  79. package/templates/default/locales/en/.mustflow/skills/multi-agent-work-coordination/SKILL.md +1 -0
  80. package/templates/default/locales/en/.mustflow/skills/observability-debuggability-review/SKILL.md +208 -0
  81. package/templates/default/locales/en/.mustflow/skills/payment-integrity-review/SKILL.md +155 -0
  82. package/templates/default/locales/en/.mustflow/skills/prompt-contract-quality-review/SKILL.md +158 -0
  83. package/templates/default/locales/en/.mustflow/skills/quadratic-scan-review/SKILL.md +155 -0
  84. package/templates/default/locales/en/.mustflow/skills/queue-processing-integrity-review/SKILL.md +193 -0
  85. package/templates/default/locales/en/.mustflow/skills/race-condition-review/SKILL.md +188 -0
  86. package/templates/default/locales/en/.mustflow/skills/rate-limit-integrity-review/SKILL.md +344 -0
  87. package/templates/default/locales/en/.mustflow/skills/retry-policy-integrity-review/SKILL.md +195 -0
  88. package/templates/default/locales/en/.mustflow/skills/routes.toml +330 -0
  89. package/templates/default/locales/en/.mustflow/skills/security-flow-review/SKILL.md +279 -0
  90. package/templates/default/locales/en/.mustflow/skills/testability-boundary-review/SKILL.md +295 -0
  91. package/templates/default/locales/en/.mustflow/skills/transaction-boundary-integrity-review/SKILL.md +196 -0
  92. package/templates/default/locales/en/.mustflow/skills/type-state-modeling-review/SKILL.md +179 -0
  93. package/templates/default/locales/en/.mustflow/skills/web-render-performance-review/SKILL.md +164 -0
  94. package/templates/default/manifest.toml +386 -1
@@ -1,7 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs';
2
+ import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync, } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { commandEffectsConflict, normalizeCommandEffects, } from './command-effects.js';
5
+ import { readUtf8FileInsideWithoutSymlinks, writeJsonFileInsideWithoutSymlinks, } from './safe-filesystem.js';
5
6
  const ACTIVE_LOCK_SCHEMA_VERSION = '1';
6
7
  const ACTIVE_LOCK_KIND = 'active_run_lock';
7
8
  const LOCK_ROOT_RELATIVE_PATH = '.mustflow/state/locks';
@@ -9,9 +10,19 @@ const LOCK_MUTEX_STALE_MS = 30_000;
9
10
  const LOCK_MUTEX_WAIT_MS = 1_000;
10
11
  const LOCK_MUTEX_SLEEP_MS = 25;
11
12
  const LOCK_MUTEX_SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
12
- const LOCK_MUTEX_RECOVERY_DIRECTORY = 'recovery';
13
+ const LOCK_MUTEX_RECOVERY_DIRECTORY = 'mutex.recovery';
14
+ const ACTIVE_LOCK_RECORD_MAX_BYTES = 256 * 1024;
15
+ const ACTIVE_LOCK_OWNER_MAX_BYTES = 16 * 1024;
13
16
  function sleep(milliseconds) {
14
- Atomics.wait(LOCK_MUTEX_SLEEP_BUFFER, 0, 0, milliseconds);
17
+ try {
18
+ Atomics.wait(LOCK_MUTEX_SLEEP_BUFFER, 0, 0, milliseconds);
19
+ }
20
+ catch {
21
+ const end = Date.now() + milliseconds;
22
+ while (Date.now() < end) {
23
+ // Fall back only for short mutex retry delays when Atomics.wait is unavailable on this runtime.
24
+ }
25
+ }
15
26
  }
16
27
  function sha256(value) {
17
28
  return createHash('sha256').update(value).digest('hex');
@@ -26,7 +37,7 @@ function activeLockMutexDirectory(projectRoot) {
26
37
  return path.join(activeLockRoot(projectRoot), 'mutex');
27
38
  }
28
39
  function activeLockMutexRecoveryDirectory(mutex) {
29
- return path.join(mutex, LOCK_MUTEX_RECOVERY_DIRECTORY);
40
+ return path.join(path.dirname(mutex), LOCK_MUTEX_RECOVERY_DIRECTORY);
30
41
  }
31
42
  function normalizeEffect(effect) {
32
43
  return {
@@ -121,9 +132,16 @@ function readActiveRecords(projectRoot) {
121
132
  return [];
122
133
  }
123
134
  const records = [];
124
- for (const name of readdirSync(directory).filter((entry) => entry.endsWith('.json')).sort()) {
135
+ const entries = readdirSync(directory, { withFileTypes: true })
136
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
137
+ .map((entry) => entry.name)
138
+ .sort();
139
+ for (const name of entries) {
125
140
  try {
126
- const record = parseRecord(JSON.parse(readFileSync(path.join(directory, name), 'utf8')));
141
+ const recordPath = path.join(directory, name);
142
+ const record = parseRecord(JSON.parse(readUtf8FileInsideWithoutSymlinks(directory, recordPath, {
143
+ maxBytes: ACTIVE_LOCK_RECORD_MAX_BYTES,
144
+ })));
127
145
  if (record) {
128
146
  records.push(record);
129
147
  }
@@ -153,9 +171,19 @@ function conflictDetail(current, active) {
153
171
  }
154
172
  function findConflicts(intentName, effects, records) {
155
173
  const conflicts = [];
174
+ const effectsByLock = new Map();
175
+ for (const effect of effects) {
176
+ const existing = effectsByLock.get(effect.lock);
177
+ if (existing) {
178
+ existing.push(effect);
179
+ }
180
+ else {
181
+ effectsByLock.set(effect.lock, [effect]);
182
+ }
183
+ }
156
184
  for (const record of records) {
157
185
  for (const activeEffect of commandEffectsFromRecord(record)) {
158
- for (const effect of effects) {
186
+ for (const effect of effectsByLock.get(activeEffect.lock) ?? []) {
159
187
  if (!commandEffectsConflict(effect, activeEffect)) {
160
188
  continue;
161
189
  }
@@ -198,7 +226,9 @@ function createRecord(projectRoot, intentName, effects, commandHash) {
198
226
  }
199
227
  function readMutexOwner(ownerPath) {
200
228
  try {
201
- const owner = JSON.parse(readFileSync(ownerPath, 'utf8'));
229
+ const owner = JSON.parse(readUtf8FileInsideWithoutSymlinks(path.dirname(ownerPath), ownerPath, {
230
+ maxBytes: ACTIVE_LOCK_OWNER_MAX_BYTES,
231
+ }));
202
232
  if (typeof owner.started_at !== 'string' || typeof owner.token !== 'string') {
203
233
  return null;
204
234
  }
@@ -249,6 +279,24 @@ function beginMutexRecovery(mutex) {
249
279
  rmSync(recoveryPath, { recursive: true, force: true });
250
280
  };
251
281
  }
282
+ function moveMutexAsideForRecovery(mutex, owner) {
283
+ const tokenSource = owner ? `${owner.pid}:${owner.startedAt}:${owner.token}` : `${process.pid}:${Date.now()}:${process.hrtime.bigint()}`;
284
+ const stalePath = path.join(path.dirname(mutex), `mutex.stale-${sha256(tokenSource)}`);
285
+ try {
286
+ rmSync(stalePath, { recursive: true, force: true });
287
+ renameSync(mutex, stalePath);
288
+ return stalePath;
289
+ }
290
+ catch {
291
+ try {
292
+ rmSync(stalePath, { recursive: true, force: true });
293
+ }
294
+ catch {
295
+ // Best-effort cleanup for a failed stale mutex move.
296
+ }
297
+ return null;
298
+ }
299
+ }
252
300
  function recoverStaleMutexWithOwner(mutex, ownerPath, staleOwner) {
253
301
  const releaseRecovery = beginMutexRecovery(mutex);
254
302
  if (!releaseRecovery) {
@@ -258,7 +306,11 @@ function recoverStaleMutexWithOwner(mutex, ownerPath, staleOwner) {
258
306
  if (!sameMutexOwner(staleOwner, readMutexOwner(ownerPath)) || !mutexOwnerIsStale(staleOwner)) {
259
307
  return false;
260
308
  }
261
- rmSync(mutex, { recursive: true, force: true });
309
+ const stalePath = moveMutexAsideForRecovery(mutex, staleOwner);
310
+ if (!stalePath) {
311
+ return false;
312
+ }
313
+ rmSync(stalePath, { recursive: true, force: true });
262
314
  return true;
263
315
  }
264
316
  finally {
@@ -275,7 +327,11 @@ function recoverStaleMutexWithoutOwner(mutex) {
275
327
  if (readMutexOwner(ownerPath) !== null) {
276
328
  return false;
277
329
  }
278
- rmSync(mutex, { recursive: true, force: true });
330
+ const stalePath = moveMutexAsideForRecovery(mutex, null);
331
+ if (!stalePath) {
332
+ return false;
333
+ }
334
+ rmSync(stalePath, { recursive: true, force: true });
279
335
  return true;
280
336
  }
281
337
  catch {
@@ -291,13 +347,13 @@ function acquireMutex(projectRoot) {
291
347
  const ownerPath = path.join(mutex, 'owner.json');
292
348
  const ownerToken = sha256(`${process.pid}:${Date.now()}:${process.hrtime.bigint()}`);
293
349
  mkdirSync(root, { recursive: true });
294
- let startedAt = Date.now();
350
+ const startedAt = Date.now();
295
351
  while (true) {
296
352
  try {
297
353
  mkdirSync(mutex);
298
354
  const ownerRecord = { pid: process.pid, started_at: new Date().toISOString(), token: ownerToken };
299
355
  try {
300
- writeFileSync(ownerPath, JSON.stringify(ownerRecord, null, 2));
356
+ writeJsonFileInsideWithoutSymlinks(root, ownerPath, ownerRecord);
301
357
  }
302
358
  catch (error) {
303
359
  rmSync(mutex, { recursive: true, force: true });
@@ -305,7 +361,9 @@ function acquireMutex(projectRoot) {
305
361
  }
306
362
  return () => {
307
363
  try {
308
- const owner = JSON.parse(readFileSync(ownerPath, 'utf8'));
364
+ const owner = JSON.parse(readUtf8FileInsideWithoutSymlinks(path.dirname(ownerPath), ownerPath, {
365
+ maxBytes: ACTIVE_LOCK_OWNER_MAX_BYTES,
366
+ }));
309
367
  if (Number(owner.pid) === ownerRecord.pid && owner.token === ownerRecord.token) {
310
368
  rmSync(mutex, { recursive: true, force: true });
311
369
  }
@@ -323,7 +381,6 @@ function acquireMutex(projectRoot) {
323
381
  const owner = readMutexOwner(ownerPath);
324
382
  if (owner) {
325
383
  if (mutexOwnerIsStale(owner) && recoverStaleMutexWithOwner(mutex, ownerPath, owner)) {
326
- startedAt = Date.now();
327
384
  continue;
328
385
  }
329
386
  }
@@ -343,12 +400,10 @@ function acquireMutex(projectRoot) {
343
400
  try {
344
401
  const mutexStat = statSync(mutex);
345
402
  if (Date.now() - mutexStat.mtimeMs > LOCK_MUTEX_STALE_MS && recoverStaleMutexWithoutOwner(mutex)) {
346
- startedAt = Date.now();
347
403
  continue;
348
404
  }
349
405
  }
350
406
  catch {
351
- startedAt = Date.now();
352
407
  continue;
353
408
  }
354
409
  }
@@ -362,7 +417,8 @@ export function inspectActiveRunLocks(projectRoot, contract, intentName) {
362
417
  const effects = normalizeCommandEffects(projectRoot, contract, intentName);
363
418
  const records = readActiveRecords(projectRoot);
364
419
  const staleRecords = records.map(staleRecordFor).filter((record) => record !== null);
365
- const liveRecords = records.filter((record) => !staleRecords.some((stale) => stale.runId === record.run_id));
420
+ const staleRecordIds = new Set(staleRecords.map((stale) => stale.runId));
421
+ const liveRecords = records.filter((record) => !staleRecordIds.has(record.run_id));
366
422
  return {
367
423
  conflicts: findConflicts(intentName, effects, liveRecords),
368
424
  staleRecords,
@@ -371,7 +427,8 @@ export function inspectActiveRunLocks(projectRoot, contract, intentName) {
371
427
  export function listActiveRunLocks(projectRoot) {
372
428
  const records = readActiveRecords(projectRoot);
373
429
  const staleRecords = records.map(staleRecordFor).filter((record) => record !== null);
374
- const activeRecords = records.filter((record) => !staleRecords.some((stale) => stale.runId === record.run_id));
430
+ const staleRecordIds = new Set(staleRecords.map((stale) => stale.runId));
431
+ const activeRecords = records.filter((record) => !staleRecordIds.has(record.run_id));
375
432
  return {
376
433
  records,
377
434
  activeRecords,
@@ -404,14 +461,15 @@ export function acquireActiveRunLock(projectRoot, contract, intentName, options
404
461
  removeRecord(projectRoot, staleRecord);
405
462
  }
406
463
  }
407
- const liveRecords = records.filter((record) => !staleRecords.some((stale) => stale.runId === record.run_id));
464
+ const staleRecordIds = new Set(staleRecords.map((stale) => stale.runId));
465
+ const liveRecords = records.filter((record) => !staleRecordIds.has(record.run_id));
408
466
  const conflicts = findConflicts(intentName, effects, liveRecords);
409
467
  if (conflicts.length > 0) {
410
468
  return { ok: false, conflicts, recoveredStaleRecords: staleRecords };
411
469
  }
412
470
  const record = createRecord(projectRoot, intentName, effects, options.commandHash ?? null);
413
471
  const recordPath = activeLockRecordPath(projectRoot, record.run_id);
414
- writeFileSync(recordPath, JSON.stringify(record, null, 2));
472
+ writeJsonFileInsideWithoutSymlinks(activeLockDirectory(projectRoot), recordPath, record);
415
473
  let released = false;
416
474
  return {
417
475
  ok: true,
@@ -41,6 +41,10 @@ function parseGitPorcelainStatusOutput(output) {
41
41
  paths.push(filePath);
42
42
  }
43
43
  if (status.includes('R') || status.includes('C')) {
44
+ const targetPath = normalizePorcelainStatusPath(parts[index + 1] ?? '');
45
+ if (targetPath.length > 0) {
46
+ paths.push(targetPath);
47
+ }
44
48
  index += 1;
45
49
  }
46
50
  }
@@ -41,7 +41,7 @@ export const LONG_RUNNING_COMMAND_TEXT_PATTERNS = [
41
41
  /\bturbo\s+dev\b/iu,
42
42
  /\btsx\s+(?:watch|--watch|-w)\b/iu,
43
43
  /\bcargo\s+(?:watch|tauri\s+dev)\b/iu,
44
- /\bzig\s+build\b(?=.*(?:--watch|-w|\bwatch\b))/iu,
44
+ /\bzig\s+build\b(?=[^\r\n;&|]*?(?:--watch(?:=|\b)|-w\b|\bwatch\b))/iu,
45
45
  /\btauri\s+dev\b/iu,
46
46
  /\bgh\s+(?:run\s+watch|codespace\s+ssh|codespace\s+logs\b.*(?:--follow|-f))\b/iu,
47
47
  /\bdeno\s+(?:serve|task\s+(?:dev|start|serve|watch|preview)|run\b.*(?:--watch|-w))\b/iu,
@@ -546,6 +546,6 @@ export function validateCommandContractStrictDefaults(projectRoot, commandsToml)
546
546
  issues.push(...validateCommandEnvInheritanceWarnings(commandsToml));
547
547
  issues.push(...validateProjectLocalBinWarnings(projectRoot, commandsToml));
548
548
  issues.push(...validateCommandEffects(projectRoot, commandsToml));
549
- issues.push(...validateCommandEffectLockWarnings(commandsToml));
549
+ issues.push(...validateCommandEffectLockWarnings(projectRoot, commandsToml));
550
550
  return issues;
551
551
  }
@@ -13,10 +13,21 @@ export function resolveSafeProjectCwd(projectRoot, rawCwd) {
13
13
  const cwd = rawCwd ?? '.';
14
14
  const resolved = path.resolve(projectRoot, cwd);
15
15
  const rootRealPath = realpathSync.native(projectRoot);
16
- if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
16
+ try {
17
+ if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
18
+ throw new Error();
19
+ }
20
+ }
21
+ catch {
22
+ throw new Error(`Intent cwd must stay inside the current root and resolve to an existing directory: ${cwd}`);
23
+ }
24
+ let cwdRealPath;
25
+ try {
26
+ cwdRealPath = realpathSync.native(resolved);
27
+ }
28
+ catch {
17
29
  throw new Error(`Intent cwd must stay inside the current root and resolve to an existing directory: ${cwd}`);
18
30
  }
19
- const cwdRealPath = realpathSync.native(resolved);
20
31
  if (!isInsideOrEqual(rootRealPath, cwdRealPath)) {
21
32
  throw new Error(`Intent cwd must stay inside the current root and resolve to an existing directory: ${cwd}`);
22
33
  }
@@ -161,11 +161,16 @@ export function validateCommandEffects(projectRoot, commandsToml) {
161
161
  }
162
162
  return issues;
163
163
  }
164
- export function validateCommandEffectLockWarnings(commandsToml) {
164
+ export function validateCommandEffectLockWarnings(projectRoot, commandsToml) {
165
165
  const issues = [];
166
166
  if (!commandsToml || !isRecord(commandsToml.intents)) {
167
167
  return issues;
168
168
  }
169
+ const commandContract = {
170
+ defaults: isRecord(commandsToml.defaults) ? commandsToml.defaults : {},
171
+ intents: commandsToml.intents,
172
+ resources: isRecord(commandsToml.resources) ? commandsToml.resources : {},
173
+ };
169
174
  const lockToIntents = new Map();
170
175
  for (const [intentName, intent] of Object.entries(commandsToml.intents)) {
171
176
  if (!isRecord(intent)) {
@@ -177,9 +182,22 @@ export function validateCommandEffectLockWarnings(commandsToml) {
177
182
  Array.isArray(intent.effects)) {
178
183
  continue;
179
184
  }
180
- for (const rawPath of readStringArray(intent, 'writes') ?? []) {
181
- const lock = pathLockKey(rawPath);
182
- lockToIntents.set(lock, [...(lockToIntents.get(lock) ?? []), intentName]);
185
+ let effects;
186
+ try {
187
+ effects = normalizeCommandEffects(projectRoot, commandContract, intentName).filter((effect) => effect.source === 'writes');
188
+ }
189
+ catch {
190
+ continue;
191
+ }
192
+ for (const effect of effects) {
193
+ const lock = effect.lock;
194
+ const intents = lockToIntents.get(lock);
195
+ if (intents) {
196
+ intents.push(intentName);
197
+ }
198
+ else {
199
+ lockToIntents.set(lock, [intentName]);
200
+ }
183
201
  }
184
202
  }
185
203
  for (const [lock, intents] of lockToIntents) {
@@ -105,14 +105,16 @@ function removeProjectLocalBinFromPath(env, projectRoot) {
105
105
  const pathKey = getPathEnvKey(env);
106
106
  const currentPath = env[pathKey];
107
107
  if (!currentPath) {
108
- return env;
108
+ return { ...env };
109
109
  }
110
110
  const localBinPath = path.join(projectRoot, 'node_modules', '.bin');
111
- env[pathKey] = currentPath
112
- .split(path.delimiter)
113
- .filter((entry) => entry.length > 0 && !sameResolvedPath(entry, localBinPath))
114
- .join(path.delimiter);
115
- return env;
111
+ return {
112
+ ...env,
113
+ [pathKey]: currentPath
114
+ .split(path.delimiter)
115
+ .filter((entry) => entry.length > 0 && !sameResolvedPath(entry, localBinPath))
116
+ .join(path.delimiter),
117
+ };
116
118
  }
117
119
  export function resolveCommandEnv(contract, intent) {
118
120
  const policy = readEnvPolicy(intent) ?? readEnvPolicy(contract.defaults) ?? DEFAULT_COMMAND_ENV_POLICY;
@@ -84,6 +84,10 @@ function globToRegExp(pattern) {
84
84
  expression += '[^/]*';
85
85
  continue;
86
86
  }
87
+ if (character === '?') {
88
+ expression += '[^/]';
89
+ continue;
90
+ }
87
91
  expression += escapeRegExp(character);
88
92
  }
89
93
  return new RegExp(`${expression}$`, 'u');
@@ -219,10 +223,32 @@ function evaluateArtifactFreshness(projectRoot, declaration, satisfyIntent, cont
219
223
  satisfyIntent,
220
224
  };
221
225
  }
222
- const artifactMtime = statSync(artifactPath).mtimeMs;
226
+ let artifactMtime;
227
+ try {
228
+ artifactMtime = statSync(artifactPath).mtimeMs;
229
+ }
230
+ catch {
231
+ return {
232
+ kind: declaration.kind,
233
+ label: declaration.label,
234
+ status: 'missing',
235
+ detail: `artifact "${artifact}" is missing.`,
236
+ path: null,
237
+ artifact,
238
+ sources: declaration.sources,
239
+ newestSource: null,
240
+ satisfyIntent,
241
+ };
242
+ }
223
243
  let newest = null;
224
244
  for (const source of sourceFiles) {
225
- const mtime = statSync(path.join(projectRoot, ...source.split('/'))).mtimeMs;
245
+ let mtime;
246
+ try {
247
+ mtime = statSync(path.join(projectRoot, ...source.split('/'))).mtimeMs;
248
+ }
249
+ catch {
250
+ continue;
251
+ }
226
252
  if (!newest || mtime > newest.mtime) {
227
253
  newest = { source, mtime };
228
254
  }
@@ -219,7 +219,7 @@ export function createVerifyCompletionVerdict(input) {
219
219
  failed_intents: normalizedInput.failedIntents,
220
220
  skipped_intents: normalizedInput.skippedIntents,
221
221
  receipt_count: normalizedInput.receiptCount,
222
- gap_count: normalizedInput.skippedIntents,
222
+ gap_count: normalizedInput.gapCount ?? 0,
223
223
  source_anchor_risk_count: normalizedInput.sourceAnchorRiskCount ?? 0,
224
224
  scope_diff_risk_count: normalizedInput.scopeDiffRiskCount ?? 0,
225
225
  repeated_failure_count: normalizedInput.repeatedFailureCount ?? 0,
@@ -21,6 +21,7 @@ function gitList(projectRoot, args) {
21
21
  encoding: 'buffer',
22
22
  env: createCommandEnv(projectRoot, { policy: 'minimal', allowlist: [] }),
23
23
  stdio: ['ignore', 'pipe', 'pipe'],
24
+ timeout: 30_000,
24
25
  windowsHide: true,
25
26
  });
26
27
  if (result.status !== 0) {
@@ -71,19 +72,22 @@ function detectLineEnding(buffer) {
71
72
  return crlfCount === lfCount ? 'crlf' : 'mixed';
72
73
  }
73
74
  function normalizeLf(buffer) {
74
- const bytes = [];
75
+ const bytes = Buffer.allocUnsafe(buffer.length);
76
+ let writeIndex = 0;
75
77
  for (let index = 0; index < buffer.length; index += 1) {
76
78
  const byte = buffer[index];
77
79
  if (byte === 0x0d) {
78
- bytes.push(0x0a);
80
+ bytes[writeIndex] = 0x0a;
81
+ writeIndex += 1;
79
82
  if (buffer[index + 1] === 0x0a) {
80
83
  index += 1;
81
84
  }
82
85
  continue;
83
86
  }
84
- bytes.push(byte);
87
+ bytes[writeIndex] = byte;
88
+ writeIndex += 1;
85
89
  }
86
- return Buffer.from(bytes);
90
+ return bytes.subarray(0, writeIndex);
87
91
  }
88
92
  export function inspectLineEndings(projectRoot, mode, options = {}) {
89
93
  const root = path.resolve(projectRoot);
@@ -14,7 +14,15 @@ function tempFilePath(targetPath) {
14
14
  return path.join(path.dirname(targetPath), `.${path.basename(targetPath)}.${suffix}.tmp`);
15
15
  }
16
16
  function sleep(milliseconds) {
17
- Atomics.wait(WRITE_SLEEP_BUFFER, 0, 0, milliseconds);
17
+ try {
18
+ Atomics.wait(WRITE_SLEEP_BUFFER, 0, 0, milliseconds);
19
+ }
20
+ catch {
21
+ const end = Date.now() + milliseconds;
22
+ while (Date.now() < end) {
23
+ // Fall back only for short retry delays when Atomics.wait is unavailable on this runtime.
24
+ }
25
+ }
18
26
  }
19
27
  function isRetryableWindowsRenameError(error) {
20
28
  if (process.platform !== 'win32' || !error || typeof error !== 'object' || !('code' in error)) {
@@ -131,7 +131,13 @@ export function validateSourceAnchorsInProject(projectRoot) {
131
131
  const anchorsById = new Map();
132
132
  for (const relativePath of sourceFiles) {
133
133
  const absolutePath = path.join(projectRoot, ...relativePath.split('/'));
134
- const content = readFileSync(absolutePath, 'utf8');
134
+ let content;
135
+ try {
136
+ content = readFileSync(absolutePath, 'utf8');
137
+ }
138
+ catch {
139
+ continue;
140
+ }
135
141
  const anchors = parseSourceAnchorsInContent(relativePath, content);
136
142
  issues.push(...validateSourceAnchorDensity(relativePath, content, anchors));
137
143
  for (const anchor of anchors) {
@@ -321,10 +321,16 @@ export function collectSourceAnchorSummaries(projectRoot) {
321
321
  const anchors = [];
322
322
  for (const relativePath of listSourceAnchorFiles(projectRoot)) {
323
323
  const filePath = path.join(projectRoot, ...relativePath.split('/'));
324
- if (!existsSync(filePath) || !statSync(filePath).isFile()) {
324
+ let content;
325
+ try {
326
+ if (!existsSync(filePath) || !statSync(filePath).isFile()) {
327
+ continue;
328
+ }
329
+ content = readFileSync(filePath, 'utf8');
330
+ }
331
+ catch {
325
332
  continue;
326
333
  }
327
- const content = readFileSync(filePath, 'utf8');
328
334
  for (const anchor of parseSourceAnchorsInContent(relativePath, content)) {
329
335
  if (!anchor.idValid || sourceAnchorTextContainsSecretLike(anchor.rawText)) {
330
336
  continue;
@@ -19,12 +19,18 @@ function toScheduleEffect(effect) {
19
19
  function isObject(value) {
20
20
  return !!value && typeof value === 'object';
21
21
  }
22
+ function isMissingPathError(error) {
23
+ return error instanceof Error && 'code' in error && error.code === 'ENOENT';
24
+ }
22
25
  function readJsonFile(projectRoot, filePath) {
23
26
  try {
24
27
  return JSON.parse(readUtf8FileInsideWithoutSymlinks(projectRoot, filePath, { maxBytes: MUSTFLOW_JSON_MAX_BYTES }));
25
28
  }
26
- catch {
27
- return null;
29
+ catch (error) {
30
+ if (isMissingPathError(error)) {
31
+ return null;
32
+ }
33
+ throw error;
28
34
  }
29
35
  }
30
36
  function getUndeclaredWriteIntent(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.39.1",
3
+ "version": "2.58.1",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",