mustflow 2.18.2 → 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.
Files changed (34) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/commands/run/builtin-dispatch.js +92 -0
  3. package/dist/cli/commands/run/executor.js +149 -0
  4. package/dist/cli/commands/run/output.js +59 -0
  5. package/dist/cli/commands/run/process-tree.js +91 -0
  6. package/dist/cli/commands/run/receipt.js +42 -0
  7. package/dist/cli/commands/run.js +17 -382
  8. package/dist/cli/commands/verify/args.js +262 -0
  9. package/dist/cli/commands/verify.js +1 -262
  10. package/dist/cli/i18n/en.js +1 -0
  11. package/dist/cli/i18n/es.js +1 -0
  12. package/dist/cli/i18n/fr.js +1 -0
  13. package/dist/cli/i18n/hi.js +1 -0
  14. package/dist/cli/i18n/ko.js +1 -0
  15. package/dist/cli/i18n/zh.js +1 -0
  16. package/dist/cli/index.js +6 -72
  17. package/dist/cli/lib/command-registry.js +27 -0
  18. package/dist/cli/lib/dashboard-export.js +2 -1
  19. package/dist/cli/lib/dashboard-html/locale-bootstrap.js +3 -2
  20. package/dist/cli/lib/dashboard-html/template.js +5 -4
  21. package/dist/cli/lib/html-json.js +11 -0
  22. package/dist/cli/lib/local-index/index.js +166 -14
  23. package/dist/cli/lib/run-plan.js +6 -0
  24. package/dist/core/check-issues.js +1 -0
  25. package/dist/core/command-contract-rules.js +0 -3
  26. package/dist/core/command-contract-validation.js +42 -4
  27. package/dist/core/command-intent-eligibility.js +4 -4
  28. package/dist/core/contract-lint.js +3 -3
  29. package/package.json +1 -1
  30. package/templates/default/i18n.toml +7 -1
  31. package/templates/default/locales/en/.mustflow/skills/INDEX.md +2 -1
  32. package/templates/default/locales/en/.mustflow/skills/routes.toml +6 -0
  33. package/templates/default/locales/en/.mustflow/skills/source-anchor-authoring/SKILL.md +147 -0
  34. package/templates/default/manifest.toml +8 -1
