mustflow 2.22.4 → 2.22.9
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 +17 -75
- package/dist/cli/commands/classify.js +2 -0
- package/dist/cli/commands/contract-lint.js +2 -2
- package/dist/cli/commands/dashboard.js +23 -75
- package/dist/cli/commands/help.js +8 -9
- package/dist/cli/commands/impact.js +2 -3
- package/dist/cli/commands/init.js +61 -5
- package/dist/cli/commands/run/receipt.js +1 -0
- package/dist/cli/commands/run.js +14 -1
- package/dist/cli/commands/update.js +2 -2
- package/dist/cli/commands/verify/evidence-input.js +269 -0
- package/dist/cli/commands/verify/input.js +212 -0
- package/dist/cli/commands/verify.js +23 -482
- package/dist/cli/commands/version-sources.js +2 -3
- package/dist/cli/i18n/en.js +5 -0
- package/dist/cli/i18n/es.js +5 -0
- package/dist/cli/i18n/fr.js +5 -0
- package/dist/cli/i18n/hi.js +5 -0
- package/dist/cli/i18n/ko.js +5 -0
- package/dist/cli/i18n/zh.js +5 -0
- package/dist/cli/lib/agent-context.js +6 -11
- package/dist/cli/lib/dashboard-export.js +2 -0
- package/dist/cli/lib/dashboard-mutations.js +79 -0
- package/dist/cli/lib/local-index/command-effect-index.js +25 -0
- package/dist/cli/lib/local-index/hashing.js +7 -0
- package/dist/cli/lib/local-index/index.js +127 -823
- package/dist/cli/lib/local-index/source-index.js +137 -0
- package/dist/cli/lib/local-index/verification-evidence.js +451 -0
- package/dist/cli/lib/local-index/workflow-documents.js +204 -0
- package/dist/cli/lib/mustflow-read.js +41 -0
- package/dist/cli/lib/project-root.js +1 -2
- package/dist/cli/lib/repo-map.js +65 -16
- package/dist/cli/lib/run-root-trust.js +27 -0
- package/dist/cli/lib/templates.js +124 -8
- package/dist/cli/lib/toml.js +6 -1
- package/dist/cli/lib/validation/constants.js +2 -0
- package/dist/cli/lib/validation/index.js +291 -22
- package/dist/cli/lib/validation/primitives.js +2 -2
- package/dist/cli/lib/validation/test-selection.js +2 -2
- package/dist/core/bounded-output.js +32 -7
- package/dist/core/change-classification-policy.js +47 -0
- package/dist/core/change-classification.js +10 -43
- package/dist/core/check-issues.js +7 -1
- package/dist/core/command-contract-validation.js +28 -4
- package/dist/core/command-env.js +1 -1
- package/dist/core/config-loading.js +9 -3
- package/dist/core/contract-lint.js +8 -3
- package/dist/core/correlation-id.js +16 -0
- package/dist/core/run-receipt.js +1 -0
- package/dist/core/safe-filesystem.js +11 -4
- package/dist/core/skill-route-alignment.js +1 -0
- package/dist/core/skill-route-explanation.js +9 -3
- package/dist/core/test-selection.js +2 -3
- package/dist/core/verification-scheduler.js +7 -6
- package/dist/core/version-sources.js +2 -3
- package/package.json +4 -1
- package/schemas/README.md +4 -0
- package/schemas/change-verification-report.schema.json +4 -0
- package/schemas/classify-report.schema.json +4 -0
- package/schemas/commands.schema.json +1 -0
- package/schemas/dashboard-export.schema.json +4 -0
- package/schemas/latest-run-pointer.schema.json +4 -0
- package/schemas/run-receipt.schema.json +4 -0
- package/schemas/verify-report.schema.json +4 -0
- package/schemas/verify-run-manifest.schema.json +4 -0
- package/templates/default/i18n.toml +3 -3
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +10 -6
- package/templates/default/locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md +25 -2
- package/templates/default/locales/en/.mustflow/skills/routes.toml +2 -2
- package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +9 -1
- package/templates/default/locales/en/.mustflow/skills/test-design-guard/SKILL.md +9 -1
- package/templates/default/manifest.toml +1 -1
package/README.md
CHANGED
|
@@ -147,7 +147,9 @@ mustflow is not an automatic project editor and is not tied to a single agent pr
|
|
|
147
147
|
|
|
148
148
|
## Installed files
|
|
149
149
|
|
|
150
|
-
`mf init` installs only the agent workflow into the current directory.
|
|
150
|
+
`mf init` installs only the agent workflow into the current directory. The exact skill files depend
|
|
151
|
+
on the selected profile; run `mf init --dry-run --profile <profile>` to preview the concrete plan
|
|
152
|
+
for a project before writing files.
|
|
151
153
|
|
|
152
154
|
```text
|
|
153
155
|
your-project/
|
|
@@ -166,83 +168,15 @@ your-project/
|
|
|
166
168
|
│ └─ agent-workflow.md
|
|
167
169
|
└─ skills/
|
|
168
170
|
├─ INDEX.md
|
|
169
|
-
├─
|
|
170
|
-
|
|
171
|
-
├─ behavior-preserving-refactor/
|
|
172
|
-
│ └─ SKILL.md
|
|
173
|
-
├─ code-review/
|
|
174
|
-
│ └─ SKILL.md
|
|
175
|
-
├─ codebase-orientation/
|
|
176
|
-
│ └─ SKILL.md
|
|
177
|
-
├─ contract-sync-check/
|
|
178
|
-
│ └─ SKILL.md
|
|
179
|
-
├─ date-number-audit/
|
|
180
|
-
│ └─ SKILL.md
|
|
181
|
-
├─ database-change-safety/
|
|
182
|
-
│ └─ SKILL.md
|
|
183
|
-
├─ dependency-reality-check/
|
|
184
|
-
│ └─ SKILL.md
|
|
185
|
-
├─ diff-risk-review/
|
|
186
|
-
│ └─ SKILL.md
|
|
187
|
-
├─ docs-prose-review/
|
|
188
|
-
│ └─ SKILL.md
|
|
189
|
-
├─ docs-update/
|
|
190
|
-
│ └─ SKILL.md
|
|
191
|
-
├─ external-prompt-injection-defense/
|
|
192
|
-
│ └─ SKILL.md
|
|
193
|
-
├─ external-skill-intake/
|
|
194
|
-
│ └─ SKILL.md
|
|
195
|
-
├─ failure-triage/
|
|
196
|
-
│ └─ SKILL.md
|
|
197
|
-
├─ instruction-conflict-scope-check/
|
|
198
|
-
│ └─ SKILL.md
|
|
199
|
-
├─ migration-safety-check/
|
|
200
|
-
│ └─ SKILL.md
|
|
201
|
-
├─ multi-agent-work-coordination/
|
|
202
|
-
│ └─ SKILL.md
|
|
203
|
-
├─ performance-budget-check/
|
|
204
|
-
│ └─ SKILL.md
|
|
205
|
-
├─ project-context-authoring/
|
|
206
|
-
│ └─ SKILL.md
|
|
207
|
-
├─ pattern-scout/
|
|
208
|
-
│ └─ SKILL.md
|
|
209
|
-
├─ repo-improvement-loop/
|
|
210
|
-
│ └─ SKILL.md
|
|
211
|
-
├─ requirement-regression-guard/
|
|
212
|
-
│ └─ SKILL.md
|
|
213
|
-
├─ repro-first-debug/
|
|
214
|
-
│ └─ SKILL.md
|
|
215
|
-
├─ security-privacy-review/
|
|
216
|
-
│ └─ SKILL.md
|
|
217
|
-
├─ source-freshness-check/
|
|
218
|
-
│ └─ SKILL.md
|
|
219
|
-
├─ structure-discovery-gate/
|
|
220
|
-
│ └─ SKILL.md
|
|
221
|
-
├─ security-regression-tests/
|
|
222
|
-
│ └─ SKILL.md
|
|
223
|
-
├─ skill-authoring/
|
|
224
|
-
│ └─ SKILL.md
|
|
225
|
-
├─ test-design-guard/
|
|
226
|
-
│ └─ SKILL.md
|
|
227
|
-
├─ test-maintenance/
|
|
228
|
-
│ └─ SKILL.md
|
|
229
|
-
├─ vertical-slice-tdd/
|
|
230
|
-
│ └─ SKILL.md
|
|
231
|
-
├─ llm-service-ux-review/
|
|
232
|
-
│ └─ SKILL.md
|
|
233
|
-
├─ search-ad-content-authoring/
|
|
234
|
-
│ └─ SKILL.md
|
|
235
|
-
├─ ui-quality-gate/
|
|
236
|
-
│ └─ SKILL.md
|
|
237
|
-
├─ visual-review-artifact/
|
|
238
|
-
│ ├─ SKILL.md
|
|
239
|
-
│ ├─ resources.toml
|
|
240
|
-
│ └─ assets/
|
|
241
|
-
│ └─ review-template.html
|
|
242
|
-
└─ web-asset-optimization/
|
|
171
|
+
├─ routes.toml
|
|
172
|
+
└─ <profile-selected-skill>/
|
|
243
173
|
└─ SKILL.md
|
|
244
174
|
```
|
|
245
175
|
|
|
176
|
+
Profiles select the installed skill surface. The package can include optional skill files that are
|
|
177
|
+
not copied into every project profile. Non-English workflow documents are localized when available;
|
|
178
|
+
skill procedures currently fall back to the canonical English skill files.
|
|
179
|
+
|
|
246
180
|
The default template does not create project-owned root documents or contract files such as `README.md`, `PROJECT.md`, `ROADMAP.md`, `DESIGN.md`, `GOVERNANCE.md`, `TESTING.md`, `API.md`, `project.contract.json`, or `openapi.yaml`. It also does not create CI configuration, general `docs/`, or general `skills/`. User projects may already use those names for their own files.
|
|
247
181
|
|
|
248
182
|
`mf init` creates `.gitignore` if it is missing. If `.gitignore` exists, mustflow updates only its managed block and preserves user rules.
|
|
@@ -433,6 +367,12 @@ bun run docs:check
|
|
|
433
367
|
bun run check:install
|
|
434
368
|
```
|
|
435
369
|
|
|
370
|
+
When Bun is not available, maintainers can still run the core CLI and package metadata checks with Node/npm:
|
|
371
|
+
|
|
372
|
+
```sh
|
|
373
|
+
npm run check:core:node
|
|
374
|
+
```
|
|
375
|
+
|
|
436
376
|
Agents working in this repository should prefer the configured mustflow intents for routine verification.
|
|
437
377
|
|
|
438
378
|
```sh
|
|
@@ -442,6 +382,7 @@ mf run test_related
|
|
|
442
382
|
mf run test
|
|
443
383
|
mf run test_coverage
|
|
444
384
|
mf run test_release
|
|
385
|
+
mf run maintainer_check_node
|
|
445
386
|
mf run docs_validate_fast
|
|
446
387
|
mf run docs_validate
|
|
447
388
|
mf run mustflow_check
|
|
@@ -479,6 +420,7 @@ The npm package includes only:
|
|
|
479
420
|
dist/
|
|
480
421
|
templates/
|
|
481
422
|
schemas/
|
|
423
|
+
examples/
|
|
482
424
|
README.md
|
|
483
425
|
LICENSE
|
|
484
426
|
```
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { createChangeClassificationReport, } from '../../core/change-classification.js';
|
|
3
|
+
import { createCorrelationId } from '../../core/correlation-id.js';
|
|
3
4
|
import { writeJsonFileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
|
|
4
5
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
5
6
|
import { requireGitChangedFiles } from '../lib/git-changes.js';
|
|
@@ -71,6 +72,7 @@ export function createClassifyOutput(projectRoot, source, paths) {
|
|
|
71
72
|
return {
|
|
72
73
|
schema_version: CLASSIFY_SCHEMA_VERSION,
|
|
73
74
|
command: 'classify',
|
|
75
|
+
correlation_id: createCorrelationId('classify'),
|
|
74
76
|
mustflow_root: projectRoot,
|
|
75
77
|
...createChangeClassificationReport(source, files),
|
|
76
78
|
};
|
|
@@ -2,11 +2,11 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { lintCommandContract } from '../../core/contract-lint.js';
|
|
4
4
|
import { readCommandContract, isRecord } from '../../core/config-loading.js';
|
|
5
|
-
import { readTomlFile } from '../../core/toml.js';
|
|
6
5
|
import { releaseVersioningIsEnabled } from '../../core/version-sources.js';
|
|
7
6
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
8
7
|
import { t } from '../lib/i18n.js';
|
|
9
8
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
9
|
+
import { readMustflowTomlFile } from '../lib/toml.js';
|
|
10
10
|
const CONTRACT_LINT_SCHEMA_VERSION = '1';
|
|
11
11
|
export function getContractLintHelp(lang = 'en') {
|
|
12
12
|
return renderHelp({
|
|
@@ -30,7 +30,7 @@ function readPreferences(projectRoot) {
|
|
|
30
30
|
if (!existsSync(preferencesPath)) {
|
|
31
31
|
return undefined;
|
|
32
32
|
}
|
|
33
|
-
const preferences =
|
|
33
|
+
const preferences = readMustflowTomlFile(projectRoot, '.mustflow/config/preferences.toml');
|
|
34
34
|
return isRecord(preferences) ? preferences : undefined;
|
|
35
35
|
}
|
|
36
36
|
function createContractLintOutput(projectRoot, coverage, suggest) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
3
|
import http from 'node:http';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
5
|
+
import { openUrlInBrowser } from '../lib/browser-open.js';
|
|
6
6
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
7
7
|
import { renderDashboardHtml, } from '../lib/dashboard-html.js';
|
|
8
8
|
import { DashboardExportPathError, writeDashboardExport, } from '../lib/dashboard-export.js';
|
|
@@ -12,12 +12,14 @@ import { parseSkillIndexRoutes } from '../../core/skill-route-alignment.js';
|
|
|
12
12
|
import { getAgentContext } from '../lib/agent-context.js';
|
|
13
13
|
import { readGitChangedFiles } from '../lib/git-changes.js';
|
|
14
14
|
import { isRecord, readCommandContract, readPositiveInteger, readString, readStringArray, } from '../lib/command-contract.js';
|
|
15
|
-
import { readDashboardPreferences,
|
|
16
|
-
import { DOC_REVIEW_LEDGER_RELATIVE_PATH, isDocReviewStatus,
|
|
15
|
+
import { readDashboardPreferences, } from '../lib/dashboard-preferences.js';
|
|
16
|
+
import { DOC_REVIEW_LEDGER_RELATIVE_PATH, isDocReviewStatus, listDocReviewEntries, } from '../lib/doc-review-ledger.js';
|
|
17
|
+
import { markDashboardDocReviewFromPayload, openDashboardMustflowFolder, updateDashboardPreferencesFromPayload, } from '../lib/dashboard-mutations.js';
|
|
17
18
|
import { MANIFEST_LOCK_RELATIVE_PATH, inspectManifestLock } from '../lib/manifest-lock.js';
|
|
18
19
|
import { getLocalIndexDatabasePath, readLatestLocalVerificationReadModelQueries, readLocalCommandEffectGraphs, } from '../lib/local-index.js';
|
|
19
20
|
import { readPackageMetadata } from '../lib/package-info.js';
|
|
20
21
|
import { t } from '../lib/i18n.js';
|
|
22
|
+
import { MUSTFLOW_JSON_MAX_BYTES, readMustflowTextFile, readMustflowTextFileIfExists, } from '../lib/mustflow-read.js';
|
|
21
23
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
22
24
|
import { detectVersionSources } from '../../core/version-sources.js';
|
|
23
25
|
import { planUpdate, summarizePlan } from './update.js';
|
|
@@ -25,7 +27,6 @@ const DEFAULT_DASHBOARD_HOST = '127.0.0.1';
|
|
|
25
27
|
const DEFAULT_DASHBOARD_PORT = 0;
|
|
26
28
|
const MAX_REQUEST_BYTES = 64 * 1024;
|
|
27
29
|
const LOCAL_DASHBOARD_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
|
|
28
|
-
const DOC_REVIEW_BULK_PAYLOAD_FIELDS = ['paths', 'documents', 'entries'];
|
|
29
30
|
const RELEASE_FILE_PATTERNS = [
|
|
30
31
|
/^package\.json$/u,
|
|
31
32
|
/^bun\.lockb?$/u,
|
|
@@ -39,6 +40,7 @@ const LATEST_RUN_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
|
|
|
39
40
|
const COMMANDS_RELATIVE_PATH = '.mustflow/config/commands.toml';
|
|
40
41
|
const AGENTS_RELATIVE_PATH = 'AGENTS.md';
|
|
41
42
|
const STATUS_BLOCK_CACHE_TTL_MS = 750;
|
|
43
|
+
const STATUS_SIGNATURE_HASH_MAX_BYTES = 1024 * 1024;
|
|
42
44
|
const dashboardStatusBlockCache = new Map();
|
|
43
45
|
function dashboardStatusBlockCacheKey(projectRoot, blockName) {
|
|
44
46
|
return `${path.resolve(projectRoot)}\0${blockName}`;
|
|
@@ -46,7 +48,11 @@ function dashboardStatusBlockCacheKey(projectRoot, blockName) {
|
|
|
46
48
|
function readFileSignature(filePath) {
|
|
47
49
|
try {
|
|
48
50
|
const stat = statSync(filePath);
|
|
49
|
-
|
|
51
|
+
if (!stat.isFile() || stat.size > STATUS_SIGNATURE_HASH_MAX_BYTES) {
|
|
52
|
+
return `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}:unhashed`;
|
|
53
|
+
}
|
|
54
|
+
const digest = createHash('sha256').update(readFileSync(filePath)).digest('hex');
|
|
55
|
+
return `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}:${digest}`;
|
|
50
56
|
}
|
|
51
57
|
catch {
|
|
52
58
|
return 'missing';
|
|
@@ -317,66 +323,6 @@ async function readRequestJson(request) {
|
|
|
317
323
|
const rawBody = Buffer.concat(chunks).toString('utf8');
|
|
318
324
|
return rawBody.trim().length === 0 ? {} : JSON.parse(rawBody);
|
|
319
325
|
}
|
|
320
|
-
function readUpdatePayload(value) {
|
|
321
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
322
|
-
throw new Error('Request body must be a JSON object.');
|
|
323
|
-
}
|
|
324
|
-
const updates = value.updates;
|
|
325
|
-
if (!Array.isArray(updates)) {
|
|
326
|
-
throw new Error('Request body must include an updates array.');
|
|
327
|
-
}
|
|
328
|
-
return updates.map((entry) => {
|
|
329
|
-
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
330
|
-
throw new Error('Each update must be a JSON object.');
|
|
331
|
-
}
|
|
332
|
-
const update = entry;
|
|
333
|
-
if (typeof update.id !== 'string' || update.id.trim().length === 0) {
|
|
334
|
-
throw new Error('Each update must include an id.');
|
|
335
|
-
}
|
|
336
|
-
return { id: update.id, value: update.value };
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
function readOptionalStringField(value, key) {
|
|
340
|
-
const field = value[key];
|
|
341
|
-
return typeof field === 'string' && field.trim().length > 0 ? field.trim() : undefined;
|
|
342
|
-
}
|
|
343
|
-
function readRequiredStringField(value, key) {
|
|
344
|
-
const field = readOptionalStringField(value, key);
|
|
345
|
-
if (!field) {
|
|
346
|
-
throw new Error(`${key} is required.`);
|
|
347
|
-
}
|
|
348
|
-
return field;
|
|
349
|
-
}
|
|
350
|
-
function readDocReviewPayload(value) {
|
|
351
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
352
|
-
throw new Error('Request body must be a JSON object.');
|
|
353
|
-
}
|
|
354
|
-
const payload = value;
|
|
355
|
-
for (const field of DOC_REVIEW_BULK_PAYLOAD_FIELDS) {
|
|
356
|
-
if (field in payload) {
|
|
357
|
-
throw new Error('Bulk documentation review updates require a separate confirmed flow.');
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
const status = readRequiredStringField(payload, 'status');
|
|
361
|
-
if (status !== 'approved' && status !== 'needs_human' && status !== 'ignored') {
|
|
362
|
-
throw new Error('status must be approved, needs_human, or ignored.');
|
|
363
|
-
}
|
|
364
|
-
const reviewerKind = readRequiredStringField(payload, 'reviewerKind');
|
|
365
|
-
if (!isReviewerKind(reviewerKind)) {
|
|
366
|
-
throw new Error('reviewerKind must be human, llm, tool, or external.');
|
|
367
|
-
}
|
|
368
|
-
return {
|
|
369
|
-
path: readRequiredStringField(payload, 'path'),
|
|
370
|
-
status,
|
|
371
|
-
reviewerKind,
|
|
372
|
-
reviewerId: readRequiredStringField(payload, 'reviewerId'),
|
|
373
|
-
reviewerLabel: readOptionalStringField(payload, 'reviewerLabel'),
|
|
374
|
-
reviewerProvider: readOptionalStringField(payload, 'reviewerProvider'),
|
|
375
|
-
reviewerModel: readOptionalStringField(payload, 'reviewerModel'),
|
|
376
|
-
reviewerCommandIntent: readOptionalStringField(payload, 'reviewerCommandIntent'),
|
|
377
|
-
summary: readOptionalStringField(payload, 'summary'),
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
326
|
function renderDocReviewResponse(projectRoot, requestUrl) {
|
|
381
327
|
const status = requestUrl.searchParams.get('status');
|
|
382
328
|
if (status && !isDocReviewStatus(status)) {
|
|
@@ -590,10 +536,12 @@ function renderSkillsResponse(projectRoot) {
|
|
|
590
536
|
routes: [],
|
|
591
537
|
};
|
|
592
538
|
}
|
|
593
|
-
const
|
|
539
|
+
const indexContent = readMustflowTextFile(projectRoot, SKILL_INDEX_RELATIVE_PATH);
|
|
540
|
+
const routes = parseSkillIndexRoutes(indexContent).map((route) => {
|
|
594
541
|
const skillPath = path.join(projectRoot, ...route.skillPath.split('/'));
|
|
595
|
-
const
|
|
596
|
-
const
|
|
542
|
+
const skillContent = readMustflowTextFileIfExists(projectRoot, route.skillPath);
|
|
543
|
+
const exists = skillContent !== null && existsSync(skillPath);
|
|
544
|
+
const declaredCommandIntents = skillContent ? readFrontmatterList(skillContent, 'command_intents') : [];
|
|
597
545
|
const sortedRouteIntents = [...route.commandIntents].sort((left, right) => left.localeCompare(right));
|
|
598
546
|
const sortedDeclaredIntents = [...declaredCommandIntents].sort((left, right) => left.localeCompare(right));
|
|
599
547
|
return {
|
|
@@ -686,7 +634,7 @@ function renderRunHistoryResponse(projectRoot) {
|
|
|
686
634
|
};
|
|
687
635
|
}
|
|
688
636
|
try {
|
|
689
|
-
const receipt = JSON.parse(
|
|
637
|
+
const receipt = JSON.parse(readMustflowTextFile(projectRoot, LATEST_RUN_RELATIVE_PATH, { maxBytes: MUSTFLOW_JSON_MAX_BYTES }));
|
|
690
638
|
if (!isRecord(receipt)) {
|
|
691
639
|
throw new Error('Run receipt must be a JSON object.');
|
|
692
640
|
}
|
|
@@ -850,7 +798,7 @@ export async function runDashboard(args, reporter, lang = 'en') {
|
|
|
850
798
|
}
|
|
851
799
|
if (request.method === 'POST') {
|
|
852
800
|
const body = await readRequestJson(request);
|
|
853
|
-
sendJson(response, 200,
|
|
801
|
+
sendJson(response, 200, updateDashboardPreferencesFromPayload(projectRoot, body));
|
|
854
802
|
return;
|
|
855
803
|
}
|
|
856
804
|
}
|
|
@@ -863,12 +811,12 @@ export async function runDashboard(args, reporter, lang = 'en') {
|
|
|
863
811
|
sendText(response, 405, 'Method not allowed');
|
|
864
812
|
return;
|
|
865
813
|
}
|
|
866
|
-
const
|
|
867
|
-
if (
|
|
814
|
+
const openResult = openDashboardMustflowFolder(projectRoot);
|
|
815
|
+
if (openResult === 'missing') {
|
|
868
816
|
sendText(response, 404, '.mustflow folder not found');
|
|
869
817
|
return;
|
|
870
818
|
}
|
|
871
|
-
if (
|
|
819
|
+
if (openResult === 'unavailable') {
|
|
872
820
|
sendText(response, 500, 'No file manager opener is available for this platform');
|
|
873
821
|
return;
|
|
874
822
|
}
|
|
@@ -886,7 +834,7 @@ export async function runDashboard(args, reporter, lang = 'en') {
|
|
|
886
834
|
}
|
|
887
835
|
if (request.method === 'POST') {
|
|
888
836
|
const body = await readRequestJson(request);
|
|
889
|
-
|
|
837
|
+
markDashboardDocReviewFromPayload(projectRoot, body);
|
|
890
838
|
sendJson(response, 200, renderDocReviewResponse(projectRoot, requestUrl));
|
|
891
839
|
return;
|
|
892
840
|
}
|
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
1
|
import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.js';
|
|
4
2
|
import { t } from '../lib/i18n.js';
|
|
3
|
+
import { readMustflowTextFileIfExists } from '../lib/mustflow-read.js';
|
|
5
4
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
6
|
-
import {
|
|
5
|
+
import { readMustflowTomlFile } from '../lib/toml.js';
|
|
7
6
|
function isRecord(value) {
|
|
8
7
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
8
|
}
|
|
10
9
|
function readTextIfExists(projectRoot, relativePath) {
|
|
11
|
-
|
|
12
|
-
return existsSync(filePath) ? readFileSync(filePath, 'utf8') : undefined;
|
|
10
|
+
return readMustflowTextFileIfExists(projectRoot, relativePath) ?? undefined;
|
|
13
11
|
}
|
|
14
12
|
function readTomlIfExists(projectRoot, relativePath) {
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
try {
|
|
14
|
+
const parsed = readMustflowTomlFile(projectRoot, relativePath);
|
|
15
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
17
18
|
return undefined;
|
|
18
19
|
}
|
|
19
|
-
const parsed = readTomlFile(filePath);
|
|
20
|
-
return isRecord(parsed) ? parsed : undefined;
|
|
21
20
|
}
|
|
22
21
|
function renderMissing(relativePath, lang) {
|
|
23
22
|
return t(lang, 'help.missingFile', { path: relativePath });
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import { createChangeClassificationReport } from '../../core/change-classification.js';
|
|
3
2
|
import { summarizeVersionImpact } from '../../core/version-impact.js';
|
|
4
3
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
@@ -6,7 +5,7 @@ import { isRecord } from '../lib/command-contract.js';
|
|
|
6
5
|
import { requireGitChangedFiles } from '../lib/git-changes.js';
|
|
7
6
|
import { t } from '../lib/i18n.js';
|
|
8
7
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
9
|
-
import {
|
|
8
|
+
import { readMustflowTomlFile } from '../lib/toml.js';
|
|
10
9
|
import { detectVersionSources, releaseVersioningIsEnabled, } from '../../core/version-sources.js';
|
|
11
10
|
const IMPACT_SCHEMA_VERSION = '1';
|
|
12
11
|
export function getImpactHelp(lang = 'en') {
|
|
@@ -47,7 +46,7 @@ function parseImpactArgs(args) {
|
|
|
47
46
|
}
|
|
48
47
|
function readPreferences(projectRoot) {
|
|
49
48
|
try {
|
|
50
|
-
const preferences =
|
|
49
|
+
const preferences = readMustflowTomlFile(projectRoot, '.mustflow/config/preferences.toml');
|
|
51
50
|
return isRecord(preferences) ? preferences : undefined;
|
|
52
51
|
}
|
|
53
52
|
catch {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { stdin as processStdin, stdout as processStdout } from 'node:process';
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
@@ -13,6 +13,9 @@ const MUSTFLOW_BLOCK_START = '<!-- mustflow:start schema=1 -->';
|
|
|
13
13
|
const MUSTFLOW_BLOCK_END = '<!-- mustflow:end -->';
|
|
14
14
|
const GITIGNORE_RELATIVE_PATH = '.gitignore';
|
|
15
15
|
const GITIGNORE_FRAGMENT_RELATIVE_PATH = 'gitignore.mustflow';
|
|
16
|
+
const NON_INTERACTIVE_PROMPT_MAX_BYTES = 16 * 1024;
|
|
17
|
+
const NON_INTERACTIVE_PROMPT_MAX_RESPONSES = 64;
|
|
18
|
+
const NON_INTERACTIVE_PROMPT_READ_CHUNK_BYTES = 4096;
|
|
16
19
|
const LOCALE_LABELS = {
|
|
17
20
|
en: 'English',
|
|
18
21
|
ko: 'Korean',
|
|
@@ -473,9 +476,52 @@ function formatLocaleChoice(locale) {
|
|
|
473
476
|
const label = LOCALE_LABELS[locale] ?? locale;
|
|
474
477
|
return `${label} (${locale})`;
|
|
475
478
|
}
|
|
476
|
-
function
|
|
479
|
+
function readNonInteractivePromptInput() {
|
|
480
|
+
const chunks = [];
|
|
481
|
+
let totalBytes = 0;
|
|
482
|
+
for (;;) {
|
|
483
|
+
const remainingBudget = NON_INTERACTIVE_PROMPT_MAX_BYTES + 1 - totalBytes;
|
|
484
|
+
if (remainingBudget <= 0) {
|
|
485
|
+
return {
|
|
486
|
+
lines: [],
|
|
487
|
+
errorKey: 'init.error.promptInputTooLarge',
|
|
488
|
+
limit: NON_INTERACTIVE_PROMPT_MAX_BYTES,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const buffer = Buffer.alloc(Math.min(NON_INTERACTIVE_PROMPT_READ_CHUNK_BYTES, remainingBudget));
|
|
492
|
+
const bytesRead = readSync(0, buffer, 0, buffer.length, null);
|
|
493
|
+
if (bytesRead === 0) {
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
totalBytes += bytesRead;
|
|
497
|
+
chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
|
|
498
|
+
if (totalBytes > NON_INTERACTIVE_PROMPT_MAX_BYTES) {
|
|
499
|
+
return {
|
|
500
|
+
lines: [],
|
|
501
|
+
errorKey: 'init.error.promptInputTooLarge',
|
|
502
|
+
limit: NON_INTERACTIVE_PROMPT_MAX_BYTES,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const lines = Buffer.concat(chunks, totalBytes).toString('utf8').split(/\r?\n/u);
|
|
507
|
+
if (lines.length > NON_INTERACTIVE_PROMPT_MAX_RESPONSES) {
|
|
508
|
+
return {
|
|
509
|
+
lines: [],
|
|
510
|
+
errorKey: 'init.error.promptInputTooManyResponses',
|
|
511
|
+
limit: NON_INTERACTIVE_PROMPT_MAX_RESPONSES,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
return { lines };
|
|
515
|
+
}
|
|
516
|
+
function createPromptReader(reporter, lang) {
|
|
477
517
|
if (!processStdin.isTTY) {
|
|
478
|
-
const
|
|
518
|
+
const input = readNonInteractivePromptInput();
|
|
519
|
+
if (input.errorKey) {
|
|
520
|
+
const paramName = input.errorKey === 'init.error.promptInputTooLarge' ? 'maxBytes' : 'maxResponses';
|
|
521
|
+
reporter.stderr(t(lang, input.errorKey, { [paramName]: input.limit }));
|
|
522
|
+
return undefined;
|
|
523
|
+
}
|
|
524
|
+
const lines = [...input.lines];
|
|
479
525
|
return {
|
|
480
526
|
async question(prompt) {
|
|
481
527
|
reporter.stdout(prompt.trimEnd());
|
|
@@ -553,7 +599,10 @@ function addPromptedPreferenceOverride(overrides, key, value, reporter, lang) {
|
|
|
553
599
|
}
|
|
554
600
|
}
|
|
555
601
|
async function promptInitOptions(template, options, reporter, lang) {
|
|
556
|
-
const reader = createPromptReader(reporter);
|
|
602
|
+
const reader = createPromptReader(reporter, lang);
|
|
603
|
+
if (!reader) {
|
|
604
|
+
return undefined;
|
|
605
|
+
}
|
|
557
606
|
try {
|
|
558
607
|
const preferenceOverrides = [...options.preferenceOverrides];
|
|
559
608
|
const localeChoices = template.manifest.locales.map((locale) => ({
|
|
@@ -916,7 +965,14 @@ export async function runInit(args, reporter, lang = 'en') {
|
|
|
916
965
|
reporter.stderr(error instanceof Error ? error.message : String(error));
|
|
917
966
|
return 1;
|
|
918
967
|
}
|
|
919
|
-
|
|
968
|
+
let options = parsedOptions;
|
|
969
|
+
if (shouldPromptForInit(args, parsedOptions)) {
|
|
970
|
+
const promptedOptions = await promptInitOptions(template, parsedOptions, reporter, lang);
|
|
971
|
+
if (!promptedOptions) {
|
|
972
|
+
return 1;
|
|
973
|
+
}
|
|
974
|
+
options = promptedOptions;
|
|
975
|
+
}
|
|
920
976
|
const selectedLocale = options.locale ?? template.manifest.defaultLocale;
|
|
921
977
|
if (!validateInitSelection(template, options, reporter, lang)) {
|
|
922
978
|
return 1;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRunReceipt, createRunReceiptRelativePath, } from '../../../core/run-receipt.js';
|
|
2
2
|
export function assembleRunReceipt(input) {
|
|
3
3
|
return createRunReceipt({
|
|
4
|
+
correlationId: input.correlationId,
|
|
4
5
|
intent: input.intentName,
|
|
5
6
|
status: input.runStatus,
|
|
6
7
|
timedOut: input.runStatus === 'timed_out',
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks';
|
|
2
2
|
import { createCommandEnv } from '../../core/command-env.js';
|
|
3
|
+
import { createCorrelationId } from '../../core/correlation-id.js';
|
|
3
4
|
import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.js';
|
|
4
5
|
import { readCommandContract, readMustflowConfigIfExists } from '../../core/config-loading.js';
|
|
5
6
|
import { resolveRunReceiptRetentionPolicy } from '../../core/retention-policy.js';
|
|
6
7
|
import { t } from '../lib/i18n.js';
|
|
7
8
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
9
|
+
import { ALLOW_UNTRUSTED_ROOT_OPTION, assessRunRootTrust } from '../lib/run-root-trust.js';
|
|
8
10
|
import { createRunPlan, createRunPreview, renderRunPreviewText, } from '../lib/run-plan.js';
|
|
9
11
|
import { writeRunReceipt, } from '../../core/run-receipt.js';
|
|
10
12
|
import { recordRunPerformanceHistory } from '../../core/run-performance-history.js';
|
|
@@ -92,6 +94,7 @@ export function getRunHelp(lang = 'en') {
|
|
|
92
94
|
{ label: '--dry-run', description: t(lang, 'run.help.option.dryRun') },
|
|
93
95
|
{ label: '--plan-only', description: t(lang, 'run.help.option.planOnly') },
|
|
94
96
|
{ label: '--json', description: t(lang, 'run.help.option.json') },
|
|
97
|
+
{ label: ALLOW_UNTRUSTED_ROOT_OPTION, description: t(lang, 'run.help.option.allowUntrustedRoot') },
|
|
95
98
|
{ label: '-h, --help', description: t(lang, 'cli.option.help') },
|
|
96
99
|
],
|
|
97
100
|
examples: ['mf run test', 'mf run lint --json', 'mf run mustflow_check --dry-run --json'],
|
|
@@ -121,7 +124,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
121
124
|
reporter.stdout(getRunHelp(lang));
|
|
122
125
|
return 0;
|
|
123
126
|
}
|
|
124
|
-
const supportedOptions = new Set(['--json', '--dry-run', '--plan-only']);
|
|
127
|
+
const supportedOptions = new Set(['--json', '--dry-run', '--plan-only', ALLOW_UNTRUSTED_ROOT_OPTION]);
|
|
125
128
|
const unsupported = args.filter((arg) => arg.startsWith('-') && !supportedOptions.has(arg));
|
|
126
129
|
if (unsupported.length > 0) {
|
|
127
130
|
printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf run --help', getRunHelp(lang), lang);
|
|
@@ -130,6 +133,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
130
133
|
const json = args.includes('--json');
|
|
131
134
|
const dryRun = args.includes('--dry-run');
|
|
132
135
|
const planOnly = args.includes('--plan-only');
|
|
136
|
+
const allowUntrustedRoot = args.includes(ALLOW_UNTRUSTED_ROOT_OPTION);
|
|
133
137
|
const previewMode = dryRun ? 'dry-run' : planOnly ? 'plan-only' : null;
|
|
134
138
|
if (dryRun && planOnly) {
|
|
135
139
|
printUsageError(reporter, t(lang, 'run.error.conflictingPreviewModes'), 'mf run --help', getRunHelp(lang), lang);
|
|
@@ -146,6 +150,14 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
146
150
|
return 1;
|
|
147
151
|
}
|
|
148
152
|
const projectRoot = profiler.measure('root_detection', () => resolveMustflowRoot());
|
|
153
|
+
const rootTrust = profiler.measure('root_trust', () => assessRunRootTrust(projectRoot));
|
|
154
|
+
if (!previewMode && !allowUntrustedRoot && !rootTrust.trusted) {
|
|
155
|
+
const message = rootTrust.reason === 'manifest_lock_invalid'
|
|
156
|
+
? t(lang, 'run.error.untrustedRootInvalid', { detail: rootTrust.detail ?? rootTrust.manifestLockPath })
|
|
157
|
+
: t(lang, 'run.error.untrustedRootMissing', { path: rootTrust.detail ?? rootTrust.manifestLockPath });
|
|
158
|
+
reporter.stderr(renderCliError(message, 'mf run --help', lang));
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
149
161
|
const contract = profiler.measure('command_contract', () => readCommandContract(projectRoot));
|
|
150
162
|
const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, intentName, { testTargets: options.testTargets }));
|
|
151
163
|
if (previewMode) {
|
|
@@ -208,6 +220,7 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
208
220
|
}
|
|
209
221
|
}
|
|
210
222
|
const receipt = profiler.measure('receipt_create', () => assembleRunReceipt({
|
|
223
|
+
correlationId: options.correlationId ?? createCorrelationId('run'),
|
|
211
224
|
intentName,
|
|
212
225
|
runStatus,
|
|
213
226
|
startedAt,
|
|
@@ -7,7 +7,7 @@ import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
|
7
7
|
import { t } from '../lib/i18n.js';
|
|
8
8
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
9
9
|
import { getDefaultTemplate, getTemplateFiles, skillNameForTemplatePath } from '../lib/templates.js';
|
|
10
|
-
import {
|
|
10
|
+
import { readMustflowTomlFile, stringifyToml } from '../lib/toml.js';
|
|
11
11
|
import { createUpdateDiffPreview, shouldPreviewUpdateDiff } from '../lib/update-diff-preview.js';
|
|
12
12
|
const UPDATE_SCHEMA_VERSION = '1';
|
|
13
13
|
const CUSTOMIZED_LOCK_ACTION = 'customized';
|
|
@@ -250,7 +250,7 @@ function updateManifestLockAfterApply(projectRoot, appliedItems) {
|
|
|
250
250
|
const lockPath = path.join(projectRoot, MANIFEST_LOCK_RELATIVE_PATH);
|
|
251
251
|
ensureInside(projectRoot, lockPath);
|
|
252
252
|
ensureFileTargetInsideWithoutSymlinks(projectRoot, lockPath);
|
|
253
|
-
const parsed =
|
|
253
|
+
const parsed = readMustflowTomlFile(projectRoot, MANIFEST_LOCK_RELATIVE_PATH);
|
|
254
254
|
if (!isMutableTable(parsed)) {
|
|
255
255
|
throw new Error(`Invalid manifest lock: ${MANIFEST_LOCK_RELATIVE_PATH} must contain a TOML table`);
|
|
256
256
|
}
|