mustflow 2.18.3 → 2.18.7

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
@@ -356,6 +356,8 @@ Runnable work is declared in `.mustflow/config/commands.toml` so agents do not g
356
356
 
357
357
  Development servers, watch modes, browser UIs, interactive commands, and background processes do not run directly. `mf run` also rejects obvious long-running `argv` shapes, such as shell-wrapper background payloads, interpreter loops, package-manager development scripts, watchers, and development servers declared as one-shot commands.
358
358
 
359
+ Command environments remove the project-local `node_modules/.bin` path from `PATH` by default. If an intent needs a project dependency binary such as `eslint`, `tsc`, or `vitest`, declare it through the package manager, for example `npm exec eslint -- ...`, `pnpm exec tsc -- --noEmit`, `bun x eslint ...`, or `yarn exec eslint ...`. `mf check --strict` warns when an agent-runnable intent uses a bare executable name that appears under the project-local `.bin` directory.
360
+
359
361
  Use `mf verify --reason <event> --plan-only --json` to inspect matching verification intents, command eligibility, 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.
360
362
 
361
363
  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.
@@ -1,8 +1,9 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { BoundedOutputBuffer } from '../../../core/bounded-output.js';
3
- import { createPendingTimeoutTermination, forceTerminateProcessTreeNonBlocking, getKillMethod, terminateProcessTreeNonBlocking, } from './process-tree.js';
3
+ import { createPendingTimeoutTermination, forceTerminateProcessTree, getKillMethod, terminateProcessTree, } from './process-tree.js';
4
4
  import { createOutputLimitError, isOutputLimitExceededError, writeStreamChunk } from './output.js';
5
- function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
5
+ const TERMINATION_CONFIRMATION_FALLBACK_MS = 1000;
6
+ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
6
7
  return new Promise((resolve) => {
7
8
  const stdout = new BoundedOutputBuffer(stdoutTailBytes);
8
9
  const stderr = new BoundedOutputBuffer(stderrTailBytes);
@@ -13,6 +14,9 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutput
13
14
  let stdoutBytes = 0;
14
15
  let stderrBytes = 0;
15
16
  let timeout;
17
+ let forceKillTimeout;
18
+ let terminationFallbackTimeout;
19
+ let terminationStarted = false;
16
20
  let termination = null;
17
21
  const child = spawn(command.executable, command.args ?? [], {
18
22
  cwd,
@@ -23,7 +27,7 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutput
23
27
  detached: process.platform !== 'win32',
24
28
  });
25
29
  childPid = child.pid;
26
- const finish = (status, signal) => {
30
+ const finish = (status, signal, terminationConfirmed = true) => {
27
31
  if (settled) {
28
32
  return;
29
33
  }
@@ -31,6 +35,19 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutput
31
35
  if (timeout) {
32
36
  clearTimeout(timeout);
33
37
  }
38
+ if (forceKillTimeout) {
39
+ clearTimeout(forceKillTimeout);
40
+ }
41
+ if (terminationFallbackTimeout) {
42
+ clearTimeout(terminationFallbackTimeout);
43
+ }
44
+ const confirmedTermination = termination ?
45
+ {
46
+ ...termination,
47
+ confirmed: terminationConfirmed,
48
+ cleanup_pending: !terminationConfirmed,
49
+ } :
50
+ null;
34
51
  resolve({
35
52
  status: timedOut ? null : status,
36
53
  signal: timedOut ? null : signal,
@@ -38,20 +55,42 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutput
38
55
  stdout: stdout.toSnapshot(),
39
56
  stderr: stderr.toSnapshot(),
40
57
  pid: childPid,
41
- termination,
58
+ termination: confirmedTermination,
42
59
  });
43
60
  };
61
+ const beginTermination = () => {
62
+ if (terminationStarted) {
63
+ return;
64
+ }
65
+ terminationStarted = true;
66
+ child.stdout?.destroy();
67
+ child.stderr?.destroy();
68
+ terminateProcessTree(childPid);
69
+ const forceAfterMs = killAfterSeconds * 1000;
70
+ forceKillTimeout = setTimeout(() => {
71
+ if (termination) {
72
+ termination = {
73
+ ...termination,
74
+ forced_kill_attempted: true,
75
+ };
76
+ }
77
+ forceTerminateProcessTree(childPid);
78
+ }, forceAfterMs);
79
+ terminationFallbackTimeout = setTimeout(() => {
80
+ child.unref();
81
+ finish(null, null, false);
82
+ }, forceAfterMs + TERMINATION_CONFIRMATION_FALLBACK_MS);
83
+ };
44
84
  const stopForOutputLimit = (stream) => {
45
85
  if (settled || childError) {
46
86
  return;
47
87
  }
48
88
  childError = createOutputLimitError(stream, maxOutputBytes);
49
- child.stdout?.destroy();
50
- child.stderr?.destroy();
51
- child.unref();
52
- terminateProcessTreeNonBlocking(childPid);
53
- forceTerminateProcessTreeNonBlocking(childPid);
54
- finish(null, null);
89
+ if (timeout) {
90
+ clearTimeout(timeout);
91
+ timeout = undefined;
92
+ }
93
+ beginTermination();
55
94
  };
56
95
  child.stdout?.on('data', (chunk) => {
57
96
  stdout.append(chunk);
@@ -80,22 +119,20 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutput
80
119
  finish(status, signal);
81
120
  });
82
121
  timeout = setTimeout(() => {
122
+ if (settled || childError) {
123
+ return;
124
+ }
83
125
  timedOut = true;
84
- child.stdout?.destroy();
85
- child.stderr?.destroy();
86
- child.unref();
87
126
  termination = createPendingTimeoutTermination(getKillMethod());
88
- terminateProcessTreeNonBlocking(childPid);
89
- forceTerminateProcessTreeNonBlocking(childPid);
90
- finish(null, null);
127
+ beginTermination();
91
128
  }, timeoutSeconds * 1000);
92
129
  });
93
130
  }
