mustflow 2.22.3 → 2.22.5

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 (42) hide show
  1. package/README.md +9 -75
  2. package/dist/cli/commands/contract-lint.js +2 -2
  3. package/dist/cli/commands/dashboard.js +14 -6
  4. package/dist/cli/commands/help.js +8 -9
  5. package/dist/cli/commands/impact.js +2 -3
  6. package/dist/cli/commands/init.js +61 -5
  7. package/dist/cli/commands/update.js +2 -2
  8. package/dist/cli/commands/version-sources.js +2 -3
  9. package/dist/cli/i18n/en.js +2 -0
  10. package/dist/cli/i18n/es.js +2 -0
  11. package/dist/cli/i18n/fr.js +2 -0
  12. package/dist/cli/i18n/hi.js +2 -0
  13. package/dist/cli/i18n/ko.js +2 -0
  14. package/dist/cli/i18n/zh.js +2 -0
  15. package/dist/cli/lib/agent-context.js +6 -11
  16. package/dist/cli/lib/local-index/index.js +14 -11
  17. package/dist/cli/lib/mustflow-read.js +41 -0
  18. package/dist/cli/lib/project-root.js +1 -2
  19. package/dist/cli/lib/repo-map.js +65 -16
  20. package/dist/cli/lib/templates.js +124 -8
  21. package/dist/cli/lib/toml.js +6 -1
  22. package/dist/cli/lib/validation/constants.js +2 -0
  23. package/dist/cli/lib/validation/index.js +291 -22
  24. package/dist/cli/lib/validation/primitives.js +2 -2
  25. package/dist/cli/lib/validation/test-selection.js +2 -2
  26. package/dist/core/bounded-output.js +32 -7
  27. package/dist/core/check-issues.js +7 -1
  28. package/dist/core/command-contract-validation.js +28 -4
  29. package/dist/core/command-env.js +1 -1
  30. package/dist/core/config-loading.js +9 -3
  31. package/dist/core/contract-lint.js +2 -1
  32. package/dist/core/safe-filesystem.js +11 -4
  33. package/dist/core/skill-route-alignment.js +1 -0
  34. package/dist/core/skill-route-explanation.js +9 -3
  35. package/dist/core/test-selection.js +2 -3
  36. package/dist/core/verification-scheduler.js +7 -6
  37. package/dist/core/version-sources.js +2 -3
  38. package/package.json +4 -2
  39. package/schemas/commands.schema.json +1 -0
  40. package/templates/default/locales/en/.mustflow/skills/INDEX.md +10 -6
  41. package/templates/default/locales/en/.mustflow/skills/routes.toml +2 -2
  42. 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.
@@ -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,4 +1,4 @@
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';
@@ -18,6 +18,7 @@ import { MANIFEST_LOCK_RELATIVE_PATH, inspectManifestLock } from '../lib/manifes
18
18
  import { getLocalIndexDatabasePath, readLatestLocalVerificationReadModelQueries, readLocalCommandEffectGraphs, } from '../lib/local-index.js';
19
19
  import { readPackageMetadata } from '../lib/package-info.js';
20
20
  import { t } from '../lib/i18n.js';
21
+ import { MUSTFLOW_JSON_MAX_BYTES, readMustflowTextFile, readMustflowTextFileIfExists, } from '../lib/mustflow-read.js';
21
22
  import { resolveMustflowRoot } from '../lib/project-root.js';
22
23
  import { detectVersionSources } from '../../core/version-sources.js';
23
24
  import { planUpdate, summarizePlan } from './update.js';
@@ -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';
@@ -590,10 +596,12 @@ function renderSkillsResponse(projectRoot) {
590
596
  routes: [],
591
597
  };
592
598
  }
