mustflow 2.22.4 → 2.22.9

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 (72) hide show
  1. package/README.md +17 -75
  2. package/dist/cli/commands/classify.js +2 -0
  3. package/dist/cli/commands/contract-lint.js +2 -2
  4. package/dist/cli/commands/dashboard.js +23 -75
  5. package/dist/cli/commands/help.js +8 -9
  6. package/dist/cli/commands/impact.js +2 -3
  7. package/dist/cli/commands/init.js +61 -5
  8. package/dist/cli/commands/run/receipt.js +1 -0
  9. package/dist/cli/commands/run.js +14 -1
  10. package/dist/cli/commands/update.js +2 -2
  11. package/dist/cli/commands/verify/evidence-input.js +269 -0
  12. package/dist/cli/commands/verify/input.js +212 -0
  13. package/dist/cli/commands/verify.js +23 -482
  14. package/dist/cli/commands/version-sources.js +2 -3
  15. package/dist/cli/i18n/en.js +5 -0
  16. package/dist/cli/i18n/es.js +5 -0
  17. package/dist/cli/i18n/fr.js +5 -0
  18. package/dist/cli/i18n/hi.js +5 -0
  19. package/dist/cli/i18n/ko.js +5 -0
  20. package/dist/cli/i18n/zh.js +5 -0
  21. package/dist/cli/lib/agent-context.js +6 -11
  22. package/dist/cli/lib/dashboard-export.js +2 -0
  23. package/dist/cli/lib/dashboard-mutations.js +79 -0
  24. package/dist/cli/lib/local-index/command-effect-index.js +25 -0
  25. package/dist/cli/lib/local-index/hashing.js +7 -0
  26. package/dist/cli/lib/local-index/index.js +127 -823
  27. package/dist/cli/lib/local-index/source-index.js +137 -0
  28. package/dist/cli/lib/local-index/verification-evidence.js +451 -0
  29. package/dist/cli/lib/local-index/workflow-documents.js +204 -0
  30. package/dist/cli/lib/mustflow-read.js +41 -0
  31. package/dist/cli/lib/project-root.js +1 -2
  32. package/dist/cli/lib/repo-map.js +65 -16
  33. package/dist/cli/lib/run-root-trust.js +27 -0
  34. package/dist/cli/lib/templates.js +124 -8
  35. package/dist/cli/lib/toml.js +6 -1
  36. package/dist/cli/lib/validation/constants.js +2 -0
  37. package/dist/cli/lib/validation/index.js +291 -22
  38. package/dist/cli/lib/validation/primitives.js +2 -2
  39. package/dist/cli/lib/validation/test-selection.js +2 -2
  40. package/dist/core/bounded-output.js +32 -7
  41. package/dist/core/change-classification-policy.js +47 -0
  42. package/dist/core/change-classification.js +10 -43
  43. package/dist/core/check-issues.js +7 -1
  44. package/dist/core/command-contract-validation.js +28 -4
  45. package/dist/core/command-env.js +1 -1
  46. package/dist/core/config-loading.js +9 -3
  47. package/dist/core/contract-lint.js +8 -3
  48. package/dist/core/correlation-id.js +16 -0
  49. package/dist/core/run-receipt.js +1 -0
  50. package/dist/core/safe-filesystem.js +11 -4
  51. package/dist/core/skill-route-alignment.js +1 -0
  52. package/dist/core/skill-route-explanation.js +9 -3
  53. package/dist/core/test-selection.js +2 -3
  54. package/dist/core/verification-scheduler.js +7 -6
  55. package/dist/core/version-sources.js +2 -3
  56. package/package.json +4 -1
  57. package/schemas/README.md +4 -0
  58. package/schemas/change-verification-report.schema.json +4 -0
  59. package/schemas/classify-report.schema.json +4 -0
  60. package/schemas/commands.schema.json +1 -0
  61. package/schemas/dashboard-export.schema.json +4 -0
  62. package/schemas/latest-run-pointer.schema.json +4 -0
  63. package/schemas/run-receipt.schema.json +4 -0
  64. package/schemas/verify-report.schema.json +4 -0
  65. package/schemas/verify-run-manifest.schema.json +4 -0
  66. package/templates/default/i18n.toml +3 -3
  67. package/templates/default/locales/en/.mustflow/skills/INDEX.md +10 -6
  68. package/templates/default/locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md +25 -2
  69. package/templates/default/locales/en/.mustflow/skills/routes.toml +2 -2
  70. package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +9 -1
  71. package/templates/default/locales/en/.mustflow/skills/test-design-guard/SKILL.md +9 -1
  72. package/templates/default/manifest.toml +1 -1
package/README.md CHANGED
@@ -147,7 +147,9 @@ mustflow is not an automatic project editor and is not tied to a single agent pr
147
147
 
