mustflow 1.17.0 → 1.18.14

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 (44) hide show
  1. package/README.md +17 -7
  2. package/dist/cli/commands/context.js +2 -2
  3. package/dist/cli/commands/dashboard.js +61 -7
  4. package/dist/cli/commands/explain.js +47 -7
  5. package/dist/cli/commands/index.js +9 -2
  6. package/dist/cli/commands/run.js +7 -15
  7. package/dist/cli/commands/verify.js +44 -9
  8. package/dist/cli/i18n/en.js +3 -0
  9. package/dist/cli/i18n/es.js +3 -0
  10. package/dist/cli/i18n/fr.js +3 -0
  11. package/dist/cli/i18n/hi.js +3 -0
  12. package/dist/cli/i18n/ko.js +3 -0
  13. package/dist/cli/i18n/zh.js +3 -0
  14. package/dist/cli/lib/agent-context.js +19 -4
  15. package/dist/cli/lib/dashboard-html.js +41 -0
  16. package/dist/cli/lib/dashboard-locale.js +2 -0
  17. package/dist/cli/lib/local-index.js +910 -32
  18. package/dist/core/change-classification.js +33 -60
  19. package/dist/core/command-classification.js +0 -2
  20. package/dist/core/source-anchor-status.js +4 -4
  21. package/dist/core/source-anchor-validation.js +2 -6
  22. package/dist/core/source-anchors.js +81 -3
  23. package/package.json +1 -1
  24. package/schemas/change-verification-report.schema.json +194 -0
  25. package/schemas/context-report.schema.json +30 -2
  26. package/schemas/explain-report.schema.json +191 -0
  27. package/templates/default/i18n.toml +20 -10
  28. package/templates/default/locales/en/.mustflow/docs/agent-workflow.md +4 -3
  29. package/templates/default/locales/en/.mustflow/skills/INDEX.md +2 -1
  30. package/templates/default/locales/en/.mustflow/skills/database-change-safety/SKILL.md +155 -0
  31. package/templates/default/locales/en/AGENTS.md +10 -10
  32. package/templates/default/locales/es/.mustflow/skills/INDEX.md +2 -1
  33. package/templates/default/locales/es/.mustflow/skills/database-change-safety/SKILL.md +155 -0
  34. package/templates/default/locales/fr/.mustflow/skills/INDEX.md +2 -1
  35. package/templates/default/locales/fr/.mustflow/skills/database-change-safety/SKILL.md +155 -0
  36. package/templates/default/locales/hi/.mustflow/skills/INDEX.md +2 -1
  37. package/templates/default/locales/hi/.mustflow/skills/database-change-safety/SKILL.md +155 -0
  38. package/templates/default/locales/ko/.mustflow/docs/agent-workflow.md +3 -3
  39. package/templates/default/locales/ko/.mustflow/skills/INDEX.md +2 -1
  40. package/templates/default/locales/ko/.mustflow/skills/database-change-safety/SKILL.md +155 -0
  41. package/templates/default/locales/ko/AGENTS.md +2 -2
  42. package/templates/default/locales/zh/.mustflow/skills/INDEX.md +2 -1
  43. package/templates/default/locales/zh/.mustflow/skills/database-change-safety/SKILL.md +155 -0
  44. package/templates/default/manifest.toml +7 -1
package/README.md CHANGED
@@ -55,7 +55,7 @@ flowchart TD
55
55
 
56
56
  `read_order` defines the required reading sequence, while `optional_read_order` and `[context]` control how task-specific context loads. The `[refresh]` policy sets when agents reread the same instructions.
57
57
 
58
- The skills index acts as an active routing step: agents compare the task with `.mustflow/skills/INDEX.md` and read matching `SKILL.md` files before editing that scope. This step is required before file edits even when `mf doctor` or `mf check` passes, because health checks do not decide which task procedure applies. Skills guide procedure only; command execution still comes from `.mustflow/config/commands.toml`.
58
+ The skills index acts as an active routing step: agents compare the task with `.mustflow/skills/INDEX.md` and read matching `SKILL.md` files before editing that scope. This step is required before file edits even when `mf doctor` or `mf check` passes, because health checks do not decide which task procedure applies. When files are created or modified, the final report should include a concise skill-selection note. Skills guide procedure only; command execution still comes from `.mustflow/config/commands.toml`.
59
59
 
60
60
  ## Quick start
61
61
 
@@ -91,11 +91,11 @@ mustflow installs and validates an agent workflow for user projects.
91
91
  - Declares runnable command rules in `.mustflow/config/commands.toml`.
92
92
  - Checks installation health and configuration structure with `mf check` and `mf doctor`.
93
93
  - Classifies changed files, public surfaces, and validation reasons with `mf classify`.
94
- - Prints execution-free verification plans with `mf verify --plan-only --json`.
94
+ - Prints execution-free verification plans with `mf verify --plan-only --json`, including read-only local-index lock explanations when available.
95
95
  - Runs only allowed one-shot commands within a timeout via `mf run <intent>` or `mf verify` when the selected intent is runnable.
96
96
  - Writes command receipts to `.mustflow/state/runs/latest.json`.
97
97
  - Generates a concise repository navigation map, `REPO_MAP.md`, with `mf map`.
98
- - Indexes and searches mustflow docs, skills, skill routes, command rules, command-effect locks, and opt-in source anchor metadata with SQLite via `mf index` and `mf search`. The local SQLite file is a rebuildable lookup cache, not a memory store, audit log, command transcript store, or source-content database.
98
+ - Indexes and searches mustflow docs, skills, skill routes, command rules, command-effect locks, file fingerprints, and opt-in source anchor metadata with SQLite via `mf index` and `mf search`. The local SQLite file is a rebuildable lookup cache, not a memory store, audit log, command transcript store, or source-content database.
99
99
  - Tracks agent-created or agent-modified documentation needing prose review with `mf docs review`.