593
- const routes = parseSkillIndexRoutes(readFileSync(indexPath, 'utf8')).map((route) => {
599
+ const indexContent = readMustflowTextFile(projectRoot, SKILL_INDEX_RELATIVE_PATH);
600
+ const routes = parseSkillIndexRoutes(indexContent).map((route) => {
594
601
  const skillPath = path.join(projectRoot, ...route.skillPath.split('/'));
595
- const exists = existsSync(skillPath);
596
- const declaredCommandIntents = exists ? readFrontmatterList(readFileSync(skillPath, 'utf8'), 'command_intents') : [];
602
+ const skillContent = readMustflowTextFileIfExists(projectRoot, route.skillPath);
603
+ const exists = skillContent !== null && existsSync(skillPath);
604
+ const declaredCommandIntents = skillContent ? readFrontmatterList(skillContent, 'command_intents') : [];
597
605
  const sortedRouteIntents = [...route.commandIntents].sort((left, right) => left.localeCompare(right));
598
606
  const sortedDeclaredIntents = [...declaredCommandIntents].sort((left, right) => left.localeCompare(right));
599
607
  return {
@@ -686,7 +694,7 @@ function renderRunHistoryResponse(projectRoot) {
686
694
  };
687
695
  }
688
696
  try {
689
- const receipt = JSON.parse(readFileSync(receiptPath, 'utf8'));
697
+ const receipt = JSON.parse(readMustflowTextFile(projectRoot, LATEST_RUN_RELATIVE_PATH, { maxBytes: MUSTFLOW_JSON_MAX_BYTES }));
690
698
  if (!isRecord(receipt)) {
691
699
  throw new Error('Run receipt must be a JSON object.');
692
700
  }
@@ -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;
@@ -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
  }
@@ -1,9 +1,8 @@
1
- import path from 'node:path';
2
1
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
3
2
  import { isRecord } from '../lib/command-contract.js';
4
3
  import { t } from '../lib/i18n.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
  import { detectVersionSources, releaseVersioningIsEnabled, } from '../../core/version-sources.js';
8
7
  const VERSION_SOURCES_SCHEMA_VERSION = '1';
9
8
  export function getVersionSourcesHelp(lang = 'en') {
@@ -23,7 +22,7 @@ export function getVersionSourcesHelp(lang = 'en') {
23
22
  }
24
23
  function readPreferences(projectRoot) {
25
24
  try {
26
- const preferences = readTomlFile(path.join(projectRoot, '.mustflow', 'config', 'preferences.toml'));
25
+ const preferences = readMustflowTomlFile(projectRoot, '.mustflow/config/preferences.toml');
27
26
  return isRecord(preferences) ? preferences : undefined;
28
27
  }
29
28
  catch {
@@ -583,6 +583,8 @@ Read these files before working:
583
583
  "init.error.invalidPreference": "Invalid init preference override: {value}",
584
584
  "init.error.invalidPreferenceValue": "Invalid value for {key}: {value}",
585
585
  "init.error.unsupportedPreference": "Unsupported init preference setting: {key}",
586
+ "init.error.promptInputTooLarge": "Interactive init stdin input is too large; expected at most {maxBytes} bytes.",
587
+ "init.error.promptInputTooManyResponses": "Interactive init stdin input has too many responses; expected at most {maxResponses} lines.",
586
588
  "init.prompt.locale": "Which language should mustflow documents use?",
587
589
  "init.prompt.profile": "Which project profile should mustflow use?",
588
590
  "init.prompt.agentLang": "Which language should agents use for final reports?",
@@ -583,6 +583,8 @@ Lee estos archivos antes de trabajar:
583
583
  "init.error.invalidPreference": "Anulación de preferencia de inicio no válida: {value}",
584
584
  "init.error.invalidPreferenceValue": "Valor no valido para {key}: {value}",
585
585
  "init.error.unsupportedPreference": "Ajuste de preferencia de inicio no admitido: {key}",
586
+ "init.error.promptInputTooLarge": "La entrada estándar de init interactivo es demasiado grande; se esperaban como máximo {maxBytes} bytes.",
587
+ "init.error.promptInputTooManyResponses": "La entrada estándar de init interactivo tiene demasiadas respuestas; se esperaban como máximo {maxResponses} líneas.",
586
588
  "init.prompt.locale": "¿Qué idioma deben usar los documentos mustflow?",
587
589
  "init.prompt.profile": "¿Qué perfil de proyecto debe usar mustflow?",
588
590
  "init.prompt.agentLang": "¿Qué idioma deben usar los agentes en los informes finales?",
@@ -583,6 +583,8 @@ Lisez ces fichiers avant de travailler :
583
583
  "init.error.invalidPreference": "Remplacement de préférence d'initialisation non valide : {value}",
584
584
  "init.error.invalidPreferenceValue": "Valeur non valide pour {key} : {value}",
585
585
  "init.error.unsupportedPreference": "Paramètre de préférence d'initialisation non pris en charge : {key}",
586
+ "init.error.promptInputTooLarge": "L'entrée standard de l'init interactif est trop volumineuse ; {maxBytes} octets maximum sont attendus.",
587
+ "init.error.promptInputTooManyResponses": "L'entrée standard de l'init interactif contient trop de réponses ; {maxResponses} lignes maximum sont attendues.",
586
588
  "init.prompt.locale": "Quelle langue les documents mustflow doivent-ils utiliser ?",
587
589
  "init.prompt.profile": "Quel profil de projet mustflow doit-il utiliser ?",
588
590
  "init.prompt.agentLang": "Quelle langue les agents doivent-ils utiliser pour les rapports finaux ?",
@@ -583,6 +583,8 @@ export const hiMessages = {
583
583
  "init.error.invalidPreference": "अमान्य init preference override: {value}",
584
584
  "init.error.invalidPreferenceValue": "{key} के लिए अमान्य मान: {value}",
585
585
  "init.error.unsupportedPreference": "असमर्थित init preference setting: {key}",
586
+ "init.error.promptInputTooLarge": "Interactive init stdin input बहुत बड़ा है; अधिकतम {maxBytes} bytes अपेक्षित हैं।",
587
+ "init.error.promptInputTooManyResponses": "Interactive init stdin input में बहुत अधिक responses हैं; अधिकतम {maxResponses} lines अपेक्षित हैं।",
586
588
  "init.prompt.locale": "mustflow दस्तावेज़ कौन सी भाषा उपयोग करें?",
587
589
  "init.prompt.profile": "mustflow कौन सा project profile उपयोग करे?",
588
590
  "init.prompt.agentLang": "एजेंट final reports के लिए कौन सी भाषा उपयोग करें?",
@@ -583,6 +583,8 @@ export const koMessages = {
583
583
  "init.error.invalidPreference": "초기 설정 항목 형식이 올바르지 않습니다: {value}",
584
584
  "init.error.invalidPreferenceValue": "{key}에 사용할 수 없는 값입니다: {value}",
585
585
  "init.error.unsupportedPreference": "지원하지 않는 초기 설정 항목입니다: {key}",
586
+ "init.error.promptInputTooLarge": "대화형 init 표준입력이 너무 큽니다. 최대 {maxBytes}바이트까지만 허용됩니다.",
587
+ "init.error.promptInputTooManyResponses": "대화형 init 표준입력 응답이 너무 많습니다. 최대 {maxResponses}줄까지만 허용됩니다.",
586
588
  "init.prompt.locale": "mustflow 문서는 어떤 언어로 설치할까요?",
587
589
  "init.prompt.profile": "이 저장소에는 어떤 프로젝트 유형을 사용할까요?",
588
590
  "init.prompt.agentLang": "에이전트 최종 응답은 어떤 언어로 받을까요?",
@@ -583,6 +583,8 @@ export const zhMessages = {
583
583
  "init.error.invalidPreference": "无效的初始化偏好覆盖:{value}",
584
584
  "init.error.invalidPreferenceValue": "{key} 的值无效:{value}",
585
585
  "init.error.unsupportedPreference": "不支持的初始化偏好设置:{key}",
586
+ "init.error.promptInputTooLarge": "交互式 init 的标准输入过大;最多允许 {maxBytes} 字节。",
587
+ "init.error.promptInputTooManyResponses": "交互式 init 的标准输入响应过多;最多允许 {maxResponses} 行。",
586
588
  "init.prompt.locale": "mustflow 文档应使用哪种语言?",
587
589
  "init.prompt.profile": "mustflow 应使用哪个项目配置?",
588
590
  "init.prompt.agentLang": "代理最终报告应使用哪种语言?",
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { existsSync } from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
3
  import path from 'node:path';
4
4
  import { isRecord, readPositiveInteger, readString, readStringArray, } from './command-contract.js';
@@ -6,7 +6,8 @@ import { readRetentionStore } from '../../core/retention-policy.js';
6
6
  import { toPosixPath } from './filesystem.js';
7
7
  import { readLocalIndexPromptContext } from './local-index.js';
8
8
  import { inspectManifestLock } from './manifest-lock.js';
9
- import { readTomlFile } from './toml.js';
9
+ import { MUSTFLOW_JSON_MAX_BYTES, readMustflowTextFile, readMustflowTextFileIfExists, } from './mustflow-read.js';
10
+ import { readMustflowTomlFile } from './toml.js';
10
11
  const CONTEXT_SCHEMA_VERSION = '1';
11
12
  const COMMANDS_RELATIVE_PATH = '.mustflow/config/commands.toml';
12
13
  const MUSTFLOW_RELATIVE_PATH = '.mustflow/config/mustflow.toml';
@@ -49,20 +50,14 @@ function safeExists(projectRoot, relativePath) {
49
50
  return existsSync(resolved);
50
51
  }
51
52
  function safeRead(projectRoot, relativePath) {
52
- const resolved = path.resolve(projectRoot, ...relativePath.split('/'));
53
- const root = path.resolve(projectRoot);
54
- const relative = path.relative(root, resolved);
55
- if (relative.startsWith('..') || path.isAbsolute(relative) || !existsSync(resolved)) {
56
- return null;
57
- }
58
- return readFileSync(resolved, 'utf8');
53
+ return readMustflowTextFileIfExists(projectRoot, relativePath);
59
54
  }
60
55
  function readTomlTableIfExists(projectRoot, relativePath) {
61
56
  const filePath = path.join(projectRoot, ...relativePath.split('/'));
62
57
  if (!existsSync(filePath)) {
63
58
  return undefined;
64
59
  }
65
- const parsed = readTomlFile(filePath);
60
+ const parsed = readMustflowTomlFile(projectRoot, relativePath);
66
61
  return isRecord(parsed) ? parsed : undefined;
67
62
  }
68
63
  function readNestedTable(table, key) {
@@ -194,7 +189,7 @@ function readLatestRunContext(projectRoot) {
194
189
  };
195
190
  }
196
191
  try {
197
- const parsed = JSON.parse(readFileSync(latestPath, 'utf8'));
192
+ const parsed = JSON.parse(readMustflowTextFile(projectRoot, LATEST_RUN_RELATIVE_PATH, { maxBytes: MUSTFLOW_JSON_MAX_BYTES }));
198
193
  if (!isRecord(parsed)) {
199
194
  throw new Error('latest run receipt must contain a JSON object');
200
195
  }
@@ -3,7 +3,8 @@ import { createHash } from 'node:crypto';
3
3
  import path from 'node:path';
4
4
  import { isRecord, readCommandContract, readString, readStringArray } from '../command-contract.js';
5
5
  import { listFilesRecursive, toPosixPath } from '../filesystem.js';
6
- import { readTomlFile } from '../toml.js';
6
+ import { readMustflowTomlFile } from '../toml.js';
7
+ import { MUSTFLOW_JSON_MAX_BYTES, readMustflowTextFile } from '../mustflow-read.js';
7
8
  import { collectSourceAnchorIndexRecords, hasHighRiskSourceAnchorRiskTags, } from '../../../core/source-anchor-status.js';
8
9
  import { listSourceAnchorFiles } from '../../../core/source-anchors.js';
9
10
  import { normalizeCommandEffects } from '../../../core/command-effects.js';
@@ -45,14 +46,14 @@ function getExistingIndexablePaths(projectRoot) {
45
46
  return Array.from(paths).sort((left, right) => left.localeCompare(right));
46
47
  }
47
48
  function readText(projectRoot, relativePath) {
48
- return readFileSync(path.join(projectRoot, ...relativePath.split('/')), 'utf8');
49
+ return readMustflowTextFile(projectRoot, relativePath);
49
50
  }
50
51
  function readMustflowToml(projectRoot) {
51
52
  const mustflowPath = path.join(projectRoot, ...MUSTFLOW_RELATIVE_PATH.split('/'));
52
53
  if (!existsSync(mustflowPath)) {
53
54
  return undefined;
54
55
  }
55
- const parsed = readTomlFile(mustflowPath);
56
+ const parsed = readMustflowTomlFile(projectRoot, MUSTFLOW_RELATIVE_PATH);
56
57
  return isRecord(parsed) ? parsed : undefined;
57
58
  }
58
59
  function readIndexToml(projectRoot) {
@@ -60,7 +61,7 @@ function readIndexToml(projectRoot) {
60
61
  if (!existsSync(indexConfigPath)) {
61
62
  return undefined;
62
63
  }
63
- const parsed = readTomlFile(indexConfigPath);
64
+ const parsed = readMustflowTomlFile(projectRoot, INDEX_CONFIG_RELATIVE_PATH);
64
65
  return isRecord(parsed) ? parsed : undefined;
65
66
  }
66
67
  function readNestedTable(table, key) {
@@ -227,7 +228,7 @@ function collectSkillRoutes(projectRoot) {
227
228
  if (!existsSync(indexPath)) {
228
229
  return [];
229
230
  }
230
- const content = readFileSync(indexPath, 'utf8');
231
+ const content = readMustflowTextFile(projectRoot, '.mustflow/skills/INDEX.md');
231
232
  const routes = [];
232
233
  let inRouteTable = false;
233
234
  for (const line of content.split(/\r?\n/u)) {
@@ -455,10 +456,11 @@ function detectLocalSearchCapabilities(database) {
455
456
  function isJsonRecord(value) {
456
457
  return typeof value === 'object' && value !== null && !Array.isArray(value);
457
458
  }
458
- function readJsonRecord(filePath) {
459
+ function readJsonRecord(projectRoot, relativePath) {
459
460
  try {
460
- const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
461
- return isJsonRecord(parsed) ? parsed : null;
461
+ const content = readMustflowTextFile(projectRoot, relativePath, { maxBytes: MUSTFLOW_JSON_MAX_BYTES });
462
+ const parsed = JSON.parse(content);
463
+ return isJsonRecord(parsed) ? { content, value: parsed } : null;
462
464
  }
463
465
  catch {
464
466
  return null;
@@ -567,8 +569,8 @@ function createVerificationEvidenceIndex(projectRoot) {
567
569
  failureFingerprintReadModels: [],
568
570
  };
569
571
  }
570
- const latest = readJsonRecord(latestPath);
571
- if (!latest) {
572
+ const latestRecord = readJsonRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH);
573
+ if (!latestRecord) {
572
574
  return {
573
575
  summaries: [],
574
576
  verificationPlans: [],
@@ -586,7 +588,8 @@ function createVerificationEvidenceIndex(projectRoot) {
586
588
  failureFingerprintReadModels: [],
587
589
  };
588
590
  }
589
- const sourceHash = sha256Bytes(readFileSync(latestPath));
591
+ const latest = latestRecord.value;
592
+ const sourceHash = sha256Bytes(Buffer.from(latestRecord.content));
590
593
  const command = stringField(latest, 'command') ?? 'unknown';
591
594
  const kind = stringField(latest, 'kind') ?? (command === 'verify' ? 'verify_run_summary' : 'run_receipt');
592
595
  const evidenceModel = recordField(latest, 'evidence_model');
@@ -0,0 +1,41 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readUtf8FileInsideWithoutSymlinks } from './filesystem.js';
4
+ export const MUSTFLOW_TEXT_MAX_BYTES = 1024 * 1024;
5
+ export const MUSTFLOW_TOML_MAX_BYTES = 256 * 1024;
6
+ export const MUSTFLOW_JSON_MAX_BYTES = 1024 * 1024;
7
+ export function mustflowProjectPath(projectRoot, relativePath) {
8
+ return path.join(projectRoot, ...relativePath.split('/'));
9
+ }
10
+ function missingPath(error) {
11
+ return error instanceof Error && 'code' in error && error.code === 'ENOENT';
12
+ }
13
+ export function readMustflowTextFile(projectRoot, relativePath, options = {}) {
14
+ return readUtf8FileInsideWithoutSymlinks(projectRoot, mustflowProjectPath(projectRoot, relativePath), { maxBytes: options.maxBytes ?? MUSTFLOW_TEXT_MAX_BYTES });
15
+ }
16
+ export function readMustflowTextFileResult(projectRoot, relativePath, options = {}) {
17
+ const filePath = mustflowProjectPath(projectRoot, relativePath);
18
+ if (!existsSync(filePath)) {
19
+ return { ok: false, exists: false, error: null };
20
+ }
21
+ try {
22
+ return {
23
+ ok: true,
24
+ content: readMustflowTextFile(projectRoot, relativePath, options),
25
+ };
26
+ }
27
+ catch (error) {
28
+ if (missingPath(error)) {
29
+ return { ok: false, exists: false, error: null };
30
+ }
31
+ return {
32
+ ok: false,
33
+ exists: true,
34
+ error: error instanceof Error ? error.message : String(error),
35
+ };
36
+ }
37
+ }
38
+ export function readMustflowTextFileIfExists(projectRoot, relativePath, options = {}) {
39
+ const result = readMustflowTextFileResult(projectRoot, relativePath, options);
40
+ return result.ok ? result.content : null;
41
+ }
@@ -2,8 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  function hasMustflowMarker(directoryPath) {
4
4
  return (existsSync(path.join(directoryPath, '.mustflow', 'config', 'mustflow.toml')) ||
5
- existsSync(path.join(directoryPath, '.mustflow', 'config', 'commands.toml')) ||
6
- existsSync(path.join(directoryPath, '.mustflow')));
5
+ existsSync(path.join(directoryPath, '.mustflow', 'config', 'commands.toml')));
7
6
  }
8
7
  export function findMustflowRoot(startPath = process.cwd()) {
9
8
  let current = path.resolve(startPath);