mustflow 2.17.0 → 2.18.2

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 +3 -3
  2. package/dist/cli/commands/classify.js +13 -3
  3. package/dist/cli/commands/dashboard.js +2 -1
  4. package/dist/cli/commands/explain-verify.js +2 -2
  5. package/dist/cli/commands/impact.js +13 -3
  6. package/dist/cli/commands/run.js +156 -104
  7. package/dist/cli/commands/verify.js +157 -45
  8. package/dist/cli/i18n/en.js +10 -1
  9. package/dist/cli/i18n/es.js +10 -1
  10. package/dist/cli/i18n/fr.js +10 -1
  11. package/dist/cli/i18n/hi.js +10 -1
  12. package/dist/cli/i18n/ko.js +10 -1
  13. package/dist/cli/i18n/zh.js +10 -1
  14. package/dist/cli/lib/git-changes.js +25 -2
  15. package/dist/cli/lib/local-index/constants.js +4 -1
  16. package/dist/cli/lib/local-index/index.js +22 -5
  17. package/dist/cli/lib/repo-map.js +90 -30
  18. package/dist/cli/lib/run-plan.js +25 -2
  19. package/dist/cli/lib/validation/index.js +2 -1
  20. package/dist/core/atomic-state-write.js +31 -0
  21. package/dist/core/bounded-output.js +23 -1
  22. package/dist/core/check-issues.js +3 -0
  23. package/dist/core/command-contract-rules.js +104 -2
  24. package/dist/core/command-contract-validation.js +71 -9
  25. package/dist/core/command-intent-eligibility.js +9 -1
  26. package/dist/core/command-output-limits.js +5 -0
  27. package/dist/core/completion-verdict.js +2 -1
  28. package/dist/core/contract-lint.js +10 -1
  29. package/dist/core/public-json-contracts.js +1 -1
  30. package/dist/core/run-receipt.js +20 -13
  31. package/dist/core/source-anchors.js +96 -24
  32. package/dist/core/verification-evidence.js +4 -1
  33. package/package.json +1 -1
  34. package/schemas/README.md +4 -4
  35. package/schemas/change-verification-report.schema.json +2 -1
  36. package/schemas/contract-lint-report.schema.json +2 -1
  37. package/schemas/explain-report.schema.json +1 -0
  38. package/schemas/latest-run-pointer.schema.json +1 -0
  39. package/schemas/run-receipt.schema.json +26 -3
  40. package/schemas/verify-report.schema.json +2 -1
  41. package/schemas/verify-run-manifest.schema.json +2 -1
  42. package/templates/default/manifest.toml +1 -1