148
148
  ## Installed files
149
149
 
150
- `mf init` installs only the agent workflow into the current directory.
150
+ `mf init` installs only the agent workflow into the current directory. The exact skill files depend
151
+ on the selected profile; run `mf init --dry-run --profile <profile>` to preview the concrete plan
152
+ for a project before writing files.
151
153
 
152
154
  ```text
153
155
  your-project/
@@ -166,83 +168,15 @@ your-project/
166
168
  │ └─ agent-workflow.md
167
169
  └─ skills/
168
170
  ├─ INDEX.md
169
- ├─ artifact-integrity-check/
170
- └─ SKILL.md
171
- ├─ behavior-preserving-refactor/
172
- │ └─ SKILL.md
173
- ├─ code-review/
174
- │ └─ SKILL.md
175
- ├─ codebase-orientation/
176
- │ └─ SKILL.md
177
- ├─ contract-sync-check/
178
- │ └─ SKILL.md
179
- ├─ date-number-audit/
180
- │ └─ SKILL.md
181
- ├─ database-change-safety/
182
- │ └─ SKILL.md
183
- ├─ dependency-reality-check/
184
- │ └─ SKILL.md
185
- ├─ diff-risk-review/
186
- │ └─ SKILL.md
187
- ├─ docs-prose-review/
188
- │ └─ SKILL.md
189
- ├─ docs-update/
190
- │ └─ SKILL.md
191
- ├─ external-prompt-injection-defense/
192
- │ └─ SKILL.md
193
- ├─ external-skill-intake/
194
- │ └─ SKILL.md
195
- ├─ failure-triage/
196
- │ └─ SKILL.md
197
- ├─ instruction-conflict-scope-check/
198
- │ └─ SKILL.md
199
- ├─ migration-safety-check/
200
- │ └─ SKILL.md
201
- ├─ multi-agent-work-coordination/
202
- │ └─ SKILL.md
203
- ├─ performance-budget-check/
204
- │ └─ SKILL.md
205
- ├─ project-context-authoring/
206
- │ └─ SKILL.md
207
- ├─ pattern-scout/
208
- │ └─ SKILL.md
209
- ├─ repo-improvement-loop/
210
- │ └─ SKILL.md
211
- ├─ requirement-regression-guard/
212
- │ └─ SKILL.md
213
- ├─ repro-first-debug/
214
- │ └─ SKILL.md
215
- ├─ security-privacy-review/
216
- │ └─ SKILL.md
217
- ├─ source-freshness-check/
218
- │ └─ SKILL.md
219
- ├─ structure-discovery-gate/
220
- │ └─ SKILL.md
221
- ├─ security-regression-tests/
222
- │ └─ SKILL.md
223
- ├─ skill-authoring/
224
- │ └─ SKILL.md
225
- ├─ test-design-guard/
226
- │ └─ SKILL.md
227
- ├─ test-maintenance/
228
- │ └─ SKILL.md
229
- ├─ vertical-slice-tdd/
230
- │ └─ SKILL.md
231
- ├─ llm-service-ux-review/
232
- │ └─ SKILL.md
233
- ├─ search-ad-content-authoring/
234
- │ └─ SKILL.md
235
- ├─ ui-quality-gate/
236
- │ └─ SKILL.md
237
- ├─ visual-review-artifact/
238
- │ ├─ SKILL.md
239
- │ ├─ resources.toml
240
- │ └─ assets/
241
- │ └─ review-template.html
242
- └─ web-asset-optimization/
171
+ ├─ routes.toml
172
+ └─ <profile-selected-skill>/
243
173
  └─ SKILL.md
244
174
  ```
245
175
 
176
+ Profiles select the installed skill surface. The package can include optional skill files that are
177
+ not copied into every project profile. Non-English workflow documents are localized when available;
178
+ skill procedures currently fall back to the canonical English skill files.
179
+
246
180
  The default template does not create project-owned root documents or contract files such as `README.md`, `PROJECT.md`, `ROADMAP.md`, `DESIGN.md`, `GOVERNANCE.md`, `TESTING.md`, `API.md`, `project.contract.json`, or `openapi.yaml`. It also does not create CI configuration, general `docs/`, or general `skills/`. User projects may already use those names for their own files.
247
181
 
248
182
  `mf init` creates `.gitignore` if it is missing. If `.gitignore` exists, mustflow updates only its managed block and preserves user rules.
@@ -433,6 +367,12 @@ bun run docs:check
433
367
  bun run check:install
434
368
  ```
435
369
 
370
+ When Bun is not available, maintainers can still run the core CLI and package metadata checks with Node/npm:
371
+
372
+ ```sh
373
+ npm run check:core:node
374
+ ```
375
+
436
376
  Agents working in this repository should prefer the configured mustflow intents for routine verification.
437
377
 
438
378
  ```sh
