mustflow 2.18.21 → 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 (43) 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 +7 -8
  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/local-index/index.js +3 -3
  13. package/dist/cli/lib/repo-map.js +3 -2
  14. package/dist/cli/lib/run-plan.js +8 -4
  15. package/dist/core/check-issues.js +1 -1
  16. package/dist/core/command-contract-validation.js +24 -10
  17. package/dist/core/command-output-limits.js +2 -1
  18. package/dist/core/line-endings.js +12 -4
  19. package/dist/core/repeated-failure.js +3 -3
  20. package/dist/core/run-performance-history.js +4 -4
  21. package/dist/core/run-profile.js +2 -3
  22. package/dist/core/run-receipt.js +11 -3
  23. package/dist/core/run-write-drift.js +60 -12
  24. package/dist/core/safe-filesystem.js +155 -0
  25. package/package.json +1 -1
  26. package/schemas/commands.schema.json +1 -0
  27. package/schemas/doctor-report.schema.json +23 -1
  28. package/schemas/run-receipt.schema.json +6 -2
  29. package/templates/default/i18n.toml +13 -13
  30. package/templates/default/locales/en/.mustflow/skills/INDEX.md +13 -13
  31. package/templates/default/locales/en/.mustflow/skills/adapter-boundary/SKILL.md +72 -4
  32. package/templates/default/locales/en/.mustflow/skills/command-contract-authoring/SKILL.md +16 -10
  33. package/templates/default/locales/en/.mustflow/skills/command-pattern/SKILL.md +64 -7
  34. package/templates/default/locales/en/.mustflow/skills/database-change-safety/SKILL.md +249 -16
  35. package/templates/default/locales/en/.mustflow/skills/dependency-reality-check/SKILL.md +37 -7
  36. package/templates/default/locales/en/.mustflow/skills/migration-safety-check/SKILL.md +74 -10
  37. package/templates/default/locales/en/.mustflow/skills/performance-budget-check/SKILL.md +132 -5
  38. package/templates/default/locales/en/.mustflow/skills/pure-core-imperative-shell/SKILL.md +12 -5
  39. package/templates/default/locales/en/.mustflow/skills/result-option/SKILL.md +4 -2
  40. package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +112 -29
  41. package/templates/default/locales/en/.mustflow/skills/state-machine-pattern/SKILL.md +17 -4
  42. package/templates/default/locales/en/.mustflow/skills/structure-discovery-gate/SKILL.md +193 -2
  43. 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';
@@ -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) {
@@ -1059,7 +1059,6 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1059
1059
  const statePaths = createVerifyRunStatePaths(projectRoot);
1060
1060
  const receipts = [];
1061
1061
  const results = [];
1062
- mkdirSync(statePaths.absoluteIntentDir, { recursive: true });
1063
1062
  for (const [index, result] of output.results.entries()) {
1064
1063
  let receiptPath = null;
1065
1064
  let receiptSha256 = null;
@@ -1075,7 +1074,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1075
1074
  };
1076
1075
  const receiptContent = `${JSON.stringify(receipt, null, 2)}\n`;
1077
1076
  receiptSha256 = hashTextSha256(receiptContent);
1078
- atomicWriteJsonFile(absoluteReceiptPath, receipt);
1077
+ writeJsonFileInsideWithoutSymlinks(projectRoot, absoluteReceiptPath, receipt);
1079
1078
  }
1080
1079
  receipts.push({
1081
1080
  intent: result.intent,
@@ -1181,7 +1180,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1181
1180
  ...(outputWithReceiptPaths.external_checks ? { external_checks: outputWithReceiptPaths.external_checks } : {}),
1182
1181
  receipts,
1183
1182
  };
1184
- atomicWriteJsonFile(statePaths.absoluteManifestPath, manifest);
1183
+ writeJsonFileInsideWithoutSymlinks(projectRoot, statePaths.absoluteManifestPath, manifest);
1185
1184
  const latest = {
1186
1185
  schema_version: '1',
1187
1186
  command: 'verify',
@@ -1202,7 +1201,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
1202
1201
  run_dir: statePaths.runDir,
1203
1202
  manifest_path: statePaths.manifestPath,
1204
1203
  };
1205
- atomicWriteJsonFile(path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), latest);
1204
+ writeJsonFileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), latest);
1206
1205
  return outputWithReceiptPaths;
1207
1206
  }
