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.
- package/README.md +2 -2
- package/dist/cli/commands/explain-verify.js +2 -2
- package/dist/cli/commands/run/builtin-dispatch.js +92 -0
- package/dist/cli/commands/run/executor.js +112 -0
- package/dist/cli/commands/run/output.js +59 -0
- package/dist/cli/commands/run/process-tree.js +91 -0
- package/dist/cli/commands/run/receipt.js +42 -0
- package/dist/cli/commands/run.js +22 -414
- package/dist/cli/commands/verify/args.js +262 -0
- package/dist/cli/commands/verify.js +106 -263
- package/dist/cli/i18n/en.js +3 -1
- package/dist/cli/i18n/es.js +3 -1
- package/dist/cli/i18n/fr.js +3 -1
- package/dist/cli/i18n/hi.js +3 -1
- package/dist/cli/i18n/ko.js +3 -1
- package/dist/cli/i18n/zh.js +3 -1
- package/dist/cli/index.js +6 -72
- package/dist/cli/lib/command-registry.js +27 -0
- package/dist/cli/lib/repo-map.js +10 -3
- package/dist/core/atomic-state-write.js +31 -0
- package/dist/core/bounded-output.js +23 -1
- package/dist/core/check-issues.js +1 -0
- package/dist/core/command-contract-validation.js +57 -7
- package/dist/core/completion-verdict.js +2 -1
- package/dist/core/public-json-contracts.js +1 -1
- package/dist/core/run-receipt.js +20 -13
- package/dist/core/source-anchors.js +96 -24
- package/dist/core/verification-evidence.js +4 -1
- package/package.json +1 -1
- package/schemas/README.md +1 -1
- package/schemas/run-receipt.schema.json +26 -3
- package/schemas/verify-report.schema.json +1 -1
- package/schemas/verify-run-manifest.schema.json +1 -1
- package/templates/default/i18n.toml +7 -1
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +2 -1
- package/templates/default/locales/en/.mustflow/skills/routes.toml +6 -0
- package/templates/default/locales/en/.mustflow/skills/source-anchor-authoring/SKILL.md +147 -0
- package/templates/default/manifest.toml +8 -1
package/dist/cli/i18n/zh.js
CHANGED
|
@@ -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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return (
|
|
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
|
+
}
|
package/dist/cli/lib/repo-map.js
CHANGED
|
@@ -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
|
|
244
|
-
const
|
|
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
|
|
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:
|
|
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
|
|
278
|
+
if (!commandsToml) {
|
|
232
279
|
return issues;
|
|
233
280
|
}
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
138
|
+
producer: '.mustflow/state/runs/verify-*/manifest.json',
|
|
139
139
|
packaged: true,
|
|
140
140
|
documented: true,
|
|
141
141
|
},
|
package/dist/core/run-receipt.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
66
|
-
return toPosixPath(path.join(RUN_RECEIPT_DIR,
|
|
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
|
-
|
|
282
|
-
|
|
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
|
}
|