mustflow 2.18.20 → 2.21.1

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 (46) hide show
  1. package/dist/cli/commands/classify.js +2 -3
  2. package/dist/cli/commands/doctor.js +46 -6
  3. package/dist/cli/commands/run/output.js +1 -1
  4. package/dist/cli/commands/run/receipt.js +1 -0
  5. package/dist/cli/commands/verify.js +52 -23
  6. package/dist/cli/i18n/en.js +1 -0
  7. package/dist/cli/i18n/es.js +1 -0
  8. package/dist/cli/i18n/fr.js +1 -0
  9. package/dist/cli/i18n/hi.js +1 -0
  10. package/dist/cli/i18n/ko.js +1 -0
  11. package/dist/cli/i18n/zh.js +1 -0
  12. package/dist/cli/lib/git-changes.js +7 -1
  13. package/dist/cli/lib/local-index/index.js +9 -30
  14. package/dist/cli/lib/repo-map.js +3 -2
  15. package/dist/cli/lib/run-plan.js +8 -4
  16. package/dist/core/change-classification.js +24 -2
  17. package/dist/core/check-issues.js +1 -1
  18. package/dist/core/command-contract-rules.js +6 -0
  19. package/dist/core/command-contract-validation.js +24 -10
  20. package/dist/core/command-output-limits.js +2 -1
  21. package/dist/core/line-endings.js +12 -4
  22. package/dist/core/repeated-failure.js +3 -3
  23. package/dist/core/run-performance-history.js +4 -4
  24. package/dist/core/run-profile.js +2 -3
  25. package/dist/core/run-receipt.js +11 -3
  26. package/dist/core/run-write-drift.js +64 -12
  27. package/dist/core/safe-filesystem.js +155 -0
  28. package/package.json +1 -1
  29. package/schemas/commands.schema.json +1 -0
  30. package/schemas/doctor-report.schema.json +23 -1
  31. package/schemas/run-receipt.schema.json +6 -2
  32. package/templates/default/i18n.toml +13 -13
  33. package/templates/default/locales/en/.mustflow/skills/INDEX.md +13 -13
  34. package/templates/default/locales/en/.mustflow/skills/adapter-boundary/SKILL.md +72 -4
  35. package/templates/default/locales/en/.mustflow/skills/command-contract-authoring/SKILL.md +16 -10
  36. package/templates/default/locales/en/.mustflow/skills/command-pattern/SKILL.md +64 -7
  37. package/templates/default/locales/en/.mustflow/skills/database-change-safety/SKILL.md +249 -16
  38. package/templates/default/locales/en/.mustflow/skills/dependency-reality-check/SKILL.md +37 -7
  39. package/templates/default/locales/en/.mustflow/skills/migration-safety-check/SKILL.md +74 -10
  40. package/templates/default/locales/en/.mustflow/skills/performance-budget-check/SKILL.md +132 -5
  41. package/templates/default/locales/en/.mustflow/skills/pure-core-imperative-shell/SKILL.md +12 -5
  42. package/templates/default/locales/en/.mustflow/skills/result-option/SKILL.md +4 -2
  43. package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +112 -29
  44. package/templates/default/locales/en/.mustflow/skills/state-machine-pattern/SKILL.md +17 -4
  45. package/templates/default/locales/en/.mustflow/skills/structure-discovery-gate/SKILL.md +193 -2
  46. package/templates/default/manifest.toml +1 -1
@@ -1,6 +1,6 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs';
2
1
  import path from 'node:path';
3
2
  import { createChangeClassificationReport, } from '../../core/change-classification.js';
