mustflow 2.18.7 → 2.18.20

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 (28) hide show
  1. package/README.md +4 -0
  2. package/dist/cli/commands/dashboard.js +68 -12
  3. package/dist/cli/commands/init.js +20 -20
  4. package/dist/cli/commands/run.js +1 -8
  5. package/dist/cli/commands/update.js +6 -11
  6. package/dist/cli/lib/dashboard-preferences.js +8 -6
  7. package/dist/cli/lib/filesystem.js +11 -1
  8. package/dist/cli/lib/local-index/index.js +30 -9
  9. package/dist/cli/lib/manifest-lock.js +38 -12
  10. package/dist/core/command-classification.js +0 -16
  11. package/dist/core/command-contract-rules.js +17 -3
  12. package/package.json +1 -1
  13. package/templates/default/i18n.toml +42 -6
  14. package/templates/default/locales/en/.mustflow/skills/INDEX.md +11 -5
  15. package/templates/default/locales/en/.mustflow/skills/cli-output-contract-review/SKILL.md +146 -0
  16. package/templates/default/locales/en/.mustflow/skills/command-contract-authoring/SKILL.md +121 -0
  17. package/templates/default/locales/en/.mustflow/skills/cross-platform-filesystem-safety/SKILL.md +137 -0
  18. package/templates/default/locales/en/.mustflow/skills/dependency-reality-check/SKILL.md +19 -6
  19. package/templates/default/locales/en/.mustflow/skills/external-prompt-injection-defense/SKILL.md +26 -10
  20. package/templates/default/locales/en/.mustflow/skills/llm-service-ux-review/SKILL.md +139 -0
  21. package/templates/default/locales/en/.mustflow/skills/process-execution-safety/SKILL.md +120 -0
  22. package/templates/default/locales/en/.mustflow/skills/routes.toml +38 -2
  23. package/templates/default/locales/en/.mustflow/skills/search-ad-content-authoring/SKILL.md +148 -0
  24. package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +46 -12
  25. package/templates/default/locales/en/.mustflow/skills/security-regression-tests/SKILL.md +43 -12
  26. package/templates/default/locales/en/.mustflow/skills/ui-quality-gate/SKILL.md +34 -14
  27. package/templates/default/manifest.toml +23 -1
  28. package/dist/cli/commands/run/builtin-dispatch.js +0 -92
package/README.md CHANGED
@@ -228,6 +228,10 @@ your-project/
228
228
  │ └─ SKILL.md
229
229
  ├─ vertical-slice-tdd/
230
230
  │ └─ SKILL.md
231
+ ├─ llm-service-ux-review/
232
+ │ └─ SKILL.md
233
+ ├─ search-ad-content-authoring/
234
+ │ └─ SKILL.md
231
235
  ├─ ui-quality-gate/
232
236
  │ └─ SKILL.md
233
237
  ├─ visual-review-artifact/
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { existsSync, readFileSync } from 'node:fs';
2
+ import { existsSync, readFileSync, statSync } from 'node:fs';
3
3
  import http from 'node:http';
4
4
  import path from 'node:path';
5
5
  import { openPathInFileManager, openUrlInBrowser } from '../lib/browser-open.js';
@@ -14,8 +14,8 @@ import { readGitChangedFiles } from '../lib/git-changes.js';
14
14
  import { isRecord, readCommandContract, readPositiveInteger, readString, readStringArray, } from '../lib/command-contract.js';
15
15
  import { readDashboardPreferences, updateDashboardPreferences, } from '../lib/dashboard-preferences.js';
16
16
  import { DOC_REVIEW_LEDGER_RELATIVE_PATH, isDocReviewStatus, isReviewerKind, listDocReviewEntries, markDocReviewEntry, } from '../lib/doc-review-ledger.js';
17
- import { inspectManifestLock } from '../lib/manifest-lock.js';
18
- import { readLatestLocalVerificationReadModelQueries, readLocalCommandEffectGraphs, } from '../lib/local-index.js';
17
+ import { MANIFEST_LOCK_RELATIVE_PATH, inspectManifestLock } from '../lib/manifest-lock.js';
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
21
  import { resolveMustflowRoot } from '../lib/project-root.js';
@@ -36,6 +36,61 @@ const RELEASE_FILE_PATTERNS = [
36
36
  ];
37
37
  const SKILL_INDEX_RELATIVE_PATH = '.mustflow/skills/INDEX.md';
38
38
  const LATEST_RUN_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