1208
1207
  async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvidence = null, externalChecks = [], parallelism = DEFAULT_VERIFY_PARALLELISM) {
@@ -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,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) {
@@ -2202,8 +2203,7 @@ export async function createLocalIndex(projectRoot, options = {}) {
2202
2203
  const database = new SQL.Database();
2203
2204
  createSchema(database, capabilities);
2204
2205
  populateDatabase(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors, indexedFiles, verificationEvidence, indexMode, sourceScopeHash, includeSource, new Date().toISOString());
2205
- mkdirSync(path.dirname(databasePath), { recursive: true });
2206
- writeFileSync(databasePath, database.export());
2206
+ writeFileInsideWithoutSymlinks(projectRoot, databasePath, database.export());
2207
2207
  database.close();
2208
2208
  }
2209
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,
@@ -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],
@@ -161,6 +161,7 @@ function validateCommandIntent(intentName, intent, issues) {
161
161
  validateAllowedStringField(intent, 'env_policy', `[commands.intents.${intentName}].env_policy`, COMMAND_ENV_POLICIES, issues);
162
162
  validateStringArrayField(intent, 'env_allowlist', `[commands.intents.${intentName}].env_allowlist`, issues);
163
163
  validateMaxOutputBytesField(intent, 'max_output_bytes', `[commands.intents.${intentName}].max_output_bytes`, issues);
164
+ validatePositiveIntegerField(intent, 'kill_after_seconds', `[commands.intents.${intentName}].kill_after_seconds`, issues);
164
165
  validateCommandIntentSelection(intentName, intent, issues);
165
166
  if (intent.status !== 'configured') {
166
167
  return;
@@ -223,10 +224,10 @@ function getEffectiveCommandEnvPolicy(defaults, intent) {
223
224
  }
224
225
  return { policy: DEFAULT_COMMAND_ENV_POLICY, source: 'implicit' };
225
226
  }
226
- function validateCommandEnvInheritanceWarnings(commandsToml) {
227
- const issues = [];
227
+ export function findCommandEnvInheritanceWarnings(commandsToml) {
228
+ const warnings = [];
228
229
  if (!commandsToml || !isRecord(commandsToml.intents)) {
229
- return issues;
230
+ return warnings;
230
231
  }
231
232
  const defaults = isRecord(commandsToml.defaults) ? commandsToml.defaults : undefined;
232
233
  for (const [intentName, intent] of Object.entries(commandsToml.intents)) {
@@ -237,13 +238,26 @@ function validateCommandEnvInheritanceWarnings(commandsToml) {
237
238
  if (envPolicy.policy !== 'inherit') {
238
239
  continue;
239
240
  }
240
- const networkScope = intent.network === true ? ' with network = true' : '';
241
- const migration = 'set env_policy = "minimal" or "allowlist" unless broad host state is required';
242
- if (envPolicy.source === 'implicit') {
243
- issues.push(commandContractWarning(`configured agent-runnable intent ${intentName} implicitly inherits the host environment${networkScope}; ${migration}`));
244
- continue;
245
- }
246
- issues.push(commandContractWarning(`configured agent-runnable intent ${intentName} uses env_policy = "inherit"${networkScope}; ${migration}`));
241
+ warnings.push({
242
+ intentName,
243
+ source: envPolicy.source,
244
+ network: intent.network === true,
245
+ });
246
+ }
247
+ return warnings;
248
+ }
249
+ function formatCommandEnvInheritanceWarning(warning) {
250
+ const networkScope = warning.network ? ' with network = true' : '';
251
+ const migration = 'set env_policy = "minimal" or "allowlist" unless broad host state is required';
252
+ if (warning.source === 'implicit') {
253
+ return `configured agent-runnable intent ${warning.intentName} implicitly inherits the host environment${networkScope}; ${migration}`;
254
+ }
255
+ return `configured agent-runnable intent ${warning.intentName} uses env_policy = "inherit"${networkScope}; ${migration}`;
256
+ }
257
+ function validateCommandEnvInheritanceWarnings(commandsToml) {
258
+ const issues = [];
259
+ for (const warning of findCommandEnvInheritanceWarnings(commandsToml)) {
260
+ issues.push(commandContractWarning(formatCommandEnvInheritanceWarning(warning)));
247
261
  }
248
262
  return issues;
249
263
  }
@@ -1,5 +1,6 @@
1
1
  export const DEFAULT_COMMAND_MAX_OUTPUT_BYTES = 1_048_576;
2
2
  export const MAX_COMMAND_OUTPUT_BYTES = 16 * 1024 * 1024;
3
+ export const COMMAND_OUTPUT_LIMIT_SCOPE = 'per_stream';
3
4
  export function commandMaxOutputBytesLimitMessage(label) {
4
- return `${label} must be less than or equal to ${MAX_COMMAND_OUTPUT_BYTES}`;
5
+ return `${label} must be less than or equal to ${MAX_COMMAND_OUTPUT_BYTES} per output stream`;
5
6
  }
@@ -1,6 +1,7 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { existsSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { readFileInsideWithoutSymlinks, writeFileInsideWithoutSymlinks } from './safe-filesystem.js';
4
5
  const GITATTRIBUTES_PATH = '.gitattributes';
5
6
  function toPosixPath(value) {
6
7
  return value.split(path.sep).join('/');
@@ -10,7 +11,7 @@ function hasLfPolicy(projectRoot) {
10
11
  if (!existsSync(attributesPath)) {
11
12
  return false;
12
13
  }
13
- const content = readFileSync(attributesPath, 'utf8');
14
+ const content = readFileInsideWithoutSymlinks(projectRoot, attributesPath).toString('utf8');
14
15
  return /^\*\s+.*(?:^|\s)eol=lf(?:\s|$)/imu.test(content);
15
16
  }
16
17
  function gitList(projectRoot, args) {
@@ -107,14 +108,21 @@ export function inspectLineEndings(projectRoot, mode, options = {}) {
107
108
  if (!existsSync(absolutePath)) {
108
109
  continue;
109
110
  }
110
- const buffer = readFileSync(absolutePath);
111
+ let buffer;
112
+ try {
113
+ buffer = readFileInsideWithoutSymlinks(root, absolutePath);
114
+ }
115
+ catch (error) {
116
+ issues.push(error instanceof Error ? error.message : String(error));
117
+ continue;
118
+ }
111
119
  const lineEnding = detectLineEnding(buffer);
112
120
  const wouldChange = policy === 'lf' && (lineEnding === 'crlf' || lineEnding === 'mixed');
113
121
  if (!wouldChange) {
114
122
  continue;
115
123
  }
116
124
  if (canApply) {
117
- writeFileSync(absolutePath, normalizeLf(buffer));
125
+ writeFileInsideWithoutSymlinks(root, absolutePath, normalizeLf(buffer));
118
126
  changedFiles.push(toPosixPath(relativePath));
119
127
  }
120
128
  nonCompliantFiles.push({
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { writeJsonFileInsideWithoutSymlinks } from './safe-filesystem.js';
4
5
  export const REPEATED_FAILURE_STATE_PATH = '.mustflow/state/repeated-failures.json';
5
6
  export const REPEATED_FAILURE_STATE_LIMIT = 50;
6
7
  const UNRESOLVED_VERIFY_STATUSES = new Set(['failed', 'blocked', 'partial']);
@@ -59,8 +60,7 @@ function readRepeatedFailureState(projectRoot) {
59
60
  }
60
61
  function writeRepeatedFailureState(projectRoot, state) {
61
62
  const statePath = repeatedFailureStatePath(projectRoot);
62
- mkdirSync(path.dirname(statePath), { recursive: true });
63
- writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
63
+ writeJsonFileInsideWithoutSymlinks(projectRoot, statePath, state);
64
64
  }
65
65
  export function createVerificationFailureFingerprint(input) {
66
66
  const failedIntents = normalizeStrings(input.failedIntents);
@@ -1,5 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { writeJsonFileInsideWithoutSymlinks } from './safe-filesystem.js';
3
4
  const PERFORMANCE_HISTORY_SCHEMA_VERSION = '1';
4
5
  const PERFORMANCE_HISTORY_DIR = path.join('.mustflow', 'state', 'perf');
5
6
  const PERFORMANCE_SAMPLES_FILE = 'samples.json';
@@ -297,9 +298,8 @@ export function recordRunPerformanceHistory(projectRoot, receipt) {
297
298
  const samples = enforceSizeLimit(pruneSamples([...readSamples(samplesPath), sample], sample.observed_day), sample.observed_day);
298
299
  const samplesFile = createSamplesFile(samples);
299
300
  const summaryFile = createSummary(samples, sample.observed_day);
300
- mkdirSync(historyDir, { recursive: true });
301
- writeFileSync(samplesPath, serialize(samplesFile));
302
- writeFileSync(summaryPath, serialize(summaryFile));
301
+ writeJsonFileInsideWithoutSymlinks(projectRoot, samplesPath, samplesFile);
302
+ writeJsonFileInsideWithoutSymlinks(projectRoot, summaryPath, summaryFile);
303
303
  }
304
304
  catch {
305
305
  // Performance history is a local optimization hint. A write failure must not affect command execution.
@@ -1,6 +1,6 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs';
2
1
  import { performance } from 'node:perf_hooks';
3
2
  import path from 'node:path';
3
+ import { writeJsonFileInsideWithoutSymlinks } from './safe-filesystem.js';
4
4
  const RUN_PROFILE_SCHEMA_VERSION = '1';
5
5
  const RUN_PROFILE_ENV = 'MUSTFLOW_RUN_PROFILE';
6
6
  const RUN_PROFILE_DIR = path.join('.mustflow', 'state', 'runs');
@@ -75,8 +75,7 @@ export class RunProfiler {
75
75
  profile_path: getProfileRelativePath(),
76
76
  };
77
77
  const profilePath = path.join(input.projectRoot, RUN_PROFILE_DIR, LATEST_RUN_PROFILE);
78
- mkdirSync(path.dirname(profilePath), { recursive: true });
79
- writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`);
78
+ writeJsonFileInsideWithoutSymlinks(input.projectRoot, profilePath, profile);
80
79
  }
81
80
  recordPhase(name, startedAtMs) {
82
81
  this.phases.push({
@@ -1,8 +1,10 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import path from 'node:path';
3
- import { atomicWriteJsonFile, createStateRunId } from './atomic-state-write.js';
3
+ import { createStateRunId } from './atomic-state-write.js';
4
+ import { COMMAND_OUTPUT_LIMIT_SCOPE } from './command-output-limits.js';
4
5
  import { decodeUtf8Tail } from './bounded-output.js';
5
6
  import { DEFAULT_RUN_RECEIPT_TAIL_BYTES } from './retention-policy.js';
7
+ import { writeJsonFileInsideWithoutSymlinks } from './safe-filesystem.js';
6
8
  import { redactSecretLikeText } from './secret-redaction.js';
7
9
  const RUN_RECEIPT_SCHEMA_VERSION = '1';
8
10
  const RUN_RECEIPT_DIR = path.join('.mustflow', 'state', 'runs');
@@ -111,6 +113,7 @@ function createPerformanceSummary(input) {
111
113
  cwd: input.cwd,
112
114
  env_allowlist: input.envAllowlist,
113
115
  env_policy: input.envPolicy,
116
+ kill_after_seconds: input.killAfterSeconds,
114
117
  lifecycle: input.lifecycle,
115
118
  max_output_bytes: input.maxOutputBytes,
116
119
  mode: input.mode,
@@ -148,8 +151,10 @@ function createPerformanceSummary(input) {
148
151
  output_summary: {
149
152
  stdout_bytes: input.stdout.bytes,
150
153
  stderr_bytes: input.stderr.bytes,
154
+ total_bytes: input.stdout.bytes + input.stderr.bytes,
151
155
  stdout_truncated: input.stdout.truncated,
152
156
  stderr_truncated: input.stderr.truncated,
157
+ max_output_bytes_scope: COMMAND_OUTPUT_LIMIT_SCOPE,
153
158
  },
154
159
  result_summary: {
155
160
  status: input.status,
@@ -235,7 +240,9 @@ export function createRunReceipt(input) {
235
240
  env_policy: input.envPolicy,
236
241
  env_allowlist: input.envAllowlist,
237
242
  timeout_seconds: input.timeoutSeconds,
243
+ kill_after_seconds: input.killAfterSeconds,
238
244
  max_output_bytes: input.maxOutputBytes,
245
+ max_output_bytes_scope: COMMAND_OUTPUT_LIMIT_SCOPE,
239
246
  success_exit_codes: input.successExitCodes,
240
247
  exit_code: input.exitCode,
241
248
  signal: input.signal,
@@ -260,6 +267,7 @@ export function createRunReceipt(input) {
260
267
  envPolicy: input.envPolicy,
261
268
  envAllowlist: input.envAllowlist,
262
269
  timeoutSeconds: input.timeoutSeconds,
270
+ killAfterSeconds: input.killAfterSeconds,
263
271
  maxOutputBytes: input.maxOutputBytes,
264
272
  successExitCodes: input.successExitCodes,
265
273
  exitCode: input.exitCode,
@@ -285,6 +293,6 @@ export function writeRunReceipt(projectRoot, receipt) {
285
293
  if (relativeToRunDir.startsWith('..') || path.isAbsolute(relativeToRunDir)) {
286
294
  throw new Error(`Run receipt path must stay inside ${RUN_RECEIPT_DIR}`);
287
295
  }
288
- atomicWriteJsonFile(receiptPath, receipt);
289
- atomicWriteJsonFile(latestPath, receipt);
296
+ writeJsonFileInsideWithoutSymlinks(projectRoot, receiptPath, receipt);
297
+ writeJsonFileInsideWithoutSymlinks(projectRoot, latestPath, receipt);
290
298
  }