100
100
  - Previews and applies bundled template updates safely with `mf update`.
101
101
  - Publishes JSON Schemas for automation-facing reports and command contracts in `schemas/`.
@@ -145,6 +145,8 @@ your-project/
145
145
  │ └─ SKILL.md
146
146
  ├─ date-number-audit/
147
147
  │ └─ SKILL.md
148
+ ├─ database-change-safety/
149
+ │ └─ SKILL.md
148
150
  ├─ dependency-reality-check/
149
151
  │ └─ SKILL.md
150
152
  ├─ diff-risk-review/
@@ -217,7 +219,8 @@ npx mf verify --from-plan .mustflow/state/change-plan.json --plan-only --json
217
219
  npx mf verify --from-plan .mustflow/state/change-plan.json --json
218
220
  ```
219
221
 
220
- Create the optional local search index if search capabilities are needed.
222
+ Create the optional local search index if search capabilities are needed. Run the normal command
223
+ when creating the index for the first time.
221
224
 
222
225
  ```sh
223
226
  npx mf index --dry-run --json
@@ -225,6 +228,13 @@ npx mf index
225
228
  npx mf search mustflow_check
226
229
  ```
227
230
 
231
+ On later runs, use incremental mode when you want to reuse a compatible fresh cache without rewriting
232
+ the SQLite file. If the cache is missing, stale, or incompatible, mustflow falls back to a full rebuild.
233
+
234
+ ```sh
235
+ npx mf index --incremental --json
236
+ ```
237
+
228
238
  Preview template updates before applying them. Files marked as customized in `.mustflow/config/manifest.lock.toml` remain as repository-specific baselines while their current content matches the lock.
229
239
 
230
240
  ```sh
@@ -261,7 +271,7 @@ mf run mustflow_update_apply
261
271
  | `mf map --stdout` | Print the current mustflow root map to stdout. |
262
272
  | `mf map --write` | Create or update `REPO_MAP.md`. |
263
273
  | `mf run <intent>` | Run an allowed one-shot command. |
264
- | `mf index` | Build a SQLite index for mustflow docs, skill routes, command rules, and command-effect locks. |
274
+ | `mf index` | Build a SQLite index for mustflow docs, skill routes, command rules, command-effect locks, and file fingerprints. Use `--incremental` to reuse a compatible fresh index without rewriting it. |
265
275
  | `mf search <query>` | Search docs, skills, skill routes, command rules, and command-effect locks in the SQLite index. |
266
276
  | `mf status` | Inspect installed state and changed or missing files. |
267
277
  | `mf update --dry-run` | Calculate a template update plan without writing files. |
@@ -294,7 +304,7 @@ Runnable work is declared in `.mustflow/config/commands.toml` so agents do not g
294
304
 
295
305
  Development servers, watch modes, browser UIs, interactive commands, and background processes do not run directly.
296
306
 
297
- Use `mf verify --reason <event> --plan-only --json` to inspect matching verification intents and missing runnable coverage without executing commands.
307
+ Use `mf verify --reason <event> --plan-only --json` to inspect matching verification intents and missing runnable coverage without executing commands. When `.mustflow/cache/mustflow.sqlite` is fresh, scheduled entries include read-only `effectGraph` metadata for write locks and lock conflicts.
298
308
 
299
309
  Each command run writes the latest run record to `.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.
300
310
 
@@ -379,7 +389,7 @@ mf run docs_validate
379
389
  mf run mustflow_check
380
390
  ```
381
391
 
382
- The Bun scripts remain available for human maintainers and release packaging. `test_fast` runs the fast CLI regression baseline, `test_related` selects tests from changed files and falls back to the fast baseline, and `test_release` keeps package metadata and packaging checks out of routine local edits. `test_coverage` runs the fast CLI baseline through Node's built-in coverage report with no enforced threshold; set `MUSTFLOW_TEST_COVERAGE_CONCURRENCY=1`, `2`, or another positive integer to adjust worker count on local machines. `lint` and test-audit are configured as narrow repository-local gates. `docs_validate_fast` checks documentation navigation and localized content links without building the entire static site; `docs_validate` performs the full static documentation build, search index, and sitemap gate for release-sensitive changes.
392
+ The Bun scripts remain available for human maintainers and release packaging. `test_fast` runs the fast CLI regression baseline, `test_related` selects tests from changed files and falls back to the fast baseline, and both use 8 Node test workers by default. Set `MUSTFLOW_TEST_CONCURRENCY=1`, `2`, or another positive integer to tune those workers on local machines. `test_release` keeps package metadata and packaging checks out of routine local edits. `test_coverage` runs the fast CLI baseline through Node's built-in coverage report with no enforced threshold; set `MUSTFLOW_TEST_COVERAGE_CONCURRENCY=1`, `2`, or another positive integer to adjust its worker count. `lint` and test-audit are configured as narrow repository-local gates. `docs_validate_fast` checks documentation navigation and localized content links without building the entire static site; `docs_validate` performs the full static documentation build, search index, and sitemap gate for release-sensitive changes.
383
393
 
384
394
  `dist/` is a generated build output and is not committed. `npm pack` and `npm publish` run `npm run build` via `prepack`, so the npm package contains the built CLI.
385
395
 
@@ -42,7 +42,7 @@ function parseCacheProfile(args) {
42
42
  }
43
43
  return { profile: value, error: null };
44
44
  }