package/dist/cli/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { realpathSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { COMMAND_DEFINITIONS } from './lib/command-registry.js';
5
+ import { COMMAND_DEFINITIONS, findCommandDefinition } from './lib/command-registry.js';
6
6
  import { renderCliError, renderHelp } from './lib/cli-output.js';
7
7
  import { DEFAULT_CLI_LANG, SUPPORTED_CLI_LANGS, isCliLang, t } from './lib/i18n.js';
8
8
  import { consoleReporter } from './lib/reporter.js';
@@ -102,77 +102,11 @@ export async function runCli(argv, reporter = consoleReporter) {
102
102
  reporter.stdout(getTopLevelHelp(parsed.lang));
103
103
  return 0;
104
104
  }
105
- if (command === '--version' || command === '-v' || command === 'version') {
106
- return (await import('./commands/version.js')).runVersion(args, reporter, parsed.lang);
107
- }
108
- if (command === 'init') {
109
- return (await import('./commands/init.js')).runInit(args, reporter, parsed.lang);
110
- }
111
- if (command === 'adapters') {
112
- return (await import('./commands/adapters.js')).runAdapters(args, reporter, parsed.lang);
113
- }
114
- if (command === 'check') {
115
- return (await import('./commands/check.js')).runCheck(args, reporter, parsed.lang);
116
- }
117
- if (command === 'classify') {
118
- return (await import('./commands/classify.js')).runClassify(args, reporter, parsed.lang);
119
- }
120
- if (command === 'contract-lint') {
121
- return (await import('./commands/contract-lint.js')).runContractLint(args, reporter, parsed.lang);
122
- }
123
- if (command === 'status') {
124
- return (await import('./commands/status.js')).runStatus(args, reporter, parsed.lang);
125
- }
126
- if (command === 'update') {
127
- return (await import('./commands/update.js')).runUpdate(args, reporter, parsed.lang);
128
- }
129
- if (command === 'upgrade') {
130
- return (await import('./commands/upgrade.js')).runUpgrade(args, reporter, parsed.lang);
131
- }
132
- if (command === 'map') {
133
- return (await import('./commands/map.js')).runMap(args, reporter, parsed.lang);
134
- }
135
- if (command === 'line-endings') {
136
- return (await import('./commands/line-endings.js')).runLineEndings(args, reporter, parsed.lang);
137
- }
138
- if (command === 'run') {
139
- return (await import('./commands/run.js')).runRun(args, reporter, parsed.lang);
140
- }
141
- if (command === 'context') {
142
- return (await import('./commands/context.js')).runContext(args, reporter, parsed.lang);
143
- }
144
- if (command === 'doctor') {
145
- return (await import('./commands/doctor.js')).runDoctor(args, reporter, parsed.lang);
146
- }
147
- if (command === 'docs') {
148
- return (await import('./commands/docs.js')).runDocs(args, reporter, parsed.lang);
149
- }
150
- if (command === 'handoff') {
151
- return (await import('./commands/handoff.js')).runHandoff(args, reporter, parsed.lang);
152
- }
153
- if (command === 'index') {
154
- return (await import('./commands/index.js')).runIndex(args, reporter, parsed.lang);
155
- }
156
- if (command === 'search') {
157
- return (await import('./commands/search.js')).runSearch(args, reporter, parsed.lang);
158
- }
159
- if (command === 'dashboard') {
160
- return (await import('./commands/dashboard.js')).runDashboard(args, reporter, parsed.lang);
161
- }
162
- if (command === 'version-sources') {
163
- return (await import('./commands/version-sources.js')).runVersionSources(args, reporter, parsed.lang);
164
- }
165
- if (command === 'verify') {
166
- return (await import('./commands/verify.js')).runVerify(args, reporter, parsed.lang);
167
- }
168
- if (command === 'explain') {
169
- return (await import('./commands/explain.js')).runExplain(args, reporter, parsed.lang);
170
- }
171
- if (command === 'impact') {
172
- return (await import('./commands/impact.js')).runImpact(args, reporter, parsed.lang);
173
- }
174
- if (command === 'help') {
175
- return (await import('./commands/help.js')).runHelp(args, reporter, parsed.lang);
105
+ const commandId = command === '--version' || command === '-v' ? 'version' : command;
106
+ const commandDefinition = findCommandDefinition(commandId);
107
+ if (commandDefinition) {
108
+ const runner = await commandDefinition.loadRunner();
109
+ return runner(args, reporter, parsed.lang);
176
110
  }
177
111
  reporter.stderr(renderCliError(t(parsed.lang, 'cli.error.unknownCommand', { command }), 'mf --help', parsed.lang));
178
112
  reporter.stdout(getTopLevelHelp(parsed.lang));
@@ -3,120 +3,147 @@ export const COMMAND_DEFINITIONS = [
3
3
  id: 'adapters',
4
4
  usage: 'mf adapters',
5
5
  summaryKey: 'command.adapters.summary',
6
+ loadRunner: async () => (await import('../commands/adapters.js')).runAdapters,
6
7
  },
7
8
  {
8
9
  id: 'init',
9
10
  usage: 'mf init',
10
11
  summaryKey: 'command.init.summary',
12
+ loadRunner: async () => (await import('../commands/init.js')).runInit,
11
13
  },
12
14
  {
13
15
  id: 'check',
14
16
  usage: 'mf check',
15
17
  summaryKey: 'command.check.summary',
18
+ loadRunner: async () => (await import('../commands/check.js')).runCheck,
16
19
  },
17
20
  {
18
21
  id: 'classify',
19
22
  usage: 'mf classify',
20
23
  summaryKey: 'command.classify.summary',
24
+ loadRunner: async () => (await import('../commands/classify.js')).runClassify,
21
25
  },
22
26
  {
23
27
  id: 'contract-lint',
24
28
  usage: 'mf contract-lint',
25
29
  summaryKey: 'command.contractLint.summary',
30
+ loadRunner: async () => (await import('../commands/contract-lint.js')).runContractLint,
26
31
  },
27
32
  {
28
33
  id: 'status',
29
34
  usage: 'mf status',
30
35
  summaryKey: 'command.status.summary',
36
+ loadRunner: async () => (await import('../commands/status.js')).runStatus,
31
37
  },
32
38
  {
33
39
  id: 'update',
34
40
  usage: 'mf update',
35
41
  summaryKey: 'command.update.summary',
42
+ loadRunner: async () => (await import('../commands/update.js')).runUpdate,
36
43
  },
37
44
  {
38
45
  id: 'upgrade',
39
46
  usage: 'mf upgrade',
40
47
  summaryKey: 'command.upgrade.summary',
48
+ loadRunner: async () => (await import('../commands/upgrade.js')).runUpgrade,
41
49
  },
42
50
  {
43
51
  id: 'map',
44
52
  usage: 'mf map',
45
53
  summaryKey: 'command.map.summary',
54
+ loadRunner: async () => (await import('../commands/map.js')).runMap,
46
55
  },
47
56
  {
48
57
  id: 'line-endings',
49
58
  usage: 'mf line-endings',
50
59
  summaryKey: 'command.lineEndings.summary',
60
+ loadRunner: async () => (await import('../commands/line-endings.js')).runLineEndings,
51
61
  },
52
62
  {
53
63
  id: 'run',
54
64
  usage: 'mf run',
55
65
  summaryKey: 'command.run.summary',
66
+ loadRunner: async () => (await import('../commands/run.js')).runRun,
56
67
  },
57
68
  {
58
69
  id: 'context',
59
70
  usage: 'mf context',
60
71
  summaryKey: 'command.context.summary',
72
+ loadRunner: async () => (await import('../commands/context.js')).runContext,
61
73
  },
62
74
  {
63
75
  id: 'doctor',
64
76
  usage: 'mf doctor',
65
77
  summaryKey: 'command.doctor.summary',
78
+ loadRunner: async () => (await import('../commands/doctor.js')).runDoctor,
66
79
  },
67
80
  {
68
81
  id: 'docs',
69
82
  usage: 'mf docs',
70
83
  summaryKey: 'command.docs.summary',
84
+ loadRunner: async () => (await import('../commands/docs.js')).runDocs,
71
85
  },
72
86
  {
73
87
  id: 'handoff',
74
88
  usage: 'mf handoff',
75
89
  summaryKey: 'command.handoff.summary',
90
+ loadRunner: async () => (await import('../commands/handoff.js')).runHandoff,
76
91
  },
77
92
  {
78
93
  id: 'index',
79
94
  usage: 'mf index',
80
95
  summaryKey: 'command.index.summary',
96
+ loadRunner: async () => (await import('../commands/index.js')).runIndex,
81
97
  },
82
98
  {
83
99
  id: 'search',
84
100
  usage: 'mf search',
85
101
  summaryKey: 'command.search.summary',
102
+ loadRunner: async () => (await import('../commands/search.js')).runSearch,
86
103
  },
87
104
  {
88
105
  id: 'dashboard',
89
106
  usage: 'mf dashboard',
90
107
  summaryKey: 'command.dashboard.summary',
108
+ loadRunner: async () => (await import('../commands/dashboard.js')).runDashboard,
91
109
  },
92
110
  {
93
111
  id: 'version',
94
112
  usage: 'mf version',
95
113
  summaryKey: 'command.version.summary',
114
+ loadRunner: async () => (await import('../commands/version.js')).runVersion,
96
115
  },
97
116
  {
98
117
  id: 'version-sources',
99
118
  usage: 'mf version-sources',
100
119
  summaryKey: 'command.versionSources.summary',
120
+ loadRunner: async () => (await import('../commands/version-sources.js')).runVersionSources,
101
121
  },
102
122
  {
103
123
  id: 'verify',
104
124
  usage: 'mf verify',
105
125
  summaryKey: 'command.verify.summary',
126
+ loadRunner: async () => (await import('../commands/verify.js')).runVerify,
106
127
  },
107
128
  {
108
129
  id: 'explain',
109
130
  usage: 'mf explain',
110
131
  summaryKey: 'command.explain.summary',
132
+ loadRunner: async () => (await import('../commands/explain.js')).runExplain,
111
133
  },
112
134
  {
113
135
  id: 'impact',
114
136
  usage: 'mf impact',
115
137
  summaryKey: 'command.impact.summary',
138
+ loadRunner: async () => (await import('../commands/impact.js')).runImpact,
116
139
  },
117
140
  {
118
141
  id: 'help',
119
142
  usage: 'mf help',
120
143
  summaryKey: 'command.help.summary',
144
+ loadRunner: async () => (await import('../commands/help.js')).runHelp,
121
145
  },
122
146
  ];
147
+ export function findCommandDefinition(command) {
148
+ return COMMAND_DEFINITIONS.find((definition) => definition.id === command);
149
+ }
@@ -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
  }