@@ -664,8 +664,12 @@ export const hiMessages = {
664
664
  "run.error.unsafeIntentDetail": "shell-safe इंटेंट नाम इस्तेमाल करें।",
665
665
  "run.error.blockedShellBackground": 'इंटेंट "{intent}" अवरुद्ध है। {detail}',
666
666
  "run.error.blockedShellBackgroundDetail": "Shell commands background work शुरू नहीं कर सकतीं।",
667
+ "run.error.blockedLongRunningCommand": 'इंटेंट "{intent}" अवरुद्ध है। {detail}',
668
+ "run.error.blockedLongRunningCommandDetail": "argv में finite one-shot command होना चाहिए, development server, watcher, shell wrapper, interpreter loop, या background process नहीं।",
667
669
  "run.error.cwdOutsideProject": 'कमांड "{intent}" का cwd अमान्य है: {detail}',
668
670
  "run.error.cwdOutsideProjectDetail": "Intent cwd current root के अंदर रहना चाहिए।",
671
+ "run.error.maxOutputBytes": 'कमांड "{intent}" में max_output_bytes अमान्य है। {detail}',
672
+ "run.error.maxOutputBytesDetail": "Output limit अनुमत maximum के अंदर रहनी चाहिए।",
669
673
  "run.error.conflictingPreviewModes": "--dry-run या --plan-only में से एक इस्तेमाल करें, दोनों नहीं",
670
674
  "run.error.timedOut": 'कमांड "{intent}" {seconds} सेकंड बाद time out हुई',
671
675
  "run.error.startFailed": 'कमांड "{intent}" शुरू नहीं हो सकी: {message}',
@@ -727,6 +731,7 @@ export const hiMessages = {
727
731
  "classify.source.changed": "बदली फ़ाइलें",
728
732
  "classify.source.paths": "दिए गए पथ",
729
733
  "classify.error.missingInput": "--changed या कम से कम एक पथ दें",
734
+ "classify.error.changed_files_unavailable": "git status से बदली फ़ाइलें नहीं पढ़ी जा सकीं",
730
735
  "classify.error.write_path_outside_root": "Classification report path mustflow root के अंदर रहना चाहिए",
731
736
  "impact.help.summary": "फ़ाइल बदले बिना बताएं कि बदले पथ package या template version decision मांगते हैं या नहीं.",
732
737
  "impact.help.option.changed": "git status --short --untracked-files=all से पथ पढ़ें",
@@ -741,14 +746,16 @@ export const hiMessages = {
741
746
  "impact.label.affectedVersionSources": "Affected version sources",
742
747
  "impact.label.affectedSurfaces": "Affected surfaces",
743
748
  "impact.error.missingInput": "--changed या कम से कम एक पथ दें",
749
+ "impact.error.changed_files_unavailable": "git status से बदली फ़ाइलें नहीं पढ़ी जा सकीं",
744
750
  "verify.help.summary": "required_after metadata से चुने गए configured verification intents चलाएँ।",
745
751
  "verify.help.option.reason": "Verify करने के लिए required_after reason चुनें",
746
752
  "verify.help.option.fromClassification": "इस repository के अंदर mf classify report से verification reasons पढ़ें",
747
- "verify.help.option.fromPlan": "--from-classification का compatibility alias",
753
+ "verify.help.option.fromPlan": "--from-classification का deprecated compatibility alias; input अब भी mf classify report होना चाहिए",
748
754
  "verify.help.option.changed": "Current Git changes classify करके matching reasons verify करें",
749
755
  "verify.help.option.writePlan": "Changed-file classification report लिखने वाला compatibility option",
750
756
  "verify.help.option.reproEvidence": "Repository-local JSON summary से structured bug reproduction evidence पढ़ें",
751
757
  "verify.help.option.externalEvidence": "Repository-local JSON summary से lower-authority external CI evidence पढ़ें",
758
+ "verify.help.option.parallel": "Safe और non-conflicting schedule batches को इतने commands तक साथ चलाएं; default 1 है",
752
759
  "verify.help.option.planOnly": "Commands चलाए बिना verification plan print करें; --json चाहिए",
753
760
  "verify.help.exit.ok": "सभी selected verification intents pass हुए",
754
761
  "verify.help.exit.fail": "Verification fail हुआ, partial रहा, blocked रहा, या input invalid था",
@@ -763,11 +770,13 @@ export const hiMessages = {
763
770
  "verify.error.planOnlyJson": "--plan-only के लिए --json चाहिए",
764
771
  "verify.error.reproEvidenceRequiresRun": "--repro-evidence को --plan-only के साथ इस्तेमाल नहीं किया जा सकता",
765
772
  "verify.error.externalEvidenceRequiresRun": "--external-evidence को --plan-only के साथ इस्तेमाल नहीं किया जा सकता",
773
+ "verify.error.invalidParallel": "--parallel positive integer होना चाहिए",
766
774
  "verify.error.invalid_plan_file": "Classification report readable JSON file होना चाहिए",
767
775
  "verify.error.unsupported_plan_source": "Verification input mf classify report होना चाहिए",
768
776
  "verify.error.plan_root_mismatch": "Classification report इसी mustflow root से आना चाहिए",
769
777
  "verify.error.missing_plan_reasons": "Classification report में summary.validationReasons होना चाहिए",
770
778
  "verify.error.plan_path_outside_root": "Classification report path mustflow root के अंदर रहना चाहिए",
779
+ "verify.error.changed_files_unavailable": "git status से बदली फ़ाइलें नहीं पढ़ी जा सकीं",
771
780
  "verify.error.invalid_repro_evidence_file": "Repro evidence structured evidence fields वाला readable JSON summary होना चाहिए",
772
781
  "verify.error.unsupported_repro_evidence_source": "Repro evidence input को command repro-evidence इस्तेमाल करना चाहिए",
773
782
  "verify.error.invalid_external_evidence_file": "External evidence checks वाला readable JSON summary होना चाहिए",
@@ -664,8 +664,12 @@ export const koMessages = {
664
664
  "run.error.unsafeIntentDetail": "셸에서 안전한 명령 의도 이름을 사용하세요.",
665
665
  "run.error.blockedShellBackground": '명령 의도 "{intent}"가 차단되었습니다. {detail}',
666
666
  "run.error.blockedShellBackgroundDetail": "셸 명령은 백그라운드 작업을 시작하면 안 됩니다.",
667
+ "run.error.blockedLongRunningCommand": '명령 의도 "{intent}"가 차단되었습니다. {detail}',
668
+ "run.error.blockedLongRunningCommandDetail": "argv는 개발 서버, 감시 명령, 셸 래퍼, 인터프리터 반복 작업, 백그라운드 프로세스가 아니라 끝나는 단발성 명령이어야 합니다.",
667
669
  "run.error.cwdOutsideProject": '명령 "{intent}"의 실행 위치(cwd)가 올바르지 않습니다: {detail}',
668
670
  "run.error.cwdOutsideProjectDetail": "명령 실행 위치(cwd)는 현재 루트 안에 있어야 합니다.",
671
+ "run.error.maxOutputBytes": '명령 "{intent}"의 max_output_bytes 값이 올바르지 않습니다. {detail}',
672
+ "run.error.maxOutputBytesDetail": "출력 상한은 허용된 최댓값 안에 있어야 합니다.",
669
673
  "run.error.conflictingPreviewModes": "--dry-run과 --plan-only 중 하나만 사용하세요",
670
674
  "run.error.timedOut": '명령 "{intent}"가 {seconds}초 뒤 시간 초과되었습니다',
671
675
  "run.error.startFailed": '명령 "{intent}"를 시작하지 못했습니다: {message}',
@@ -727,6 +731,7 @@ export const koMessages = {
727
731
  "classify.source.changed": "변경 파일",
728
732
  "classify.source.paths": "지정한 경로",
729
733
  "classify.error.missingInput": "--changed 또는 하나 이상의 경로를 지정하세요",
734
+ "classify.error.changed_files_unavailable": "git status로 변경 파일을 확인할 수 없습니다",
730
735
  "classify.error.write_path_outside_root": "분류 보고서 경로는 mustflow 루트 안에 있어야 합니다",
731
736
  "impact.help.summary": "파일을 수정하지 않고 변경 경로가 패키지나 템플릿 버전 결정을 요구하는지 보고합니다.",
732
737
  "impact.help.option.changed": "git status --short --untracked-files=all에서 경로를 읽습니다",
@@ -741,14 +746,16 @@ export const koMessages = {
741
746
  "impact.label.affectedVersionSources": "영향받은 버전 기준 원본",
742
747
  "impact.label.affectedSurfaces": "영향받은 공개 표면",
743
748
  "impact.error.missingInput": "--changed 또는 하나 이상의 경로를 지정하세요",
749
+ "impact.error.changed_files_unavailable": "git status로 변경 파일을 확인할 수 없습니다",
744
750
  "verify.help.summary": "required_after 메타데이터로 선택된 설정된 검증 의도를 실행합니다.",
745
751
  "verify.help.option.reason": "검증할 required_after 이유를 지정합니다",
746
752
  "verify.help.option.fromClassification": "이 저장소 안의 mf classify 보고서에서 검증 이유를 읽습니다",
747
- "verify.help.option.fromPlan": "--from-classification과 같은 호환 옵션입니다",
753
+ "verify.help.option.fromPlan": "--from-classification과 같은 폐기 예정 호환 옵션입니다. 입력은 여전히 mf classify 보고서여야 합니다",
748
754
  "verify.help.option.changed": "현재 Git 변경을 분류하고 맞는 검증 이유를 실행합니다",
749
755
  "verify.help.option.writePlan": "변경 파일 분류 보고서를 쓰는 호환 옵션입니다",
750
756
  "verify.help.option.reproEvidence": "저장소 안의 JSON 요약에서 구조화된 버그 재현 증거를 읽습니다",
751
757
  "verify.help.option.externalEvidence": "저장소 안의 JSON 요약에서 낮은 권한의 외부 CI 증거를 읽습니다",
758
+ "verify.help.option.parallel": "안전하고 서로 충돌하지 않는 예정 실행 묶음을 이 개수까지 함께 실행합니다. 기본값은 1입니다",
752
759
  "verify.help.option.planOnly": "명령을 실행하지 않고 검증 계획만 출력합니다. --json이 필요합니다",
753
760
  "verify.help.exit.ok": "선택된 모든 검증 의도가 통과했습니다",
754
761
  "verify.help.exit.fail": "검증이 실패했거나, 일부만 실행됐거나, 막혔거나, 입력이 올바르지 않습니다",
@@ -763,11 +770,13 @@ export const koMessages = {
763
770
  "verify.error.planOnlyJson": "--plan-only에는 --json이 필요합니다",
764
771
  "verify.error.reproEvidenceRequiresRun": "--repro-evidence는 --plan-only와 함께 사용할 수 없습니다",
765
772
  "verify.error.externalEvidenceRequiresRun": "--external-evidence는 --plan-only와 함께 사용할 수 없습니다",
773
+ "verify.error.invalidParallel": "--parallel 값은 양의 정수여야 합니다",
766
774
  "verify.error.invalid_plan_file": "분류 보고서는 읽을 수 있는 JSON 파일이어야 합니다",
767
775
  "verify.error.unsupported_plan_source": "검증 입력은 mf classify 보고서여야 합니다",
768
776
  "verify.error.plan_root_mismatch": "분류 보고서는 현재 mustflow 루트에서 나온 것이어야 합니다",
769
777
  "verify.error.missing_plan_reasons": "분류 보고서에는 summary.validationReasons가 있어야 합니다",
770
778
  "verify.error.plan_path_outside_root": "분류 보고서 경로는 mustflow 루트 안에 있어야 합니다",
779
+ "verify.error.changed_files_unavailable": "git status로 변경 파일을 확인할 수 없습니다",
771
780
  "verify.error.invalid_repro_evidence_file": "재현 증거는 구조화된 증거 필드를 포함한 읽을 수 있는 JSON 요약이어야 합니다",
772
781
  "verify.error.unsupported_repro_evidence_source": "재현 증거 입력은 command repro-evidence를 사용해야 합니다",
773
782
  "verify.error.invalid_external_evidence_file": "외부 증거는 checks를 포함한 읽을 수 있는 JSON 요약이어야 합니다",
@@ -664,8 +664,12 @@ export const zhMessages = {
664
664
  "run.error.unsafeIntentDetail": "请使用 shell 安全的意图名称。",
665
665
  "run.error.blockedShellBackground": '意图 "{intent}" 已被阻止。{detail}',
666
666
  "run.error.blockedShellBackgroundDetail": "Shell 命令不得启动后台工作。",
667
+ "run.error.blockedLongRunningCommand": '意图 "{intent}" 已被阻止。{detail}',
668
+ "run.error.blockedLongRunningCommandDetail": "argv 必须描述会结束的单次命令,而不是开发服务器、监听命令、shell 包装器、解释器循环或后台进程。",
667
669
  "run.error.cwdOutsideProject": '命令 "{intent}" 的 cwd 无效:{detail}',
668
670
  "run.error.cwdOutsideProjectDetail": "意图 cwd 必须位于当前根目录内。",
671
+ "run.error.maxOutputBytes": '命令 "{intent}" 的 max_output_bytes 无效。{detail}',
672
+ "run.error.maxOutputBytesDetail": "输出限制必须保持在允许的最大值内。",
669
673
  "run.error.conflictingPreviewModes": "只能使用 --dry-run 或 --plan-only,不能同时使用",
670
674
  "run.error.timedOut": '命令 "{intent}" 在 {seconds} 秒后超时',
671
675
  "run.error.startFailed": '命令 "{intent}" 启动失败:{message}',
@@ -727,6 +731,7 @@ export const zhMessages = {
727
731
  "classify.source.changed": "变更文件",
728
732
  "classify.source.paths": "指定路径",
729
733
  "classify.error.missingInput": "请指定 --changed 或至少一个路径",
734
+ "classify.error.changed_files_unavailable": "无法通过 git status 检查变更文件",
730
735
  "classify.error.write_path_outside_root": "分类报告路径必须位于 mustflow 根目录内",
731
736
  "impact.help.summary": "在不修改文件的情况下报告变更路径是否需要包或模板版本决策。",
732
737
  "impact.help.option.changed": "从 git status --short --untracked-files=all 读取路径",
@@ -741,14 +746,16 @@ export const zhMessages = {
741
746
  "impact.label.affectedVersionSources": "受影响的版本来源",
742
747
  "impact.label.affectedSurfaces": "受影响的公开表面",
743
748
  "impact.error.missingInput": "请指定 --changed 或至少一个路径",
749
+ "impact.error.changed_files_unavailable": "无法通过 git status 检查变更文件",
744
750
  "verify.help.summary": "运行由 required_after 元数据选出的已配置验证意图。",
745
751
  "verify.help.option.reason": "选择要验证的 required_after 原因",
746
752
  "verify.help.option.fromClassification": "从此仓库内的 mf classify 报告读取验证原因",
747
- "verify.help.option.fromPlan": "--from-classification 的兼容别名",
753
+ "verify.help.option.fromPlan": "--from-classification 的已弃用兼容别名;输入仍必须是 mf classify 报告",
748
754
  "verify.help.option.changed": "分类当前 Git 变更并验证匹配的原因",
749
755
  "verify.help.option.writePlan": "写入变更文件分类报告的兼容选项",
750
756
  "verify.help.option.reproEvidence": "从仓库本地 JSON 摘要读取结构化的 bug 复现证据",
751
757
  "verify.help.option.externalEvidence": "从仓库本地 JSON 摘要读取低权限外部 CI 证据",
758
+ "verify.help.option.parallel": "最多并行执行这个数量的安全、无冲突计划批次命令;默认值为 1",
752
759
  "verify.help.option.planOnly": "仅输出验证计划,不执行命令;需要 --json",
753
760
  "verify.help.exit.ok": "选中的所有验证意图均已通过",
754
761
  "verify.help.exit.fail": "验证失败、部分完成、被阻止,或输入无效",
@@ -763,11 +770,13 @@ export const zhMessages = {
763
770
  "verify.error.planOnlyJson": "--plan-only 需要 --json",
764
771
  "verify.error.reproEvidenceRequiresRun": "--repro-evidence 不能与 --plan-only 一起使用",
765
772
  "verify.error.externalEvidenceRequiresRun": "--external-evidence 不能与 --plan-only 一起使用",
773
+ "verify.error.invalidParallel": "--parallel 必须是正整数",
766
774
  "verify.error.invalid_plan_file": "分类报告必须是可读取的 JSON 文件",
767
775
  "verify.error.unsupported_plan_source": "验证输入必须是 mf classify 报告",
768
776
  "verify.error.plan_root_mismatch": "分类报告必须来自当前 mustflow 根目录",
769
777
  "verify.error.missing_plan_reasons": "分类报告必须包含 summary.validationReasons",
770
778
  "verify.error.plan_path_outside_root": "分类报告路径必须位于 mustflow 根目录内",
779
+ "verify.error.changed_files_unavailable": "无法通过 git status 检查变更文件",
771
780
  "verify.error.invalid_repro_evidence_file": "复现证据必须是包含结构化证据字段的可读取 JSON 摘要",
772
781
  "verify.error.unsupported_repro_evidence_source": "复现证据输入必须使用 command repro-evidence",
773
782
  "verify.error.invalid_external_evidence_file": "外部证据必须是包含 checks 的可读取 JSON 摘要",
@@ -1,5 +1,13 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { parseGitStatusOutput } from '../../core/change-classification.js';
3
+ export class GitChangedFilesError extends Error {
4
+ result;
5
+ constructor(result) {
6
+ super('git_changed_files_unavailable');
7
+ this.name = 'GitChangedFilesError';
8
+ this.result = result;
9
+ }
10
+ }
3
11
  export function readGitChangedFiles(projectRoot) {
4
12
  const result = spawnSync('git', ['status', '--short', '--untracked-files=all'], {
5
13
  cwd: projectRoot,
@@ -7,7 +15,22 @@ export function readGitChangedFiles(projectRoot) {
7
15
  windowsHide: true,
8
16
  });
9
17
  if (result.status !== 0 || typeof result.stdout !== 'string') {
10
- return [];
18
+ const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
19
+ const message = result.error?.message ??
20
+ (stderr || (result.status === null ? 'git status did not complete' : `git status exited with code ${result.status}`));
21
+ return {
22
+ ok: false,
23
+ message,
24
+ status: result.status,
25
+ stderr,
26
+ };
27
+ }
28
+ return { ok: true, files: parseGitStatusOutput(result.stdout) };
29
+ }
30
+ export function requireGitChangedFiles(projectRoot) {
31
+ const result = readGitChangedFiles(projectRoot);
32
+ if (!result.ok) {
33
+ throw new GitChangedFilesError(result);
11
34
  }
12
- return parseGitStatusOutput(result.stdout);
35
+ return result.files;
13
36
  }
@@ -1,4 +1,4 @@
1
- export const LOCAL_INDEX_SCHEMA_VERSION = '19';
1
+ export const LOCAL_INDEX_SCHEMA_VERSION = '20';
2
2
  export const LOCAL_INDEX_PARSER_VERSION = '1';
3
3
  export const DEFAULT_DATABASE_RELATIVE_PATH = '.mustflow/cache/mustflow.sqlite';
4
4
  export const LATEST_RUN_STATE_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
@@ -22,6 +22,9 @@ export const SEARCH_MATCH_CONTEXT_AFTER_CHARS = 96;
22
22
  export const SEARCH_MATCH_TRUNCATION_MARKER = '...';
23
23
  export const SEARCH_NGRAM_MIN_LENGTH = 2;
24
24
  export const SEARCH_NGRAM_MAX_LENGTH = 3;
25
+ export const SEARCH_NGRAM_MAX_TOKEN_CHARS = 64;
26
+ export const SEARCH_NGRAM_MAX_GRAMS_PER_TARGET = 512;
27
+ export const SOURCE_INDEX_MAX_FILE_BYTES = 262144;
25
28
  export const SEARCH_BACKEND_FTS5 = 'fts5';
26
29
  export const SEARCH_BACKEND_TABLE_SCAN = 'table_scan';
27
30
  export const TEST_DISABLE_FTS5_ENV = 'MUSTFLOW_TEST_DISABLE_FTS5';
@@ -7,7 +7,7 @@ import { readTomlFile } from '../toml.js';
7
7
  import { collectSourceAnchorIndexRecords, hasHighRiskSourceAnchorRiskTags, } from '../../../core/source-anchor-status.js';
8
8
  import { normalizeCommandEffects } from '../../../core/command-effects.js';
9
9
  import { listChangeClassificationRuleDescriptors } from '../../../core/change-classification.js';
10
- 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_LENGTH, SEARCH_NGRAM_MIN_LENGTH, TEST_DISABLE_FTS5_ENV, } from './constants.js';
10
+ 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';
11
11
  import { loadSqlJs } from './sql.js';
12
12
  export function getLocalIndexDatabasePath(projectRoot) {
13
13
  return path.join(projectRoot, ...DEFAULT_DATABASE_RELATIVE_PATH.split('/'));
@@ -83,11 +83,12 @@ function readPositiveInteger(table, key) {
83
83
  }
84
84
  function readLocalIndexSourceConfig(projectRoot) {
85
85
  const sourceIndexTable = readNestedTable(readIndexToml(projectRoot), 'source_index');
86
+ const configuredMaxFileBytes = readPositiveInteger(sourceIndexTable, 'max_file_bytes');
86
87
  return {
87
88
  enabledByDefault: readBoolean(sourceIndexTable, 'enabled_by_default') === true,
88
89
  include: readOptionalStringArray(sourceIndexTable, 'include') ?? [],
89
90
  exclude: readOptionalStringArray(sourceIndexTable, 'exclude') ?? [],
90
- maxFileBytes: readPositiveInteger(sourceIndexTable, 'max_file_bytes'),
91
+ maxFileBytes: Math.min(configuredMaxFileBytes ?? SOURCE_INDEX_MAX_FILE_BYTES, SOURCE_INDEX_MAX_FILE_BYTES),
91
92
  allowedExtensions: readOptionalStringArray(sourceIndexTable, 'allowed_extensions') ?? [],
92
93
  };
93
94
  }
@@ -321,10 +322,14 @@ function buildSearchNgrams(values) {
321
322
  const grams = new Set();
322
323
  for (const value of values) {
323
324
  for (const token of extractSearchTokens(value)) {
324
- const maxLength = Math.min(SEARCH_NGRAM_MAX_LENGTH, token.length);
325
+ const boundedToken = token.slice(0, SEARCH_NGRAM_MAX_TOKEN_CHARS);
326
+ const maxLength = Math.min(SEARCH_NGRAM_MAX_LENGTH, boundedToken.length);
325
327
  for (let length = SEARCH_NGRAM_MIN_LENGTH; length <= maxLength; length += 1) {
326
- for (let index = 0; index <= token.length - length; index += 1) {
327
- grams.add(token.slice(index, index + length));
328
+ for (let index = 0; index <= boundedToken.length - length; index += 1) {
329
+ grams.add(boundedToken.slice(index, index + length));
330
+ if (grams.size >= SEARCH_NGRAM_MAX_GRAMS_PER_TARGET) {
331
+ return [...grams].sort((left, right) => left.localeCompare(right));
332
+ }
328
333
  }
329
334
  }
330
335
  }
@@ -1650,6 +1655,18 @@ function populateDatabase(database, capabilities, documents, skills, skillRoutes
1650
1655
  'max_snippet_bytes_per_document',
1651
1656
  String(MAX_SNIPPET_BYTES_PER_DOCUMENT),
1652
1657
  ]);
1658
+ database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', [
1659
+ 'search_ngram_max_token_chars',
1660
+ String(SEARCH_NGRAM_MAX_TOKEN_CHARS),
1661
+ ]);
1662
+ database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', [
1663
+ 'search_ngram_max_grams_per_target',
1664
+ String(SEARCH_NGRAM_MAX_GRAMS_PER_TARGET),
1665
+ ]);
1666
+ database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', [
1667
+ 'source_index_max_file_bytes',
1668
+ String(SOURCE_INDEX_MAX_FILE_BYTES),
1669
+ ]);
1653
1670
  database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', [
1654
1671
  'excluded_raw_data_kinds',
1655
1672
  LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS.join(','),
@@ -1,8 +1,8 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
- import { existsSync, readdirSync, statSync, writeFileSync } from 'node:fs';
3
+ import { existsSync, lstatSync, readdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
4
4
  import path from 'node:path';
5
- import { listFilesRecursive, toPosixPath } from './filesystem.js';
5
+ import { toPosixPath } from './filesystem.js';
6
6
  import { readTomlFile } from './toml.js';
7
7
  const DEFAULT_DEPTH = 3;
8
8
  const REPO_MAP_DOC_ID = 'repo-map';
@@ -11,6 +11,8 @@ const REPO_MAP_GENERATOR = 'mustflow';
11
11
  const REPO_MAP_RELATIVE_ROOT = '.';
12
12
  const REPO_MAP_SOURCE_POLICY = 'anchors_only';
13
13
  const REPO_MAP_PRIVACY_MODE = 'minimal';
14
+ const GIT_LS_FILES_TIMEOUT_MS = 5_000;
15
+ const GIT_LS_FILES_MAX_BUFFER_BYTES = 1_048_576;
14
16
  const EXCLUDED_SEGMENTS = new Set([
15
17
  '.astro',
16
18
  '.cache',
@@ -240,25 +242,57 @@ function getRepoMapConfig(projectRoot) {
240
242
  },
241
243
  };
242
244
  }
243
- function getGitFiles(projectRoot) {
244
- const result = spawnSync('git', ['ls-files'], {
245
+ export function listGitFilesForRepoMap(projectRoot, options = {}) {
246
+ const spawnGit = options.spawnGit ??
247
+ ((command, args, spawnOptions) => spawnSync(command, [...args], spawnOptions));
248
+ const result = spawnGit('git', ['ls-files', '-z'], {
245
249
  cwd: projectRoot,
246
250
  encoding: 'utf8',
251
+ maxBuffer: options.maxBuffer ?? GIT_LS_FILES_MAX_BUFFER_BYTES,
252
+ timeout: options.timeout ?? GIT_LS_FILES_TIMEOUT_MS,
253
+ windowsHide: true,
247
254
  });
248
255
  if (result.status !== 0 || result.error) {
249
256
  return [];
250
257
  }
251
258
  return result.stdout
252
- .split(/\r?\n/)
253
- .map((line) => line.trim())
259
+ .split('\0')
260
+ .map((line) => toPosixPath(line))
254
261
  .filter(Boolean);
255
262
  }
256
- function getRepositoryFiles(projectRoot) {
263
+ function isAnchorCandidatePath(relativePath, priorityPaths) {
264
+ return priorityPaths.has(relativePath) || Boolean(getAnchorDescription(relativePath));
265
+ }
266
+ function listAnchorCandidateFilesRecursive(rootPath, depth, priorityPaths) {
267
+ const results = [];
268
+ function visit(currentPath, directoryDepth) {
269
+ for (const entry of readdirSync(currentPath, { withFileTypes: true })) {
270
+ const entryPath = path.join(currentPath, entry.name);
271
+ const relativePath = toPosixPath(path.relative(rootPath, entryPath));
272
+ if (entry.isDirectory()) {
273
+ if (EXCLUDED_SEGMENTS.has(entry.name) || directoryDepth >= depth) {
274
+ continue;
275
+ }
276
+ visit(entryPath, directoryDepth + 1);
277
+ continue;
278
+ }
279
+ if (entry.isFile() && isAnchorCandidatePath(relativePath, priorityPaths)) {
280
+ results.push(relativePath);
281
+ }
282
+ }
283
+ }
284
+ if (!existsSync(rootPath) || !statSync(rootPath).isDirectory()) {
285
+ return [];
286
+ }
287
+ visit(rootPath, 0);
288
+ return results.sort();
289
+ }
290
+ function getRepositoryFiles(projectRoot, depth, priorityPaths) {
257
291
  const files = new Set();
258
- for (const relativePath of getGitFiles(projectRoot)) {
292
+ for (const relativePath of listGitFilesForRepoMap(projectRoot)) {
259
293
  files.add(relativePath);
260
294
  }
261
- for (const relativePath of listFilesRecursive(projectRoot, { ignoredDirectoryNames: EXCLUDED_SEGMENTS })) {
295
+ for (const relativePath of listAnchorCandidateFilesRecursive(projectRoot, depth, priorityPaths)) {
262
296
  files.add(relativePath);
263
297
  }
264
298
  return Array.from(files);
@@ -306,7 +340,7 @@ function isUnderExcludedPrefix(relativePath, excludedPrefixes) {
306
340
  return excludedPrefixes.some((prefix) => relativePath === prefix.slice(0, -1) || relativePath.startsWith(prefix));
307
341
  }
308
342
  function discoverAnchors(projectRoot, depth, priorityPaths, nestedRepositories, excludedPrefixes) {
309
- return getRepositoryFiles(projectRoot)
343
+ return getRepositoryFiles(projectRoot, depth, priorityPaths)
310
344
  .filter(shouldIncludePath)
311
345
  .filter((relativePath) => !isUnderNestedRepository(relativePath, nestedRepositories))
312
346
  .filter((relativePath) => !isUnderExcludedPrefix(relativePath, excludedPrefixes))
@@ -350,13 +384,9 @@ function renderDirectoryAnchors(anchors) {
350
384
  function hasGitMarker(directoryPath) {
351
385
  return existsSync(path.join(directoryPath, '.git'));
352
386
  }
353
- function isDirectory(directoryPath) {
354
- try {
355
- return statSync(directoryPath).isDirectory();
356
- }
357
- catch {
358
- return false;
359
- }
387
+ function isRealPathInside(parentRealPath, childRealPath) {
388
+ const relative = path.relative(parentRealPath, childRealPath);
389
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
360
390
  }
361
391
  function isSafeWorkspaceRoot(projectRoot, workspaceRoot) {
362
392
  const absoluteRoot = path.resolve(projectRoot, workspaceRoot);
@@ -371,6 +401,32 @@ function getWorkspaceRootPrefixes(projectRoot, workspaceConfig) {
371
401
  .filter((workspaceRoot) => isSafeWorkspaceRoot(projectRoot, workspaceRoot))
372
402
  .map((workspaceRoot) => `${toPosixPath(workspaceRoot).replace(/\/+$/, '')}/`);
373
403
  }
404
+ function resolveSafeDirectoryTarget(projectRootRealPath, logicalPath, followSymlinks) {
405
+ try {
406
+ const stats = lstatSync(logicalPath);
407
+ if (stats.isSymbolicLink()) {
408
+ if (!followSymlinks) {
409
+ return undefined;
410
+ }
411
+ const realPath = realpathSync(logicalPath);
412
+ if (!isRealPathInside(projectRootRealPath, realPath) || !statSync(realPath).isDirectory()) {
413
+ return undefined;
414
+ }
415
+ return { logicalPath, realPath };
416
+ }
417
+ if (!stats.isDirectory()) {
418
+ return undefined;
419
+ }
420
+ const realPath = realpathSync(logicalPath);
421
+ if (!isRealPathInside(projectRootRealPath, realPath)) {
422
+ return undefined;
423
+ }
424
+ return { logicalPath, realPath };
425
+ }
426
+ catch {
427
+ return undefined;
428
+ }
429
+ }
374
430
  function collectNestedRepository(projectRoot, repositoryPath, anchorFiles) {
375
431
  const relativeRoot = `${toPosixPath(path.relative(projectRoot, repositoryPath))}/`;
376
432
  const existingAnchors = new Set();
@@ -422,31 +478,34 @@ function discoverNestedRepositories(projectRoot, mapConfig, workspaceConfig) {
422
478
  }
423
479
  const repositories = [];
424
480
  const seenRepositoryPaths = new Set();
425
- function visit(directoryPath, depth) {
481
+ const seenDirectoryPaths = new Set();
482
+ const projectRootRealPath = realpathSync(projectRoot);
483
+ function visit(directoryTarget, depth) {
426
484
  if (repositories.length >= workspaceConfig.maxRepositories || depth > workspaceConfig.maxDepth) {
427
485
  return;
428
486
  }
429
- if (hasGitMarker(directoryPath)) {
430
- const resolvedRepositoryPath = path.resolve(directoryPath);
487
+ if (seenDirectoryPaths.has(directoryTarget.realPath)) {
488
+ return;
489
+ }
490
+ seenDirectoryPaths.add(directoryTarget.realPath);
491
+ if (hasGitMarker(directoryTarget.logicalPath)) {
492
+ const resolvedRepositoryPath = directoryTarget.realPath;
431
493
  if (!seenRepositoryPaths.has(resolvedRepositoryPath)) {
432
494
  seenRepositoryPaths.add(resolvedRepositoryPath);
433
- repositories.push(collectNestedRepository(projectRoot, resolvedRepositoryPath, mapConfig.anchorFiles));
495
+ repositories.push(collectNestedRepository(projectRoot, directoryTarget.logicalPath, mapConfig.anchorFiles));
434
496
  }
435
497
  if (workspaceConfig.stopAtRepositoryRoot) {
436
498
  return;
437
499
  }
438
500
  }
439
- for (const entry of readdirSync(directoryPath, { withFileTypes: true })) {
440
- if (!entry.isDirectory()) {
441
- continue;
442
- }
501
+ for (const entry of readdirSync(directoryTarget.logicalPath, { withFileTypes: true })) {
443
502
  if (EXCLUDED_SEGMENTS.has(entry.name)) {
444
503
  continue;
445
504
  }
446
- if (entry.isSymbolicLink() && !workspaceConfig.followSymlinks) {
447
- continue;
505
+ const childDirectoryTarget = resolveSafeDirectoryTarget(projectRootRealPath, path.join(directoryTarget.logicalPath, entry.name), workspaceConfig.followSymlinks);
506
+ if (childDirectoryTarget) {
507
+ visit(childDirectoryTarget, depth + 1);
448
508
  }
449
- visit(path.join(directoryPath, entry.name), depth + 1);
450
509
  }
451
510
  }
452
511
  for (const workspaceRoot of workspaceConfig.roots) {
@@ -454,10 +513,11 @@ function discoverNestedRepositories(projectRoot, mapConfig, workspaceConfig) {
454
513
  continue;
455
514
  }
456
515
  const absoluteWorkspaceRoot = path.resolve(projectRoot, workspaceRoot);
457
- if (!isDirectory(absoluteWorkspaceRoot)) {
516
+ const workspaceTarget = resolveSafeDirectoryTarget(projectRootRealPath, absoluteWorkspaceRoot, workspaceConfig.followSymlinks);
517
+ if (!workspaceTarget) {
458
518
  continue;
459
519
  }
460
- visit(absoluteWorkspaceRoot, 0);
520
+ visit(workspaceTarget, 0);
461
521
  }
462
522
  return repositories.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
463
523
  }
@@ -4,6 +4,7 @@ import { resolveSafeProjectCwd } from '../../core/command-cwd.js';
4
4
  import { resolveCommandEnv } from '../../core/command-env.js';
5
5
  import { evaluateCommandIntentEligibility, } from '../../core/command-intent-eligibility.js';
6
6
  import { isRecord, readPositiveInteger, readString, readStringArray, } from '../../core/config-loading.js';
7
+ import { DEFAULT_COMMAND_MAX_OUTPUT_BYTES, MAX_COMMAND_OUTPUT_BYTES, commandMaxOutputBytesLimitMessage, } from '../../core/command-output-limits.js';
7
8
  import { t } from './i18n.js';
8
9
  function getSuccessExitCodes(intent) {
9
10
  const value = intent.success_exit_codes;
@@ -72,6 +73,24 @@ function getRunPlanMode(commandArgv, intent) {
72
73
  }
73
74
  return intent.mode === 'shell' ? 'shell' : null;
74
75
  }
76
+ function readEffectiveMaxOutputBytes(contract, intent) {
77
+ return readPositiveInteger(intent, 'max_output_bytes') ??
78
+ readPositiveInteger(contract.defaults, 'max_output_bytes') ??
79
+ DEFAULT_COMMAND_MAX_OUTPUT_BYTES;
80
+ }
81
+ function getMaxOutputBytesLimitDetail(contract, intent) {
82
+ const intentValue = readPositiveInteger(intent, 'max_output_bytes');
83
+ if (intentValue !== undefined) {
84
+ return intentValue > MAX_COMMAND_OUTPUT_BYTES ?
85
+ commandMaxOutputBytesLimitMessage('[commands.intents.<intent>].max_output_bytes') :
86
+ null;
87
+ }
88
+ const defaultValue = readPositiveInteger(contract.defaults, 'max_output_bytes');
89
+ if (defaultValue !== undefined && defaultValue > MAX_COMMAND_OUTPUT_BYTES) {
90
+ return commandMaxOutputBytesLimitMessage('[commands.defaults].max_output_bytes');
91
+ }
92
+ return null;
93
+ }
75
94
  function readRunIntentMetadata(contract, intent) {
76
95
  const configuredCwd = readString(intent, 'cwd') ?? readString(contract.defaults, 'default_cwd') ?? '.';
77
96
  const commandArgv = readStringArray(intent, 'argv');
@@ -84,7 +103,7 @@ function readRunIntentMetadata(contract, intent) {
84
103
  kind: readString(intent, 'kind') ?? null,
85
104
  configuredCwd,
86
105
  timeoutSeconds: readPositiveInteger(intent, 'timeout_seconds') ?? null,
87
- maxOutputBytes: readPositiveInteger(intent, 'max_output_bytes') ?? readPositiveInteger(contract.defaults, 'max_output_bytes') ?? 1_048_576,
106
+ maxOutputBytes: readEffectiveMaxOutputBytes(contract, intent),
88
107
  successExitCodes: getSuccessExitCodes(intent),
89
108
  commandArgv,
90
109
  shellCommand,
@@ -149,6 +168,10 @@ export function createRunPlan(projectRoot, contract, intentName, options = {}) {
149
168
  return createBlockedRunPlan(contract, intentName, rawIntent, eligibility, eligibility.code, eligibility.detail);
150
169
  }
151
170
  const metadata = readRunIntentMetadata(contract, rawIntent);
171
+ const maxOutputBytesLimitDetail = getMaxOutputBytesLimitDetail(contract, rawIntent);
172
+ if (maxOutputBytesLimitDetail) {
173
+ return createBlockedRunPlan(contract, intentName, rawIntent, eligibility, 'max_output_bytes_exceeds_limit', maxOutputBytesLimitDetail);
174
+ }
152
175
  let cwd;
153
176
  try {
154
177
  cwd = resolveSafeProjectCwd(projectRoot, metadata.configuredCwd);
@@ -207,7 +230,7 @@ function createSuggestedIntentSnippet(intentName, metadata, reasonCode) {
207
230
  return null;
208
231
  }
209
232
  let commandLines;
210
- if (reasonCode === 'blocked_shell_background_pattern') {
233
+ if (reasonCode === 'blocked_shell_background_pattern' || reasonCode === 'blocked_long_running_command_pattern') {
211
234
  commandLines = [`argv = ${formatTomlStringArray(['TODO_REPLACE_WITH_FINITE_COMMAND'])}`];
212
235
  }
213
236
  else if (metadata?.shellCommand) {
@@ -605,7 +605,8 @@ function validateStrictVersionSources(projectRoot, preferencesToml, versioningTo
605
605
  pushStrictIssue(issues, '[release.versioning] is enabled but no version source was detected; add .mustflow/config/versioning.toml or a package/template version source');
606
606
  }
607
607
  function validateStrictTemplateVersionSync(projectRoot, preferencesToml, issues) {
608
- const changedPaths = existsSync(path.join(projectRoot, '.git')) ? readGitChangedFiles(projectRoot) : undefined;
608
+ const changedPathResult = existsSync(path.join(projectRoot, '.git')) ? readGitChangedFiles(projectRoot) : undefined;
609
+ const changedPaths = changedPathResult?.ok ? changedPathResult.files : undefined;
609
610
  for (const issue of validateTemplateVersionSync(projectRoot, preferencesToml, changedPaths)) {
610
611
  if (issue.severity === 'warning') {
611
612
  pushStrictWarning(issues, issue.message);
@@ -0,0 +1,31 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { mkdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ function tempFilePath(targetPath) {
5
+ const suffix = `${process.pid}-${Date.now()}-${randomBytes(6).toString('hex')}`;
6
+ return path.join(path.dirname(targetPath), `.${path.basename(targetPath)}.${suffix}.tmp`);
7
+ }
8
+ export function createStateRunId(prefix) {
9
+ const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-');
10
+ return `${prefix}-${timestamp}-${process.pid}-${randomBytes(6).toString('hex')}`;
11
+ }
12
+ export function atomicWriteTextFile(targetPath, content) {
13
+ mkdirSync(path.dirname(targetPath), { recursive: true });
14
+ const temporaryPath = tempFilePath(targetPath);
15
+ try {
16
+ writeFileSync(temporaryPath, content, { encoding: 'utf8', flag: 'wx' });
17
+ renameSync(temporaryPath, targetPath);
18
+ }
19
+ catch (error) {
20
+ try {
21
+ unlinkSync(temporaryPath);
22
+ }
23
+ catch {
24
+ // Best-effort cleanup for a temporary file that may not have been created.
25
+ }
26
+ throw error;
27
+ }
28
+ }
29
+ export function atomicWriteJsonFile(targetPath, value) {
30
+ atomicWriteTextFile(targetPath, `${JSON.stringify(value, null, 2)}\n`);
31
+ }
@@ -1,3 +1,24 @@
1
+ function isUtf8ContinuationByte(value) {
2
+ return value !== undefined && (value & 0xc0) === 0x80;
3
+ }
4
+ function findUtf8TailStart(buffer, startOffset) {
5
+ let start = Math.min(buffer.byteLength, Math.max(0, Math.trunc(startOffset)));
6
+ while (start < buffer.byteLength && isUtf8ContinuationByte(buffer[start])) {
7
+ start += 1;
8
+ }
9
+ return start;
10
+ }
11
+ export function decodeUtf8Tail(buffer, maxTailBytes) {
12
+ if (maxTailBytes <= 0) {
13
+ return { text: '', truncated: buffer.byteLength > 0 };
14
+ }
15
+ const rawStart = buffer.byteLength > maxTailBytes ? buffer.byteLength - maxTailBytes : 0;
16
+ const start = findUtf8TailStart(buffer, rawStart);
17
+ return {
18
+ text: buffer.subarray(start).toString('utf8'),
19
+ truncated: buffer.byteLength > maxTailBytes || start > 0,
20
+ };
21
+ }
1
22
  export class BoundedOutputBuffer {
2
23
  #maxTailBytes;
3
24
  #chunks = [];
@@ -30,9 +51,10 @@ export class BoundedOutputBuffer {
30
51
  }
31
52
  }
32
53
  toSnapshot() {
54
+ const tail = decodeUtf8Tail(Buffer.concat(this.#chunks, this.#tailBytes), this.#maxTailBytes);
33
55
  return {
34
56
  bytes: this.#bytes,
35
- tail: Buffer.concat(this.#chunks, this.#tailBytes).toString('utf8'),
57
+ tail: tail.text,
36
58
  };
37
59
  }
38
60
  }