mustflow 2.18.0 → 2.18.3

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 (38) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/commands/explain-verify.js +2 -2
  3. package/dist/cli/commands/run/builtin-dispatch.js +92 -0
  4. package/dist/cli/commands/run/executor.js +112 -0
  5. package/dist/cli/commands/run/output.js +59 -0
  6. package/dist/cli/commands/run/process-tree.js +91 -0
  7. package/dist/cli/commands/run/receipt.js +42 -0
  8. package/dist/cli/commands/run.js +22 -414
  9. package/dist/cli/commands/verify/args.js +262 -0
  10. package/dist/cli/commands/verify.js +106 -263
  11. package/dist/cli/i18n/en.js +3 -1
  12. package/dist/cli/i18n/es.js +3 -1
  13. package/dist/cli/i18n/fr.js +3 -1
  14. package/dist/cli/i18n/hi.js +3 -1
  15. package/dist/cli/i18n/ko.js +3 -1
  16. package/dist/cli/i18n/zh.js +3 -1
  17. package/dist/cli/index.js +6 -72
  18. package/dist/cli/lib/command-registry.js +27 -0
  19. package/dist/cli/lib/repo-map.js +10 -3
  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 +1 -0
  23. package/dist/core/command-contract-validation.js +57 -7
  24. package/dist/core/completion-verdict.js +2 -1
  25. package/dist/core/public-json-contracts.js +1 -1
  26. package/dist/core/run-receipt.js +20 -13
  27. package/dist/core/source-anchors.js +96 -24
  28. package/dist/core/verification-evidence.js +4 -1
  29. package/package.json +1 -1
  30. package/schemas/README.md +1 -1
  31. package/schemas/run-receipt.schema.json +26 -3
  32. package/schemas/verify-report.schema.json +1 -1
  33. package/schemas/verify-run-manifest.schema.json +1 -1
  34. package/templates/default/i18n.toml +7 -1
  35. package/templates/default/locales/en/.mustflow/skills/INDEX.md +2 -1
  36. package/templates/default/locales/en/.mustflow/skills/routes.toml +6 -0
  37. package/templates/default/locales/en/.mustflow/skills/source-anchor-authoring/SKILL.md +147 -0
  38. package/templates/default/manifest.toml +8 -1