39
+ const COMMANDS_RELATIVE_PATH = '.mustflow/config/commands.toml';
40
+ const AGENTS_RELATIVE_PATH = 'AGENTS.md';
41
+ const STATUS_BLOCK_CACHE_TTL_MS = 750;
42
+ const dashboardStatusBlockCache = new Map();
43
+ function dashboardStatusBlockCacheKey(projectRoot, blockName) {
44
+ return `${path.resolve(projectRoot)}\0${blockName}`;
45
+ }
46
+ function readFileSignature(filePath) {
47
+ try {
48
+ const stat = statSync(filePath);
49
+ return `${stat.mtimeMs}:${stat.size}`;
50
+ }
51
+ catch {
52
+ return 'missing';
53
+ }
54
+ }
55
+ function readProjectFileSignature(projectRoot, relativePath) {
56
+ return `${relativePath}=${readFileSignature(path.join(projectRoot, ...relativePath.split('/')))}`;
57
+ }
58
+ function readStatusBlockSignature(projectRoot, relativePaths) {
59
+ return relativePaths.map((relativePath) => readProjectFileSignature(projectRoot, relativePath)).join('|');
60
+ }
61
+ function readLocalIndexSignature(projectRoot) {
62
+ return `local_index=${readFileSignature(getLocalIndexDatabasePath(projectRoot))}`;
63
+ }
64
+ function readDashboardStatusBlock(projectRoot, blockName, signature, readBlock) {
65
+ const key = dashboardStatusBlockCacheKey(projectRoot, blockName);
66
+ const cached = dashboardStatusBlockCache.get(key);
67
+ const now = Date.now();
68
+ if (cached && cached.signature === signature && cached.expiresAt > now) {
69
+ return cached.value;
70
+ }
71
+ const value = readBlock();
72
+ dashboardStatusBlockCache.set(key, {
73
+ signature,
74
+ expiresAt: Date.now() + STATUS_BLOCK_CACHE_TTL_MS,
75
+ value,
76
+ });
77
+ return value;
78
+ }
79
+ async function readDashboardStatusBlockAsync(projectRoot, blockName, signature, readBlock) {
80
+ const key = dashboardStatusBlockCacheKey(projectRoot, blockName);
81
+ const cached = dashboardStatusBlockCache.get(key);
82
+ const now = Date.now();
83
+ if (cached && cached.signature === signature && cached.expiresAt > now) {
84
+ return cached.value;
85
+ }
86
+ const value = await readBlock();
87
+ dashboardStatusBlockCache.set(key, {
88
+ signature,
89
+ expiresAt: Date.now() + STATUS_BLOCK_CACHE_TTL_MS,
90
+ value,
91
+ });
92
+ return value;
93
+ }
39
94
  function readFrontmatterLines(content) {
40
95
  if (!content.startsWith('---')) {
41
96
  return [];
@@ -677,16 +732,17 @@ function renderRunHistoryResponse(projectRoot) {
677
732
  }
678
733
  async function renderStatusResponse(projectRoot) {
679
734
  const context = getAgentContext(projectRoot);
680
- const manifest = inspectManifestLock(projectRoot);
735
+ const manifest = readDashboardStatusBlock(projectRoot, 'manifest', readStatusBlockSignature(projectRoot, [MANIFEST_LOCK_RELATIVE_PATH]), () => inspectManifestLock(projectRoot));
681
736
  const lock = manifest.readResult.kind === 'present' ? manifest.readResult.lock : undefined;
682
- const activeDocuments = listDocReviewEntries(projectRoot);
683
- const rawCommandContract = readDashboardCommandContract(projectRoot);
684
- const commandContract = await renderCommandContractResponse(projectRoot, rawCommandContract);
737
+ const activeDocuments = readDashboardStatusBlock(projectRoot, 'doc_review', readStatusBlockSignature(projectRoot, [DOC_REVIEW_LEDGER_RELATIVE_PATH]), () => listDocReviewEntries(projectRoot));
738
+ const rawCommandContractSignature = readStatusBlockSignature(projectRoot, [COMMANDS_RELATIVE_PATH]);
739
+ const rawCommandContract = readDashboardStatusBlock(projectRoot, 'raw_command_contract', rawCommandContractSignature, () => readDashboardCommandContract(projectRoot));
740
+ const commandContract = await readDashboardStatusBlockAsync(projectRoot, 'command_contract', `${rawCommandContractSignature}|${readLocalIndexSignature(projectRoot)}`, () => renderCommandContractResponse(projectRoot, rawCommandContract));
685
741
  const gitChangedFilesResult = readGitChangedFiles(projectRoot);
686
742
  const gitChangedFiles = gitChangedFilesResult.ok ? gitChangedFilesResult.files : [];
687
743
  const packageMetadata = readPackageMetadata();
688
744
  const verification = createDashboardVerificationSnapshot(projectRoot, rawCommandContract, commandContract.intents, gitChangedFiles, manifest.changedFiles, manifest.missingFiles);
689
- const readModel = await readLatestLocalVerificationReadModelQueries(projectRoot);
745
+ const readModel = await readDashboardStatusBlockAsync(projectRoot, 'verification_read_model', readLocalIndexSignature(projectRoot), () => readLatestLocalVerificationReadModelQueries(projectRoot));
690
746
  return {
691
747
  schema_version: '1',
692
748
  command: 'dashboard status',
@@ -696,12 +752,12 @@ async function renderStatusResponse(projectRoot) {
696
752
  release: {
697
753
  package_name: packageMetadata.name,
698
754
  package_version: packageMetadata.version,
699
- version_sources: detectVersionSources(projectRoot),
755
+ version_sources: readDashboardStatusBlock(projectRoot, 'version_sources', readStatusBlockSignature(projectRoot, ['package.json']), () => detectVersionSources(projectRoot)),
700
756
  release_sensitive_changed_files: matchingFiles(gitChangedFiles, RELEASE_FILE_PATTERNS),
701
757
  },
702
- update: renderUpdateResponse(projectRoot),
703
- run_history: renderRunHistoryResponse(projectRoot),
704
- skills: renderSkillsResponse(projectRoot),
758
+ update: readDashboardStatusBlock(projectRoot, 'update', readStatusBlockSignature(projectRoot, [AGENTS_RELATIVE_PATH, MANIFEST_LOCK_RELATIVE_PATH]), () => renderUpdateResponse(projectRoot)),
759
+ run_history: readDashboardStatusBlock(projectRoot, 'run_history', readStatusBlockSignature(projectRoot, [LATEST_RUN_RELATIVE_PATH]), () => renderRunHistoryResponse(projectRoot)),
760
+ skills: readDashboardStatusBlock(projectRoot, 'skills', readStatusBlockSignature(projectRoot, [SKILL_INDEX_RELATIVE_PATH]), () => renderSkillsResponse(projectRoot)),
705
761
  tracked_files: lock?.files.length ?? 0,
706
762
  changed_files: manifest.changedFiles,
707
763
  missing_files: manifest.missingFiles,
@@ -1,9 +1,9 @@
1
- import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync } 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';
5
5
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
6
- import { ensureFileTargetInsideWithoutSymlinks, ensureInside, readUtf8FileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks, } from '../lib/filesystem.js';
6
+ import { copyFileInsideWithoutSymlinks, ensureFileTargetInsideWithoutSymlinks, ensureInside, readUtf8FileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks, } from '../lib/filesystem.js';
7
7
  import { localeMessage, t } from '../lib/i18n.js';
8
8
  import { isLocaleTag } from '../lib/locale-tags.js';
9
9
  import { MANIFEST_LOCK_RELATIVE_PATH, sha256File } from '../lib/manifest-lock.js';
@@ -463,8 +463,11 @@ function parseOptions(args, reporter, lang) {
463
463
  preferenceOverrides,
464
464
  };
465
465
  }
466
- function sameTemplateFileContent(projectRoot, source, targetPath) {
467
- return (source.content ?? readFileSync(source.sourcePath, 'utf8')) === readUtf8FileInsideWithoutSymlinks(projectRoot, targetPath);
466
+ function readTemplateSourceText(templateRoot, sourcePath) {
467
+ return readUtf8FileInsideWithoutSymlinks(templateRoot, sourcePath);
468
+ }
469
+ function sameTemplateFileContent(projectRoot, templateRoot, source, targetPath) {
470
+ return (source.content ?? readTemplateSourceText(templateRoot, source.sourcePath)) === readUtf8FileInsideWithoutSymlinks(projectRoot, targetPath);
468
471
  }
469
472
  function formatLocaleChoice(locale) {
470
473
  const label = LOCALE_LABELS[locale] ?? locale;
@@ -605,11 +608,11 @@ async function promptInitOptions(template, options, reporter, lang) {
605
608
  function escapeRegExp(value) {
606
609
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
607
610
  }
608
- function planStatus(projectRoot, source, targetPath, options) {
611
+ function planStatus(projectRoot, templateRoot, source, targetPath, options) {
609
612
  if (!existsSync(targetPath)) {
610
613
  return 'create';
611
614
  }
612
- if (sameTemplateFileContent(projectRoot, source, targetPath)) {
615
+ if (sameTemplateFileContent(projectRoot, templateRoot, source, targetPath)) {
613
616
  return 'unchanged';
614
617
  }
615
618
  if (options.force) {
@@ -620,17 +623,15 @@ function planStatus(projectRoot, source, targetPath, options) {
620
623
  }
621
624
  return 'conflict';
622
625
  }
623
- function writeTemplateFile(projectRoot, source, targetPath) {
626
+ function writeTemplateFile(projectRoot, templateRoot, source, targetPath) {
624
627
  if (source.content !== undefined) {
625
628
  writeUtf8FileInsideWithoutSymlinks(projectRoot, targetPath, source.content);
626
629
  return;
627
630
  }
628
- ensureFileTargetInsideWithoutSymlinks(projectRoot, targetPath, { allowMissingLeaf: true });
629
- mkdirSync(path.dirname(targetPath), { recursive: true });
630
- copyFileSync(source.sourcePath, targetPath);
631
+ copyFileInsideWithoutSymlinks(templateRoot, source.sourcePath, projectRoot, targetPath);
631
632
  }
632
633
  function writePlannedFile(projectRoot, file) {
633
- writeTemplateFile(projectRoot, file, file.targetPath);
634
+ writeTemplateFile(projectRoot, file.sourceRoot, file, file.targetPath);
634
635
  }
635
636
  function gitignoreFragmentPath(template) {
636
637
  return path.join(template.templateRoot, template.manifest.commonRoot, GITIGNORE_FRAGMENT_RELATIVE_PATH);
@@ -646,11 +647,11 @@ function mergeGitignoreContent(existingContent, fragmentContent) {
646
647
  }
647
648
  return `${existingContent.trimEnd()}\n\n${normalizedFragment}\n`;
648
649
  }
649
- function planGitignoreStatus(projectRoot, sourcePath, targetPath) {
650
+ function planGitignoreStatus(projectRoot, sourceRoot, sourcePath, targetPath) {
650
651
  if (!existsSync(targetPath)) {
651
652
  return 'create';
652
653
  }
653
- const mergedContent = mergeGitignoreContent(readUtf8FileInsideWithoutSymlinks(projectRoot, targetPath), readFileSync(sourcePath, 'utf8'));
654
+ const mergedContent = mergeGitignoreContent(readUtf8FileInsideWithoutSymlinks(projectRoot, targetPath), readTemplateSourceText(sourceRoot, sourcePath));
654
655
  return mergedContent === readUtf8FileInsideWithoutSymlinks(projectRoot, targetPath) ? 'unchanged' : 'merge';
655
656
  }
656
657
  function renderPlanVerb(status) {
@@ -699,10 +700,11 @@ function buildPlannedFiles(template, selectedLocale, targetRoot, options) {
699
700
  return {
700
701
  relativePath: source.relativePath,
701
702
  sourcePath: source.sourcePath,
703
+ sourceRoot: template.templateRoot,
702
704
  sourceKind: source.sourceKind,
703
705
  content: source.content,
704
706
  targetPath,
705
- status: planStatus(targetRoot, source, targetPath, options),
707
+ status: planStatus(targetRoot, template.templateRoot, source, targetPath, options),
706
708
  lock: true,
707
709
  };
708
710
  });
@@ -714,9 +716,10 @@ function buildPlannedFiles(template, selectedLocale, targetRoot, options) {
714
716
  plannedFiles.push({
715
717
  relativePath: GITIGNORE_RELATIVE_PATH,
716
718
  sourcePath,
719
+ sourceRoot: template.templateRoot,
717
720
  sourceKind: 'common',
718
721
  targetPath,
719
- status: planGitignoreStatus(targetRoot, sourcePath, targetPath),
722
+ status: planGitignoreStatus(targetRoot, template.templateRoot, sourcePath, targetPath),
720
723
  lock: false,
721
724
  });
722
725
  return plannedFiles;
@@ -730,10 +733,7 @@ function backupConflictingFiles(projectRoot, conflicts) {
730
733
  for (const conflict of conflicts) {
731
734
  const backupPath = path.join(backupRoot, conflict.relativePath);
732
735
  ensureInside(backupRoot, backupPath);
733
- ensureFileTargetInsideWithoutSymlinks(projectRoot, conflict.targetPath);
734
- ensureFileTargetInsideWithoutSymlinks(projectRoot, backupPath, { allowMissingLeaf: true });
735
- mkdirSync(path.dirname(backupPath), { recursive: true });
736
- copyFileSync(conflict.targetPath, backupPath);
736
+ copyFileInsideWithoutSymlinks(projectRoot, conflict.targetPath, projectRoot, backupPath);
737
737
  }
738
738
  return backupRoot;
739
739
  }
@@ -960,7 +960,7 @@ export async function runInit(args, reporter, lang = 'en') {
960
960
  if (file.status === 'merge') {
961
961
  ensureFileTargetInsideWithoutSymlinks(targetRoot, file.targetPath);
962
962
  const mergedContent = file.relativePath === GITIGNORE_RELATIVE_PATH
963
- ? mergeGitignoreContent(readUtf8FileInsideWithoutSymlinks(targetRoot, file.targetPath), readFileSync(file.sourcePath, 'utf8'))
963
+ ? mergeGitignoreContent(readUtf8FileInsideWithoutSymlinks(targetRoot, file.targetPath), readTemplateSourceText(file.sourceRoot, file.sourcePath))
964
964
  : mergeAgentsContent(readUtf8FileInsideWithoutSymlinks(targetRoot, file.targetPath), selectedLocale);
965
965
  writeUtf8FileInsideWithoutSymlinks(targetRoot, file.targetPath, mergedContent);
966
966
  merged += 1;
@@ -5,12 +5,11 @@ import { readCommandContract, readMustflowConfigIfExists } from '../../core/conf
5
5
  import { resolveRunReceiptRetentionPolicy } from '../../core/retention-policy.js';
6
6
  import { t } from '../lib/i18n.js';
7
7
  import { resolveMustflowRoot } from '../lib/project-root.js';
8
- import { createRunPlan, createRunPreview, isMustflowBuiltinIntent, renderRunPreviewText, } from '../lib/run-plan.js';
8
+ import { createRunPlan, createRunPreview, renderRunPreviewText, } from '../lib/run-plan.js';
9
9
  import { writeRunReceipt, } from '../../core/run-receipt.js';
10
10
  import { recordRunPerformanceHistory } from '../../core/run-performance-history.js';
11
11
  import { RunProfiler } from '../../core/run-profile.js';
12
12
  import { finishRunWriteTracking, startRunWriteTracking } from '../../core/run-write-drift.js';
13
- import { runBuiltinArgvInProcess } from './run/builtin-dispatch.js';
14
13
  import { getRunStatus, runArgvCommandStreaming, runShellCommandStreaming } from './run/executor.js';
15
14
  import { emitOutput, isOutputLimitExceededError } from './run/output.js';
16
15
  import { createPendingTimeoutTermination, getKillMethod, terminateProcessTree } from './run/process-tree.js';
@@ -179,12 +178,6 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
179
178
  const childStartedAtMs = performance.now();
180
179
  const startedAt = new Date();
181
180
  const result = await profiler.measureAsync('child_command', async () => {
182
- if (plan.commandArgv && isMustflowBuiltinIntent(plan.intent)) {
183
- const builtinResult = await runBuiltinArgvInProcess(plan.commandArgv, plan.cwd, lang);
184
- if (builtinResult) {
185
- return builtinResult;
186
- }
187
- }
188
181
  if (plan.commandArgv) {
189
182
  streamedOutput = !json;
190
183
  return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { existsSync } from 'node:fs';
3
3
  import path from 'node:path';
4
- import { ensureFileTargetInsideWithoutSymlinks, ensureInside, writeUtf8FileInsideWithoutSymlinks, } from '../lib/filesystem.js';
4
+ import { copyFileInsideWithoutSymlinks, ensureFileTargetInsideWithoutSymlinks, ensureInside, writeUtf8FileInsideWithoutSymlinks, } from '../lib/filesystem.js';
5
5
  import { MANIFEST_LOCK_RELATIVE_PATH, readManifestLock, sha256File } from '../lib/manifest-lock.js';
6
6
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
7
7
  import { t } from '../lib/i18n.js';
@@ -70,14 +70,12 @@ function lockedTemplateSkillNames(files) {
70
70
  function getInstalledTemplateFiles(projectRoot, template, lock) {
71
71
  return getTemplateFiles(template, lock.templateLocale ?? template.manifest.defaultLocale, lock.templateProfile ?? template.manifest.defaultProfile, { extraSkillNames: lockedTemplateSkillNames(lock.files) });
72
72
  }
73
- function writeTemplateFile(projectRoot, source, targetPath) {
73
+ function writeTemplateFile(projectRoot, templateRoot, source, targetPath) {
74
74
  if (source.content !== undefined) {
75
75
  writeUtf8FileInsideWithoutSymlinks(projectRoot, targetPath, source.content);
76
76
  return;
77
77
  }
78
- ensureFileTargetInsideWithoutSymlinks(projectRoot, targetPath, { allowMissingLeaf: true });
79
- mkdirSync(path.dirname(targetPath), { recursive: true });
80
- copyFileSync(source.sourcePath, targetPath);
78
+ copyFileInsideWithoutSymlinks(templateRoot, source.sourcePath, projectRoot, targetPath);
81
79
  }
82
80
  function templateTargetSafetyIssue(projectRoot, targetPath, allowMissingLeaf) {
83
81
  try {
@@ -223,7 +221,7 @@ function copyTemplateFile(projectRoot, relativePath) {
223
221
  ensureInside(template.templateRoot, source.sourcePath);
224
222
  ensureInside(projectRoot, targetPath);
225
223
  ensureFileTargetInsideWithoutSymlinks(projectRoot, targetPath, { allowMissingLeaf: true });
226
- writeTemplateFile(projectRoot, source, targetPath);
224
+ writeTemplateFile(projectRoot, template.templateRoot, source, targetPath);
227
225
  }
228
226
  function backupUpdateFiles(projectRoot, items, reporter, lang) {
229
227
  const updateItems = items.filter((item) => item.action === 'update');
@@ -237,10 +235,7 @@ function backupUpdateFiles(projectRoot, items, reporter, lang) {
237
235
  const backupPath = path.join(backupRoot, item.relativePath);
238
236
  ensureInside(projectRoot, sourcePath);
239
237
  ensureInside(backupRoot, backupPath);
240
- ensureFileTargetInsideWithoutSymlinks(projectRoot, sourcePath);
241
- ensureFileTargetInsideWithoutSymlinks(projectRoot, backupPath, { allowMissingLeaf: true });
242
- mkdirSync(path.dirname(backupPath), { recursive: true });
243
- copyFileSync(sourcePath, backupPath);
238
+ copyFileInsideWithoutSymlinks(projectRoot, sourcePath, projectRoot, backupPath);
244
239
  }
245
240
  reporter.stdout(t(lang, 'update.backup.files', {
246
241
  count: updateItems.length,
@@ -1,10 +1,11 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { isRecord } from './command-contract.js';
4
+ import { readUtf8FileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks } from './filesystem.js';
4
5
  import { isLocaleTag } from './locale-tags.js';
5
- import { markManifestLockFileCustomized } from './manifest-lock.js';
6
+ import { ensureManifestLockTargetSafe, markManifestLockFileCustomized } from './manifest-lock.js';
6
7
  import { COMMIT_MESSAGE_STYLES, TEST_AUTHORING_POLICIES } from './preferences-options.js';
7
- import { readTomlFile } from './toml.js';
8
+ import { parseTomlText } from './toml.js';
8
9
  const PREFERENCES_RELATIVE_PATH = '.mustflow/config/preferences.toml';
9
10
  export const DASHBOARD_PREFERENCE_SETTINGS = [
10
11
  {
@@ -281,7 +282,7 @@ export function readDashboardPreferences(projectRoot) {
281
282
  if (!existsSync(preferencesPath)) {
282
283
  throw new Error('Missing .mustflow/config/preferences.toml. Run mf init first or switch to a mustflow root.');
283
284
  }
284
- const parsed = readTomlFile(preferencesPath);
285
+ const parsed = parseTomlText(readUtf8FileInsideWithoutSymlinks(projectRoot, preferencesPath));
285
286
  if (!isRecord(parsed)) {
286
287
  throw new Error('.mustflow/config/preferences.toml must contain a TOML table.');
287
288
  }
@@ -390,7 +391,7 @@ function coerceUpdateValue(definition, value) {
390
391
  export function updateDashboardPreferences(projectRoot, updates) {
391
392
  const preferencesPath = getPreferencesPath(projectRoot);
392
393
  const definitionsById = new Map(DASHBOARD_PREFERENCE_SETTINGS.map((definition) => [definition.id, definition]));
393
- let content = readFileSync(preferencesPath, 'utf8');
394
+ let content = readUtf8FileInsideWithoutSymlinks(projectRoot, preferencesPath);
394
395
  for (const update of updates) {
395
396
  const definition = definitionsById.get(update.id);
396
397
  if (!definition) {
@@ -399,7 +400,8 @@ export function updateDashboardPreferences(projectRoot, updates) {
399
400
  const value = coerceUpdateValue(definition, update.value);
400
401
  content = setTomlScalar(content, definition.path, value);
401
402
  }
402
- writeFileSync(preferencesPath, content);
403
+ ensureManifestLockTargetSafe(projectRoot);
404
+ writeUtf8FileInsideWithoutSymlinks(projectRoot, preferencesPath, content);
403
405
  markManifestLockFileCustomized(projectRoot, PREFERENCES_RELATIVE_PATH);
404
406
  return readDashboardPreferences(projectRoot);
405
407
  }
@@ -48,11 +48,14 @@ export function ensureInsideWithoutSymlinks(parentPath, childPath, options = {})
48
48
  }
49
49
  }
50
50
  export function readUtf8FileInsideWithoutSymlinks(parentPath, childPath) {
51
+ return readFileInsideWithoutSymlinks(parentPath, childPath).toString('utf8');
52
+ }
53
+ export function readFileInsideWithoutSymlinks(parentPath, childPath) {
51
54
  const absoluteChildPath = path.resolve(childPath);
52
55
  ensureInsideWithoutSymlinks(parentPath, absoluteChildPath);
53
56
  const fileDescriptor = openSync(absoluteChildPath, constants.O_RDONLY | NOFOLLOW_FLAG);
54
57
  try {
55
- return readFileSync(fileDescriptor, 'utf8');
58
+ return readFileSync(fileDescriptor);
56
59
  }
57
60
  finally {
58
61
  closeSync(fileDescriptor);
@@ -79,6 +82,9 @@ export function ensureFileTargetInsideWithoutSymlinks(parentPath, childPath, opt
79
82
  }
80
83
  }
81
84
  export function writeUtf8FileInsideWithoutSymlinks(parentPath, childPath, content) {
85
+ writeFileInsideWithoutSymlinks(parentPath, childPath, content);
86
+ }
87
+ export function writeFileInsideWithoutSymlinks(parentPath, childPath, content) {
82
88
  const absoluteChildPath = path.resolve(childPath);
83
89
  const directoryPath = path.dirname(absoluteChildPath);
84
90
  ensureInsideWithoutSymlinks(parentPath, directoryPath, { allowMissingLeaf: true });
@@ -92,6 +98,10 @@ export function writeUtf8FileInsideWithoutSymlinks(parentPath, childPath, conten
92
98
  closeSync(fileDescriptor);
93
99
  }
94
100
  }
101
+ export function copyFileInsideWithoutSymlinks(sourceParentPath, sourcePath, targetParentPath, targetPath) {
102
+ const content = readFileInsideWithoutSymlinks(sourceParentPath, sourcePath);
103
+ writeFileInsideWithoutSymlinks(targetParentPath, targetPath, content);
104
+ }
95
105
  export function copyFileIfMissing(sourcePath, targetPath, relativePath) {
96
106
  if (existsSync(targetPath)) {
97
107
  return { status: 'skipped', relativePath };
@@ -5,6 +5,7 @@ import { isRecord, readCommandContract, readString, readStringArray } from '../c
5
5
  import { listFilesRecursive, toPosixPath } from '../filesystem.js';
6
6
  import { readTomlFile } from '../toml.js';
7
7
  import { collectSourceAnchorIndexRecords, hasHighRiskSourceAnchorRiskTags, } from '../../../core/source-anchor-status.js';
8
+ import { listSourceAnchorFiles } from '../../../core/source-anchors.js';
8
9
  import { normalizeCommandEffects } from '../../../core/command-effects.js';
9
10
  import { listChangeClassificationRuleDescriptors } from '../../../core/change-classification.js';
10
11
  import { DEFAULT_DATABASE_RELATIVE_PATH, DEFAULT_PROMPT_CACHE_STABLE_READ, DEFAULT_PROMPT_CACHE_TASK_SOURCES, DEFAULT_PROMPT_CACHE_VOLATILE_SOURCES, INDEX_CONFIG_RELATIVE_PATH, LOCAL_INDEX_CONTENT_MODE, LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS, LOCAL_INDEX_PARSER_VERSION, LOCAL_INDEX_SCHEMA_VERSION, LOCAL_INDEX_STORE_FULL_CONTENT, LATEST_RUN_STATE_RELATIVE_PATH, MAX_SEARCH_MATCH_SNIPPET_CHARS, MAX_SNIPPET_BYTES_PER_DOCUMENT, MUSTFLOW_RELATIVE_PATH, SEARCH_BACKEND_FTS5, SEARCH_BACKEND_TABLE_SCAN, SEARCH_MATCH_CONTEXT_AFTER_CHARS, SEARCH_MATCH_CONTEXT_BEFORE_CHARS, SEARCH_MATCH_TRUNCATION_MARKER, SEARCH_NGRAM_MAX_GRAMS_PER_TARGET, SEARCH_NGRAM_MAX_LENGTH, SEARCH_NGRAM_MAX_TOKEN_CHARS, SEARCH_NGRAM_MIN_LENGTH, SOURCE_INDEX_MAX_FILE_BYTES, TEST_DISABLE_FTS5_ENV, } from './constants.js';
@@ -308,12 +309,13 @@ function readIndexedFileMetadataRecord(projectRoot, relativePath, sourceScope) {
308
309
  mtimeMs: Math.round(stats.mtimeMs),
309
310
  };
310
311
  }
311
- function collectIndexedFileRecords(projectRoot, documents, sourceAnchors) {
312
+ function collectIndexedFileRecords(projectRoot, documents, sourceAnchors, sourceAnchorCandidatePaths = []) {
312
313
  const records = new Map();
313
314
  for (const document of documents) {
314
315
  records.set(document.path, readIndexedFileRecord(projectRoot, document.path, 'workflow', document.contentHash));
315
316
  }
316
- for (const anchorPath of [...new Set(sourceAnchors.map((anchor) => anchor.path))].sort((left, right) => left.localeCompare(right))) {
317
+ const sourcePaths = new Set([...sourceAnchorCandidatePaths, ...sourceAnchors.map((anchor) => anchor.path)]);
318
+ for (const anchorPath of [...sourcePaths].sort((left, right) => left.localeCompare(right))) {
317
319
  if (!records.has(anchorPath)) {
318
320
  records.set(anchorPath, readIndexedFileRecord(projectRoot, anchorPath, 'source_anchor'));
319
321
  }
@@ -323,15 +325,33 @@ function collectIndexedFileRecords(projectRoot, documents, sourceAnchors) {
323
325
  }
324
326
  return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
325
327
  }
326
- function collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource) {
328
+ function collectSourceAnchorCandidatePaths(projectRoot, sourceConfig) {
329
+ return listSourceAnchorFiles(projectRoot, {
330
+ ...sourceConfig,
331
+ excludeGeneratedOrVendor: true,
332
+ });
333
+ }
334
+ function collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource, sourceConfig) {
335
+ const records = new Map();
336
+ for (const relativePath of getExistingIndexablePaths(projectRoot)) {
337
+ records.set(relativePath, readIndexedFileMetadataRecord(projectRoot, relativePath, 'workflow'));
338
+ }
327
339
  if (includeSource) {
328
- return null;
340
+ try {
341
+ for (const sourcePath of collectSourceAnchorCandidatePaths(projectRoot, sourceConfig)) {
342
+ if (!records.has(sourcePath)) {
343
+ records.set(sourcePath, readIndexedFileMetadataRecord(projectRoot, sourcePath, 'source_anchor'));
344
+ }
345
+ }
346
+ }
347
+ catch {
348
+ return null;
349
+ }
329
350
  }
330
- const records = getExistingIndexablePaths(projectRoot).map((relativePath) => readIndexedFileMetadataRecord(projectRoot, relativePath, 'workflow'));
331
351
  if (existsSync(path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/')))) {
332
- records.push(readIndexedFileMetadataRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
352
+ records.set(LATEST_RUN_STATE_RELATIVE_PATH, readIndexedFileMetadataRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
333
353
  }
334
- return records.sort((left, right) => left.path.localeCompare(right.path));
354
+ return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
335
355
  }
336
356
  function normalizeSearchText(value) {
337
357
  return value.trim().replace(/\s+/g, ' ');
@@ -2170,7 +2190,7 @@ export async function createLocalIndex(projectRoot, options = {}) {
2170
2190
  capabilities = detectLocalSearchCapabilities(capabilityDatabase);
2171
2191
  capabilityDatabase.close();
2172
2192
  if (incremental) {
2173
- const preflightFiles = collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource);
2193
+ const preflightFiles = collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource, sourceConfig);
2174
2194
  const preflightReuse = await readIncrementalPreflightReuse(SQL, databasePath, projectRoot, preflightFiles, sourceScopeHash, dryRun, indexMode);
2175
2195
  if (preflightReuse.result) {
2176
2196
  return preflightReuse.result;
@@ -2184,6 +2204,7 @@ export async function createLocalIndex(projectRoot, options = {}) {
2184
2204
  const previousSourceAnchors = includeSource
2185
2205
  ? await readPreviousSourceAnchorSnapshots(databasePath).catch(() => [])
2186
2206
  : [];
2207
+ const sourceAnchorCandidatePaths = includeSource ? collectSourceAnchorCandidatePaths(projectRoot, sourceConfig) : [];
2187
2208
  const sourceAnchors = includeSource
2188
2209
  ? collectSourceAnchorIndexRecords(projectRoot, previousSourceAnchors, {
2189
2210
  ...sourceConfig,
@@ -2191,7 +2212,7 @@ export async function createLocalIndex(projectRoot, options = {}) {
2191
2212
  })
2192
2213
  : [];
2193
2214
  const verificationEvidence = createVerificationEvidenceIndex(projectRoot);
2194
- const indexedFiles = collectIndexedFileRecords(projectRoot, documents, sourceAnchors);
2215
+ const indexedFiles = collectIndexedFileRecords(projectRoot, documents, sourceAnchors, sourceAnchorCandidatePaths);
2195
2216
  if (incremental) {
2196
2217
  const reuseDecision = await readIncrementalReuseDecision(SQL, databasePath, indexedFiles, sourceScopeHash);
2197
2218
  reusedExisting = reuseDecision.reusable;
@@ -1,8 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { existsSync } from 'node:fs';
3
3
  import path from 'node:path';
4
- import { ensureInside } from './filesystem.js';
5
- import { readTomlFile, stringifyToml } from './toml.js';
4
+ import { ensureFileTargetInsideWithoutSymlinks, ensureInside, readFileInsideWithoutSymlinks, readUtf8FileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks, } from './filesystem.js';
5
+ import { parseTomlText, stringifyToml } from './toml.js';
6
6
  export const MANIFEST_LOCK_RELATIVE_PATH = '.mustflow/config/manifest.lock.toml';
7
7
  function isRecord(value) {
8
8
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -48,20 +48,31 @@ function parseManifestLock(raw) {
48
48
  };
49
49
  }
50
50
  export function sha256File(filePath) {
51
- return `sha256:${createHash('sha256').update(readFileSync(filePath)).digest('hex')}`;
51
+ return `sha256:${createHash('sha256')
52
+ .update(readFileInsideWithoutSymlinks(path.dirname(filePath), filePath))
53
+ .digest('hex')}`;
54
+ }
55
+ function sha256ProjectFile(projectRoot, filePath) {
56
+ return `sha256:${createHash('sha256').update(readFileInsideWithoutSymlinks(projectRoot, filePath)).digest('hex')}`;
57
+ }
58
+ export function ensureManifestLockTargetSafe(projectRoot) {
59
+ const lockPath = path.join(projectRoot, MANIFEST_LOCK_RELATIVE_PATH);
60
+ ensureInside(projectRoot, lockPath);
61
+ ensureFileTargetInsideWithoutSymlinks(projectRoot, lockPath, { allowMissingLeaf: true });
62
+ return existsSync(lockPath);
52
63
  }
53
64
  export function markManifestLockFileCustomized(projectRoot, relativePath) {
54
65
  const lockPath = path.join(projectRoot, MANIFEST_LOCK_RELATIVE_PATH);
55
66
  const filePath = path.join(projectRoot, relativePath);
56
- ensureInside(projectRoot, lockPath);
57
67
  ensureInside(projectRoot, filePath);
58
- if (!existsSync(lockPath)) {
68
+ if (!ensureManifestLockTargetSafe(projectRoot)) {
59
69
  return false;
60
70
  }
71
+ ensureFileTargetInsideWithoutSymlinks(projectRoot, filePath, { allowMissingLeaf: true });
61
72
  if (!existsSync(filePath)) {
62
73
  throw new Error(`Cannot refresh manifest lock for missing file: ${relativePath}`);
63
74
  }
64
- const parsed = readTomlFile(lockPath);
75
+ const parsed = parseTomlText(readUtf8FileInsideWithoutSymlinks(projectRoot, lockPath));
65
76
  if (!isRecord(parsed)) {
66
77
  throw new Error(`Invalid manifest lock: ${MANIFEST_LOCK_RELATIVE_PATH} must contain a TOML table`);
67
78
  }
@@ -71,20 +82,27 @@ export function markManifestLockFileCustomized(projectRoot, relativePath) {
71
82
  filesTable[relativePath] = {
72
83
  source: typeof existingTable.source === 'string' ? existingTable.source : 'template_common',
73
84
  last_action: 'customized',
74
- content_hash: sha256File(filePath),
85
+ content_hash: sha256ProjectFile(projectRoot, filePath),
75
86
  };
76
87
  parsed.files = filesTable;
77
- writeFileSync(lockPath, stringifyToml(parsed));
88
+ writeUtf8FileInsideWithoutSymlinks(projectRoot, lockPath, stringifyToml(parsed));
78
89
  return true;
79
90
  }
80
91
  export function readManifestLock(projectRoot) {
81
92
  const lockPath = path.join(projectRoot, MANIFEST_LOCK_RELATIVE_PATH);
82
- ensureInside(projectRoot, lockPath);
93
+ try {
94
+ ensureInside(projectRoot, lockPath);
95
+ ensureFileTargetInsideWithoutSymlinks(projectRoot, lockPath, { allowMissingLeaf: true });
96
+ }
97
+ catch (error) {
98
+ const message = error instanceof Error ? error.message : String(error);
99
+ return { kind: 'invalid', lockPath, message };
100
+ }
83
101
  if (!existsSync(lockPath)) {
84
102
  return { kind: 'missing', lockPath };
85
103
  }
86
104
  try {
87
- return { kind: 'present', lockPath, lock: parseManifestLock(readTomlFile(lockPath)) };
105
+ return { kind: 'present', lockPath, lock: parseManifestLock(parseTomlText(readUtf8FileInsideWithoutSymlinks(projectRoot, lockPath))) };
88
106
  }
89
107
  catch (error) {
90
108
  const message = error instanceof Error ? error.message : String(error);
@@ -121,7 +139,15 @@ export function inspectManifestLock(projectRoot) {
121
139
  issues.push(`Locked file missing: ${lockedFile.relativePath}`);
122
140
  continue;
123
141
  }
124
- const actualHash = sha256File(filePath);
142
+ let actualHash;
143
+ try {
144
+ actualHash = sha256ProjectFile(projectRoot, filePath);
145
+ }
146
+ catch (error) {
147
+ const message = error instanceof Error ? error.message : String(error);
148
+ issues.push(`Locked file cannot be read safely: ${lockedFile.relativePath}: ${message}`);
149
+ continue;
150
+ }
125
151
  if (actualHash !== lockedFile.contentHash) {
126
152
  changedFiles.push(lockedFile.relativePath);
127
153
  issues.push(`Lock hash mismatch: ${lockedFile.relativePath}`);
@@ -1,20 +1,4 @@
1
1
  const MUSTFLOW_BIN_NAMES = new Set(['mf', 'mustflow']);
2
- const IN_PROCESS_MUSTFLOW_BUILTIN_COMMANDS = new Set([
3
- 'check',
4
- 'classify',
5
- 'context',
6
- 'doctor',
7
- 'help',
8
- 'impact',
9
- 'line-endings',
10
- 'map',
11
- 'status',
12
- 'update',
13
- 'version-sources',
14
- ]);
15
2
  export function isMustflowBinName(command) {
16
3
  return MUSTFLOW_BIN_NAMES.has(command.toLowerCase());
17
4
  }
18
- export function canRunMustflowBuiltinInProcess(command) {
19
- return command !== undefined && IN_PROCESS_MUSTFLOW_BUILTIN_COMMANDS.has(command);
20
- }