45
- export function runContext(args, reporter, lang = 'en') {
45
+ export async function runContext(args, reporter, lang = 'en') {
46
46
  if (args.includes('--help') || args.includes('-h')) {
47
47
  reporter.stdout(getContextHelp(lang));
48
48
  return 0;
@@ -77,7 +77,7 @@ export function runContext(args, reporter, lang = 'en') {
77
77
  const mustflowRoot = resolveMustflowRoot();
78
78
  if (args.includes('--json')) {
79
79
  if (cacheProfile.profile) {
80
- reporter.stdout(JSON.stringify(getPromptCacheProfileContext(mustflowRoot, cacheProfile.profile), null, 2));
80
+ reporter.stdout(JSON.stringify(await getPromptCacheProfileContext(mustflowRoot, cacheProfile.profile), null, 2));
81
81
  return 0;
82
82
  }
83
83
  const context = getAgentContext(mustflowRoot);
@@ -4,7 +4,7 @@ import http from 'node:http';
4
4
  import path from 'node:path';
5
5
  import { openPathInFileManager, openUrlInBrowser } from '../lib/browser-open.js';
6
6
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
7
- import { renderDashboardHtml } from '../lib/dashboard-html.js';
7
+ import { renderDashboardHtml, } from '../lib/dashboard-html.js';
8
8
  import { DASHBOARD_VERIFICATION_MAX_FILE_MATCHES, createDashboardVerificationSnapshot, } from '../../core/dashboard-verification.js';
9
9
  import { parseSkillIndexRoutes } from '../../core/skill-route-alignment.js';
10
10
  import { getAgentContext } from '../lib/agent-context.js';
@@ -13,6 +13,7 @@ import { isRecord, readCommandContract, readPositiveInteger, readString, readStr
13
13
  import { readDashboardPreferences, updateDashboardPreferences, } from '../lib/dashboard-preferences.js';
14
14
  import { DOC_REVIEW_LEDGER_RELATIVE_PATH, isDocReviewStatus, isReviewerKind, listDocReviewEntries, markDocReviewEntry, } from '../lib/doc-review-ledger.js';
15
15
  import { inspectManifestLock } from '../lib/manifest-lock.js';
16
+ import { readLocalCommandEffectGraphs } from '../lib/local-index.js';
16
17
  import { readPackageMetadata } from '../lib/package-info.js';
17
18
  import { t } from '../lib/i18n.js';
18
19
  import { resolveMustflowRoot } from '../lib/project-root.js';
@@ -278,7 +279,55 @@ function readDashboardCommandContract(projectRoot) {
278
279
  return null;
279
280
  }
280
281
  }
281
- function renderCommandContractResponse(contract) {
282
+ function toDashboardCommandEffectGraphStatus(graph) {
283
+ return {
284
+ source: graph.source,
285
+ status: graph.status,
286
+ database_path: graph.databasePath,
287
+ index_fresh: graph.indexFresh,
288
+ stale_paths: graph.stalePaths,
289
+ refresh_hint: graph.refreshHint,
290
+ };
291
+ }
292
+ function toDashboardCommandEffectGraph(graph) {
293
+ return {
294
+ ...toDashboardCommandEffectGraphStatus(graph),
295
+ write_locks: graph.writeLocks.map((writeLock) => ({
296
+ lock: writeLock.lock,
297
+ paths: writeLock.paths,
298
+ modes: writeLock.modes,
299
+ sources: writeLock.sources,
300
+ concurrencies: writeLock.concurrencies,
301
+ effect_count: writeLock.effectCount,
302
+ })),
303
+ lock_conflicts: graph.lockConflicts.map((conflict) => ({
304
+ intent: conflict.intent,
305
+ lock: conflict.lock,
306
+ paths: conflict.paths,
307
+ modes: conflict.modes,
308
+ concurrencies: conflict.concurrencies,
309
+ conflicting_paths: conflict.conflictingPaths,
310
+ conflicting_modes: conflict.conflictingModes,
311
+ conflicting_concurrencies: conflict.conflictingConcurrencies,
312
+ })),
313
+ };
314
+ }
315
+ async function readCommandEffectGraphMap(projectRoot, intentNames) {
316
+ if (intentNames.length === 0) {
317
+ return { graphs: new Map() };
318
+ }
319
+ const graphs = new Map();
320
+ let status;
321
+ const localGraphs = await readLocalCommandEffectGraphs(projectRoot, intentNames);
322
+ for (const [intentName, graph] of localGraphs) {
323
+ status ??= toDashboardCommandEffectGraphStatus(graph);
324
+ if (graph.status === 'fresh') {
325
+ graphs.set(intentName, toDashboardCommandEffectGraph(graph));
326
+ }
327
+ }
328
+ return { status, graphs };
329
+ }
330
+ async function renderCommandContractResponse(projectRoot, contract) {
282
331
  if (!contract) {
283
332
  return {
284
333
  path: '.mustflow/config/commands.toml',
@@ -321,10 +370,15 @@ function renderCommandContractResponse(contract) {
321
370
  },
322
371
  ];
323
372
  });
373
+ const effectGraphs = await readCommandEffectGraphMap(projectRoot, intents.map((intent) => intent.name));
324
374
  return {
325
375
  path: '.mustflow/config/commands.toml',
326
376
  exists: true,
327
- intents,
377
+ effect_graph_status: effectGraphs.status,
378
+ intents: intents.map((intent) => {
379
+ const graph = effectGraphs.graphs.get(intent.name);
380
+ return graph ? { ...intent, effect_graph: graph } : intent;
381
+ }),
328
382
  };
329
383
  }
330
384
  function pathMatches(filePath, patterns) {
@@ -475,13 +529,13 @@ function renderRunHistoryResponse(projectRoot) {
475
529
  };
476
530
  }
477
531
  }
478
- function renderStatusResponse(projectRoot) {
532
+ async function renderStatusResponse(projectRoot) {
479
533
  const context = getAgentContext(projectRoot);
480
534
  const manifest = inspectManifestLock(projectRoot);
481
535
  const lock = manifest.readResult.kind === 'present' ? manifest.readResult.lock : undefined;
482
536
  const activeDocuments = listDocReviewEntries(projectRoot);
483
537
  const rawCommandContract = readDashboardCommandContract(projectRoot);
484
- const commandContract = renderCommandContractResponse(rawCommandContract);
538
+ const commandContract = await renderCommandContractResponse(projectRoot, rawCommandContract);
485
539
  const gitChangedFiles = readGitChangedFiles(projectRoot);
486
540
  const packageMetadata = readPackageMetadata();
487
541
  return {
@@ -536,7 +590,7 @@ export async function runDashboard(args, reporter, lang = 'en') {
536
590
  'cache-control': 'no-store',
537
591
  'content-type': 'text/html; charset=utf-8',
538
592
  });
539
- response.end(renderDashboardHtml(readDashboardPreferences(projectRoot), token, renderStatusResponse(projectRoot), renderDocReviewResponse(projectRoot, new URL('/api/docs/review', 'http://localhost'))));
593
+ response.end(renderDashboardHtml(readDashboardPreferences(projectRoot), token, await renderStatusResponse(projectRoot), renderDocReviewResponse(projectRoot, new URL('/api/docs/review', 'http://localhost'))));
540
594
  return;
541
595
  }
542
596
  if (request.method === 'GET' && requestUrl.pathname === '/favicon.ico') {
@@ -550,7 +604,7 @@ export async function runDashboard(args, reporter, lang = 'en') {
550
604
  return;
551
605
  }
552
606
  if (request.method === 'GET') {
553
- sendJson(response, 200, renderStatusResponse(projectRoot));
607
+ sendJson(response, 200, await renderStatusResponse(projectRoot));
554
608
  return;
555
609
  }
556
610
  }
@@ -10,6 +10,7 @@ import { explainSkillRouteAlignment, } from '../../core/skill-route-alignment.js
10
10
  import { explainPublicSurface } from '../../core/public-surface-explanation.js';
11
11
  import { explainSourceAnchor } from '../../core/source-anchor-explanation.js';
12
12
  import { checkMustflowProject } from '../lib/validation.js';
13
+ import { readLocalCommandEffectGraph, readLocalPathSurface, } from '../lib/local-index.js';
13
14
  const EXPLAIN_SCHEMA_VERSION = '1';
14
15
  export function getExplainHelp(lang = 'en') {
15
16
  return renderHelp({
@@ -71,13 +72,15 @@ function getAnchorExplainOutput(projectRoot, anchorId) {
71
72
  decision: explainSourceAnchor(projectRoot, anchorId),
72
73
  };
73
74
  }
74
- function getCommandExplainOutput(projectRoot, commandName) {
75
+ async function getCommandExplainOutput(projectRoot, commandName) {
76
+ const decision = explainCommandIntent(readCommandContract(projectRoot), commandName);
77
+ const effectGraph = decision.intent ? await readLocalCommandEffectGraph(projectRoot, commandName) : undefined;
75
78
  return {
76
79
  schema_version: EXPLAIN_SCHEMA_VERSION,
77
80
  command: 'explain',
78
81
  topic: 'command',
79
82
  mustflow_root: projectRoot,
80
- decision: explainCommandIntent(readCommandContract(projectRoot), commandName),
83
+ decision: effectGraph ? { ...decision, effectGraph } : decision,
81
84
  };
82
85
  }
83
86
  function getRetentionExplainOutput(projectRoot) {
@@ -107,13 +110,15 @@ function getSkillExplainOutput(projectRoot, skillName) {
107
110
  decision: explainSkillRoute(projectRoot, skillName),
108
111
  };
109
112
  }
110
- function getSurfaceExplainOutput(projectRoot, pathArg) {
113
+ async function getSurfaceExplainOutput(projectRoot, pathArg) {
114
+ const decision = explainPublicSurface(pathArg);
115
+ const readModel = await readLocalPathSurface(projectRoot, pathArg);
111
116
  return {
112
117
  schema_version: EXPLAIN_SCHEMA_VERSION,
113
118
  command: 'explain',
114
119
  topic: 'surface',
115
120
  mustflow_root: projectRoot,
116
- decision: explainPublicSurface(pathArg),
121
+ decision: { ...decision, readModel },
117
122
  };
118
123
  }
119
124
  function formatNullable(value, lang) {
@@ -160,6 +165,28 @@ function renderExplainDecision(output, lang) {
160
165
  const intent = output.decision.intent;
161
166
  lines.push('', t(lang, 'explain.label.commandIntent'), `- ${t(lang, 'explain.label.commandName')}: ${intent.name}`, `- status: ${intent.status ?? t(lang, 'value.none')}`, `- lifecycle: ${intent.lifecycle ?? t(lang, 'value.none')}`, `- run_policy: ${intent.runPolicy ?? t(lang, 'value.none')}`, `- stdin: ${intent.stdin ?? t(lang, 'value.none')}`, `- timeout_seconds: ${intent.timeoutSeconds ?? t(lang, 'value.none')}`, `- mode: ${intent.mode}`, `- cwd: ${intent.cwd ?? t(lang, 'value.none')}`, `- writes: ${intent.writes.join(', ') || t(lang, 'value.none')}`, `- network: ${formatNullable(intent.network, lang)}`, `- destructive: ${formatNullable(intent.destructive, lang)}`, `- success_exit_codes: ${intent.successExitCodes.join(', ') || t(lang, 'value.none')}`, `- required_after: ${intent.requiredAfter.join(', ') || t(lang, 'value.none')}`);
162
167
  }
168
+ if ('effectGraph' in output.decision && output.decision.effectGraph) {
169
+ const graph = output.decision.effectGraph;
170
+ lines.push('', 'Command effect graph', `- source: ${graph.source}`, `- status: ${graph.status}`, `- index_fresh: ${graph.indexFresh ? t(lang, 'value.yes') : t(lang, 'value.no')}`);
171
+ if (graph.refreshHint) {
172
+ lines.push(`- refresh_hint: ${graph.refreshHint}`);
173
+ }
174
+ if (graph.stalePaths.length > 0) {
175
+ lines.push(`- stale_paths: ${graph.stalePaths.join(', ')}`);
176
+ }
177
+ if (graph.writeLocks.length > 0) {
178
+ lines.push('- write_locks:');
179
+ for (const lock of graph.writeLocks) {
180
+ lines.push(` - ${lock.lock}`, ` paths: ${lock.paths.join(', ') || t(lang, 'value.none')}`, ` modes: ${lock.modes.join(', ') || t(lang, 'value.none')}`, ` concurrencies: ${lock.concurrencies.join(', ') || t(lang, 'value.none')}`);
181
+ }
182
+ }
183
+ if (graph.lockConflicts.length > 0) {
184
+ lines.push('- lock_conflicts:');
185
+ for (const conflict of graph.lockConflicts) {
186
+ lines.push(` - ${conflict.intent} (${conflict.lock})`, ` paths: ${conflict.paths.join(', ') || t(lang, 'value.none')}`, ` conflicting_paths: ${conflict.conflictingPaths.join(', ') || t(lang, 'value.none')}`);
187
+ }
188
+ }
189
+ }
163
190
  if ('retention' in output.decision) {
164
191
  const retention = output.decision.retention;
165
192
  lines.push('', t(lang, 'explain.label.retentionPolicy'), `- enabled: ${formatNullable(retention.enabled, lang)}`, `- raw_events.store: ${formatNullable(retention.rawEvents.store, lang)}`, `- raw_events.on_limit: ${formatNullable(retention.rawEvents.onLimit, lang)}`, `- run_receipts.store: ${retention.runReceipts.store}`, `- run_receipts.max_file_kb: ${retention.runReceipts.maxFileKb}`, `- run_receipts.max_items: ${retention.runReceipts.maxItems}`, `- run_receipts.keep_stdout_tail_bytes: ${retention.runReceipts.stdoutTailBytes}`, `- run_receipts.keep_stderr_tail_bytes: ${retention.runReceipts.stderrTailBytes}`, `- knowledge.enabled: ${formatNullable(retention.knowledge.enabled, lang)}`, `- context.max_file_kb: ${retention.context.maxFileKb}`, `- repo_map.max_file_kb: ${retention.repoMap.maxFileKb}`, `- repo_map.fail_if_larger: ${formatNullable(retention.repoMap.failIfLarger, lang)}`);
@@ -187,6 +214,19 @@ function renderExplainDecision(output, lang) {
187
214
  if ('surface' in output.decision) {
188
215
  const surface = output.decision.surface;
189
216
  lines.push('', t(lang, 'explain.label.publicSurface'), `- kind: ${surface.kind}`, `- category: ${surface.category}`, `- is_public_surface: ${surface.isPublicSurface ? t(lang, 'value.yes') : t(lang, 'value.no')}`, `- ${t(lang, 'explain.label.validationReasons')}: ${surface.validationReasons.join(', ') || t(lang, 'value.none')}`, `- ${t(lang, 'explain.label.affectedContracts')}: ${surface.affectedContracts.join(', ') || t(lang, 'value.none')}`, `- ${t(lang, 'classify.label.updatePolicy')}: ${surface.updatePolicy}`, `- ${t(lang, 'classify.label.driftChecks')}: ${surface.driftChecks.join(', ') || t(lang, 'value.none')}`);
217
+ const readModel = output.decision.readModel;
218
+ if (readModel) {
219
+ lines.push('', 'Local index path-surface model', `- status: ${readModel.status}`, `- index_fresh: ${readModel.indexFresh ? t(lang, 'value.yes') : t(lang, 'value.no')}`);
220
+ if (readModel.refreshHint) {
221
+ lines.push(`- refresh_hint: ${readModel.refreshHint}`);
222
+ }
223
+ if (readModel.stalePaths.length > 0) {
224
+ lines.push(`- stale_paths: ${readModel.stalePaths.join(', ')}`);
225
+ }
226
+ if (readModel.match) {
227
+ lines.push(`- rule_id: ${readModel.match.ruleId}`, `- pattern: ${readModel.match.pattern}`, `- change_kinds: ${readModel.match.changeKinds.join(', ') || t(lang, 'value.none')}`);
228
+ }
229
+ }
190
230
  }
191
231
  if ('anchor' in output.decision) {
192
232
  const anchor = output.decision.anchor;
@@ -200,7 +240,7 @@ function renderExplainDecision(output, lang) {
200
240
  }
201
241
  return lines.join('\n');
202
242
  }
203
- export function runExplain(args, reporter, lang = 'en') {
243
+ export async function runExplain(args, reporter, lang = 'en') {
204
244
  if (args.includes('--help') || args.includes('-h')) {
205
245
  reporter.stdout(getExplainHelp(lang));
206
246
  return 0;
@@ -269,7 +309,7 @@ export function runExplain(args, reporter, lang = 'en') {
269
309
  output = getAuthorityExplainOutput(projectRoot, targetArg);
270
310
  break;
271
311
  case 'command':
272
- output = getCommandExplainOutput(projectRoot, targetArg);
312
+ output = await getCommandExplainOutput(projectRoot, targetArg);
273
313
  break;
274
314
  case 'retention':
275
315
  output = getRetentionExplainOutput(projectRoot);
@@ -278,7 +318,7 @@ export function runExplain(args, reporter, lang = 'en') {
278
318
  output = getSkillExplainOutput(projectRoot, targetArg);
279
319
  break;
280
320
  case 'surface':
281
- output = getSurfaceExplainOutput(projectRoot, targetArg);
321
+ output = await getSurfaceExplainOutput(projectRoot, targetArg);
282
322
  break;
283
323
  case 'skills':
284
324
  output = getSkillsExplainOutput(projectRoot);
@@ -15,10 +15,14 @@ export function getIndexHelp(lang = 'en') {
15
15
  label: '--source',
16
16
  description: t(lang, 'index.help.option.source'),
17
17
  },
18
+ {
19
+ label: '--incremental',
20
+ description: t(lang, 'index.help.option.incremental'),
21
+ },
18
22
  { label: '--json', description: t(lang, 'cli.option.json') },
19
23
  { label: '-h, --help', description: t(lang, 'cli.option.help') },
20
24
  ],
21
- examples: ['mf index --dry-run --json', 'mf index', 'mf index --json'],
25
+ examples: ['mf index --dry-run --json', 'mf index', 'mf index --incremental --json'],
22
26
  exitCodes: [
23
27
  {
24
28
  label: '0',
@@ -39,6 +43,8 @@ function renderIndexSummary(result, lang) {
39
43
  `${t(lang, 'label.commandIntents')}: ${result.command_intent_count}`,
40
44
  `command_effects: ${result.command_effect_count}`,
41
45
  `source_anchors: ${result.source_anchor_count}`,
46
+ `index_mode: ${result.index_mode}`,
47
+ `reused_existing: ${result.reused_existing ? 'yes' : 'no'}`,
42
48
  `${t(lang, 'label.wroteFiles')}: ${result.wrote_files ? 'yes' : 'no'}`,
43
49
  ];
44
50
  if (result.dry_run) {
@@ -51,7 +57,7 @@ export async function runIndex(args, reporter, lang = 'en') {
51
57
  reporter.stdout(getIndexHelp(lang));
52
58
  return 0;
53
59
  }
54
- const supported = new Set(['--dry-run', '--json', '--source']);
60
+ const supported = new Set(['--dry-run', '--json', '--source', '--incremental']);
55
61
  const unsupported = args.filter((arg) => !supported.has(arg));
56
62
  if (unsupported.length > 0) {
57
63
  printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf index --help', getIndexHelp(lang), lang);
@@ -60,6 +66,7 @@ export async function runIndex(args, reporter, lang = 'en') {
60
66
  const result = await createLocalIndex(resolveMustflowRoot(), {
61
67
  dryRun: args.includes('--dry-run'),
62
68
  includeSource: args.includes('--source'),
69
+ incremental: args.includes('--incremental'),
63
70
  });
64
71
  if (args.includes('--json')) {
65
72
  reporter.stdout(JSON.stringify(result, null, 2));
@@ -4,14 +4,12 @@ import { runCheck } from './check.js';
4
4
  import { runClassify } from './classify.js';
5
5
  import { runContext } from './context.js';
6
6
  import { runDoctor } from './doctor.js';
7
- import { runExplain } from './explain.js';
8
7
  import { runHelp } from './help.js';
9
8
  import { runImpact } from './impact.js';
10
9
  import { runLineEndings } from './line-endings.js';
11
10
  import { runMap } from './map.js';
12
11
  import { runStatus } from './status.js';
13
12
  import { runUpdate } from './update.js';
14
- import { runVerify } from './verify.js';
15
13
  import { runVersionSources } from './version-sources.js';
16
14
  import { canRunMustflowBuiltinInProcess, isMustflowBinName } from '../../core/command-classification.js';
17
15
  import { resolveSafeProjectCwd } from '../../core/command-cwd.js';
@@ -144,7 +142,7 @@ function createBufferedReporter() {
144
142
  * invariant: Only commands classified by command-classification can use this path.
145
143
  * risk: config, state
146
144
  */
147
- function runKnownBuiltinCommand(args, reporter, lang) {
145
+ async function runKnownBuiltinCommand(args, reporter, lang) {
148
146
  const [command, ...commandArgs] = args;
149
147
  if ((command === '--version' || command === '-v' || command === 'version') && commandArgs.length === 0) {
150
148
  reporter.stdout(getPackageVersion());
@@ -165,9 +163,6 @@ function runKnownBuiltinCommand(args, reporter, lang) {
165
163
  if (command === 'doctor') {
166
164
  return runDoctor(commandArgs, reporter, lang);
167
165
  }
168
- if (command === 'explain') {
169
- return runExplain(commandArgs, reporter, lang);
170
- }
171
166
  if (command === 'help') {
172
167
  return runHelp(commandArgs, reporter, lang);
173
168
  }
@@ -189,29 +184,26 @@ function runKnownBuiltinCommand(args, reporter, lang) {
189
184
  if (command === 'version-sources') {
190
185
  return runVersionSources(commandArgs, reporter, lang);
191
186
  }
192
- if (command === 'verify') {
193
- return runVerify(commandArgs, reporter, lang);
194
- }
195
187
  return undefined;
196
188
  }
197
- function withWorkingDirectory(cwd, callback) {
189
+ async function withWorkingDirectory(cwd, callback) {
198
190
  const previousCwd = process.cwd();
199
191
  process.chdir(cwd);
200
192
  try {
201
- return callback();
193
+ return await callback();
202
194
  }
203
195
  finally {
204
196
  process.chdir(previousCwd);
205
197
  }
206
198
  }
207
- function runBuiltinArgvInProcess(commandArgv, cwd, lang) {
199
+ async function runBuiltinArgvInProcess(commandArgv, cwd, lang) {
208
200
  const [command = '', ...builtinArgs] = commandArgv;
209
201
  if (!isMustflowBinName(command)) {
210
202
  return undefined;
211
203
  }
212
204
  const output = createBufferedReporter();
213
205
  try {
214
- const status = withWorkingDirectory(cwd, () => runKnownBuiltinCommand(builtinArgs, output.reporter, lang));
206
+ const status = await withWorkingDirectory(cwd, () => runKnownBuiltinCommand(builtinArgs, output.reporter, lang));
215
207
  if (status === undefined) {
216
208
  return undefined;
217
209
  }
@@ -296,7 +288,7 @@ export function getRunHelp(lang = 'en') {
296
288
  * invariant: Execution requires configured status, oneshot lifecycle, agent_allowed policy, and closed stdin.
297
289
  * risk: config, security, state
298
290
  */
299
- export function runRun(args, reporter, lang = 'en') {
291
+ export async function runRun(args, reporter, lang = 'en') {
300
292
  if (args.includes('--help') || args.includes('-h')) {
301
293
  reporter.stdout(getRunHelp(lang));
302
294
  return 0;
@@ -384,7 +376,7 @@ export function runRun(args, reporter, lang = 'en') {
384
376
  const startedAt = new Date();
385
377
  const argvCommand = commandArgv ? resolveArgvCommand(intent, commandArgv) : undefined;
386
378
  const result = commandArgv && isMustflowBuiltinIntent(intent)
387
- ? (runBuiltinArgvInProcess(commandArgv, cwd, lang) ??
379
+ ? ((await runBuiltinArgvInProcess(commandArgv, cwd, lang)) ??
388
380
  runArgvCommand(argvCommand, cwd, maxOutputBytes, env, timeoutSeconds))
389
381
  : commandArgv
390
382
  ? runArgvCommand(argvCommand, cwd, maxOutputBytes, env, timeoutSeconds)
@@ -6,6 +6,7 @@ import { createVerificationPlan } from '../../core/verification-plan.js';
6
6
  import { readCommandContract } from '../../core/config-loading.js';
7
7
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
8
8
  import { t } from '../lib/i18n.js';
9
+ import { readLocalCommandEffectGraph, readLocalPathSurfaces, } from '../lib/local-index.js';
9
10
  import { resolveMustflowRoot } from '../lib/project-root.js';
10
11
  const VERIFY_SCHEMA_VERSION = '1';
11
12
  function createBufferedOutput() {
@@ -276,9 +277,9 @@ function skippedResult(candidate) {
276
277
  receipt: null,
277
278
  };
278
279
  }
279
- function runVerificationIntent(intent, lang) {
280
+ async function runVerificationIntent(intent, lang) {
280
281
  const output = createBufferedOutput();
281
- const exitCode = runRun([intent, '--json'], output.reporter, lang);
282
+ const exitCode = await runRun([intent, '--json'], output.reporter, lang);
282
283
  const rawStdout = output.stdout().trim();
283
284
  let receipt = null;
284
285
  let status = exitCode === 0 ? 'passed' : 'failed';
@@ -332,7 +333,7 @@ function getVerificationStatus(summary) {
332
333
  }
333
334
  return 'passed';
334
335
  }
335
- function createVerifyOutput(reasons, planSource, projectRoot, lang) {
336
+ async function createVerifyOutput(reasons, planSource, projectRoot, lang) {
336
337
  const contract = readCommandContract(projectRoot);
337
338
  const candidatesByIntent = new Map();
338
339
  const unmatchedCandidates = [];
@@ -349,7 +350,10 @@ function createVerifyOutput(reasons, planSource, projectRoot, lang) {
349
350
  }
350
351
  }
351
352
  const candidates = [...candidatesByIntent.values(), ...unmatchedCandidates].sort((left, right) => left.intent.localeCompare(right.intent));
352
- const results = candidates.map((candidate) => candidate.status === 'runnable' ? runVerificationIntent(candidate.intent, lang) : skippedResult(candidate));
353
+ const results = [];
354
+ for (const candidate of candidates) {
355
+ results.push(candidate.status === 'runnable' ? await runVerificationIntent(candidate.intent, lang) : skippedResult(candidate));
356
+ }
353
357
  const summary = summarizeResults(results);
354
358
  return {
355
359
  schema_version: VERIFY_SCHEMA_VERSION,
@@ -363,9 +367,40 @@ function createVerifyOutput(reasons, planSource, projectRoot, lang) {
363
367
  results,
364
368
  };
365
369
  }
366
- function createPlanOnlyOutput(input, projectRoot) {
370
+ async function createPlanOnlyOutput(input, projectRoot) {
367
371
  const contract = readCommandContract(projectRoot);
368
- return createChangeVerificationReport(input.classificationReport, contract, projectRoot);
372
+ const report = createChangeVerificationReport(input.classificationReport, contract, projectRoot);
373
+ const localSurfaceReadModels = await readLocalPathSurfaces(projectRoot, report.files);
374
+ const [firstEntry] = report.schedule.entries;
375
+ const requirements = report.requirements.map((requirement) => {
376
+ const surfaceReadModels = requirement.files
377
+ .map((filePath) => localSurfaceReadModels.get(filePath))
378
+ .filter((readModel) => Boolean(readModel));
379
+ return surfaceReadModels.length > 0 ? { ...requirement, surfaceReadModels } : requirement;
380
+ });
381
+ if (!firstEntry) {
382
+ return { ...report, requirements };
383
+ }
384
+ const firstGraph = await readLocalCommandEffectGraph(projectRoot, firstEntry.intent);
385
+ const graphsByIntent = new Map([[firstEntry.intent, firstGraph]]);
386
+ if (firstGraph.status === 'fresh') {
387
+ for (const entry of report.schedule.entries.slice(1)) {
388
+ if (!graphsByIntent.has(entry.intent)) {
389
+ graphsByIntent.set(entry.intent, await readLocalCommandEffectGraph(projectRoot, entry.intent));
390
+ }
391
+ }
392
+ }
393
+ return {
394
+ ...report,
395
+ requirements,
396
+ schedule: {
397
+ ...report.schedule,
398
+ entries: report.schedule.entries.map((entry) => ({
399
+ ...entry,
400
+ effectGraph: graphsByIntent.get(entry.intent) ?? firstGraph,
401
+ })),
402
+ },
403
+ };
369
404
  }
370
405
  function renderVerifyOutput(output, lang) {
371
406
  const lines = [
@@ -389,7 +424,7 @@ function renderVerifyOutput(output, lang) {
389
424
  }
390
425
  return lines.join('\n');
391
426
  }
392
- export function runVerify(args, reporter, lang = 'en') {
427
+ export async function runVerify(args, reporter, lang = 'en') {
393
428
  if (args.includes('--help') || args.includes('-h')) {
394
429
  reporter.stdout(getVerifyHelp(lang));
395
430
  return 0;
@@ -434,10 +469,10 @@ export function runVerify(args, reporter, lang = 'en') {
434
469
  return 1;
435
470
  }
436
471
  if (parsed.planOnly) {
437
- reporter.stdout(JSON.stringify(createPlanOnlyOutput(input, projectRoot), null, 2));
472
+ reporter.stdout(JSON.stringify(await createPlanOnlyOutput(input, projectRoot), null, 2));
438
473
  return 0;
439
474
  }
440
- const output = createVerifyOutput(input.reasons, parsed.fromPlan ?? null, projectRoot, lang);
475
+ const output = await createVerifyOutput(input.reasons, parsed.fromPlan ?? null, projectRoot, lang);
441
476
  if (parsed.json) {
442
477
  reporter.stdout(JSON.stringify(output, null, 2));
443
478
  }
@@ -179,6 +179,8 @@ export const enMessages = {
179
179
  "dashboard.commands.writes": "writes",
180
180
  "dashboard.commands.reason": "reason",
181
181
  "dashboard.commands.agentAction": "agent action",
182
+ "dashboard.commands.effectGraph": "effect graph",
183
+ "dashboard.commands.effectGraphUnavailable": "Command effect graph unavailable",
182
184
  "dashboard.release.reloaded": "Release status reloaded",
183
185
  "dashboard.release.copied": "Command copied",
184
186
  "dashboard.release.overview": "Overview",
@@ -459,6 +461,7 @@ export const enMessages = {
459
461
  "index.help.summary": "Build a SQLite index that can be regenerated for the mustflow workflow.",
460
462
  "index.help.option.dryRun": "Calculate index targets without writing files",
461
463
  "index.help.option.source": "Include structured source-code anchors without storing source content",
464
+ "index.help.option.incremental": "Reuse a fresh compatible local index instead of rewriting it",
462
465
  "index.help.exit.ok": "Index targets were calculated and optionally written",
463
466
  "index.title": "mustflow index",
464
467
  "index.dryRunNoFiles": "Dry run: no files were written.",
@@ -179,6 +179,8 @@ export const esMessages = {
179
179
  "dashboard.commands.writes": "rutas de escritura",
180
180
  "dashboard.commands.reason": "motivo",
181
181
  "dashboard.commands.agentAction": "acción del agente",
182
+ "dashboard.commands.effectGraph": "efectos del comando",
183
+ "dashboard.commands.effectGraphUnavailable": "Efectos del comando no disponibles",
182
184
  "dashboard.release.reloaded": "Estado de lanzamiento recargado",
183
185
  "dashboard.release.copied": "Comando copiado",
184
186
  "dashboard.release.overview": "Resumen",
@@ -459,6 +461,7 @@ export const esMessages = {
459
461
  "index.help.summary": "Construye un índice SQLite que se puede regenerar para el flujo de trabajo mustflow.",
460
462
  "index.help.option.dryRun": "Calcula los objetivos del índice sin escribir archivos",
461
463
  "index.help.option.source": "Incluye anclas estructuradas de código fuente sin guardar el contenido fuente",
464
+ "index.help.option.incremental": "Reutiliza un índice local fresco y compatible en lugar de reescribirlo",
462
465
  "index.help.exit.ok": "Los objetivos del índice se calcularon y se escribieron opcionalmente",
463
466
  "index.title": "índice mustflow",
464
467
  "index.dryRunNoFiles": "Ensayo: no se escribieron archivos.",
@@ -179,6 +179,8 @@ export const frMessages = {
179
179
  "dashboard.commands.writes": "chemins écrits",
180
180
  "dashboard.commands.reason": "raison",
181
181
  "dashboard.commands.agentAction": "action agent",
182
+ "dashboard.commands.effectGraph": "effets de commande",
183
+ "dashboard.commands.effectGraphUnavailable": "Effets de commande indisponibles",
182
184
  "dashboard.release.reloaded": "État de publication rechargé",
183
185
  "dashboard.release.copied": "Commande copiée",
184
186
  "dashboard.release.overview": "Aperçu",
@@ -459,6 +461,7 @@ export const frMessages = {
459
461
  "index.help.summary": "Construit un index SQLite régénérable pour le flux de travail mustflow.",
460
462
  "index.help.option.dryRun": "Calcule les cibles d'index sans écrire de fichiers",
461
463
  "index.help.option.source": "Inclure les ancres structurées du code source sans stocker le contenu source",
464
+ "index.help.option.incremental": "Réutilise un index local récent et compatible au lieu de le réécrire",
462
465
  "index.help.exit.ok": "Les cibles d'index ont été calculées et éventuellement écrites",
463
466
  "index.title": "index mustflow",
464
467
  "index.dryRunNoFiles": "Simulation : aucun fichier n'a été écrit.",