94
- export function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
95
- return runSpawnedCommandStreaming({ executable: command?.executable ?? '', args: command?.args ?? [], shell: command?.shell ?? false }, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
131
+ export function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
132
+ return runSpawnedCommandStreaming({ executable: command?.executable ?? '', args: command?.args ?? [], shell: command?.shell ?? false }, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
96
133
  }
97
- export function runShellCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
98
- return runSpawnedCommandStreaming({ executable: command ?? '', shell: true }, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
134
+ export function runShellCommandStreaming(command, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
135
+ return runSpawnedCommandStreaming({ executable: command ?? '', shell: true }, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
99
136
  }
100
137
  export function getRunStatus(error, exitCode, successExitCodes) {
101
138
  const errorWithCode = error;
@@ -78,13 +78,13 @@ export function forceTerminateProcessTreeNonBlocking(pid) {
78
78
  export function getKillMethod() {
79
79
  return process.platform === 'win32' ? 'taskkill_process_tree' : 'process_group_sigterm';
80
80
  }
81
- export function createPendingTimeoutTermination(method) {
81
+ export function createPendingTimeoutTermination(method, forcedKillAttempted = false) {
82
82
  return {
83
83
  reason: 'timeout',
84
84
  method,
85
85
  graceful_signal: 'SIGTERM',
86
86
  forced_signal: 'SIGKILL',
87
- forced_kill_attempted: true,
87
+ forced_kill_attempted: forcedKillAttempted,
88
88
  confirmed: false,
89
89
  cleanup_pending: true,
90
90
  };
@@ -12,7 +12,7 @@ import { RunProfiler } from '../../core/run-profile.js';
12
12
  import { finishRunWriteTracking, startRunWriteTracking } from '../../core/run-write-drift.js';
13
13
  import { runBuiltinArgvInProcess } from './run/builtin-dispatch.js';
14
14
  import { getRunStatus, runArgvCommandStreaming, runShellCommandStreaming } from './run/executor.js';
15
- import { emitOutput } from './run/output.js';
15
+ import { emitOutput, isOutputLimitExceededError } from './run/output.js';
16
16
  import { createPendingTimeoutTermination, getKillMethod, terminateProcessTree } from './run/process-tree.js';
17
17
  import { assembleRunReceipt } from './run/receipt.js';
18
18
  function getRunPlanDetail(plan, lang, fallbackKey) {
@@ -187,10 +187,10 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
187
187
  }
188
188
  if (plan.commandArgv) {
189
189
  streamedOutput = !json;
190
- return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, json);
190
+ return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
191
191
  }
192
192
  streamedOutput = !json;
193
- return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, json);
193
+ return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
194
194
  });
195
195
  const childDurationMs = performance.now() - childStartedAtMs;
196
196
  const finishedAt = new Date();
@@ -247,6 +247,10 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
247
247
  reporter.stderr(t(lang, 'run.error.timedOut', { intent: intentName, seconds: plan.timeoutSeconds }));
248
248
  return 1;
249
249
  }
250
+ if (isOutputLimitExceededError(result.error)) {
251
+ reporter.stderr(t(lang, 'run.error.outputLimitExceeded', { intent: intentName, message: result.error.message }));
252
+ return 1;
253
+ }
250
254
  reporter.stderr(t(lang, 'run.error.startFailed', { intent: intentName, message: result.error.message }));
251
255
  return 1;
252
256
  }
@@ -672,6 +672,7 @@ Read these files before working:
672
672
  "run.error.maxOutputBytesDetail": "The output limit must stay within the allowed maximum.",
673
673
  "run.error.conflictingPreviewModes": "Use either --dry-run or --plan-only, not both",
674
674
  "run.error.timedOut": 'Command "{intent}" timed out after {seconds} seconds',
675
+ "run.error.outputLimitExceeded": 'Command "{intent}" exceeded max_output_bytes: {message}',
675
676
  "run.error.startFailed": 'Command "{intent}" failed to start: {message}',
676
677
  "search.help.summary": "Search the local SQLite index for the mustflow workflow.",
677
678
  "search.help.option.limit": "Set the number of results to print. Default: 10, max: 50",
@@ -672,6 +672,7 @@ Lee estos archivos antes de trabajar:
672
672
  "run.error.maxOutputBytesDetail": "El límite de salida debe permanecer dentro del máximo permitido.",
673
673
  "run.error.conflictingPreviewModes": "Usa --dry-run o --plan-only, no ambos",
674
674
  "run.error.timedOut": 'El comando "{intent}" agotó el tiempo después de {seconds} segundos',
675
+ "run.error.outputLimitExceeded": 'El comando "{intent}" superó max_output_bytes: {message}',
675
676
  "run.error.startFailed": 'No se pudo iniciar el comando "{intent}": {message}',
676
677
  "search.help.summary": "Busca en el índice SQLite local del flujo de trabajo mustflow.",
677
678
  "search.help.option.limit": "Define la cantidad de resultados que se imprimen. Predeterminado: 10, máximo: 50",
@@ -672,6 +672,7 @@ Lisez ces fichiers avant de travailler :
672
672
  "run.error.maxOutputBytesDetail": "La limite de sortie doit rester dans le maximum autorisé.",
673
673
  "run.error.conflictingPreviewModes": "Utilisez --dry-run ou --plan-only, pas les deux",
674
674
  "run.error.timedOut": 'La commande "{intent}" a expiré après {seconds} secondes',
675
+ "run.error.outputLimitExceeded": 'La commande "{intent}" a dépassé max_output_bytes : {message}',
675
676
  "run.error.startFailed": 'Impossible de démarrer la commande "{intent}" : {message}',
676
677
  "search.help.summary": "Recherche dans l'index SQLite local du flux de travail mustflow.",
677
678
  "search.help.option.limit": "Définit le nombre de résultats à imprimer. Par défaut : 10, max : 50",
@@ -672,6 +672,7 @@ export const hiMessages = {
672
672
  "run.error.maxOutputBytesDetail": "Output limit अनुमत maximum के अंदर रहनी चाहिए।",
673
673
  "run.error.conflictingPreviewModes": "--dry-run या --plan-only में से एक इस्तेमाल करें, दोनों नहीं",
674
674
  "run.error.timedOut": 'कमांड "{intent}" {seconds} सेकंड बाद time out हुई',
675
+ "run.error.outputLimitExceeded": 'कमांड "{intent}" ने max_output_bytes सीमा पार की: {message}',
675
676
  "run.error.startFailed": 'कमांड "{intent}" शुरू नहीं हो सकी: {message}',
676
677
  "search.help.summary": "mustflow वर्कफ़्लो के लिए स्थानीय SQLite इंडेक्स में खोजें।",
677
678
  "search.help.option.limit": "प्रिंट किए जाने वाले परिणामों की संख्या सेट करें। डिफ़ॉल्ट: 10, अधिकतम: 50",
@@ -672,6 +672,7 @@ export const koMessages = {
672
672
  "run.error.maxOutputBytesDetail": "출력 상한은 허용된 최댓값 안에 있어야 합니다.",
673
673
  "run.error.conflictingPreviewModes": "--dry-run과 --plan-only 중 하나만 사용하세요",
674
674
  "run.error.timedOut": '명령 "{intent}"가 {seconds}초 뒤 시간 초과되었습니다',
675
+ "run.error.outputLimitExceeded": '명령 "{intent}"가 max_output_bytes 제한을 넘었습니다: {message}',
675
676
  "run.error.startFailed": '명령 "{intent}"를 시작하지 못했습니다: {message}',
676
677
  "search.help.summary": "로컬 SQLite 색인에서 mustflow 워크플로우를 검색합니다.",
677
678
  "search.help.option.limit": "출력할 검색 결과 수를 설정합니다. 기본값: 10, 최대: 50",
@@ -672,6 +672,7 @@ export const zhMessages = {
672
672
  "run.error.maxOutputBytesDetail": "输出限制必须保持在允许的最大值内。",
673
673
  "run.error.conflictingPreviewModes": "只能使用 --dry-run 或 --plan-only,不能同时使用",
674
674
  "run.error.timedOut": '命令 "{intent}" 在 {seconds} 秒后超时',
675
+ "run.error.outputLimitExceeded": '命令 "{intent}" 超过 max_output_bytes:{message}',
675
676
  "run.error.startFailed": '命令 "{intent}" 启动失败:{message}',
676
677
  "search.help.summary": "搜索本地 SQLite 索引中的 mustflow 工作流。",
677
678
  "search.help.option.limit": "设置要输出的结果数量。默认值:10,最大:50",
@@ -4,6 +4,7 @@ import { createDashboardCompletionVerdict } from '../../core/completion-verdict.
4
4
  import { createDashboardEvidenceModel } from '../../core/verification-evidence.js';
5
5
  import { redactSecretLikeText } from '../../core/secret-redaction.js';
6
6
  import { ensureFileTargetInsideWithoutSymlinks, ensureInside, toPosixPath, writeUtf8FileInsideWithoutSymlinks, } from './filesystem.js';
7
+ import { safeJsonForInlineScript } from './html-json.js';
7
8
  export class DashboardExportPathError extends Error {
8
9
  targetPath;
9
10
  constructor(targetPath) {
@@ -634,7 +635,7 @@ export function renderDashboardExportHtml(snapshot) {
634
635
  const graphSummary = asRecord(harnessVerification.decision_graph_summary);
635
636
  const harnessRunHistory = asRecord(harnessReport.run_history);
636
637
  const harnessDocsReview = asRecord(harnessReport.docs_review);
637
- const embeddedJson = JSON.stringify(snapshot).replace(/</gu, '\\u003c');
638
+ const embeddedJson = safeJsonForInlineScript(snapshot);
638
639
  return `<!doctype html>
639
640
  <html lang="en">
640
641
  <head>
@@ -1,8 +1,9 @@
1
1
  import { getDashboardLocaleBundle } from '../dashboard-locale.js';
2
+ import { safeJsonForInlineScript } from '../html-json.js';
2
3
  export function createDashboardLocaleBootstrap() {
3
4
  const localeBundle = getDashboardLocaleBundle();
4
5
  return {
5
- serializedLocaleBundle: JSON.stringify(localeBundle),
6
- serializedAvailableLocales: JSON.stringify(localeBundle.locales),
6
+ serializedLocaleBundle: safeJsonForInlineScript(localeBundle),
7
+ serializedAvailableLocales: safeJsonForInlineScript(localeBundle.locales),
7
8
  };
8
9
  }
@@ -1,3 +1,4 @@
1
+ import { safeJsonForInlineScript } from '../html-json.js';
1
2
  import { renderDashboardClientScript } from './client-script.js';
2
3
  import { createDashboardLocaleBootstrap } from './locale-bootstrap.js';
3
4
  import { renderDashboardStyles } from './styles.js';
@@ -12,10 +13,10 @@ function escapeHtml(value) {
12
13
  export function renderDashboardHtml(snapshot, token, statusSnapshot, docReviewSnapshot) {
13
14
  const root = escapeHtml(snapshot.projectRoot);
14
15
  const preferencesPath = escapeHtml(snapshot.preferencesPath);
15
- const serializedSnapshot = JSON.stringify(snapshot);
16
- const serializedStatusSnapshot = JSON.stringify(statusSnapshot);
17
- const serializedDocReviewSnapshot = JSON.stringify(docReviewSnapshot);
18
- const serializedToken = JSON.stringify(token);
16
+ const serializedSnapshot = safeJsonForInlineScript(snapshot);
17
+ const serializedStatusSnapshot = safeJsonForInlineScript(statusSnapshot);
18
+ const serializedDocReviewSnapshot = safeJsonForInlineScript(docReviewSnapshot);
19
+ const serializedToken = safeJsonForInlineScript(token);
19
20
  const { serializedLocaleBundle, serializedAvailableLocales } = createDashboardLocaleBootstrap();
20
21
  return `<!doctype html>
21
22
  <html lang="en">
@@ -0,0 +1,11 @@
1
+ const INLINE_SCRIPT_JSON_ESCAPES = {
2
+ '<': '\\u003C',
3
+ '>': '\\u003E',
4
+ '&': '\\u0026',
5
+ '\u2028': '\\u2028',
6
+ '\u2029': '\\u2029',
7
+ };
8
+ export function safeJsonForInlineScript(value) {
9
+ const json = JSON.stringify(value);
10
+ return (json ?? 'null').replace(/[<>&\u2028\u2029]/gu, (character) => INLINE_SCRIPT_JSON_ESCAPES[character]);
11
+ }
@@ -284,7 +284,21 @@ function collectCommandIntents(projectRoot) {
284
284
  }
285
285
  return intents;
286
286
  }
287
+ function normalizeIndexedFileSourceScope(value) {
288
+ const sourceScope = toSearchString(value);
289
+ if (sourceScope === 'source_anchor' || sourceScope === 'state') {
290
+ return sourceScope;
291
+ }
292
+ return 'workflow';
293
+ }
287
294
  function readIndexedFileRecord(projectRoot, relativePath, sourceScope, contentHash = null) {
295
+ const metadata = readIndexedFileMetadataRecord(projectRoot, relativePath, sourceScope);
296
+ return {
297
+ ...metadata,
298
+ contentHash: contentHash ?? sha256Bytes(readFileSync(path.join(projectRoot, ...relativePath.split('/')))),
299
+ };
300
+ }
301
+ function readIndexedFileMetadataRecord(projectRoot, relativePath, sourceScope) {
288
302
  const fullPath = path.join(projectRoot, ...relativePath.split('/'));
289
303
  const stats = statSync(fullPath);
290
304
  return {
@@ -292,7 +306,6 @@ function readIndexedFileRecord(projectRoot, relativePath, sourceScope, contentHa
292
306
  sourceScope,
293
307
  sizeBytes: stats.size,
294
308
  mtimeMs: Math.round(stats.mtimeMs),
295
- contentHash: contentHash ?? sha256Bytes(readFileSync(fullPath)),
296
309
  };
297
310
  }
298
311
  function collectIndexedFileRecords(projectRoot, documents, sourceAnchors) {
@@ -305,8 +318,21 @@ function collectIndexedFileRecords(projectRoot, documents, sourceAnchors) {
305
318
  records.set(anchorPath, readIndexedFileRecord(projectRoot, anchorPath, 'source_anchor'));
306
319
  }
307
320
  }
321
+ if (existsSync(path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/')))) {
322
+ records.set(LATEST_RUN_STATE_RELATIVE_PATH, readIndexedFileRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
323
+ }
308
324
  return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
309
325
  }
326
+ function collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource) {
327
+ if (includeSource) {
328
+ return null;
329
+ }
330
+ const records = getExistingIndexablePaths(projectRoot).map((relativePath) => readIndexedFileMetadataRecord(projectRoot, relativePath, 'workflow'));
331
+ if (existsSync(path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/')))) {
332
+ records.push(readIndexedFileMetadataRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
333
+ }
334
+ return records.sort((left, right) => left.path.localeCompare(right.path));
335
+ }
310
336
  function normalizeSearchText(value) {
311
337
  return value.trim().replace(/\s+/g, ' ');
312
338
  }
@@ -1948,6 +1974,86 @@ function populateDatabase(database, capabilities, documents, skills, skillRoutes
1948
1974
  populatePathSurfaceReadModel(database);
1949
1975
  populateSearchTables(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors);
1950
1976
  }
1977
+ function readCount(database, tableName) {
1978
+ if (!hasTable(database, tableName)) {
1979
+ return 0;
1980
+ }
1981
+ const [row] = queryRows(database, `SELECT COUNT(*) AS count FROM ${tableName}`);
1982
+ const count = row?.count;
1983
+ return typeof count === 'number' && Number.isFinite(count) ? count : 0;
1984
+ }
1985
+ function readStoredIndexedPaths(database) {
1986
+ if (!hasTable(database, 'documents')) {
1987
+ return [];
1988
+ }
1989
+ return queryRows(database, 'SELECT path FROM documents ORDER BY path')
1990
+ .map((row) => toSearchString(row.path))
1991
+ .filter(Boolean);
1992
+ }
1993
+ function createStoredLocalIndexResult(projectRoot, databasePath, dryRun, indexMode, database, capabilities) {
1994
+ return {
1995
+ schema_version: LOCAL_INDEX_SCHEMA_VERSION,
1996
+ command: 'index',
1997
+ ok: true,
1998
+ mustflow_root: path.resolve(projectRoot),
1999
+ database_path: databasePath,
2000
+ dry_run: dryRun,
2001
+ wrote_files: false,
2002
+ index_mode: indexMode,
2003
+ reused_existing: true,
2004
+ rebuild_reason: null,
2005
+ document_count: readCount(database, 'documents'),
2006
+ skill_count: readCount(database, 'skills'),
2007
+ skill_route_count: readCount(database, 'skill_routes'),
2008
+ command_intent_count: readCount(database, 'command_intents'),
2009
+ command_effect_count: readCount(database, 'command_effects'),
2010
+ verification_evidence_summary_count: readCount(database, 'verification_evidence_summaries'),
2011
+ verification_plan_count: readCount(database, 'verification_plans'),
2012
+ acceptance_criteria_count: readCount(database, 'acceptance_criteria'),
2013
+ criterion_coverage_count: readCount(database, 'criterion_coverage'),
2014
+ verification_receipt_summary_count: readCount(database, 'verification_receipt_summaries'),
2015
+ command_receipt_summary_count: readCount(database, 'command_receipt_summaries'),
2016
+ verification_coverage_state_count: readCount(database, 'verification_coverage_states'),
2017
+ verification_risk_signal_count: readCount(database, 'verification_risk_signals'),
2018
+ validation_ratchet_signal_count: readCount(database, 'validation_ratchet_signals'),
2019
+ completion_verdict_summary_count: readCount(database, 'completion_verdict_summaries'),
2020
+ repro_route_count: readCount(database, 'repro_routes'),
2021
+ repro_observation_count: readCount(database, 'repro_observations'),
2022
+ failure_fingerprint_count: readCount(database, 'verification_failure_fingerprints'),
2023
+ source_index_enabled: readMetadataValue(database, 'source_index_enabled') === 'true',
2024
+ source_anchor_count: readCount(database, 'source_anchors'),
2025
+ source_anchor_risk_signal_count: readCount(database, 'source_anchor_risk_signals'),
2026
+ search_backend: capabilities.backend,
2027
+ search_fts5_available: capabilities.fts5Available,
2028
+ content_mode: LOCAL_INDEX_CONTENT_MODE,
2029
+ store_full_content: LOCAL_INDEX_STORE_FULL_CONTENT,
2030
+ max_snippet_bytes_per_document: MAX_SNIPPET_BYTES_PER_DOCUMENT,
2031
+ excluded_raw_data_kinds: [...LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS],
2032
+ indexed_file_count: readCount(database, 'indexed_files'),
2033
+ indexed_paths: readStoredIndexedPaths(database),
2034
+ };
2035
+ }
2036
+ function indexedFileMetadataMatch(database, currentFiles) {
2037
+ const rows = queryRows(database, 'SELECT path, source_scope, size_bytes, mtime_ms, parser_version FROM indexed_files ORDER BY path');
2038
+ if (rows.length !== currentFiles.length) {
2039
+ return false;
2040
+ }
2041
+ const currentByPath = new Map(currentFiles.map((file) => [file.path, file]));
2042
+ for (const row of rows) {
2043
+ const storedPath = toSearchString(row.path);
2044
+ const current = currentByPath.get(storedPath);
2045
+ if (!current) {
2046
+ return false;
2047
+ }
2048
+ if (normalizeIndexedFileSourceScope(row.source_scope) !== current.sourceScope ||
2049
+ row.size_bytes !== current.sizeBytes ||
2050
+ row.mtime_ms !== current.mtimeMs ||
2051
+ toSearchString(row.parser_version) !== LOCAL_INDEX_PARSER_VERSION) {
2052
+ return false;
2053
+ }
2054
+ }
2055
+ return true;
2056
+ }
1951
2057
  function indexedFilesMatch(database, currentFiles) {
1952
2058
  const rows = queryRows(database, 'SELECT path, source_scope, content_hash, parser_version FROM indexed_files ORDER BY path');
1953
2059
  if (rows.length !== currentFiles.length) {
@@ -1960,7 +2066,7 @@ function indexedFilesMatch(database, currentFiles) {
1960
2066
  if (!current) {
1961
2067
  return false;
1962
2068
  }
1963
- if (toSearchString(row.source_scope) !== current.sourceScope ||
2069
+ if (normalizeIndexedFileSourceScope(row.source_scope) !== current.sourceScope ||
1964
2070
  toSearchString(row.content_hash) !== current.contentHash ||
1965
2071
  toSearchString(row.parser_version) !== LOCAL_INDEX_PARSER_VERSION) {
1966
2072
  return false;
@@ -1968,6 +2074,44 @@ function indexedFilesMatch(database, currentFiles) {
1968
2074
  }
1969
2075
  return true;
1970
2076
  }
2077
+ async function readIncrementalPreflightReuse(SQL, databasePath, projectRoot, currentFiles, sourceScopeHash, dryRun, indexMode) {
2078
+ if (!currentFiles) {
2079
+ return { result: null, rebuildReason: null };
2080
+ }
2081
+ if (!existsSync(databasePath)) {
2082
+ return { result: null, rebuildReason: 'missing_index' };
2083
+ }
2084
+ let database;
2085
+ try {
2086
+ database = new SQL.Database(readFileSync(databasePath));
2087
+ if (readStoredSchemaVersion(database) !== LOCAL_INDEX_SCHEMA_VERSION) {
2088
+ return { result: null, rebuildReason: 'schema_version_mismatch' };
2089
+ }
2090
+ if (readMetadataValue(database, 'parser_version') !== LOCAL_INDEX_PARSER_VERSION) {
2091
+ return { result: null, rebuildReason: 'parser_version_mismatch' };
2092
+ }
2093
+ if (readMetadataValue(database, 'source_scope_hash') !== sourceScopeHash) {
2094
+ return { result: null, rebuildReason: 'source_scope_mismatch' };
2095
+ }
2096
+ if (!hasTable(database, 'indexed_files')) {
2097
+ return { result: null, rebuildReason: 'indexed_files_missing' };
2098
+ }
2099
+ if (!indexedFileMetadataMatch(database, currentFiles)) {
2100
+ return { result: null, rebuildReason: 'file_fingerprint_mismatch' };
2101
+ }
2102
+ const capabilities = readStoredSearchCapabilities(database);
2103
+ return {
2104
+ result: createStoredLocalIndexResult(projectRoot, databasePath, dryRun, indexMode, database, capabilities),
2105
+ rebuildReason: null,
2106
+ };
2107
+ }
2108
+ catch {
2109
+ return { result: null, rebuildReason: 'unreadable_index' };
2110
+ }
2111
+ finally {
2112
+ database?.close();
2113
+ }
2114
+ }
1971
2115
  async function readIncrementalReuseDecision(SQL, databasePath, currentFiles, sourceScopeHash) {
1972
2116
  if (!existsSync(databasePath)) {
1973
2117
  return { reusable: false, rebuildReason: 'missing_index', capabilities: null };
@@ -2015,13 +2159,28 @@ export async function createLocalIndex(projectRoot, options = {}) {
2015
2159
  const dryRun = options.dryRun === true;
2016
2160
  const incremental = options.incremental === true;
2017
2161
  const indexMode = incremental ? 'incremental' : 'full';
2162
+ const sourceConfig = readLocalIndexSourceConfig(projectRoot);
2163
+ const includeSource = options.includeSource === true || sourceConfig.enabledByDefault;
2164
+ const sourceScopeHash = getSourceScopeHash(includeSource, sourceConfig);
2165
+ let capabilities = searchCapabilities(false);
2166
+ let reusedExisting = false;
2167
+ let rebuildReason = null;
2168
+ const SQL = await loadSqlJs();
2169
+ const capabilityDatabase = new SQL.Database();
2170
+ capabilities = detectLocalSearchCapabilities(capabilityDatabase);
2171
+ capabilityDatabase.close();
2172
+ if (incremental) {
2173
+ const preflightFiles = collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource);
2174
+ const preflightReuse = await readIncrementalPreflightReuse(SQL, databasePath, projectRoot, preflightFiles, sourceScopeHash, dryRun, indexMode);
2175
+ if (preflightReuse.result) {
2176
+ return preflightReuse.result;
2177
+ }
2178
+ rebuildReason = preflightReuse.rebuildReason;
2179
+ }
2018
2180
  const documents = collectDocuments(projectRoot);
2019
2181
  const skills = collectSkills(documents);
2020
2182
  const skillRoutes = collectSkillRoutes(projectRoot);
2021
2183
  const commandIntents = collectCommandIntents(projectRoot);
2022
- const sourceConfig = readLocalIndexSourceConfig(projectRoot);
2023
- const includeSource = options.includeSource === true || sourceConfig.enabledByDefault;
2024
- const sourceScopeHash = getSourceScopeHash(includeSource, sourceConfig);
2025
2184
  const previousSourceAnchors = includeSource
2026
2185
  ? await readPreviousSourceAnchorSnapshots(databasePath).catch(() => [])
2027
2186
  : [];
@@ -2033,17 +2192,10 @@ export async function createLocalIndex(projectRoot, options = {}) {
2033
2192
  : [];
2034
2193
  const verificationEvidence = createVerificationEvidenceIndex(projectRoot);
2035
2194
  const indexedFiles = collectIndexedFileRecords(projectRoot, documents, sourceAnchors);
2036
- let capabilities = searchCapabilities(false);
2037
- let reusedExisting = false;
2038
- let rebuildReason = null;
2039
- const SQL = await loadSqlJs();
2040
- const capabilityDatabase = new SQL.Database();
2041
- capabilities = detectLocalSearchCapabilities(capabilityDatabase);
2042
- capabilityDatabase.close();
2043
2195
  if (incremental) {
2044
2196
  const reuseDecision = await readIncrementalReuseDecision(SQL, databasePath, indexedFiles, sourceScopeHash);
2045
2197
  reusedExisting = reuseDecision.reusable;
2046
- rebuildReason = reuseDecision.rebuildReason;
2198
+ rebuildReason = reuseDecision.rebuildReason ?? rebuildReason;
2047
2199
  capabilities = reuseDecision.capabilities ?? capabilities;
2048
2200
  }
2049
2201
  if (!dryRun && !reusedExisting) {
@@ -2110,7 +2262,7 @@ function getStalePaths(projectRoot, database) {
2110
2262
  const indexedPaths = new Set(indexedRows.map((row) => toSearchString(row.path)));
2111
2263
  for (const row of indexedRows) {
2112
2264
  const indexedPath = toSearchString(row.path);
2113
- const sourceScope = toSearchString(row.source_scope) === 'source_anchor' ? 'source_anchor' : 'workflow';
2265
+ const sourceScope = normalizeIndexedFileSourceScope(row.source_scope);
2114
2266
  try {
2115
2267
  const current = readIndexedFileRecord(projectRoot, indexedPath, sourceScope);
2116
2268
  if (current.contentHash !== toSearchString(row.content_hash)) {
@@ -78,6 +78,9 @@ function readEffectiveMaxOutputBytes(contract, intent) {
78
78
  readPositiveInteger(contract.defaults, 'max_output_bytes') ??
79
79
  DEFAULT_COMMAND_MAX_OUTPUT_BYTES;
80
80
  }
81
+ function readEffectiveKillAfterSeconds(contract) {
82
+ return readPositiveInteger(contract.defaults, 'kill_after_seconds') ?? 5;
83
+ }
81
84
  function getMaxOutputBytesLimitDetail(contract, intent) {
82
85
  const intentValue = readPositiveInteger(intent, 'max_output_bytes');
83
86
  if (intentValue !== undefined) {
@@ -103,6 +106,7 @@ function readRunIntentMetadata(contract, intent) {
103
106
  kind: readString(intent, 'kind') ?? null,
104
107
  configuredCwd,
105
108
  timeoutSeconds: readPositiveInteger(intent, 'timeout_seconds') ?? null,
109
+ killAfterSeconds: readEffectiveKillAfterSeconds(contract),
106
110
  maxOutputBytes: readEffectiveMaxOutputBytes(contract, intent),
107
111
  successExitCodes: getSuccessExitCodes(intent),
108
112
  commandArgv,
@@ -138,6 +142,7 @@ function createBlockedRunPlan(contract, intentName, intent, eligibility, reasonC
138
142
  cwd: null,
139
143
  relativeCwd: null,
140
144
  timeoutSeconds: metadata?.timeoutSeconds ?? null,
145
+ killAfterSeconds: metadata?.killAfterSeconds ?? null,
141
146
  maxOutputBytes: metadata?.maxOutputBytes ?? null,
142
147
  successExitCodes: metadata?.successExitCodes ?? null,
143
148
  commandArgv: metadata?.commandArgv,
@@ -199,6 +204,7 @@ export function createRunPlan(projectRoot, contract, intentName, options = {}) {
199
204
  cwd,
200
205
  relativeCwd: getRelativeProjectPath(projectRoot, cwd),
201
206
  timeoutSeconds: metadata.timeoutSeconds,
207
+ killAfterSeconds: metadata.killAfterSeconds,
202
208
  maxOutputBytes: metadata.maxOutputBytes,
203
209
  successExitCodes: metadata.successExitCodes,
204
210
  commandArgv,
@@ -15,6 +15,7 @@ const CHECK_ISSUE_ID_RULES = [
15
15
  ['mustflow.command_contract.effect_path_escape', /^Strict: Command effect path must stay inside the current root:/u],
16
16
  ['mustflow.command_contract.shared_writes_without_effects', /^Strict warning: configured agent-runnable intents .+ share path:.+ through writes without explicit effects or resource locks$/u],
17
17
  ['mustflow.command_contract.broad_env_inheritance', /^Strict warning: configured agent-runnable intent [^\s]+ (?:implicitly inherits the host environment|uses env_policy = "inherit")/u],
18
+ ['mustflow.command_contract.project_local_bin_bare_executable', /^Strict warning: configured agent-runnable intent [^\s]+ uses bare executable "[^"]+" that matches project-local node_modules\/\.bin/u],
18
19
  ['mustflow.prompt_cache.required', /^Strict: \[prompt_cache\] table is required$/u],
19
20
  ['mustflow.prompt_cache.volatile_in_stable', /^Strict: \[prompt_cache\.layers\.stable\]\.read must not include volatile path /u],
20
21
  ['mustflow.refresh.hash_method_required', /^Strict: \[refresh\]\.default_method should be "hash_check" for cache-friendly refresh$/u],
@@ -45,9 +45,6 @@ export function commandIntentHasCommandSource(intent) {
45
45
  export function shellCommandHasBlockedBackgroundPattern(command) {
46
46
  return BACKGROUND_SHELL_PATTERNS.some((pattern) => pattern.test(command));
47
47
  }
48
- export function commandIntentHasBlockedShellBackgroundPattern(intent) {
49
- return intent.mode === 'shell' && typeof intent.cmd === 'string' && shellCommandHasBlockedBackgroundPattern(intent.cmd);
50
- }
51
48
  function normalizeExecutableName(value) {
52
49
  return path.basename(value).replace(/\.(?:cmd|exe|ps1)$/iu, '').toLowerCase();
53
50
  }
@@ -1,7 +1,9 @@
1
- import { COMMAND_LIFECYCLES, COMMAND_RUN_POLICIES, LONG_RUNNING_LIFECYCLES, isRecord, } from './config-loading.js';
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { COMMAND_LIFECYCLES, COMMAND_RUN_POLICIES, LONG_RUNNING_LIFECYCLES, isRecord, readStringArray, } from './config-loading.js';
2
4
  import { COMMAND_ENV_POLICIES, DEFAULT_COMMAND_ENV_POLICY } from './command-env.js';
3
5
  import { COMMAND_EFFECT_CONCURRENCY, COMMAND_EFFECT_MODES, COMMAND_EFFECT_TYPES, validateCommandEffectLockWarnings, validateCommandEffects, } from './command-effects.js';
4
- import { commandIntentBlockedCommandPattern, commandIntentHasBlockedShellBackgroundPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
6
+ import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
5
7
  import { MAX_COMMAND_OUTPUT_BYTES, commandMaxOutputBytesLimitMessage } from './command-output-limits.js';
6
8
  function commandContractIssue(message) {
7
9
  return { message };
@@ -186,10 +188,10 @@ function validateCommandIntent(intentName, intent, issues) {
186
188
  if (!commandIntentHasCommandSource(intent)) {
187
189
  issues.push(commandContractIssue(`Configured intent ${intentName} must define argv or mode = "shell" with cmd`));
188
190
  }
189
- if (commandIntentHasBlockedShellBackgroundPattern(intent)) {
191
+ const blockedCommandPattern = commandIntentBlockedCommandPattern(intent);
192
+ if (blockedCommandPattern?.code === 'shell_background_pattern') {
190
193
  issues.push(commandContractIssue(`Shell intent ${intentName} contains a blocked long-running or background pattern`));
191
194
  }
192
- const blockedCommandPattern = commandIntentBlockedCommandPattern(intent);
193
195
  if (blockedCommandPattern?.code === 'long_running_command_pattern') {
194
196
  issues.push(commandContractIssue(`Intent ${intentName} contains a blocked long-running or background command pattern`));
195
197
  }
@@ -245,6 +247,41 @@ function validateCommandEnvInheritanceWarnings(commandsToml) {
245
247
  }
246
248
  return issues;
247
249
  }
250
+ function projectLocalBinExecutableExists(projectRoot, executable) {
251
+ const localBinPath = path.join(projectRoot, 'node_modules', '.bin');
252
+ const executableName = path.basename(executable).replace(/\.(?:cmd|exe|ps1)$/iu, '');
253
+ const candidates = [
254
+ executableName,
255
+ `${executableName}.cmd`,
256
+ `${executableName}.exe`,
257
+ `${executableName}.ps1`,
258
+ ];
259
+ return candidates.some((candidate) => existsSync(path.join(localBinPath, candidate)));
260
+ }
261
+ function validateProjectLocalBinWarnings(projectRoot, commandsToml) {
262
+ const issues = [];
263
+ if (!isRecord(commandsToml?.intents)) {
264
+ return issues;
265
+ }
266
+ for (const [intentName, intent] of Object.entries(commandsToml.intents)) {
267
+ if (!isRecord(intent)) {
268
+ continue;
269
+ }
270
+ if (intent.status !== 'configured' || intent.lifecycle !== 'oneshot' || intent.run_policy !== 'agent_allowed') {
271
+ continue;
272
+ }
273
+ const argv = readStringArray(intent, 'argv');
274
+ const executable = argv?.[0];
275
+ if (!executable || executable.includes('/') || executable.includes('\\')) {
276
+ continue;
277
+ }
278
+ if (!projectLocalBinExecutableExists(projectRoot, executable)) {
279
+ continue;
280
+ }
281
+ issues.push(commandContractWarning(`configured agent-runnable intent ${intentName} uses bare executable "${executable}" that matches project-local node_modules/.bin; use a package-manager mediated command such as npm exec, pnpm exec, bun x, or yarn exec`));
282
+ }
283
+ return issues;
284
+ }
248
285
  /**
249
286
  * mf:anchor core.command-contract-validation
250
287
  * purpose: Validate command intent declarations that gate agent-executable repository commands.
@@ -287,6 +324,7 @@ export function validateCommandContractStrictDefaults(projectRoot, commandsToml)
287
324
  }
288
325
  }
289
326
  issues.push(...validateCommandEnvInheritanceWarnings(commandsToml));
327
+ issues.push(...validateProjectLocalBinWarnings(projectRoot, commandsToml));
290
328
  issues.push(...validateCommandEffects(projectRoot, commandsToml));
291
329
  issues.push(...validateCommandEffectLockWarnings(commandsToml));
292
330
  return issues;
@@ -1,5 +1,5 @@
1
1
  import { isRecord, readString } from './config-loading.js';
2
- import { commandIntentBlockedCommandPattern, commandIntentHasBlockedShellBackgroundPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
2
+ import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
3
3
  export function evaluateCommandIntentEligibility(intentName, rawIntent) {
4
4
  if (!commandIntentNameIsSafe(intentName)) {
5
5
  return {
@@ -61,14 +61,14 @@ export function evaluateCommandIntentEligibility(intentName, rawIntent) {
61
61
  detail: 'Intent does not define argv or shell cmd.',
62
62
  };
63
63
  }
64
- if (commandIntentHasBlockedShellBackgroundPattern(rawIntent)) {
64
+ const blockedPattern = commandIntentBlockedCommandPattern(rawIntent);
65
+ if (blockedPattern?.code === 'shell_background_pattern') {
65
66
  return {
66
67
  ok: false,
67
68
  code: 'blocked_shell_background_pattern',
68
- detail: 'Shell command contains a blocked long-running or background pattern.',
69
+ detail: blockedPattern.detail,
69
70
  };
70
71
  }
71
- const blockedPattern = commandIntentBlockedCommandPattern(rawIntent);
72
72
  if (blockedPattern?.code === 'long_running_command_pattern') {
73
73
  return {
74
74
  ok: false,
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { COMMAND_LIFECYCLES, COMMAND_RUN_POLICIES, LONG_RUNNING_LIFECYCLES, isRecord, readPositiveInteger, readString, readStringArray, } from './config-loading.js';
4
4
  import { evaluateCommandIntentEligibility, } from './command-intent-eligibility.js';
5
- import { commandIntentBlockedCommandPattern, commandIntentHasBlockedShellBackgroundPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
5
+ import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
6
6
  import { MAX_COMMAND_OUTPUT_BYTES } from './command-output-limits.js';
7
7
  import { commandEffectsConflict, normalizeCommandEffects } from './command-effects.js';
8
8
  import { listChangeClassificationValidationReasons } from './change-classification.js';
@@ -323,10 +323,10 @@ function lintIntent(name, value, issues) {
323
323
  if (!commandIntentHasCommandSource(value)) {
324
324
  pushIssue(issues, 'error', 'executable_source_missing', name, `Configured intent ${name} must define argv or shell cmd.`);
325
325
  }
326
- if (commandIntentHasBlockedShellBackgroundPattern(value)) {
326
+ const blockedCommandPattern = commandIntentBlockedCommandPattern(value);
327
+ if (blockedCommandPattern?.code === 'shell_background_pattern') {
327
328
  pushIssue(issues, 'error', 'shell_background_pattern', name, `Shell intent ${name} contains a blocked long-running or background pattern.`);
328
329
  }
329
- const blockedCommandPattern = commandIntentBlockedCommandPattern(value);
330
330
  if (blockedCommandPattern?.code === 'long_running_command_pattern') {
331
331
  pushIssue(issues, 'error', 'long_running_command_pattern', name, `Intent ${name} contains a blocked long-running or background command pattern.`);
332
332
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.18.3",
3
+ "version": "2.18.7",
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.3"
3
+ version = "2.18.7"
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"