@@ -750,11 +750,12 @@ export const zhMessages = {
750
750
  "verify.help.summary": "运行由 required_after 元数据选出的已配置验证意图。",
751
751
  "verify.help.option.reason": "选择要验证的 required_after 原因",
752
752
  "verify.help.option.fromClassification": "从此仓库内的 mf classify 报告读取验证原因",
753
- "verify.help.option.fromPlan": "--from-classification 的兼容别名",
753
+ "verify.help.option.fromPlan": "--from-classification 的已弃用兼容别名;输入仍必须是 mf classify 报告",
754
754
  "verify.help.option.changed": "分类当前 Git 变更并验证匹配的原因",
755
755
  "verify.help.option.writePlan": "写入变更文件分类报告的兼容选项",
756
756
  "verify.help.option.reproEvidence": "从仓库本地 JSON 摘要读取结构化的 bug 复现证据",
757
757
  "verify.help.option.externalEvidence": "从仓库本地 JSON 摘要读取低权限外部 CI 证据",
758
+ "verify.help.option.parallel": "最多并行执行这个数量的安全、无冲突计划批次命令;默认值为 1",
758
759
  "verify.help.option.planOnly": "仅输出验证计划,不执行命令;需要 --json",
759
760
  "verify.help.exit.ok": "选中的所有验证意图均已通过",
760
761
  "verify.help.exit.fail": "验证失败、部分完成、被阻止,或输入无效",
@@ -769,6 +770,7 @@ export const zhMessages = {
769
770
  "verify.error.planOnlyJson": "--plan-only 需要 --json",
770
771
  "verify.error.reproEvidenceRequiresRun": "--repro-evidence 不能与 --plan-only 一起使用",
771
772
  "verify.error.externalEvidenceRequiresRun": "--external-evidence 不能与 --plan-only 一起使用",
773
+ "verify.error.invalidParallel": "--parallel 必须是正整数",
772
774
  "verify.error.invalid_plan_file": "分类报告必须是可读取的 JSON 文件",
773
775
  "verify.error.unsupported_plan_source": "验证输入必须是 mf classify 报告",
774
776
  "verify.error.plan_root_mismatch": "分类报告必须来自当前 mustflow 根目录",
package/dist/cli/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { realpathSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { COMMAND_DEFINITIONS } from './lib/command-registry.js';
5
+ import { COMMAND_DEFINITIONS, findCommandDefinition } from './lib/command-registry.js';
6
6
  import { renderCliError, renderHelp } from './lib/cli-output.js';
7
7
  import { DEFAULT_CLI_LANG, SUPPORTED_CLI_LANGS, isCliLang, t } from './lib/i18n.js';
8
8
  import { consoleReporter } from './lib/reporter.js';
@@ -102,77 +102,11 @@ export async function runCli(argv, reporter = consoleReporter) {
102
102
  reporter.stdout(getTopLevelHelp(parsed.lang));
103
103
  return 0;
104
104
  }
105
- if (command === '--version' || command === '-v' || command === 'version') {
106
- return (await import('./commands/version.js')).runVersion(args, reporter, parsed.lang);
107
- }
108
- if (command === 'init') {
109
- return (await import('./commands/init.js')).runInit(args, reporter, parsed.lang);
110
- }
111
- if (command === 'adapters') {
112
- return (await import('./commands/adapters.js')).runAdapters(args, reporter, parsed.lang);
113
- }
114
- if (command === 'check') {
115
- return (await import('./commands/check.js')).runCheck(args, reporter, parsed.lang);
116
- }
117
- if (command === 'classify') {
118
- return (await import('./commands/classify.js')).runClassify(args, reporter, parsed.lang);
119
- }
120
- if (command === 'contract-lint') {
121
- return (await import('./commands/contract-lint.js')).runContractLint(args, reporter, parsed.lang);
122
- }
123
- if (command === 'status') {
124
- return (await import('./commands/status.js')).runStatus(args, reporter, parsed.lang);
125
- }
126
- if (command === 'update') {
127
- return (await import('./commands/update.js')).runUpdate(args, reporter, parsed.lang);
128
- }
129
- if (command === 'upgrade') {
130
- return (await import('./commands/upgrade.js')).runUpgrade(args, reporter, parsed.lang);
131
- }
132
- if (command === 'map') {
133
- return (await import('./commands/map.js')).runMap(args, reporter, parsed.lang);
134
- }
135
- if (command === 'line-endings') {
136
- return (await import('./commands/line-endings.js')).runLineEndings(args, reporter, parsed.lang);
137
- }
138
- if (command === 'run') {
139
- return (await import('./commands/run.js')).runRun(args, reporter, parsed.lang);
140
- }
141
- if (command === 'context') {
142
- return (await import('./commands/context.js')).runContext(args, reporter, parsed.lang);
143
- }
144
- if (command === 'doctor') {
145
- return (await import('./commands/doctor.js')).runDoctor(args, reporter, parsed.lang);
146
- }
147
- if (command === 'docs') {
148
- return (await import('./commands/docs.js')).runDocs(args, reporter, parsed.lang);
149
- }
150
- if (command === 'handoff') {
151
- return (await import('./commands/handoff.js')).runHandoff(args, reporter, parsed.lang);
152
- }
153
- if (command === 'index') {
154
- return (await import('./commands/index.js')).runIndex(args, reporter, parsed.lang);
155
- }
156
- if (command === 'search') {
157
- return (await import('./commands/search.js')).runSearch(args, reporter, parsed.lang);
158
- }
159
- if (command === 'dashboard') {
160
- return (await import('./commands/dashboard.js')).runDashboard(args, reporter, parsed.lang);
161
- }
162
- if (command === 'version-sources') {
163
- return (await import('./commands/version-sources.js')).runVersionSources(args, reporter, parsed.lang);
164
- }
165
- if (command === 'verify') {
166
- return (await import('./commands/verify.js')).runVerify(args, reporter, parsed.lang);
167
- }
168
- if (command === 'explain') {
169
- return (await import('./commands/explain.js')).runExplain(args, reporter, parsed.lang);
170
- }
171
- if (command === 'impact') {
172
- return (await import('./commands/impact.js')).runImpact(args, reporter, parsed.lang);
173
- }
174
- if (command === 'help') {
175
- return (await import('./commands/help.js')).runHelp(args, reporter, parsed.lang);
105
+ const commandId = command === '--version' || command === '-v' ? 'version' : command;
106
+ const commandDefinition = findCommandDefinition(commandId);
107
+ if (commandDefinition) {
108
+ const runner = await commandDefinition.loadRunner();
109
+ return runner(args, reporter, parsed.lang);
176
110
  }
177
111
  reporter.stderr(renderCliError(t(parsed.lang, 'cli.error.unknownCommand', { command }), 'mf --help', parsed.lang));
178
112
  reporter.stdout(getTopLevelHelp(parsed.lang));
@@ -3,120 +3,147 @@ export const COMMAND_DEFINITIONS = [
3
3
  id: 'adapters',
4
4
  usage: 'mf adapters',
5
5
  summaryKey: 'command.adapters.summary',
6
+ loadRunner: async () => (await import('../commands/adapters.js')).runAdapters,
6
7
  },
7
8
  {
8
9
  id: 'init',
9
10
  usage: 'mf init',
10
11
  summaryKey: 'command.init.summary',
12
+ loadRunner: async () => (await import('../commands/init.js')).runInit,
11
13
  },
12
14
  {
13
15
  id: 'check',
14
16
  usage: 'mf check',
15
17
  summaryKey: 'command.check.summary',
18
+ loadRunner: async () => (await import('../commands/check.js')).runCheck,
16
19
  },
17
20
  {
18
21
  id: 'classify',
19
22
  usage: 'mf classify',
20
23
  summaryKey: 'command.classify.summary',
24
+ loadRunner: async () => (await import('../commands/classify.js')).runClassify,
21
25
  },
22
26
  {
23
27
  id: 'contract-lint',
24
28
  usage: 'mf contract-lint',
25
29
  summaryKey: 'command.contractLint.summary',
30
+ loadRunner: async () => (await import('../commands/contract-lint.js')).runContractLint,
26
31
  },
27
32
  {
28
33
  id: 'status',
29
34
  usage: 'mf status',
30
35
  summaryKey: 'command.status.summary',
36
+ loadRunner: async () => (await import('../commands/status.js')).runStatus,
31
37
  },
32
38
  {
33
39
  id: 'update',
34
40
  usage: 'mf update',
35
41
  summaryKey: 'command.update.summary',
42
+ loadRunner: async () => (await import('../commands/update.js')).runUpdate,
36
43
  },
37
44
  {
38
45
  id: 'upgrade',
39
46
  usage: 'mf upgrade',
40
47
  summaryKey: 'command.upgrade.summary',
48
+ loadRunner: async () => (await import('../commands/upgrade.js')).runUpgrade,
41
49
  },
42
50
  {
43
51
  id: 'map',
44
52
  usage: 'mf map',
45
53
  summaryKey: 'command.map.summary',
54
+ loadRunner: async () => (await import('../commands/map.js')).runMap,
46
55
  },
47
56
  {
48
57
  id: 'line-endings',
49
58
  usage: 'mf line-endings',
50
59
  summaryKey: 'command.lineEndings.summary',
60
+ loadRunner: async () => (await import('../commands/line-endings.js')).runLineEndings,
51
61
  },
52
62
  {
53
63
  id: 'run',
54
64
  usage: 'mf run',
55
65
  summaryKey: 'command.run.summary',
66
+ loadRunner: async () => (await import('../commands/run.js')).runRun,
56
67
  },
57
68
  {
58
69
  id: 'context',
59
70
  usage: 'mf context',
60
71
  summaryKey: 'command.context.summary',
72
+ loadRunner: async () => (await import('../commands/context.js')).runContext,
61
73
  },
62
74
  {
63
75
  id: 'doctor',
64
76
  usage: 'mf doctor',
65
77
  summaryKey: 'command.doctor.summary',
78
+ loadRunner: async () => (await import('../commands/doctor.js')).runDoctor,
66
79
  },
67
80
  {
68
81
  id: 'docs',
69
82
  usage: 'mf docs',
70
83
  summaryKey: 'command.docs.summary',
84
+ loadRunner: async () => (await import('../commands/docs.js')).runDocs,
71
85
  },
72
86
  {
73
87
  id: 'handoff',
74
88
  usage: 'mf handoff',
75
89
  summaryKey: 'command.handoff.summary',
90
+ loadRunner: async () => (await import('../commands/handoff.js')).runHandoff,
76
91
  },
77
92
  {
78
93
  id: 'index',
79
94
  usage: 'mf index',
80
95
  summaryKey: 'command.index.summary',
96
+ loadRunner: async () => (await import('../commands/index.js')).runIndex,
81
97
  },
82
98
  {
83
99
  id: 'search',
84
100
  usage: 'mf search',
85
101
  summaryKey: 'command.search.summary',
102
+ loadRunner: async () => (await import('../commands/search.js')).runSearch,
86
103
  },
87
104
  {
88
105
  id: 'dashboard',
89
106
  usage: 'mf dashboard',
90
107
  summaryKey: 'command.dashboard.summary',
108
+ loadRunner: async () => (await import('../commands/dashboard.js')).runDashboard,
91
109
  },
92
110
  {
93
111
  id: 'version',
94
112
  usage: 'mf version',
95
113
  summaryKey: 'command.version.summary',
114
+ loadRunner: async () => (await import('../commands/version.js')).runVersion,
96
115
  },
97
116
  {
98
117
  id: 'version-sources',
99
118
  usage: 'mf version-sources',
100
119
  summaryKey: 'command.versionSources.summary',
120
+ loadRunner: async () => (await import('../commands/version-sources.js')).runVersionSources,
101
121
  },
102
122
  {
103
123
  id: 'verify',
104
124
  usage: 'mf verify',
105
125
  summaryKey: 'command.verify.summary',
126
+ loadRunner: async () => (await import('../commands/verify.js')).runVerify,
106
127
  },
107
128
  {
108
129
  id: 'explain',
109
130
  usage: 'mf explain',
110
131
  summaryKey: 'command.explain.summary',
132
+ loadRunner: async () => (await import('../commands/explain.js')).runExplain,
111
133
  },
112
134
  {
113
135
  id: 'impact',
114
136
  usage: 'mf impact',
115
137
  summaryKey: 'command.impact.summary',
138
+ loadRunner: async () => (await import('../commands/impact.js')).runImpact,
116
139
  },
117
140
  {
118
141
  id: 'help',
119
142
  usage: 'mf help',
120
143
  summaryKey: 'command.help.summary',
144
+ loadRunner: async () => (await import('../commands/help.js')).runHelp,
121
145
  },
122
146
  ];
147
+ export function findCommandDefinition(command) {
148
+ return COMMAND_DEFINITIONS.find((definition) => definition.id === command);
149
+ }
@@ -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,10 +242,15 @@ function getRepoMapConfig(projectRoot) {
240
242
  },
241
243
  };
242
244
  }
243
- function getGitFiles(projectRoot) {
244
- const result = spawnSync('git', ['ls-files', '-z'], {
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 [];
@@ -282,7 +289,7 @@ function listAnchorCandidateFilesRecursive(rootPath, depth, priorityPaths) {
282
289
  }
283
290
  function getRepositoryFiles(projectRoot, depth, priorityPaths) {
284
291
  const files = new Set();
285
- for (const relativePath of getGitFiles(projectRoot)) {
292
+ for (const relativePath of listGitFilesForRepoMap(projectRoot)) {
286
293
  files.add(relativePath);
287
294
  }
288
295
  for (const relativePath of listAnchorCandidateFilesRecursive(projectRoot, depth, priorityPaths)) {
@@ -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
  }
@@ -14,6 +14,7 @@ const CHECK_ISSUE_ID_RULES = [
14
14
  ['mustflow.command_contract.effects_invalid', /^(?:Strict: )?(?:\[commands\.(?:resources|intents\.[^\]]+\.effects)[^\]]*\]|Command effect for intent [^\s]+ must define path, paths, or lock)/u],
15
15
  ['mustflow.command_contract.effect_path_escape', /^Strict: Command effect path must stay inside the current root:/u],
16
16
  ['mustflow.command_contract.shared_writes_without_effects', /^Strict warning: configured agent-runnable intents .+ share path:.+ through writes without explicit effects or resource locks$/u],
17
+ ['mustflow.command_contract.broad_env_inheritance', /^Strict warning: configured agent-runnable intent [^\s]+ (?:implicitly inherits the host environment|uses env_policy = "inherit")/u],
17
18
  ['mustflow.prompt_cache.required', /^Strict: \[prompt_cache\] table is required$/u],
18
19
  ['mustflow.prompt_cache.volatile_in_stable', /^Strict: \[prompt_cache\.layers\.stable\]\.read must not include volatile path /u],
19
20
  ['mustflow.refresh.hash_method_required', /^Strict: \[refresh\]\.default_method should be "hash_check" for cache-friendly refresh$/u],
@@ -1,11 +1,14 @@
1
1
  import { COMMAND_LIFECYCLES, COMMAND_RUN_POLICIES, LONG_RUNNING_LIFECYCLES, isRecord, } from './config-loading.js';
2
- import { COMMAND_ENV_POLICIES } from './command-env.js';
2
+ import { COMMAND_ENV_POLICIES, DEFAULT_COMMAND_ENV_POLICY } from './command-env.js';
3
3
  import { COMMAND_EFFECT_CONCURRENCY, COMMAND_EFFECT_MODES, COMMAND_EFFECT_TYPES, validateCommandEffectLockWarnings, validateCommandEffects, } from './command-effects.js';
4
4
  import { commandIntentBlockedCommandPattern, commandIntentHasBlockedShellBackgroundPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
5
5
  import { MAX_COMMAND_OUTPUT_BYTES, commandMaxOutputBytesLimitMessage } from './command-output-limits.js';
6
6
  function commandContractIssue(message) {
7
7
  return { message };
8
8
  }
9
+ function commandContractWarning(message) {
10
+ return { message, severity: 'warning' };
11
+ }
9
12
  function hasOwn(table, key) {
10
13
  return Object.prototype.hasOwnProperty.call(table, key);
11
14
  }
@@ -198,6 +201,50 @@ function validateCommandIntent(intentName, intent, issues) {
198
201
  }
199
202
  validateCommandIntentEffects(intentName, intent, issues);
200
203
  }
204
+ function readValidCommandEnvPolicy(table) {
205
+ if (!table || !hasOwn(table, 'env_policy')) {
206
+ return undefined;
207
+ }
208
+ const value = table.env_policy;
209
+ return typeof value === 'string' && COMMAND_ENV_POLICIES.has(value)
210
+ ? value
211
+ : undefined;
212
+ }
213
+ function getEffectiveCommandEnvPolicy(defaults, intent) {
214
+ const intentPolicy = readValidCommandEnvPolicy(intent);
215
+ if (intentPolicy) {
216
+ return { policy: intentPolicy, source: 'intent' };
217
+ }
218
+ const defaultPolicy = readValidCommandEnvPolicy(defaults);
219
+ if (defaultPolicy) {
220
+ return { policy: defaultPolicy, source: 'defaults' };
221
+ }
222
+ return { policy: DEFAULT_COMMAND_ENV_POLICY, source: 'implicit' };
223
+ }
224
+ function validateCommandEnvInheritanceWarnings(commandsToml) {
225
+ const issues = [];
226
+ if (!commandsToml || !isRecord(commandsToml.intents)) {
227
+ return issues;
228
+ }
229
+ const defaults = isRecord(commandsToml.defaults) ? commandsToml.defaults : undefined;
230
+ for (const [intentName, intent] of Object.entries(commandsToml.intents)) {
231
+ if (!isRecord(intent) || intent.status !== 'configured' || intent.run_policy !== 'agent_allowed') {
232
+ continue;
233
+ }
234
+ const envPolicy = getEffectiveCommandEnvPolicy(defaults, intent);
235
+ if (envPolicy.policy !== 'inherit') {
236
+ continue;
237
+ }
238
+ const networkScope = intent.network === true ? ' with network = true' : '';
239
+ const migration = 'set env_policy = "minimal" or "allowlist" unless broad host state is required';
240
+ if (envPolicy.source === 'implicit') {
241
+ issues.push(commandContractWarning(`configured agent-runnable intent ${intentName} implicitly inherits the host environment${networkScope}; ${migration}`));
242
+ continue;
243
+ }
244
+ issues.push(commandContractWarning(`configured agent-runnable intent ${intentName} uses env_policy = "inherit"${networkScope}; ${migration}`));
245
+ }
246
+ return issues;
247
+ }
201
248
  /**
202
249
  * mf:anchor core.command-contract-validation
203
250
  * purpose: Validate command intent declarations that gate agent-executable repository commands.
@@ -228,15 +275,18 @@ export function validateCommandContractConfig(commandsToml) {
228
275
  }
229
276
  export function validateCommandContractStrictDefaults(projectRoot, commandsToml) {
230
277
  const issues = [];
231
- if (!commandsToml || !isRecord(commandsToml.defaults)) {
278
+ if (!commandsToml) {
232
279
  return issues;
233
280
  }
234
- if (!hasOwn(commandsToml.defaults, 'max_output_bytes')) {
235
- issues.push(commandContractIssue('[commands.defaults].max_output_bytes is required'));
236
- }
237
- if (!hasOwn(commandsToml.defaults, 'on_timeout')) {
238
- issues.push(commandContractIssue('[commands.defaults].on_timeout is required'));
281
+ if (isRecord(commandsToml.defaults)) {
282
+ if (!hasOwn(commandsToml.defaults, 'max_output_bytes')) {
283
+ issues.push(commandContractIssue('[commands.defaults].max_output_bytes is required'));
284
+ }
285
+ if (!hasOwn(commandsToml.defaults, 'on_timeout')) {
286
+ issues.push(commandContractIssue('[commands.defaults].on_timeout is required'));
287
+ }
239
288
  }
289
+ issues.push(...validateCommandEnvInheritanceWarnings(commandsToml));
240
290
  issues.push(...validateCommandEffects(projectRoot, commandsToml));
241
291
  issues.push(...validateCommandEffectLockWarnings(commandsToml));
242
292
  return issues;
@@ -244,7 +244,8 @@ export function createDashboardCompletionVerdict(input) {
244
244
  const receiptBinding = input.receiptBinding ?? emptyReceiptBindingEvidence();
245
245
  const latestRunFailed = input.latestRunStatus === 'failed' ||
246
246
  input.latestRunStatus === 'timed_out' ||
247
- input.latestRunStatus === 'start_failed';
247
+ input.latestRunStatus === 'start_failed' ||
248
+ input.latestRunStatus === 'output_limit_exceeded';
248
249
  let status = 'unverified';
249
250
  let primaryReason = 'dashboard_does_not_execute_verification';
250
251
  const blockers = [];
@@ -135,7 +135,7 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
135
135
  {
136
136
  id: 'verify-run-manifest',
137
137
  schemaFile: 'verify-run-manifest.schema.json',
138
- producer: '.mustflow/state/runs/verify-latest/manifest.json',
138
+ producer: '.mustflow/state/runs/verify-*/manifest.json',
139
139
  packaged: true,
140
140
  documented: true,
141
141
  },
@@ -1,6 +1,7 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs';
2
1
  import { createHash } from 'node:crypto';
3
2
  import path from 'node:path';
3
+ import { atomicWriteJsonFile, createStateRunId } from './atomic-state-write.js';
4
+ import { decodeUtf8Tail } from './bounded-output.js';
4
5
  import { DEFAULT_RUN_RECEIPT_TAIL_BYTES } from './retention-policy.js';
5
6
  import { redactSecretLikeText } from './secret-redaction.js';
6
7
  const RUN_RECEIPT_SCHEMA_VERSION = '1';
@@ -11,13 +12,7 @@ function toPosixPath(value) {
11
12
  }
12
13
  function truncateTextByBytes(text, maxBytes) {
13
14
  const buffer = Buffer.from(text, 'utf8');
14
- if (buffer.byteLength <= maxBytes) {
15
- return { text, truncated: false };
16
- }
17
- return {
18
- text: buffer.subarray(buffer.byteLength - maxBytes).toString('utf8'),
19
- truncated: true,
20
- };
15
+ return decodeUtf8Tail(buffer, maxBytes);
21
16
  }
22
17
  function recordRedaction(state, field, result) {
23
18
  if (!result.redacted) {
@@ -62,8 +57,11 @@ function summarizeOutput(output, maxOutputBytes, tailBytes, field, state) {
62
57
  redaction_kinds: redaction.redactionKinds,
63
58
  };
64
59
  }
65
- function getReceiptRelativePath() {
66
- return toPosixPath(path.join(RUN_RECEIPT_DIR, LATEST_RUN_RECEIPT));
60
+ export function createRunReceiptRelativePath() {
61
+ return toPosixPath(path.join(RUN_RECEIPT_DIR, createStateRunId('run'), 'receipt.json'));
62
+ }
63
+ function getReceiptRelativePath(receiptPath) {
64
+ return receiptPath ?? toPosixPath(path.join(RUN_RECEIPT_DIR, LATEST_RUN_RECEIPT));
67
65
  }
68
66
  function stableJson(value) {
69
67
  if (Array.isArray(value)) {
@@ -100,6 +98,9 @@ function getErrorKind(status, exitCode) {
100
98
  if (status === 'start_failed') {
101
99
  return 'start_failed';
102
100
  }
101
+ if (status === 'output_limit_exceeded') {
102
+ return 'output_limit_exceeded';
103
+ }
103
104
  if (status === 'failed' && exitCode !== null) {
104
105
  return 'exit_code';
105
106
  }
@@ -240,6 +241,7 @@ export function createRunReceipt(input) {
240
241
  signal: input.signal,
241
242
  error,
242
243
  kill_method: input.killMethod,
244
+ ...(input.termination ? { termination: input.termination } : {}),
243
245
  stdout,
244
246
  stderr,
245
247
  write_drift: input.writeDrift,
@@ -272,12 +274,17 @@ export function createRunReceipt(input) {
272
274
  redaction_kinds: [...redactionState.kinds].sort(),
273
275
  fields: [...redactionState.fields].sort(),
274
276
  },
275
- receipt_path: getReceiptRelativePath(),
277
+ receipt_path: getReceiptRelativePath(input.receiptPath),
276
278
  };
277
279
  }
278
280
  export function writeRunReceipt(projectRoot, receipt) {
279
281
  const receiptDir = path.join(projectRoot, RUN_RECEIPT_DIR);
280
282
  const latestPath = path.join(receiptDir, LATEST_RUN_RECEIPT);
281
- mkdirSync(receiptDir, { recursive: true });
282
- writeFileSync(latestPath, `${JSON.stringify(receipt, null, 2)}\n`);
283
+ const receiptPath = path.resolve(projectRoot, receipt.receipt_path);
284
+ const relativeToRunDir = path.relative(receiptDir, receiptPath);
285
+ if (relativeToRunDir.startsWith('..') || path.isAbsolute(relativeToRunDir)) {
286
+ throw new Error(`Run receipt path must stay inside ${RUN_RECEIPT_DIR}`);
287
+ }
288
+ atomicWriteJsonFile(receiptPath, receipt);
289
+ atomicWriteJsonFile(latestPath, receipt);
283
290
  }