@@ -442,6 +382,7 @@ mf run test_related
442
382
  mf run test
443
383
  mf run test_coverage
444
384
  mf run test_release
385
+ mf run maintainer_check_node
445
386
  mf run docs_validate_fast
446
387
  mf run docs_validate
447
388
  mf run mustflow_check
@@ -479,6 +420,7 @@ The npm package includes only:
479
420
  dist/
480
421
  templates/
481
422
  schemas/
423
+ examples/
482
424
  README.md
483
425
  LICENSE
484
426
  ```
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { createChangeClassificationReport, } from '../../core/change-classification.js';
3
+ import { createCorrelationId } from '../../core/correlation-id.js';
3
4
  import { writeJsonFileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
4
5
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
5
6
  import { requireGitChangedFiles } from '../lib/git-changes.js';
@@ -71,6 +72,7 @@ export function createClassifyOutput(projectRoot, source, paths) {
71
72
  return {
72
73
  schema_version: CLASSIFY_SCHEMA_VERSION,
73
74
  command: 'classify',
75
+ correlation_id: createCorrelationId('classify'),
74
76
  mustflow_root: projectRoot,
75
77
  ...createChangeClassificationReport(source, files),
76
78
  };
@@ -2,11 +2,11 @@ import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { lintCommandContract } from '../../core/contract-lint.js';
4
4
  import { readCommandContract, isRecord } from '../../core/config-loading.js';
5
- import { readTomlFile } from '../../core/toml.js';
6
5
  import { releaseVersioningIsEnabled } from '../../core/version-sources.js';
7
6
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
8
7
  import { t } from '../lib/i18n.js';
9
8
  import { resolveMustflowRoot } from '../lib/project-root.js';
9
+ import { readMustflowTomlFile } from '../lib/toml.js';
10
10
  const CONTRACT_LINT_SCHEMA_VERSION = '1';
11
11
  export function getContractLintHelp(lang = 'en') {
12
12
  return renderHelp({
@@ -30,7 +30,7 @@ function readPreferences(projectRoot) {
30
30
  if (!existsSync(preferencesPath)) {
31
31
  return undefined;
32
32
  }
33
- const preferences = readTomlFile(preferencesPath);
33
+ const preferences = readMustflowTomlFile(projectRoot, '.mustflow/config/preferences.toml');
34
34
  return isRecord(preferences) ? preferences : undefined;
35
35
  }
36
36
  function createContractLintOutput(projectRoot, coverage, suggest) {
@@ -1,8 +1,8 @@
1
- import { randomBytes } from 'node:crypto';
1
+ import { createHash, randomBytes } from 'node:crypto';
2
2
  import { existsSync, readFileSync, statSync } from 'node:fs';
3
3
  import http from 'node:http';
4
4
  import path from 'node:path';
5
- import { openPathInFileManager, openUrlInBrowser } from '../lib/browser-open.js';
5
+ import { openUrlInBrowser } from '../lib/browser-open.js';
6
6
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
7
7
  import { renderDashboardHtml, } from '../lib/dashboard-html.js';
8
8
  import { DashboardExportPathError, writeDashboardExport, } from '../lib/dashboard-export.js';
@@ -12,12 +12,14 @@ import { parseSkillIndexRoutes } from '../../core/skill-route-alignment.js';
12
12
  import { getAgentContext } from '../lib/agent-context.js';
13
13
  import { readGitChangedFiles } from '../lib/git-changes.js';
14
14
  import { isRecord, readCommandContract, readPositiveInteger, readString, readStringArray, } from '../lib/command-contract.js';
15
- import { readDashboardPreferences, updateDashboardPreferences, } from '../lib/dashboard-preferences.js';
16
- import { DOC_REVIEW_LEDGER_RELATIVE_PATH, isDocReviewStatus, isReviewerKind, listDocReviewEntries, markDocReviewEntry, } from '../lib/doc-review-ledger.js';
15
+ import { readDashboardPreferences, } from '../lib/dashboard-preferences.js';
16
+ import { DOC_REVIEW_LEDGER_RELATIVE_PATH, isDocReviewStatus, listDocReviewEntries, } from '../lib/doc-review-ledger.js';
17
+ import { markDashboardDocReviewFromPayload, openDashboardMustflowFolder, updateDashboardPreferencesFromPayload, } from '../lib/dashboard-mutations.js';
17
18
  import { MANIFEST_LOCK_RELATIVE_PATH, inspectManifestLock } from '../lib/manifest-lock.js';
18
19
  import { getLocalIndexDatabasePath, readLatestLocalVerificationReadModelQueries, readLocalCommandEffectGraphs, } from '../lib/local-index.js';
19
20
  import { readPackageMetadata } from '../lib/package-info.js';
20
21
  import { t } from '../lib/i18n.js';
22
+ import { MUSTFLOW_JSON_MAX_BYTES, readMustflowTextFile, readMustflowTextFileIfExists, } from '../lib/mustflow-read.js';
21
23
  import { resolveMustflowRoot } from '../lib/project-root.js';
22
24
  import { detectVersionSources } from '../../core/version-sources.js';
23
25
  import { planUpdate, summarizePlan } from './update.js';
@@ -25,7 +27,6 @@ const DEFAULT_DASHBOARD_HOST = '127.0.0.1';
25
27
  const DEFAULT_DASHBOARD_PORT = 0;
26
28
  const MAX_REQUEST_BYTES = 64 * 1024;
27
29
  const LOCAL_DASHBOARD_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
28
- const DOC_REVIEW_BULK_PAYLOAD_FIELDS = ['paths', 'documents', 'entries'];
29
30
  const RELEASE_FILE_PATTERNS = [
30
31
  /^package\.json$/u,
31
32
  /^bun\.lockb?$/u,
@@ -39,6 +40,7 @@ const LATEST_RUN_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
39
40
  const COMMANDS_RELATIVE_PATH = '.mustflow/config/commands.toml';
40
41
  const AGENTS_RELATIVE_PATH = 'AGENTS.md';
41
42
  const STATUS_BLOCK_CACHE_TTL_MS = 750;
43
+ const STATUS_SIGNATURE_HASH_MAX_BYTES = 1024 * 1024;
42
44
  const dashboardStatusBlockCache = new Map();
43
45
  function dashboardStatusBlockCacheKey(projectRoot, blockName) {
44
46
  return `${path.resolve(projectRoot)}\0${blockName}`;
@@ -46,7 +48,11 @@ function dashboardStatusBlockCacheKey(projectRoot, blockName) {
46
48
  function readFileSignature(filePath) {
47
49
  try {
48
50
  const stat = statSync(filePath);
49
- return `${stat.mtimeMs}:${stat.size}`;
51
+ if (!stat.isFile() || stat.size > STATUS_SIGNATURE_HASH_MAX_BYTES) {
52
+ return `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}:unhashed`;
53
+ }
54
+ const digest = createHash('sha256').update(readFileSync(filePath)).digest('hex');
55
+ return `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}:${digest}`;
50
56
  }
51
57
  catch {
52
58
  return 'missing';
@@ -317,66 +323,6 @@ async function readRequestJson(request) {
317
323
  const rawBody = Buffer.concat(chunks).toString('utf8');
318
324
  return rawBody.trim().length === 0 ? {} : JSON.parse(rawBody);
319
325
  }
320
- function readUpdatePayload(value) {
321
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
322
- throw new Error('Request body must be a JSON object.');
323
- }
324
- const updates = value.updates;
325
- if (!Array.isArray(updates)) {
326
- throw new Error('Request body must include an updates array.');
327
- }
328
- return updates.map((entry) => {
329
- if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
330
- throw new Error('Each update must be a JSON object.');
331
- }
332
- const update = entry;
333
- if (typeof update.id !== 'string' || update.id.trim().length === 0) {
334
- throw new Error('Each update must include an id.');
335
- }
336
- return { id: update.id, value: update.value };
337
- });
338
- }
339
- function readOptionalStringField(value, key) {
340
- const field = value[key];
341
- return typeof field === 'string' && field.trim().length > 0 ? field.trim() : undefined;
342
- }
343
- function readRequiredStringField(value, key) {
344
- const field = readOptionalStringField(value, key);
345
- if (!field) {
346
- throw new Error(`${key} is required.`);
347
- }
348
- return field;
349
- }
350
- function readDocReviewPayload(value) {
351
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
352
- throw new Error('Request body must be a JSON object.');
353
- }
354
- const payload = value;
355
- for (const field of DOC_REVIEW_BULK_PAYLOAD_FIELDS) {
356
- if (field in payload) {
357
- throw new Error('Bulk documentation review updates require a separate confirmed flow.');
358
- }
359
- }
360
- const status = readRequiredStringField(payload, 'status');
361
- if (status !== 'approved' && status !== 'needs_human' && status !== 'ignored') {
362
- throw new Error('status must be approved, needs_human, or ignored.');
363
- }
364
- const reviewerKind = readRequiredStringField(payload, 'reviewerKind');
365
- if (!isReviewerKind(reviewerKind)) {
366
- throw new Error('reviewerKind must be human, llm, tool, or external.');
367
- }
368
- return {
369
- path: readRequiredStringField(payload, 'path'),
370
- status,
371
- reviewerKind,
372
- reviewerId: readRequiredStringField(payload, 'reviewerId'),
373
- reviewerLabel: readOptionalStringField(payload, 'reviewerLabel'),
374
- reviewerProvider: readOptionalStringField(payload, 'reviewerProvider'),
375
- reviewerModel: readOptionalStringField(payload, 'reviewerModel'),
376
- reviewerCommandIntent: readOptionalStringField(payload, 'reviewerCommandIntent'),
377
- summary: readOptionalStringField(payload, 'summary'),
378
- };
379
- }
380
326
  function renderDocReviewResponse(projectRoot, requestUrl) {
381
327
  const status = requestUrl.searchParams.get('status');
382
328
  if (status && !isDocReviewStatus(status)) {
@@ -590,10 +536,12 @@ function renderSkillsResponse(projectRoot) {
590
536
  routes: [],
591
537
  };
592
538
  }
593
- const routes = parseSkillIndexRoutes(readFileSync(indexPath, 'utf8')).map((route) => {
539
+ const indexContent = readMustflowTextFile(projectRoot, SKILL_INDEX_RELATIVE_PATH);
540
+ const routes = parseSkillIndexRoutes(indexContent).map((route) => {
594
541
  const skillPath = path.join(projectRoot, ...route.skillPath.split('/'));
595
- const exists = existsSync(skillPath);
596
- const declaredCommandIntents = exists ? readFrontmatterList(readFileSync(skillPath, 'utf8'), 'command_intents') : [];
542
+ const skillContent = readMustflowTextFileIfExists(projectRoot, route.skillPath);
543
+ const exists = skillContent !== null && existsSync(skillPath);
544
+ const declaredCommandIntents = skillContent ? readFrontmatterList(skillContent, 'command_intents') : [];
597
545
  const sortedRouteIntents = [...route.commandIntents].sort((left, right) => left.localeCompare(right));
598
546
  const sortedDeclaredIntents = [...declaredCommandIntents].sort((left, right) => left.localeCompare(right));
599
547
  return {
@@ -686,7 +634,7 @@ function renderRunHistoryResponse(projectRoot) {
686
634
  };
687
635
  }
688
636
  try {
689
- const receipt = JSON.parse(readFileSync(receiptPath, 'utf8'));
637
+ const receipt = JSON.parse(readMustflowTextFile(projectRoot, LATEST_RUN_RELATIVE_PATH, { maxBytes: MUSTFLOW_JSON_MAX_BYTES }));
690
638
  if (!isRecord(receipt)) {
691
639
  throw new Error('Run receipt must be a JSON object.');
692
640
  }
@@ -850,7 +798,7 @@ export async function runDashboard(args, reporter, lang = 'en') {
850
798
  }
851
799
  if (request.method === 'POST') {
852
800
  const body = await readRequestJson(request);
853
- sendJson(response, 200, updateDashboardPreferences(projectRoot, readUpdatePayload(body)));
801
+ sendJson(response, 200, updateDashboardPreferencesFromPayload(projectRoot, body));
854
802
  return;
855
803
  }
856
804
  }
@@ -863,12 +811,12 @@ export async function runDashboard(args, reporter, lang = 'en') {
863
811
  sendText(response, 405, 'Method not allowed');
864
812
  return;
865
813
  }
866
- const mustflowPath = path.join(projectRoot, '.mustflow');
867
- if (!existsSync(mustflowPath)) {
814
+ const openResult = openDashboardMustflowFolder(projectRoot);
815
+ if (openResult === 'missing') {
868
816
  sendText(response, 404, '.mustflow folder not found');
869
817
  return;
870
818
  }
871
- if (!openPathInFileManager(mustflowPath)) {
819
+ if (openResult === 'unavailable') {
872
820
  sendText(response, 500, 'No file manager opener is available for this platform');
873
821
  return;
874
822
  }
@@ -886,7 +834,7 @@ export async function runDashboard(args, reporter, lang = 'en') {
886
834
  }
887
835
  if (request.method === 'POST') {
888
836
  const body = await readRequestJson(request);
889
- markDocReviewEntry(projectRoot, readDocReviewPayload(body));
837
+ markDashboardDocReviewFromPayload(projectRoot, body);
890
838
  sendJson(response, 200, renderDocReviewResponse(projectRoot, requestUrl));
891
839
  return;
892
840
  }
@@ -1,23 +1,22 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
1
  import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.js';
4
2
  import { t } from '../lib/i18n.js';
3
+ import { readMustflowTextFileIfExists } from '../lib/mustflow-read.js';
5
4
  import { resolveMustflowRoot } from '../lib/project-root.js';
6
- import { readTomlFile } from '../lib/toml.js';
5
+ import { readMustflowTomlFile } from '../lib/toml.js';
7
6
  function isRecord(value) {
8
7
  return typeof value === 'object' && value !== null && !Array.isArray(value);
9
8
  }
10
9
  function readTextIfExists(projectRoot, relativePath) {
11
- const filePath = path.join(projectRoot, ...relativePath.split('/'));
12
- return existsSync(filePath) ? readFileSync(filePath, 'utf8') : undefined;
10
+ return readMustflowTextFileIfExists(projectRoot, relativePath) ?? undefined;
13
11
  }
14
12
  function readTomlIfExists(projectRoot, relativePath) {
15
- const filePath = path.join(projectRoot, ...relativePath.split('/'));
16
- if (!existsSync(filePath)) {
13
+ try {
14
+ const parsed = readMustflowTomlFile(projectRoot, relativePath);
15
+ return isRecord(parsed) ? parsed : undefined;
16
+ }
17
+ catch {
17
18
  return undefined;
18
19
  }
19
- const parsed = readTomlFile(filePath);
20
- return isRecord(parsed) ? parsed : undefined;
21
20
  }
22
21
  function renderMissing(relativePath, lang) {
23
22
  return t(lang, 'help.missingFile', { path: relativePath });
@@ -1,4 +1,3 @@
1
- import path from 'node:path';
2
1
  import { createChangeClassificationReport } from '../../core/change-classification.js';
3
2
  import { summarizeVersionImpact } from '../../core/version-impact.js';
4
3
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
@@ -6,7 +5,7 @@ import { isRecord } from '../lib/command-contract.js';
6
5
  import { requireGitChangedFiles } from '../lib/git-changes.js';
7
6
  import { t } from '../lib/i18n.js';
8
7
  import { resolveMustflowRoot } from '../lib/project-root.js';
9
- import { readTomlFile } from '../lib/toml.js';
8
+ import { readMustflowTomlFile } from '../lib/toml.js';
10
9
  import { detectVersionSources, releaseVersioningIsEnabled, } from '../../core/version-sources.js';
11
10
  const IMPACT_SCHEMA_VERSION = '1';
12
11
  export function getImpactHelp(lang = 'en') {
@@ -47,7 +46,7 @@ function parseImpactArgs(args) {
47
46
  }
48
47
  function readPreferences(projectRoot) {
49
48
  try {
50
- const preferences = readTomlFile(path.join(projectRoot, '.mustflow', 'config', 'preferences.toml'));
49
+ const preferences = readMustflowTomlFile(projectRoot, '.mustflow/config/preferences.toml');
51
50
  return isRecord(preferences) ? preferences : undefined;
52
51
  }
53
52
  catch {
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { existsSync, readSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { stdin as processStdin, stdout as processStdout } from 'node:process';
4
4
  import { createInterface } from 'node:readline/promises';
@@ -13,6 +13,9 @@ const MUSTFLOW_BLOCK_START = '<!-- mustflow:start schema=1 -->';
13
13
  const MUSTFLOW_BLOCK_END = '<!-- mustflow:end -->';
14
14
  const GITIGNORE_RELATIVE_PATH = '.gitignore';
15
15
  const GITIGNORE_FRAGMENT_RELATIVE_PATH = 'gitignore.mustflow';
16
+ const NON_INTERACTIVE_PROMPT_MAX_BYTES = 16 * 1024;
17
+ const NON_INTERACTIVE_PROMPT_MAX_RESPONSES = 64;
18
+ const NON_INTERACTIVE_PROMPT_READ_CHUNK_BYTES = 4096;
16
19
  const LOCALE_LABELS = {
17
20
  en: 'English',
18
21
  ko: 'Korean',
@@ -473,9 +476,52 @@ function formatLocaleChoice(locale) {
473
476
  const label = LOCALE_LABELS[locale] ?? locale;
474
477
  return `${label} (${locale})`;
475
478
  }
476
- function createPromptReader(reporter) {
479
+ function readNonInteractivePromptInput() {
480
+ const chunks = [];
481
+ let totalBytes = 0;
482
+ for (;;) {
483
+ const remainingBudget = NON_INTERACTIVE_PROMPT_MAX_BYTES + 1 - totalBytes;
484
+ if (remainingBudget <= 0) {
485
+ return {
486
+ lines: [],
487
+ errorKey: 'init.error.promptInputTooLarge',
488
+ limit: NON_INTERACTIVE_PROMPT_MAX_BYTES,
489
+ };
490
+ }
491
+ const buffer = Buffer.alloc(Math.min(NON_INTERACTIVE_PROMPT_READ_CHUNK_BYTES, remainingBudget));
492
+ const bytesRead = readSync(0, buffer, 0, buffer.length, null);
493
+ if (bytesRead === 0) {
494
+ break;
495
+ }
496
+ totalBytes += bytesRead;
497
+ chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
498
+ if (totalBytes > NON_INTERACTIVE_PROMPT_MAX_BYTES) {
499
+ return {
500
+ lines: [],
501
+ errorKey: 'init.error.promptInputTooLarge',
502
+ limit: NON_INTERACTIVE_PROMPT_MAX_BYTES,
503
+ };
504
+ }
505
+ }
506
+ const lines = Buffer.concat(chunks, totalBytes).toString('utf8').split(/\r?\n/u);
507
+ if (lines.length > NON_INTERACTIVE_PROMPT_MAX_RESPONSES) {
508
+ return {
509
+ lines: [],
510
+ errorKey: 'init.error.promptInputTooManyResponses',
511
+ limit: NON_INTERACTIVE_PROMPT_MAX_RESPONSES,
512
+ };
513
+ }
514
+ return { lines };
515
+ }
516
+ function createPromptReader(reporter, lang) {
477
517
  if (!processStdin.isTTY) {
478
- const lines = readFileSync(0, 'utf8').split(/\r?\n/u);
518
+ const input = readNonInteractivePromptInput();
519
+ if (input.errorKey) {
520
+ const paramName = input.errorKey === 'init.error.promptInputTooLarge' ? 'maxBytes' : 'maxResponses';
521
+ reporter.stderr(t(lang, input.errorKey, { [paramName]: input.limit }));
522
+ return undefined;
523
+ }
524
+ const lines = [...input.lines];
479
525
  return {
480
526
  async question(prompt) {
481
527
  reporter.stdout(prompt.trimEnd());
@@ -553,7 +599,10 @@ function addPromptedPreferenceOverride(overrides, key, value, reporter, lang) {
553
599
  }
554
600
  }
555
601
  async function promptInitOptions(template, options, reporter, lang) {
556
- const reader = createPromptReader(reporter);
602
+ const reader = createPromptReader(reporter, lang);
603
+ if (!reader) {
604
+ return undefined;
605
+ }
557
606
  try {
558
607
  const preferenceOverrides = [...options.preferenceOverrides];
559
608
  const localeChoices = template.manifest.locales.map((locale) => ({
@@ -916,7 +965,14 @@ export async function runInit(args, reporter, lang = 'en') {
916
965
  reporter.stderr(error instanceof Error ? error.message : String(error));
917
966
  return 1;
918
967
  }
919
- const options = shouldPromptForInit(args, parsedOptions) ? await promptInitOptions(template, parsedOptions, reporter, lang) : parsedOptions;
968
+ let options = parsedOptions;
969
+ if (shouldPromptForInit(args, parsedOptions)) {
970
+ const promptedOptions = await promptInitOptions(template, parsedOptions, reporter, lang);
971
+ if (!promptedOptions) {
972
+ return 1;
973
+ }
974
+ options = promptedOptions;
975
+ }
920
976
  const selectedLocale = options.locale ?? template.manifest.defaultLocale;
921
977
  if (!validateInitSelection(template, options, reporter, lang)) {
922
978
  return 1;
@@ -1,6 +1,7 @@
1
1
  import { createRunReceipt, createRunReceiptRelativePath, } from '../../../core/run-receipt.js';
2
2
  export function assembleRunReceipt(input) {
3
3
  return createRunReceipt({
4
+ correlationId: input.correlationId,
4
5
  intent: input.intentName,
5
6
  status: input.runStatus,
6
7
  timedOut: input.runStatus === 'timed_out',
@@ -1,10 +1,12 @@
1
1
  import { performance } from 'node:perf_hooks';
2
2
  import { createCommandEnv } from '../../core/command-env.js';
3
+ import { createCorrelationId } from '../../core/correlation-id.js';
3
4
  import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.js';
4
5
  import { readCommandContract, readMustflowConfigIfExists } from '../../core/config-loading.js';
5
6
  import { resolveRunReceiptRetentionPolicy } from '../../core/retention-policy.js';
6
7
  import { t } from '../lib/i18n.js';
7
8
  import { resolveMustflowRoot } from '../lib/project-root.js';
9
+ import { ALLOW_UNTRUSTED_ROOT_OPTION, assessRunRootTrust } from '../lib/run-root-trust.js';
8
10
  import { createRunPlan, createRunPreview, renderRunPreviewText, } from '../lib/run-plan.js';
9
11
  import { writeRunReceipt, } from '../../core/run-receipt.js';
10
12
  import { recordRunPerformanceHistory } from '../../core/run-performance-history.js';
@@ -92,6 +94,7 @@ export function getRunHelp(lang = 'en') {
92
94
  { label: '--dry-run', description: t(lang, 'run.help.option.dryRun') },
93
95
  { label: '--plan-only', description: t(lang, 'run.help.option.planOnly') },
94
96
  { label: '--json', description: t(lang, 'run.help.option.json') },
97
+ { label: ALLOW_UNTRUSTED_ROOT_OPTION, description: t(lang, 'run.help.option.allowUntrustedRoot') },
95
98
  { label: '-h, --help', description: t(lang, 'cli.option.help') },
96
99
  ],
97
100
  examples: ['mf run test', 'mf run lint --json', 'mf run mustflow_check --dry-run --json'],
@@ -121,7 +124,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
121
124
  reporter.stdout(getRunHelp(lang));
122
125
  return 0;
123
126
  }
124
- const supportedOptions = new Set(['--json', '--dry-run', '--plan-only']);
127
+ const supportedOptions = new Set(['--json', '--dry-run', '--plan-only', ALLOW_UNTRUSTED_ROOT_OPTION]);
125
128
  const unsupported = args.filter((arg) => arg.startsWith('-') && !supportedOptions.has(arg));
126
129
  if (unsupported.length > 0) {
127
130
  printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf run --help', getRunHelp(lang), lang);
@@ -130,6 +133,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
130
133
  const json = args.includes('--json');
131
134
  const dryRun = args.includes('--dry-run');
132
135
  const planOnly = args.includes('--plan-only');
136
+ const allowUntrustedRoot = args.includes(ALLOW_UNTRUSTED_ROOT_OPTION);
133
137
  const previewMode = dryRun ? 'dry-run' : planOnly ? 'plan-only' : null;
134
138
  if (dryRun && planOnly) {
135
139
  printUsageError(reporter, t(lang, 'run.error.conflictingPreviewModes'), 'mf run --help', getRunHelp(lang), lang);
@@ -146,6 +150,14 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
146
150
  return 1;
147
151
  }
148
152
  const projectRoot = profiler.measure('root_detection', () => resolveMustflowRoot());
153
+ const rootTrust = profiler.measure('root_trust', () => assessRunRootTrust(projectRoot));
154
+ if (!previewMode && !allowUntrustedRoot && !rootTrust.trusted) {
155
+ const message = rootTrust.reason === 'manifest_lock_invalid'
156
+ ? t(lang, 'run.error.untrustedRootInvalid', { detail: rootTrust.detail ?? rootTrust.manifestLockPath })
157
+ : t(lang, 'run.error.untrustedRootMissing', { path: rootTrust.detail ?? rootTrust.manifestLockPath });
158
+ reporter.stderr(renderCliError(message, 'mf run --help', lang));
159
+ return 1;
160
+ }
149
161
  const contract = profiler.measure('command_contract', () => readCommandContract(projectRoot));
150
162
  const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, intentName, { testTargets: options.testTargets }));
151
163
  if (previewMode) {
@@ -208,6 +220,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
208
220
  }
209
221
  }
210
222
  const receipt = profiler.measure('receipt_create', () => assembleRunReceipt({
223
+ correlationId: options.correlationId ?? createCorrelationId('run'),
211
224
  intentName,
212
225
  runStatus,
213
226
  startedAt,
@@ -7,7 +7,7 @@ import { printUsageError, renderHelp } from '../lib/cli-output.js';
7
7
  import { t } from '../lib/i18n.js';
8
8
  import { resolveMustflowRoot } from '../lib/project-root.js';
9
9
  import { getDefaultTemplate, getTemplateFiles, skillNameForTemplatePath } from '../lib/templates.js';
10
- import { readTomlFile, stringifyToml } from '../lib/toml.js';
10
+ import { readMustflowTomlFile, stringifyToml } from '../lib/toml.js';
11
11
  import { createUpdateDiffPreview, shouldPreviewUpdateDiff } from '../lib/update-diff-preview.js';
12
12
  const UPDATE_SCHEMA_VERSION = '1';
13
13
  const CUSTOMIZED_LOCK_ACTION = 'customized';
@@ -250,7 +250,7 @@ function updateManifestLockAfterApply(projectRoot, appliedItems) {
250
250
  const lockPath = path.join(projectRoot, MANIFEST_LOCK_RELATIVE_PATH);
251
251
  ensureInside(projectRoot, lockPath);
252
252
  ensureFileTargetInsideWithoutSymlinks(projectRoot, lockPath);
253
- const parsed = readTomlFile(lockPath);
253
+ const parsed = readMustflowTomlFile(projectRoot, MANIFEST_LOCK_RELATIVE_PATH);
254
254
  if (!isMutableTable(parsed)) {
255
255
  throw new Error(`Invalid manifest lock: ${MANIFEST_LOCK_RELATIVE_PATH} must contain a TOML table`);
256
256
  }