3
+ import { writeJsonFileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
4
4
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
5
5
  import { requireGitChangedFiles } from '../lib/git-changes.js';
6
6
  import { t } from '../lib/i18n.js';
@@ -111,8 +111,7 @@ function resolveWritePath(projectRoot, inputPath) {
111
111
  }
112
112
  function writeClassifyOutput(projectRoot, inputPath, output) {
113
113
  const outputPath = resolveWritePath(projectRoot, inputPath);
114
- mkdirSync(path.dirname(outputPath), { recursive: true });
115
- writeFileSync(outputPath, `${JSON.stringify(output, null, 2)}\n`, 'utf8');
114
+ writeJsonFileInsideWithoutSymlinks(projectRoot, outputPath, output);
116
115
  }
117
116
  export function runClassify(args, reporter, lang = 'en') {
118
117
  if (args.includes('--help') || args.includes('-h')) {
@@ -5,7 +5,9 @@ import { getAgentContext, } from '../lib/agent-context.js';
5
5
  import { t } from '../lib/i18n.js';
6
6
  import { getLocalIndexDatabasePath } from '../lib/local-index.js';
7
7
  import { resolveMustflowRoot } from '../lib/project-root.js';
8
- import { checkMustflowProject } from '../lib/validation.js';
8
+ import { checkMustflowProjectReport } from '../lib/validation.js';
9
+ import { findCommandEnvInheritanceWarnings } from '../../core/command-contract-validation.js';
10
+ import { readCommandContract } from '../../core/config-loading.js';
9
11
  import { summarizeSkillRouteAlignment } from '../../core/skill-route-alignment.js';
10
12
  const DOCTOR_SCHEMA_VERSION = '1';
11
13
  export function getDoctorHelp(lang = 'en') {
@@ -46,6 +48,7 @@ function createDiagnostics(output) {
46
48
  const localIndexExists = existsSync(getLocalIndexDatabasePath(output.mustflow_root));
47
49
  const runnableIntentCount = output.context.runnable_intents.length;
48
50
  const skillRouteAlignment = summarizeSkillRouteAlignment(output.check.issues);
51
+ const inheritedEnvIntentCount = output.command_environment.inherited_intents.length;
49
52
  diagnostics.push({
50
53
  id: 'install',
51
54
  status: output.installed ? 'ok' : 'fail',
@@ -55,7 +58,9 @@ function createDiagnostics(output) {
55
58
  diagnostics.push({
56
59
  id: 'validation',
57
60
  status: output.check.ok ? 'ok' : 'fail',
58
- summary: `${output.check.issue_count} ${pluralize(output.check.issue_count, 'issue', 'issues')}`,
61
+ summary: output.check.warning_count === 0
62
+ ? `${output.check.issue_count} ${pluralize(output.check.issue_count, 'issue', 'issues')}`
63
+ : `${output.check.issue_count} ${pluralize(output.check.issue_count, 'issue', 'issues')}, ${output.check.warning_count} ${pluralize(output.check.warning_count, 'warning', 'warnings')}`,
59
64
  action: output.check.ok ? null : checkCommand,
60
65
  });
61
66
  diagnostics.push({
@@ -72,6 +77,16 @@ function createDiagnostics(output) {
72
77
  : 'missing',
73
78
  action: output.context.command_contract_exists ? 'mf help commands' : 'mf init --dry-run',
74
79
  });
80
+ diagnostics.push({
81
+ id: 'environment',
82
+ status: output.context.command_contract_exists ? (inheritedEnvIntentCount > 0 ? 'warn' : 'ok') : 'info',
83
+ summary: output.context.command_contract_exists
84
+ ? inheritedEnvIntentCount === 0
85
+ ? 'no inherited host-environment intents'
86
+ : `${inheritedEnvIntentCount} inherited host-environment ${pluralize(inheritedEnvIntentCount, 'intent', 'intents')}: ${output.command_environment.inherited_intents.join(', ')}`
87
+ : 'command contract missing',
88
+ action: inheritedEnvIntentCount > 0 ? 'mf check --strict --json' : null,
89
+ });
75
90
  diagnostics.push({
76
91
  id: 'read_order',
77
92
  status: output.context.missing_read_order.length === 0 ? 'ok' : 'fail',
@@ -124,15 +139,37 @@ function getNextSteps(output) {
124
139
  }
125
140
  return nextSteps;
126
141
  }
142
+ function readCommandEnvironmentSummary(projectRoot) {
143
+ try {
144
+ const contract = readCommandContract(projectRoot);
145
+ const warnings = findCommandEnvInheritanceWarnings(contract);
146
+ return {
147
+ inherited_intents: warnings.map((warning) => warning.intentName).sort((left, right) => left.localeCompare(right)),
148
+ inherited_network_intents: warnings
149
+ .filter((warning) => warning.network)
150
+ .map((warning) => warning.intentName)
151
+ .sort((left, right) => left.localeCompare(right)),
152
+ };
153
+ }
154
+ catch {
155
+ return {
156
+ inherited_intents: [],
157
+ inherited_network_intents: [],
158
+ };
159
+ }
160
+ }
127
161
  function createDoctorOutput(strict) {
128
162
  const mustflowRoot = resolveMustflowRoot();
129
163
  const context = getAgentContext(mustflowRoot);
130
- const issues = checkMustflowProject(mustflowRoot, { strict });
164
+ const checkReport = checkMustflowProjectReport(mustflowRoot, { strict });
131
165
  const check = {
132
- ok: issues.length === 0,
133
- issue_count: issues.length,
134
- issues,
166
+ ok: checkReport.issues.length === 0,
167
+ issue_count: checkReport.issues.length,
168
+ issues: checkReport.issues,
169
+ warning_count: checkReport.warnings.length,
170
+ warnings: checkReport.warnings,
135
171
  };
172
+ const commandEnvironment = readCommandEnvironmentSummary(mustflowRoot);
136
173
  const baseOutput = {
137
174
  schema_version: DOCTOR_SCHEMA_VERSION,
138
175
  command: 'doctor',
@@ -150,6 +187,7 @@ function createDoctorOutput(strict) {
150
187
  missing_optional_read_order: context.optional_read_order.filter((entry) => !entry.exists).map((entry) => entry.path),
151
188
  latest_run_exists: context.latest_run.exists,
152
189
  },
190
+ command_environment: commandEnvironment,
153
191
  effective_policy: context.effective_policy,
154
192
  state_policy: context.state_policy,
155
193
  blocked_actions: context.blocked_actions,
@@ -171,6 +209,8 @@ function getDiagnosticLabel(id, lang) {
171
209
  return t(lang, 'doctor.diagnostic.skillRoutes');
172
210
  case 'commands':
173
211
  return t(lang, 'doctor.diagnostic.commands');
212
+ case 'environment':
213
+ return t(lang, 'doctor.diagnostic.environment');
174
214
  case 'read_order':
175
215
  return t(lang, 'doctor.diagnostic.readOrder');
176
216
  case 'optional_read_order':
@@ -46,7 +46,7 @@ export function writeStreamChunk(reporter, stream, chunk) {
46
46
  reporter.stderr(chunk.toString());
47
47
  }
48
48
  export function createOutputLimitError(stream, maxOutputBytes) {
49
- return Object.assign(new Error(`${stream} exceeded max_output_bytes (${maxOutputBytes})`), {
49
+ return Object.assign(new Error(`${stream} exceeded per-stream max_output_bytes (${maxOutputBytes})`), {
50
50
  code: OUTPUT_LIMIT_ERROR_CODE,
51
51
  });
52
52
  }
@@ -16,6 +16,7 @@ export function assembleRunReceipt(input) {
16
16
  envPolicy: input.plan.envPolicy,
17
17
  envAllowlist: input.plan.envAllowlist,
18
18
  timeoutSeconds: input.plan.timeoutSeconds,
19
+ killAfterSeconds: input.plan.killAfterSeconds,
19
20
  maxOutputBytes: input.plan.maxOutputBytes,
20
21
  successExitCodes: input.plan.successExitCodes,
21
22
  exitCode: input.exitCode,
@@ -1,11 +1,12 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { readFileSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { createClassifyOutput } from './classify.js';
5
5
  import { runRun } from './run.js';
6
6
  import { createChangeVerificationReport, } from '../../core/change-verification.js';
7
+ import { writeJsonFileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
7
8
  import { createVerifyCompletionVerdict, } from '../../core/completion-verdict.js';
8
- import { atomicWriteJsonFile, createStateRunId } from '../../core/atomic-state-write.js';
9
+ import { createStateRunId } from '../../core/atomic-state-write.js';
9
10
  import { createExternalEvidenceRisks, } from '../../core/external-evidence.js';
10
11
  import { createRepeatedFailureRisks, createVerificationFailureFingerprint, updateRepeatedFailureState, } from '../../core/repeated-failure.js';
11
12
  import { countReproEvidenceVerdictEffects, createReproEvidenceRisks, } from '../../core/repro-evidence.js';
@@ -16,7 +17,7 @@ import { readCommandContract } from '../../core/config-loading.js';
16
17
  import { DEFAULT_VERIFY_PARALLELISM, parseVerifyArgs } from './verify/args.js';
17
18
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
18
19
  import { t } from '../lib/i18n.js';
19
- import { readLocalCommandEffectGraph, readLocalPathSurfaces, readLocalSourceAnchorVerdictRisks, } from '../lib/local-index.js';
20
+ import { readLocalCommandEffectGraphs, readLocalPathSurfaces, readLocalSourceAnchorVerdictRisks, } from '../lib/local-index.js';
20
21
  import { resolveMustflowRoot } from '../lib/project-root.js';
21
22
  const VERIFY_SCHEMA_VERSION = '1';
22
23
  const RUN_STATE_DIR = path.join('.mustflow', 'state', 'runs');
@@ -545,8 +546,7 @@ function createInputFromChanged(projectRoot) {
545
546
  }
546
547
  function writeChangedPlan(projectRoot, inputPath, plan) {
547
548
  const planPath = resolvePlanPath(projectRoot, inputPath);
548
- mkdirSync(path.dirname(planPath), { recursive: true });
549
- writeFileSync(planPath, `${JSON.stringify(plan, null, 2)}\n`, 'utf8');
549
+ writeJsonFileInsideWithoutSymlinks(projectRoot, planPath, plan);
550
550
  }
551
551
  export function planErrorMessageKey(code) {
552
552
  switch (code) {
@@ -578,6 +578,20 @@ function skippedResult(candidate) {
578
578
  receipt: null,
579
579
  };
580
580
  }
581
+ function stoppedAfterFailedBatchResult(entry, verificationPlanId) {
582
+ return {
583
+ intent: entry.intent,
584
+ status: 'skipped',
585
+ skipped: true,
586
+ reason: 'stopped_after_failed_batch',
587
+ detail: 'Skipped because an earlier verification batch failed and the schedule failure policy stops before the next batch.',
588
+ exit_code: null,
589
+ verification_plan_id: verificationPlanId,
590
+ receipt_path: null,
591
+ receipt_sha256: null,
592
+ receipt: null,
593
+ };
594
+ }
581
595
  function candidateResultKey(candidate) {
582
596
  return candidate.intent
583
597
  ? `intent:${candidate.intent}`
@@ -670,21 +684,40 @@ async function runVerificationEntriesInParallelChunks(entries, parallelism, lang
670
684
  }
671
685
  return results;
672
686
  }
687
+ function verificationResultFailed(result) {
688
+ return (!result.skipped &&
689
+ (result.status === 'failed' ||
690
+ result.status === 'timed_out' ||
691
+ result.status === 'start_failed' ||
692
+ result.status === 'output_limit_exceeded'));
693
+ }
673
694
  async function runScheduledVerificationIntents(report, lang, verificationPlanId, scheduledTestTargets, parallelism) {
674
- if (parallelism <= DEFAULT_VERIFY_PARALLELISM) {
675
- return runVerificationEntriesSequentially(report.schedule.entries, lang, verificationPlanId, scheduledTestTargets);
676
- }
677
695
  const results = [];
678
- for (const batch of report.schedule.batches) {
696
+ for (let batchIndex = 0; batchIndex < report.schedule.batches.length; batchIndex += 1) {
697
+ const batch = report.schedule.batches[batchIndex];
679
698
  const entries = entriesForScheduleBatch(report.schedule.entries, batch);
680
699
  if (entries.length === 0) {
681
700
  continue;
682
701
  }
702
+ let batchResults;
683
703
  if (entries.length > 1 && entries.every((entry) => entry.parallelEligible)) {
684
- results.push(...(await runVerificationEntriesInParallelChunks(entries, parallelism, lang, verificationPlanId, scheduledTestTargets)));
704
+ batchResults =
705
+ parallelism > DEFAULT_VERIFY_PARALLELISM
706
+ ? await runVerificationEntriesInParallelChunks(entries, parallelism, lang, verificationPlanId, scheduledTestTargets)
707
+ : await runVerificationEntriesSequentially(entries, lang, verificationPlanId, scheduledTestTargets);
708
+ }
709
+ else {
710
+ batchResults = await runVerificationEntriesSequentially(entries, lang, verificationPlanId, scheduledTestTargets);
711
+ }
712
+ results.push(...batchResults);
713
+ if (!batchResults.some(verificationResultFailed)) {
685
714
  continue;
686
715
  }
687
- results.push(...(await runVerificationEntriesSequentially(entries, lang, verificationPlanId, scheduledTestTargets)));
716
+ const remainingEntries = report.schedule.batches
717
+ .slice(batchIndex + 1)
718
+ .flatMap((remainingBatch) => entriesForScheduleBatch(report.schedule.entries, remainingBatch));
719
+ results.push(...remainingEntries.map((entry) => stoppedAfterFailedBatchResult(entry, verificationPlanId)));
720
+ break;
688
721
  }
689
722
  return results;
690
723
  }
@@ -1026,7 +1059,6 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1026
1059
  const statePaths = createVerifyRunStatePaths(projectRoot);
1027
1060
  const receipts = [];
1028
1061
  const results = [];
1029
- mkdirSync(statePaths.absoluteIntentDir, { recursive: true });
1030
1062
  for (const [index, result] of output.results.entries()) {
1031
1063
  let receiptPath = null;
1032
1064
  let receiptSha256 = null;
@@ -1042,7 +1074,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1042
1074
  };
1043
1075
  const receiptContent = `${JSON.stringify(receipt, null, 2)}\n`;
1044
1076
  receiptSha256 = hashTextSha256(receiptContent);
1045
- atomicWriteJsonFile(absoluteReceiptPath, receipt);
1077
+ writeJsonFileInsideWithoutSymlinks(projectRoot, absoluteReceiptPath, receipt);
1046
1078
  }
1047
1079
  receipts.push({
1048
1080
  intent: result.intent,
@@ -1148,7 +1180,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1148
1180
  ...(outputWithReceiptPaths.external_checks ? { external_checks: outputWithReceiptPaths.external_checks } : {}),
1149
1181
  receipts,
1150
1182
  };
1151
- atomicWriteJsonFile(statePaths.absoluteManifestPath, manifest);
1183
+ writeJsonFileInsideWithoutSymlinks(projectRoot, statePaths.absoluteManifestPath, manifest);
1152
1184
  const latest = {
1153
1185
  schema_version: '1',
1154
1186
  command: 'verify',
@@ -1169,7 +1201,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1169
1201
  run_dir: statePaths.runDir,
1170
1202
  manifest_path: statePaths.manifestPath,
1171
1203
  };
1172
- atomicWriteJsonFile(path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), latest);
1204
+ writeJsonFileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), latest);
1173
1205
  return outputWithReceiptPaths;
1174
1206
  }
1175
1207
  async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvidence = null, externalChecks = [], parallelism = DEFAULT_VERIFY_PARALLELISM) {
@@ -1276,14 +1308,11 @@ async function createPlanOnlyOutput(input, projectRoot) {
1276
1308
  if (!firstEntry) {
1277
1309
  return { ...report, verification_plan_id: verificationPlanId, requirements };
1278
1310
  }
1279
- const firstGraph = await readLocalCommandEffectGraph(projectRoot, firstEntry.intent);
1280
- const graphsByIntent = new Map([[firstEntry.intent, firstGraph]]);
1281
- if (firstGraph.status === 'fresh') {
1282
- for (const entry of report.schedule.entries.slice(1)) {
1283
- if (!graphsByIntent.has(entry.intent)) {
1284
- graphsByIntent.set(entry.intent, await readLocalCommandEffectGraph(projectRoot, entry.intent));
1285
- }
1286
- }
1311
+ const scheduledIntents = Array.from(new Set(report.schedule.entries.map((entry) => entry.intent)));
1312
+ const graphsByIntent = await readLocalCommandEffectGraphs(projectRoot, scheduledIntents);
1313
+ const firstGraph = graphsByIntent.get(firstEntry.intent);
1314
+ if (!firstGraph) {
1315
+ return { ...report, verification_plan_id: verificationPlanId, requirements };
1287
1316
  }
1288
1317
  return {
1289
1318
  ...report,
@@ -488,6 +488,7 @@ export const enMessages = {
488
488
  "doctor.diagnostic.validation": "Validation",
489
489
  "doctor.diagnostic.skillRoutes": "Skill routes",
490
490
  "doctor.diagnostic.commands": "Command specification",
491
+ "doctor.diagnostic.environment": "Environment",
491
492
  "doctor.diagnostic.readOrder": "Read order",
492
493
  "doctor.diagnostic.optionalReadOrder": "Optional read order",
493
494
  "doctor.diagnostic.repoMap": "REPO_MAP.md",
@@ -488,6 +488,7 @@ export const esMessages = {
488
488
  "doctor.diagnostic.validation": "Validación",
489
489
  "doctor.diagnostic.skillRoutes": "Rutas de skills",
490
490
  "doctor.diagnostic.commands": "Especificación de comandos",
491
+ "doctor.diagnostic.environment": "Entorno",
491
492
  "doctor.diagnostic.readOrder": "Orden de lectura",
492
493
  "doctor.diagnostic.optionalReadOrder": "Orden de lectura opcional",
493
494
  "doctor.diagnostic.repoMap": "REPO_MAP.md",
@@ -488,6 +488,7 @@ export const frMessages = {
488
488
  "doctor.diagnostic.validation": "Validation",
489
489
  "doctor.diagnostic.skillRoutes": "Routage des skills",
490
490
  "doctor.diagnostic.commands": "Spécification des commandes",
491
+ "doctor.diagnostic.environment": "Environnement",
491
492
  "doctor.diagnostic.readOrder": "Ordre de lecture",
492
493
  "doctor.diagnostic.optionalReadOrder": "Ordre de lecture optionnel",
493
494
  "doctor.diagnostic.repoMap": "REPO_MAP.md",
@@ -488,6 +488,7 @@ export const hiMessages = {
488
488
  "doctor.diagnostic.validation": "सत्यापन",
489
489
  "doctor.diagnostic.skillRoutes": "स्किल रूट",
490
490
  "doctor.diagnostic.commands": "कमांड विनिर्देश",
491
+ "doctor.diagnostic.environment": "एनवायरनमेंट",
491
492
  "doctor.diagnostic.readOrder": "पढ़ने का क्रम",
492
493
  "doctor.diagnostic.optionalReadOrder": "वैकल्पिक पढ़ने का क्रम",
493
494
  "doctor.diagnostic.repoMap": "REPO_MAP.md",
@@ -488,6 +488,7 @@ export const koMessages = {
488
488
  "doctor.diagnostic.validation": "검증",
489
489
  "doctor.diagnostic.skillRoutes": "스킬 라우팅",
490
490
  "doctor.diagnostic.commands": "명령",
491
+ "doctor.diagnostic.environment": "실행 환경",
491
492
  "doctor.diagnostic.readOrder": "읽기 순서",
492
493
  "doctor.diagnostic.optionalReadOrder": "선택적 읽기 순서",
493
494
  "doctor.diagnostic.repoMap": "REPO_MAP.md",
@@ -488,6 +488,7 @@ export const zhMessages = {
488
488
  "doctor.diagnostic.validation": "验证",
489
489
  "doctor.diagnostic.skillRoutes": "技能路由",
490
490
  "doctor.diagnostic.commands": "命令规范",
491
+ "doctor.diagnostic.environment": "环境",
491
492
  "doctor.diagnostic.readOrder": "读取顺序",
492
493
  "doctor.diagnostic.optionalReadOrder": "可选读取顺序",
493
494
  "doctor.diagnostic.repoMap": "REPO_MAP.md",
@@ -1,5 +1,7 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { parseGitStatusOutput } from '../../core/change-classification.js';
3
+ const GIT_STATUS_TIMEOUT_MS = 10_000;
4
+ const GIT_STATUS_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
3
5
  export class GitChangedFilesError extends Error {
4
6
  result;
5
7
  constructor(result) {
@@ -9,9 +11,13 @@ export class GitChangedFilesError extends Error {
9
11
  }
10
12
  }
11
13
  export function readGitChangedFiles(projectRoot) {
12
- const result = spawnSync('git', ['status', '--short', '--untracked-files=all'], {
14
+ const result = spawnSync('git', ['status', '--porcelain=v1', '-z', '--untracked-files=all'], {
13
15
  cwd: projectRoot,
14
16
  encoding: 'utf8',
17
+ input: '',
18
+ maxBuffer: GIT_STATUS_MAX_BUFFER_BYTES,
19
+ stdio: ['ignore', 'pipe', 'pipe'],
20
+ timeout: GIT_STATUS_TIMEOUT_MS,
15
21
  windowsHide: true,
16
22
  });
17
23
  if (result.status !== 0 || typeof result.stdout !== 'string') {
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
3
  import path from 'node:path';
4
4
  import { isRecord, readCommandContract, readString, readStringArray } from '../command-contract.js';
@@ -8,6 +8,7 @@ import { collectSourceAnchorIndexRecords, hasHighRiskSourceAnchorRiskTags, } fro
8
8
  import { listSourceAnchorFiles } from '../../../core/source-anchors.js';
9
9
  import { normalizeCommandEffects } from '../../../core/command-effects.js';
10
10
  import { listChangeClassificationRuleDescriptors } from '../../../core/change-classification.js';
11
+ import { writeFileInsideWithoutSymlinks } from '../../../core/safe-filesystem.js';
11
12
  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';
12
13
  import { loadSqlJs } from './sql.js';
13
14
  export function getLocalIndexDatabasePath(projectRoot) {
@@ -331,16 +332,16 @@ function collectSourceAnchorCandidatePaths(projectRoot, sourceConfig) {
331
332
  excludeGeneratedOrVendor: true,
332
333
  });
333
334
  }
334
- function collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource, sourceConfig) {
335
+ function collectFastPreflightIndexedFileRecords(projectRoot, includeSource, sourceConfig) {
335
336
  const records = new Map();
336
337
  for (const relativePath of getExistingIndexablePaths(projectRoot)) {
337
- records.set(relativePath, readIndexedFileMetadataRecord(projectRoot, relativePath, 'workflow'));
338
+ records.set(relativePath, readIndexedFileRecord(projectRoot, relativePath, 'workflow'));
338
339
  }
339
340
  if (includeSource) {
340
341
  try {
341
342
  for (const sourcePath of collectSourceAnchorCandidatePaths(projectRoot, sourceConfig)) {
342
343
  if (!records.has(sourcePath)) {
343
- records.set(sourcePath, readIndexedFileMetadataRecord(projectRoot, sourcePath, 'source_anchor'));
344
+ records.set(sourcePath, readIndexedFileRecord(projectRoot, sourcePath, 'source_anchor'));
344
345
  }
345
346
  }
346
347
  }
@@ -349,7 +350,7 @@ function collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSour
349
350
  }
350
351
  }
351
352
  if (existsSync(path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/')))) {
352
- records.set(LATEST_RUN_STATE_RELATIVE_PATH, readIndexedFileMetadataRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
353
+ records.set(LATEST_RUN_STATE_RELATIVE_PATH, readIndexedFileRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
353
354
  }
354
355
  return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
355
356
  }
@@ -2053,27 +2054,6 @@ function createStoredLocalIndexResult(projectRoot, databasePath, dryRun, indexMo
2053
2054
  indexed_paths: readStoredIndexedPaths(database),
2054
2055
  };
2055
2056
  }
2056
- function indexedFileMetadataMatch(database, currentFiles) {
2057
- const rows = queryRows(database, 'SELECT path, source_scope, size_bytes, mtime_ms, parser_version FROM indexed_files ORDER BY path');
2058
- if (rows.length !== currentFiles.length) {
2059
- return false;
2060
- }
2061
- const currentByPath = new Map(currentFiles.map((file) => [file.path, file]));
2062
- for (const row of rows) {
2063
- const storedPath = toSearchString(row.path);
2064
- const current = currentByPath.get(storedPath);
2065
- if (!current) {
2066
- return false;
2067
- }
2068
- if (normalizeIndexedFileSourceScope(row.source_scope) !== current.sourceScope ||
2069
- row.size_bytes !== current.sizeBytes ||
2070
- row.mtime_ms !== current.mtimeMs ||
2071
- toSearchString(row.parser_version) !== LOCAL_INDEX_PARSER_VERSION) {
2072
- return false;
2073
- }
2074
- }
2075
- return true;
2076
- }
2077
2057
  function indexedFilesMatch(database, currentFiles) {
2078
2058
  const rows = queryRows(database, 'SELECT path, source_scope, content_hash, parser_version FROM indexed_files ORDER BY path');
2079
2059
  if (rows.length !== currentFiles.length) {
@@ -2116,7 +2096,7 @@ async function readIncrementalPreflightReuse(SQL, databasePath, projectRoot, cur
2116
2096
  if (!hasTable(database, 'indexed_files')) {
2117
2097
  return { result: null, rebuildReason: 'indexed_files_missing' };
2118
2098
  }
2119
- if (!indexedFileMetadataMatch(database, currentFiles)) {
2099
+ if (!indexedFilesMatch(database, currentFiles)) {
2120
2100
  return { result: null, rebuildReason: 'file_fingerprint_mismatch' };
2121
2101
  }
2122
2102
  const capabilities = readStoredSearchCapabilities(database);
@@ -2190,7 +2170,7 @@ export async function createLocalIndex(projectRoot, options = {}) {
2190
2170
  capabilities = detectLocalSearchCapabilities(capabilityDatabase);
2191
2171
  capabilityDatabase.close();
2192
2172
  if (incremental) {
2193
- const preflightFiles = collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource, sourceConfig);
2173
+ const preflightFiles = collectFastPreflightIndexedFileRecords(projectRoot, includeSource, sourceConfig);
2194
2174
  const preflightReuse = await readIncrementalPreflightReuse(SQL, databasePath, projectRoot, preflightFiles, sourceScopeHash, dryRun, indexMode);
2195
2175
  if (preflightReuse.result) {
2196
2176
  return preflightReuse.result;
@@ -2223,8 +2203,7 @@ export async function createLocalIndex(projectRoot, options = {}) {
2223
2203
  const database = new SQL.Database();
2224
2204
  createSchema(database, capabilities);
2225
2205
  populateDatabase(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors, indexedFiles, verificationEvidence, indexMode, sourceScopeHash, includeSource, new Date().toISOString());
2226
- mkdirSync(path.dirname(databasePath), { recursive: true });
2227
- writeFileSync(databasePath, database.export());
2206
+ writeFileInsideWithoutSymlinks(projectRoot, databasePath, database.export());
2228
2207
  database.close();
2229
2208
  }
2230
2209
  return {
@@ -1,8 +1,9 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
- import { existsSync, lstatSync, readdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
3
+ import { existsSync, lstatSync, readdirSync, realpathSync, statSync } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { toPosixPath } from './filesystem.js';
6
+ import { writeUtf8FileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
6
7
  import { readTomlFile } from './toml.js';
7
8
  const DEFAULT_DEPTH = 3;
8
9
  const REPO_MAP_DOC_ID = 'repo-map';
@@ -691,5 +692,5 @@ export function generateRepoMap(projectRoot, options = {}) {
691
692
  ].join('\n');
692
693
  }
693
694
  export function writeRepoMap(projectRoot, content) {
694
- writeFileSync(path.join(projectRoot, 'REPO_MAP.md'), content);
695
+ writeUtf8FileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, 'REPO_MAP.md'), content);
695
696
  }
@@ -4,7 +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
+ import { DEFAULT_COMMAND_MAX_OUTPUT_BYTES, COMMAND_OUTPUT_LIMIT_SCOPE, MAX_COMMAND_OUTPUT_BYTES, commandMaxOutputBytesLimitMessage, } from '../../core/command-output-limits.js';
8
8
  import { t } from './i18n.js';
9
9
  function getSuccessExitCodes(intent) {
10
10
  const value = intent.success_exit_codes;
@@ -78,8 +78,10 @@ function readEffectiveMaxOutputBytes(contract, intent) {
78
78
  readPositiveInteger(contract.defaults, 'max_output_bytes') ??
79
79
  DEFAULT_COMMAND_MAX_OUTPUT_BYTES;
80
80
  }
81
- function readEffectiveKillAfterSeconds(contract) {
82
- return readPositiveInteger(contract.defaults, 'kill_after_seconds') ?? 5;
81
+ function readEffectiveKillAfterSeconds(contract, intent) {
82
+ return readPositiveInteger(intent, 'kill_after_seconds') ??
83
+ readPositiveInteger(contract.defaults, 'kill_after_seconds') ??
84
+ 5;
83
85
  }
84
86
  function getMaxOutputBytesLimitDetail(contract, intent) {
85
87
  const intentValue = readPositiveInteger(intent, 'max_output_bytes');
@@ -106,7 +108,7 @@ function readRunIntentMetadata(contract, intent) {
106
108
  kind: readString(intent, 'kind') ?? null,
107
109
  configuredCwd,
108
110
  timeoutSeconds: readPositiveInteger(intent, 'timeout_seconds') ?? null,
109
- killAfterSeconds: readEffectiveKillAfterSeconds(contract),
111
+ killAfterSeconds: readEffectiveKillAfterSeconds(contract, intent),
110
112
  maxOutputBytes: readEffectiveMaxOutputBytes(contract, intent),
111
113
  successExitCodes: getSuccessExitCodes(intent),
112
114
  commandArgv,
@@ -284,7 +286,9 @@ export function createRunPreview(plan, previewMode) {
284
286
  cwd: plan.relativeCwd,
285
287
  resolved_cwd: plan.cwd,
286
288
  timeout_seconds: plan.timeoutSeconds,
289
+ kill_after_seconds: plan.killAfterSeconds,
287
290
  max_output_bytes: plan.maxOutputBytes,
291
+ max_output_bytes_scope: plan.maxOutputBytes === null ? null : COMMAND_OUTPUT_LIMIT_SCOPE,
288
292
  mode: plan.mode,
289
293
  argv: plan.commandArgv,
290
294
  resolved_argv: plan.argvCommand,
@@ -46,14 +46,36 @@ function uniqueSorted(values) {
46
46
  return [...new Set(values)].sort((left, right) => left.localeCompare(right));
47
47
  }
48
48
  function toPosixPath(value) {
49
- return value.trim().replaceAll('\\', '/');
49
+ return value.replaceAll('\\', '/');
50
50
  }
51
51
  export function normalizeStatusPath(value) {
52
- const pathText = toPosixPath(value);
52
+ const pathText = toPosixPath(value.trim());
53
53
  const renameTarget = pathText.includes(' -> ') ? (pathText.split(' -> ').pop() ?? pathText) : pathText;
54
54
  return renameTarget.replace(/^"|"$/gu, '');
55
55
  }
56
+ function normalizePorcelainStatusPath(value) {
57
+ return toPosixPath(value);
58
+ }
59
+ function parseGitPorcelainStatusOutput(output) {
60
+ const paths = [];
61
+ const parts = output.split('\0').filter((part) => part.length > 0);
62
+ for (let index = 0; index < parts.length; index += 1) {
63
+ const entry = parts[index] ?? '';
64
+ const status = entry.slice(0, 2);
65
+ const filePath = normalizePorcelainStatusPath(entry.slice(3));
66
+ if (filePath.length > 0) {
67
+ paths.push(filePath);
68
+ }
69
+ if (status.includes('R') || status.includes('C')) {
70
+ index += 1;
71
+ }
72
+ }
73
+ return uniqueSorted(paths);
74
+ }
56
75
  export function parseGitStatusOutput(output) {
76
+ if (output.includes('\0')) {
77
+ return parseGitPorcelainStatusOutput(output);
78
+ }
57
79
  const paths = output
58
80
  .split(/\r?\n/u)
59
81
  .map((line) => line.slice(3))
@@ -4,7 +4,7 @@ const CHECK_ISSUE_ID_RULES = [
4
4
  ['mustflow.command_contract.configured_missing_lifecycle', /^Configured intent [^\s]+ must define lifecycle$/u],
5
5
  ['mustflow.command_contract.configured_missing_run_policy', /^Configured intent [^\s]+ must define run_policy$/u],
6
6
  ['mustflow.command_contract.oneshot_missing_timeout', /^Oneshot intent [^\s]+ must define timeout_seconds$/u],
7
- ['mustflow.command_contract.max_output_bytes_exceeds_limit', /^\[commands\.(?:defaults|intents\.[^\]]+)\]\.max_output_bytes must be less than or equal to \d+$/u],
7
+ ['mustflow.command_contract.max_output_bytes_exceeds_limit', /^\[commands\.(?:defaults|intents\.[^\]]+)\]\.max_output_bytes must be less than or equal to \d+ per output stream$/u],
8
8
  ['mustflow.command_contract.oneshot_stdin_not_closed', /^Oneshot intent [^\s]+ must set stdin = "closed"$/u],
9
9
  ['mustflow.command_contract.long_running_agent_allowed', /^Long-running intent [^\s]+ must not use run_policy = "agent_allowed"$/u],
10
10
  ['mustflow.command_contract.executable_source_missing', /^Configured intent [^\s]+ must define argv or mode = "shell" with cmd$/u],
@@ -125,6 +125,12 @@ export function commandIntentBlockedCommandPattern(intent) {
125
125
  detail: 'Shell command contains a blocked long-running or background pattern.',
126
126
  };
127
127
  }
128
+ if (intent.mode === 'shell' && typeof intent.cmd === 'string' && commandTextHasLongRunningPattern(intent.cmd)) {
129
+ return {
130
+ code: 'long_running_command_pattern',
131
+ detail: `Shell command contains a blocked long-running pattern: ${intent.cmd}.`,
132
+ };
133
+ }
128
134
  const argv = readStringArray(intent, 'argv');
129
135
  if (!argv) {
130
136
  return null;