mustflow 2.18.3 → 2.18.20
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 +6 -0
- package/dist/cli/commands/dashboard.js +68 -12
- package/dist/cli/commands/init.js +20 -20
- package/dist/cli/commands/run/executor.js +57 -20
- package/dist/cli/commands/run/process-tree.js +2 -2
- package/dist/cli/commands/run.js +8 -11
- package/dist/cli/commands/update.js +6 -11
- package/dist/cli/i18n/en.js +1 -0
- package/dist/cli/i18n/es.js +1 -0
- package/dist/cli/i18n/fr.js +1 -0
- package/dist/cli/i18n/hi.js +1 -0
- package/dist/cli/i18n/ko.js +1 -0
- package/dist/cli/i18n/zh.js +1 -0
- package/dist/cli/lib/dashboard-export.js +2 -1
- package/dist/cli/lib/dashboard-html/locale-bootstrap.js +3 -2
- package/dist/cli/lib/dashboard-html/template.js +5 -4
- package/dist/cli/lib/dashboard-preferences.js +8 -6
- package/dist/cli/lib/filesystem.js +11 -1
- package/dist/cli/lib/html-json.js +11 -0
- package/dist/cli/lib/local-index/index.js +190 -17
- package/dist/cli/lib/manifest-lock.js +38 -12
- package/dist/cli/lib/run-plan.js +6 -0
- package/dist/core/check-issues.js +1 -0
- package/dist/core/command-classification.js +0 -16
- package/dist/core/command-contract-rules.js +17 -6
- package/dist/core/command-contract-validation.js +42 -4
- package/dist/core/command-intent-eligibility.js +4 -4
- package/dist/core/contract-lint.js +3 -3
- package/package.json +1 -1
- package/templates/default/i18n.toml +42 -6
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +11 -5
- package/templates/default/locales/en/.mustflow/skills/cli-output-contract-review/SKILL.md +146 -0
- package/templates/default/locales/en/.mustflow/skills/command-contract-authoring/SKILL.md +121 -0
- package/templates/default/locales/en/.mustflow/skills/cross-platform-filesystem-safety/SKILL.md +137 -0
- package/templates/default/locales/en/.mustflow/skills/dependency-reality-check/SKILL.md +19 -6
- package/templates/default/locales/en/.mustflow/skills/external-prompt-injection-defense/SKILL.md +26 -10
- package/templates/default/locales/en/.mustflow/skills/llm-service-ux-review/SKILL.md +139 -0
- package/templates/default/locales/en/.mustflow/skills/process-execution-safety/SKILL.md +120 -0
- package/templates/default/locales/en/.mustflow/skills/routes.toml +38 -2
- package/templates/default/locales/en/.mustflow/skills/search-ad-content-authoring/SKILL.md +148 -0
- package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +46 -12
- package/templates/default/locales/en/.mustflow/skills/security-regression-tests/SKILL.md +43 -12
- package/templates/default/locales/en/.mustflow/skills/ui-quality-gate/SKILL.md +34 -14
- package/templates/default/manifest.toml +23 -1
- package/dist/cli/commands/run/builtin-dispatch.js +0 -92
package/README.md
CHANGED
|
@@ -228,6 +228,10 @@ your-project/
|
|
|
228
228
|
│ └─ SKILL.md
|
|
229
229
|
├─ vertical-slice-tdd/
|
|
230
230
|
│ └─ SKILL.md
|
|
231
|
+
├─ llm-service-ux-review/
|
|
232
|
+
│ └─ SKILL.md
|
|
233
|
+
├─ search-ad-content-authoring/
|
|
234
|
+
│ └─ SKILL.md
|
|
231
235
|
├─ ui-quality-gate/
|
|
232
236
|
│ └─ SKILL.md
|
|
233
237
|
├─ visual-review-artifact/
|
|
@@ -356,6 +360,8 @@ Runnable work is declared in `.mustflow/config/commands.toml` so agents do not g
|
|
|
356
360
|
|
|
357
361
|
Development servers, watch modes, browser UIs, interactive commands, and background processes do not run directly. `mf run` also rejects obvious long-running `argv` shapes, such as shell-wrapper background payloads, interpreter loops, package-manager development scripts, watchers, and development servers declared as one-shot commands.
|
|
358
362
|
|
|
363
|
+
Command environments remove the project-local `node_modules/.bin` path from `PATH` by default. If an intent needs a project dependency binary such as `eslint`, `tsc`, or `vitest`, declare it through the package manager, for example `npm exec eslint -- ...`, `pnpm exec tsc -- --noEmit`, `bun x eslint ...`, or `yarn exec eslint ...`. `mf check --strict` warns when an agent-runnable intent uses a bare executable name that appears under the project-local `.bin` directory.
|
|
364
|
+
|
|
359
365
|
Use `mf verify --reason <event> --plan-only --json` to inspect matching verification intents, command eligibility, remaining gaps, and missing runnable coverage without executing commands. Use `mf run <intent> --dry-run --json` to inspect one resolved command intent without spawning a process or writing a run receipt. Plan-only verification includes a `decision_graph` that connects changed surfaces, classification reasons, command candidates, eligibility checks, effects, and gaps. When `.mustflow/cache/mustflow.sqlite` is fresh, scheduled entries also include read-only `effectGraph` metadata for write locks and lock conflicts. These graph rows are marked `explanation_only` and never grant command authority; `.mustflow/config/commands.toml` remains the only runnable command source.
|
|
360
366
|
|
|
361
367
|
Each executed command run writes a run record under `.mustflow/state/runs/run-*` and atomically updates `.mustflow/state/runs/latest.json`. The record includes the intent name, working directory, timeout, exit code, timeout status, and the tail of stdout and stderr.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
3
|
import http from 'node:http';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { openPathInFileManager, openUrlInBrowser } from '../lib/browser-open.js';
|
|
@@ -14,8 +14,8 @@ import { readGitChangedFiles } from '../lib/git-changes.js';
|
|
|
14
14
|
import { isRecord, readCommandContract, readPositiveInteger, readString, readStringArray, } from '../lib/command-contract.js';
|
|
15
15
|
import { readDashboardPreferences, updateDashboardPreferences, } from '../lib/dashboard-preferences.js';
|
|
16
16
|
import { DOC_REVIEW_LEDGER_RELATIVE_PATH, isDocReviewStatus, isReviewerKind, listDocReviewEntries, markDocReviewEntry, } from '../lib/doc-review-ledger.js';
|
|
17
|
-
import { inspectManifestLock } from '../lib/manifest-lock.js';
|
|
18
|
-
import { readLatestLocalVerificationReadModelQueries, readLocalCommandEffectGraphs, } from '../lib/local-index.js';
|
|
17
|
+
import { MANIFEST_LOCK_RELATIVE_PATH, inspectManifestLock } from '../lib/manifest-lock.js';
|
|
18
|
+
import { getLocalIndexDatabasePath, readLatestLocalVerificationReadModelQueries, readLocalCommandEffectGraphs, } from '../lib/local-index.js';
|
|
19
19
|
import { readPackageMetadata } from '../lib/package-info.js';
|
|
20
20
|
import { t } from '../lib/i18n.js';
|
|
21
21
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
@@ -36,6 +36,61 @@ const RELEASE_FILE_PATTERNS = [
|
|
|
36
36
|
];
|
|
37
37
|
const SKILL_INDEX_RELATIVE_PATH = '.mustflow/skills/INDEX.md';
|
|
38
38
|
const LATEST_RUN_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
|
|
39
|
+
const COMMANDS_RELATIVE_PATH = '.mustflow/config/commands.toml';
|
|
40
|
+
const AGENTS_RELATIVE_PATH = 'AGENTS.md';
|
|
41
|
+
const STATUS_BLOCK_CACHE_TTL_MS = 750;
|
|
42
|
+
const dashboardStatusBlockCache = new Map();
|
|
43
|
+
function dashboardStatusBlockCacheKey(projectRoot, blockName) {
|
|
44
|
+
return `${path.resolve(projectRoot)}\0${blockName}`;
|
|
45
|
+
}
|
|
46
|
+
function readFileSignature(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
const stat = statSync(filePath);
|
|
49
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return 'missing';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function readProjectFileSignature(projectRoot, relativePath) {
|
|
56
|
+
return `${relativePath}=${readFileSignature(path.join(projectRoot, ...relativePath.split('/')))}`;
|
|
57
|
+
}
|
|
58
|
+
function readStatusBlockSignature(projectRoot, relativePaths) {
|
|
59
|
+
return relativePaths.map((relativePath) => readProjectFileSignature(projectRoot, relativePath)).join('|');
|
|
60
|
+
}
|
|
61
|
+
function readLocalIndexSignature(projectRoot) {
|
|
62
|
+
return `local_index=${readFileSignature(getLocalIndexDatabasePath(projectRoot))}`;
|
|
63
|
+
}
|
|
64
|
+
function readDashboardStatusBlock(projectRoot, blockName, signature, readBlock) {
|
|
65
|
+
const key = dashboardStatusBlockCacheKey(projectRoot, blockName);
|
|
66
|
+
const cached = dashboardStatusBlockCache.get(key);
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
if (cached && cached.signature === signature && cached.expiresAt > now) {
|
|
69
|
+
return cached.value;
|
|
70
|
+
}
|
|
71
|
+
const value = readBlock();
|
|
72
|
+
dashboardStatusBlockCache.set(key, {
|
|
73
|
+
signature,
|
|
74
|
+
expiresAt: Date.now() + STATUS_BLOCK_CACHE_TTL_MS,
|
|
75
|
+
value,
|
|
76
|
+
});
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
async function readDashboardStatusBlockAsync(projectRoot, blockName, signature, readBlock) {
|
|
80
|
+
const key = dashboardStatusBlockCacheKey(projectRoot, blockName);
|
|
81
|
+
const cached = dashboardStatusBlockCache.get(key);
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
if (cached && cached.signature === signature && cached.expiresAt > now) {
|
|
84
|
+
return cached.value;
|
|
85
|
+
}
|
|
86
|
+
const value = await readBlock();
|
|
87
|
+
dashboardStatusBlockCache.set(key, {
|
|
88
|
+
signature,
|
|
89
|
+
expiresAt: Date.now() + STATUS_BLOCK_CACHE_TTL_MS,
|
|
90
|
+
value,
|
|
91
|
+
});
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
39
94
|
function readFrontmatterLines(content) {
|
|
40
95
|
if (!content.startsWith('---')) {
|
|
41
96
|
return [];
|
|
@@ -677,16 +732,17 @@ function renderRunHistoryResponse(projectRoot) {
|
|
|
677
732
|
}
|
|
678
733
|
async function renderStatusResponse(projectRoot) {
|
|
679
734
|
const context = getAgentContext(projectRoot);
|
|
680
|
-
const manifest = inspectManifestLock(projectRoot);
|
|
735
|
+
const manifest = readDashboardStatusBlock(projectRoot, 'manifest', readStatusBlockSignature(projectRoot, [MANIFEST_LOCK_RELATIVE_PATH]), () => inspectManifestLock(projectRoot));
|
|
681
736
|
const lock = manifest.readResult.kind === 'present' ? manifest.readResult.lock : undefined;
|
|
682
|
-
const activeDocuments = listDocReviewEntries(projectRoot);
|
|
683
|
-
const
|
|
684
|
-
const
|
|
737
|
+
const activeDocuments = readDashboardStatusBlock(projectRoot, 'doc_review', readStatusBlockSignature(projectRoot, [DOC_REVIEW_LEDGER_RELATIVE_PATH]), () => listDocReviewEntries(projectRoot));
|
|
738
|
+
const rawCommandContractSignature = readStatusBlockSignature(projectRoot, [COMMANDS_RELATIVE_PATH]);
|
|
739
|
+
const rawCommandContract = readDashboardStatusBlock(projectRoot, 'raw_command_contract', rawCommandContractSignature, () => readDashboardCommandContract(projectRoot));
|
|
740
|
+
const commandContract = await readDashboardStatusBlockAsync(projectRoot, 'command_contract', `${rawCommandContractSignature}|${readLocalIndexSignature(projectRoot)}`, () => renderCommandContractResponse(projectRoot, rawCommandContract));
|
|
685
741
|
const gitChangedFilesResult = readGitChangedFiles(projectRoot);
|
|
686
742
|
const gitChangedFiles = gitChangedFilesResult.ok ? gitChangedFilesResult.files : [];
|
|
687
743
|
const packageMetadata = readPackageMetadata();
|
|
688
744
|
const verification = createDashboardVerificationSnapshot(projectRoot, rawCommandContract, commandContract.intents, gitChangedFiles, manifest.changedFiles, manifest.missingFiles);
|
|
689
|
-
const readModel = await readLatestLocalVerificationReadModelQueries(projectRoot);
|
|
745
|
+
const readModel = await readDashboardStatusBlockAsync(projectRoot, 'verification_read_model', readLocalIndexSignature(projectRoot), () => readLatestLocalVerificationReadModelQueries(projectRoot));
|
|
690
746
|
return {
|
|
691
747
|
schema_version: '1',
|
|
692
748
|
command: 'dashboard status',
|
|
@@ -696,12 +752,12 @@ async function renderStatusResponse(projectRoot) {
|
|
|
696
752
|
release: {
|
|
697
753
|
package_name: packageMetadata.name,
|
|
698
754
|
package_version: packageMetadata.version,
|
|
699
|
-
version_sources: detectVersionSources(projectRoot),
|
|
755
|
+
version_sources: readDashboardStatusBlock(projectRoot, 'version_sources', readStatusBlockSignature(projectRoot, ['package.json']), () => detectVersionSources(projectRoot)),
|
|
700
756
|
release_sensitive_changed_files: matchingFiles(gitChangedFiles, RELEASE_FILE_PATTERNS),
|
|
701
757
|
},
|
|
702
|
-
update: renderUpdateResponse(projectRoot),
|
|
703
|
-
run_history: renderRunHistoryResponse(projectRoot),
|
|
704
|
-
skills: renderSkillsResponse(projectRoot),
|
|
758
|
+
update: readDashboardStatusBlock(projectRoot, 'update', readStatusBlockSignature(projectRoot, [AGENTS_RELATIVE_PATH, MANIFEST_LOCK_RELATIVE_PATH]), () => renderUpdateResponse(projectRoot)),
|
|
759
|
+
run_history: readDashboardStatusBlock(projectRoot, 'run_history', readStatusBlockSignature(projectRoot, [LATEST_RUN_RELATIVE_PATH]), () => renderRunHistoryResponse(projectRoot)),
|
|
760
|
+
skills: readDashboardStatusBlock(projectRoot, 'skills', readStatusBlockSignature(projectRoot, [SKILL_INDEX_RELATIVE_PATH]), () => renderSkillsResponse(projectRoot)),
|
|
705
761
|
tracked_files: lock?.files.length ?? 0,
|
|
706
762
|
changed_files: manifest.changedFiles,
|
|
707
763
|
missing_files: manifest.missingFiles,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync } 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';
|
|
5
5
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
6
|
-
import { ensureFileTargetInsideWithoutSymlinks, ensureInside, readUtf8FileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks, } from '../lib/filesystem.js';
|
|
6
|
+
import { copyFileInsideWithoutSymlinks, ensureFileTargetInsideWithoutSymlinks, ensureInside, readUtf8FileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks, } from '../lib/filesystem.js';
|
|
7
7
|
import { localeMessage, t } from '../lib/i18n.js';
|
|
8
8
|
import { isLocaleTag } from '../lib/locale-tags.js';
|
|
9
9
|
import { MANIFEST_LOCK_RELATIVE_PATH, sha256File } from '../lib/manifest-lock.js';
|
|
@@ -463,8 +463,11 @@ function parseOptions(args, reporter, lang) {
|
|
|
463
463
|
preferenceOverrides,
|
|
464
464
|
};
|
|
465
465
|
}
|
|
466
|
-
function
|
|
467
|
-
return
|
|
466
|
+
function readTemplateSourceText(templateRoot, sourcePath) {
|
|
467
|
+
return readUtf8FileInsideWithoutSymlinks(templateRoot, sourcePath);
|
|
468
|
+
}
|
|
469
|
+
function sameTemplateFileContent(projectRoot, templateRoot, source, targetPath) {
|
|
470
|
+
return (source.content ?? readTemplateSourceText(templateRoot, source.sourcePath)) === readUtf8FileInsideWithoutSymlinks(projectRoot, targetPath);
|
|
468
471
|
}
|
|
469
472
|
function formatLocaleChoice(locale) {
|
|
470
473
|
const label = LOCALE_LABELS[locale] ?? locale;
|
|
@@ -605,11 +608,11 @@ async function promptInitOptions(template, options, reporter, lang) {
|
|
|
605
608
|
function escapeRegExp(value) {
|
|
606
609
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
607
610
|
}
|
|
608
|
-
function planStatus(projectRoot, source, targetPath, options) {
|
|
611
|
+
function planStatus(projectRoot, templateRoot, source, targetPath, options) {
|
|
609
612
|
if (!existsSync(targetPath)) {
|
|
610
613
|
return 'create';
|
|
611
614
|
}
|
|
612
|
-
if (sameTemplateFileContent(projectRoot, source, targetPath)) {
|
|
615
|
+
if (sameTemplateFileContent(projectRoot, templateRoot, source, targetPath)) {
|
|
613
616
|
return 'unchanged';
|
|
614
617
|
}
|
|
615
618
|
if (options.force) {
|
|
@@ -620,17 +623,15 @@ function planStatus(projectRoot, source, targetPath, options) {
|
|
|
620
623
|
}
|
|
621
624
|
return 'conflict';
|
|
622
625
|
}
|
|
623
|
-
function writeTemplateFile(projectRoot, source, targetPath) {
|
|
626
|
+
function writeTemplateFile(projectRoot, templateRoot, source, targetPath) {
|
|
624
627
|
if (source.content !== undefined) {
|
|
625
628
|
writeUtf8FileInsideWithoutSymlinks(projectRoot, targetPath, source.content);
|
|
626
629
|
return;
|
|
627
630
|
}
|
|
628
|
-
|
|
629
|
-
mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
630
|
-
copyFileSync(source.sourcePath, targetPath);
|
|
631
|
+
copyFileInsideWithoutSymlinks(templateRoot, source.sourcePath, projectRoot, targetPath);
|
|
631
632
|
}
|
|
632
633
|
function writePlannedFile(projectRoot, file) {
|
|
633
|
-
writeTemplateFile(projectRoot, file, file.targetPath);
|
|
634
|
+
writeTemplateFile(projectRoot, file.sourceRoot, file, file.targetPath);
|
|
634
635
|
}
|
|
635
636
|
function gitignoreFragmentPath(template) {
|
|
636
637
|
return path.join(template.templateRoot, template.manifest.commonRoot, GITIGNORE_FRAGMENT_RELATIVE_PATH);
|
|
@@ -646,11 +647,11 @@ function mergeGitignoreContent(existingContent, fragmentContent) {
|
|
|
646
647
|
}
|
|
647
648
|
return `${existingContent.trimEnd()}\n\n${normalizedFragment}\n`;
|
|
648
649
|
}
|
|
649
|
-
function planGitignoreStatus(projectRoot, sourcePath, targetPath) {
|
|
650
|
+
function planGitignoreStatus(projectRoot, sourceRoot, sourcePath, targetPath) {
|
|
650
651
|
if (!existsSync(targetPath)) {
|
|
651
652
|
return 'create';
|
|
652
653
|
}
|
|
653
|
-
const mergedContent = mergeGitignoreContent(readUtf8FileInsideWithoutSymlinks(projectRoot, targetPath),
|
|
654
|
+
const mergedContent = mergeGitignoreContent(readUtf8FileInsideWithoutSymlinks(projectRoot, targetPath), readTemplateSourceText(sourceRoot, sourcePath));
|
|
654
655
|
return mergedContent === readUtf8FileInsideWithoutSymlinks(projectRoot, targetPath) ? 'unchanged' : 'merge';
|
|
655
656
|
}
|
|
656
657
|
function renderPlanVerb(status) {
|
|
@@ -699,10 +700,11 @@ function buildPlannedFiles(template, selectedLocale, targetRoot, options) {
|
|
|
699
700
|
return {
|
|
700
701
|
relativePath: source.relativePath,
|
|
701
702
|
sourcePath: source.sourcePath,
|
|
703
|
+
sourceRoot: template.templateRoot,
|
|
702
704
|
sourceKind: source.sourceKind,
|
|
703
705
|
content: source.content,
|
|
704
706
|
targetPath,
|
|
705
|
-
status: planStatus(targetRoot, source, targetPath, options),
|
|
707
|
+
status: planStatus(targetRoot, template.templateRoot, source, targetPath, options),
|
|
706
708
|
lock: true,
|
|
707
709
|
};
|
|
708
710
|
});
|
|
@@ -714,9 +716,10 @@ function buildPlannedFiles(template, selectedLocale, targetRoot, options) {
|
|
|
714
716
|
plannedFiles.push({
|
|
715
717
|
relativePath: GITIGNORE_RELATIVE_PATH,
|
|
716
718
|
sourcePath,
|
|
719
|
+
sourceRoot: template.templateRoot,
|
|
717
720
|
sourceKind: 'common',
|
|
718
721
|
targetPath,
|
|
719
|
-
status: planGitignoreStatus(targetRoot, sourcePath, targetPath),
|
|
722
|
+
status: planGitignoreStatus(targetRoot, template.templateRoot, sourcePath, targetPath),
|
|
720
723
|
lock: false,
|
|
721
724
|
});
|
|
722
725
|
return plannedFiles;
|
|
@@ -730,10 +733,7 @@ function backupConflictingFiles(projectRoot, conflicts) {
|
|
|
730
733
|
for (const conflict of conflicts) {
|
|
731
734
|
const backupPath = path.join(backupRoot, conflict.relativePath);
|
|
732
735
|
ensureInside(backupRoot, backupPath);
|
|
733
|
-
|
|
734
|
-
ensureFileTargetInsideWithoutSymlinks(projectRoot, backupPath, { allowMissingLeaf: true });
|
|
735
|
-
mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
736
|
-
copyFileSync(conflict.targetPath, backupPath);
|
|
736
|
+
copyFileInsideWithoutSymlinks(projectRoot, conflict.targetPath, projectRoot, backupPath);
|
|
737
737
|
}
|
|
738
738
|
return backupRoot;
|
|
739
739
|
}
|
|
@@ -960,7 +960,7 @@ export async function runInit(args, reporter, lang = 'en') {
|
|
|
960
960
|
if (file.status === 'merge') {
|
|
961
961
|
ensureFileTargetInsideWithoutSymlinks(targetRoot, file.targetPath);
|
|
962
962
|
const mergedContent = file.relativePath === GITIGNORE_RELATIVE_PATH
|
|
963
|
-
? mergeGitignoreContent(readUtf8FileInsideWithoutSymlinks(targetRoot, file.targetPath),
|
|
963
|
+
? mergeGitignoreContent(readUtf8FileInsideWithoutSymlinks(targetRoot, file.targetPath), readTemplateSourceText(file.sourceRoot, file.sourcePath))
|
|
964
964
|
: mergeAgentsContent(readUtf8FileInsideWithoutSymlinks(targetRoot, file.targetPath), selectedLocale);
|
|
965
965
|
writeUtf8FileInsideWithoutSymlinks(targetRoot, file.targetPath, mergedContent);
|
|
966
966
|
merged += 1;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { BoundedOutputBuffer } from '../../../core/bounded-output.js';
|
|
3
|
-
import { createPendingTimeoutTermination,
|
|
3
|
+
import { createPendingTimeoutTermination, forceTerminateProcessTree, getKillMethod, terminateProcessTree, } from './process-tree.js';
|
|
4
4
|
import { createOutputLimitError, isOutputLimitExceededError, writeStreamChunk } from './output.js';
|
|
5
|
-
|
|
5
|
+
const TERMINATION_CONFIRMATION_FALLBACK_MS = 1000;
|
|
6
|
+
function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
6
7
|
return new Promise((resolve) => {
|
|
7
8
|
const stdout = new BoundedOutputBuffer(stdoutTailBytes);
|
|
8
9
|
const stderr = new BoundedOutputBuffer(stderrTailBytes);
|
|
@@ -13,6 +14,9 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutput
|
|
|
13
14
|
let stdoutBytes = 0;
|
|
14
15
|
let stderrBytes = 0;
|
|
15
16
|
let timeout;
|
|
17
|
+
let forceKillTimeout;
|
|
18
|
+
let terminationFallbackTimeout;
|
|
19
|
+
let terminationStarted = false;
|
|
16
20
|
let termination = null;
|
|
17
21
|
const child = spawn(command.executable, command.args ?? [], {
|
|
18
22
|
cwd,
|
|
@@ -23,7 +27,7 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutput
|
|
|
23
27
|
detached: process.platform !== 'win32',
|
|
24
28
|
});
|
|
25
29
|
childPid = child.pid;
|
|
26
|
-
const finish = (status, signal) => {
|
|
30
|
+
const finish = (status, signal, terminationConfirmed = true) => {
|
|
27
31
|
if (settled) {
|
|
28
32
|
return;
|
|
29
33
|
}
|
|
@@ -31,6 +35,19 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutput
|
|
|
31
35
|
if (timeout) {
|
|
32
36
|
clearTimeout(timeout);
|
|
33
37
|
}
|
|
38
|
+
if (forceKillTimeout) {
|
|
39
|
+
clearTimeout(forceKillTimeout);
|
|
40
|
+
}
|
|
41
|
+
if (terminationFallbackTimeout) {
|
|
42
|
+
clearTimeout(terminationFallbackTimeout);
|
|
43
|
+
}
|
|
44
|
+
const confirmedTermination = termination ?
|
|
45
|
+
{
|
|
46
|
+
...termination,
|
|
47
|
+
confirmed: terminationConfirmed,
|
|
48
|
+
cleanup_pending: !terminationConfirmed,
|
|
49
|
+
} :
|
|
50
|
+
null;
|
|
34
51
|
resolve({
|
|
35
52
|
status: timedOut ? null : status,
|
|
36
53
|
signal: timedOut ? null : signal,
|
|
@@ -38,20 +55,42 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutput
|
|
|
38
55
|
stdout: stdout.toSnapshot(),
|
|
39
56
|
stderr: stderr.toSnapshot(),
|
|
40
57
|
pid: childPid,
|
|
41
|
-
termination,
|
|
58
|
+
termination: confirmedTermination,
|
|
42
59
|
});
|
|
43
60
|
};
|
|
61
|
+
const beginTermination = () => {
|
|
62
|
+
if (terminationStarted) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
terminationStarted = true;
|
|
66
|
+
child.stdout?.destroy();
|
|
67
|
+
child.stderr?.destroy();
|
|
68
|
+
terminateProcessTree(childPid);
|
|
69
|
+
const forceAfterMs = killAfterSeconds * 1000;
|
|
70
|
+
forceKillTimeout = setTimeout(() => {
|
|
71
|
+
if (termination) {
|
|
72
|
+
termination = {
|
|
73
|
+
...termination,
|
|
74
|
+
forced_kill_attempted: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
forceTerminateProcessTree(childPid);
|
|
78
|
+
}, forceAfterMs);
|
|
79
|
+
terminationFallbackTimeout = setTimeout(() => {
|
|
80
|
+
child.unref();
|
|
81
|
+
finish(null, null, false);
|
|
82
|
+
}, forceAfterMs + TERMINATION_CONFIRMATION_FALLBACK_MS);
|
|
83
|
+
};
|
|
44
84
|
const stopForOutputLimit = (stream) => {
|
|
45
85
|
if (settled || childError) {
|
|
46
86
|
return;
|
|
47
87
|
}
|
|
48
88
|
childError = createOutputLimitError(stream, maxOutputBytes);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
finish(null, null);
|
|
89
|
+
if (timeout) {
|
|
90
|
+
clearTimeout(timeout);
|
|
91
|
+
timeout = undefined;
|
|
92
|
+
}
|
|
93
|
+
beginTermination();
|
|
55
94
|
};
|
|
56
95
|
child.stdout?.on('data', (chunk) => {
|
|
57
96
|
stdout.append(chunk);
|
|
@@ -80,22 +119,20 @@ function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, maxOutput
|
|
|
80
119
|
finish(status, signal);
|
|
81
120
|
});
|
|
82
121
|
timeout = setTimeout(() => {
|
|
122
|
+
if (settled || childError) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
83
125
|
timedOut = true;
|
|
84
|
-
child.stdout?.destroy();
|
|
85
|
-
child.stderr?.destroy();
|
|
86
|
-
child.unref();
|
|
87
126
|
termination = createPendingTimeoutTermination(getKillMethod());
|
|
88
|
-
|
|
89
|
-
forceTerminateProcessTreeNonBlocking(childPid);
|
|
90
|
-
finish(null, null);
|
|
127
|
+
beginTermination();
|
|
91
128
|
}, timeoutSeconds * 1000);
|
|
92
129
|
});
|
|
93
130
|
}
|
|
94
|
-
export function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
95
|
-
return runSpawnedCommandStreaming({ executable: command?.executable ?? '', args: command?.args ?? [], shell: command?.shell ?? false }, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
|
|
131
|
+
export function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
132
|
+
return runSpawnedCommandStreaming({ executable: command?.executable ?? '', args: command?.args ?? [], shell: command?.shell ?? false }, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
|
|
96
133
|
}
|
|
97
|
-
export function runShellCommandStreaming(command, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
98
|
-
return runSpawnedCommandStreaming({ executable: command ?? '', shell: true }, cwd, env, timeoutSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
|
|
134
|
+
export function runShellCommandStreaming(command, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
|
|
135
|
+
return runSpawnedCommandStreaming({ executable: command ?? '', shell: true }, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit);
|
|
99
136
|
}
|
|
100
137
|
export function getRunStatus(error, exitCode, successExitCodes) {
|
|
101
138
|
const errorWithCode = error;
|
|
@@ -78,13 +78,13 @@ export function forceTerminateProcessTreeNonBlocking(pid) {
|
|
|
78
78
|
export function getKillMethod() {
|
|
79
79
|
return process.platform === 'win32' ? 'taskkill_process_tree' : 'process_group_sigterm';
|
|
80
80
|
}
|
|
81
|
-
export function createPendingTimeoutTermination(method) {
|
|
81
|
+
export function createPendingTimeoutTermination(method, forcedKillAttempted = false) {
|
|
82
82
|
return {
|
|
83
83
|
reason: 'timeout',
|
|
84
84
|
method,
|
|
85
85
|
graceful_signal: 'SIGTERM',
|
|
86
86
|
forced_signal: 'SIGKILL',
|
|
87
|
-
forced_kill_attempted:
|
|
87
|
+
forced_kill_attempted: forcedKillAttempted,
|
|
88
88
|
confirmed: false,
|
|
89
89
|
cleanup_pending: true,
|
|
90
90
|
};
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -5,14 +5,13 @@ import { readCommandContract, readMustflowConfigIfExists } from '../../core/conf
|
|
|
5
5
|
import { resolveRunReceiptRetentionPolicy } from '../../core/retention-policy.js';
|
|
6
6
|
import { t } from '../lib/i18n.js';
|
|
7
7
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
8
|
-
import { createRunPlan, createRunPreview,
|
|
8
|
+
import { createRunPlan, createRunPreview, renderRunPreviewText, } from '../lib/run-plan.js';
|
|
9
9
|
import { writeRunReceipt, } from '../../core/run-receipt.js';
|
|
10
10
|
import { recordRunPerformanceHistory } from '../../core/run-performance-history.js';
|
|
11
11
|
import { RunProfiler } from '../../core/run-profile.js';
|
|
12
12
|
import { finishRunWriteTracking, startRunWriteTracking } from '../../core/run-write-drift.js';
|
|
13
|
-
import { runBuiltinArgvInProcess } from './run/builtin-dispatch.js';
|
|
14
13
|
import { getRunStatus, runArgvCommandStreaming, runShellCommandStreaming } from './run/executor.js';
|
|
15
|
-
import { emitOutput } from './run/output.js';
|
|
14
|
+
import { emitOutput, isOutputLimitExceededError } from './run/output.js';
|
|
16
15
|
import { createPendingTimeoutTermination, getKillMethod, terminateProcessTree } from './run/process-tree.js';
|
|
17
16
|
import { assembleRunReceipt } from './run/receipt.js';
|
|
18
17
|
function getRunPlanDetail(plan, lang, fallbackKey) {
|
|
@@ -179,18 +178,12 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
179
178
|
const childStartedAtMs = performance.now();
|
|
180
179
|
const startedAt = new Date();
|
|
181
180
|
const result = await profiler.measureAsync('child_command', async () => {
|
|
182
|
-
if (plan.commandArgv && isMustflowBuiltinIntent(plan.intent)) {
|
|
183
|
-
const builtinResult = await runBuiltinArgvInProcess(plan.commandArgv, plan.cwd, lang);
|
|
184
|
-
if (builtinResult) {
|
|
185
|
-
return builtinResult;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
181
|
if (plan.commandArgv) {
|
|
189
182
|
streamedOutput = !json;
|
|
190
|
-
return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json,
|
|
183
|
+
return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
|
|
191
184
|
}
|
|
192
185
|
streamedOutput = !json;
|
|
193
|
-
return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json,
|
|
186
|
+
return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, plan.killAfterSeconds, plan.maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, !json, true);
|
|
194
187
|
});
|
|
195
188
|
const childDurationMs = performance.now() - childStartedAtMs;
|
|
196
189
|
const finishedAt = new Date();
|
|
@@ -247,6 +240,10 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
247
240
|
reporter.stderr(t(lang, 'run.error.timedOut', { intent: intentName, seconds: plan.timeoutSeconds }));
|
|
248
241
|
return 1;
|
|
249
242
|
}
|
|
243
|
+
if (isOutputLimitExceededError(result.error)) {
|
|
244
|
+
reporter.stderr(t(lang, 'run.error.outputLimitExceeded', { intent: intentName, message: result.error.message }));
|
|
245
|
+
return 1;
|
|
246
|
+
}
|
|
250
247
|
reporter.stderr(t(lang, 'run.error.startFailed', { intent: intentName, message: result.error.message }));
|
|
251
248
|
return 1;
|
|
252
249
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { ensureFileTargetInsideWithoutSymlinks, ensureInside, writeUtf8FileInsideWithoutSymlinks, } from '../lib/filesystem.js';
|
|
4
|
+
import { copyFileInsideWithoutSymlinks, ensureFileTargetInsideWithoutSymlinks, ensureInside, writeUtf8FileInsideWithoutSymlinks, } from '../lib/filesystem.js';
|
|
5
5
|
import { MANIFEST_LOCK_RELATIVE_PATH, readManifestLock, sha256File } from '../lib/manifest-lock.js';
|
|
6
6
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
7
7
|
import { t } from '../lib/i18n.js';
|
|
@@ -70,14 +70,12 @@ function lockedTemplateSkillNames(files) {
|
|
|
70
70
|
function getInstalledTemplateFiles(projectRoot, template, lock) {
|
|
71
71
|
return getTemplateFiles(template, lock.templateLocale ?? template.manifest.defaultLocale, lock.templateProfile ?? template.manifest.defaultProfile, { extraSkillNames: lockedTemplateSkillNames(lock.files) });
|
|
72
72
|
}
|
|
73
|
-
function writeTemplateFile(projectRoot, source, targetPath) {
|
|
73
|
+
function writeTemplateFile(projectRoot, templateRoot, source, targetPath) {
|
|
74
74
|
if (source.content !== undefined) {
|
|
75
75
|
writeUtf8FileInsideWithoutSymlinks(projectRoot, targetPath, source.content);
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
80
|
-
copyFileSync(source.sourcePath, targetPath);
|
|
78
|
+
copyFileInsideWithoutSymlinks(templateRoot, source.sourcePath, projectRoot, targetPath);
|
|
81
79
|
}
|
|
82
80
|
function templateTargetSafetyIssue(projectRoot, targetPath, allowMissingLeaf) {
|
|
83
81
|
try {
|
|
@@ -223,7 +221,7 @@ function copyTemplateFile(projectRoot, relativePath) {
|
|
|
223
221
|
ensureInside(template.templateRoot, source.sourcePath);
|
|
224
222
|
ensureInside(projectRoot, targetPath);
|
|
225
223
|
ensureFileTargetInsideWithoutSymlinks(projectRoot, targetPath, { allowMissingLeaf: true });
|
|
226
|
-
writeTemplateFile(projectRoot, source, targetPath);
|
|
224
|
+
writeTemplateFile(projectRoot, template.templateRoot, source, targetPath);
|
|
227
225
|
}
|
|
228
226
|
function backupUpdateFiles(projectRoot, items, reporter, lang) {
|
|
229
227
|
const updateItems = items.filter((item) => item.action === 'update');
|
|
@@ -237,10 +235,7 @@ function backupUpdateFiles(projectRoot, items, reporter, lang) {
|
|
|
237
235
|
const backupPath = path.join(backupRoot, item.relativePath);
|
|
238
236
|
ensureInside(projectRoot, sourcePath);
|
|
239
237
|
ensureInside(backupRoot, backupPath);
|
|
240
|
-
|
|
241
|
-
ensureFileTargetInsideWithoutSymlinks(projectRoot, backupPath, { allowMissingLeaf: true });
|
|
242
|
-
mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
243
|
-
copyFileSync(sourcePath, backupPath);
|
|
238
|
+
copyFileInsideWithoutSymlinks(projectRoot, sourcePath, projectRoot, backupPath);
|
|
244
239
|
}
|
|
245
240
|
reporter.stdout(t(lang, 'update.backup.files', {
|
|
246
241
|
count: updateItems.length,
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -672,6 +672,7 @@ Read these files before working:
|
|
|
672
672
|
"run.error.maxOutputBytesDetail": "The output limit must stay within the allowed maximum.",
|
|
673
673
|
"run.error.conflictingPreviewModes": "Use either --dry-run or --plan-only, not both",
|
|
674
674
|
"run.error.timedOut": 'Command "{intent}" timed out after {seconds} seconds',
|
|
675
|
+
"run.error.outputLimitExceeded": 'Command "{intent}" exceeded max_output_bytes: {message}',
|
|
675
676
|
"run.error.startFailed": 'Command "{intent}" failed to start: {message}',
|
|
676
677
|
"search.help.summary": "Search the local SQLite index for the mustflow workflow.",
|
|
677
678
|
"search.help.option.limit": "Set the number of results to print. Default: 10, max: 50",
|
package/dist/cli/i18n/es.js
CHANGED
|
@@ -672,6 +672,7 @@ Lee estos archivos antes de trabajar:
|
|
|
672
672
|
"run.error.maxOutputBytesDetail": "El límite de salida debe permanecer dentro del máximo permitido.",
|
|
673
673
|
"run.error.conflictingPreviewModes": "Usa --dry-run o --plan-only, no ambos",
|
|
674
674
|
"run.error.timedOut": 'El comando "{intent}" agotó el tiempo después de {seconds} segundos',
|
|
675
|
+
"run.error.outputLimitExceeded": 'El comando "{intent}" superó max_output_bytes: {message}',
|
|
675
676
|
"run.error.startFailed": 'No se pudo iniciar el comando "{intent}": {message}',
|
|
676
677
|
"search.help.summary": "Busca en el índice SQLite local del flujo de trabajo mustflow.",
|
|
677
678
|
"search.help.option.limit": "Define la cantidad de resultados que se imprimen. Predeterminado: 10, máximo: 50",
|
package/dist/cli/i18n/fr.js
CHANGED
|
@@ -672,6 +672,7 @@ Lisez ces fichiers avant de travailler :
|
|
|
672
672
|
"run.error.maxOutputBytesDetail": "La limite de sortie doit rester dans le maximum autorisé.",
|
|
673
673
|
"run.error.conflictingPreviewModes": "Utilisez --dry-run ou --plan-only, pas les deux",
|
|
674
674
|
"run.error.timedOut": 'La commande "{intent}" a expiré après {seconds} secondes',
|
|
675
|
+
"run.error.outputLimitExceeded": 'La commande "{intent}" a dépassé max_output_bytes : {message}',
|
|
675
676
|
"run.error.startFailed": 'Impossible de démarrer la commande "{intent}" : {message}',
|
|
676
677
|
"search.help.summary": "Recherche dans l'index SQLite local du flux de travail mustflow.",
|
|
677
678
|
"search.help.option.limit": "Définit le nombre de résultats à imprimer. Par défaut : 10, max : 50",
|
package/dist/cli/i18n/hi.js
CHANGED
|
@@ -672,6 +672,7 @@ export const hiMessages = {
|
|
|
672
672
|
"run.error.maxOutputBytesDetail": "Output limit अनुमत maximum के अंदर रहनी चाहिए।",
|
|
673
673
|
"run.error.conflictingPreviewModes": "--dry-run या --plan-only में से एक इस्तेमाल करें, दोनों नहीं",
|
|
674
674
|
"run.error.timedOut": 'कमांड "{intent}" {seconds} सेकंड बाद time out हुई',
|
|
675
|
+
"run.error.outputLimitExceeded": 'कमांड "{intent}" ने max_output_bytes सीमा पार की: {message}',
|
|
675
676
|
"run.error.startFailed": 'कमांड "{intent}" शुरू नहीं हो सकी: {message}',
|
|
676
677
|
"search.help.summary": "mustflow वर्कफ़्लो के लिए स्थानीय SQLite इंडेक्स में खोजें।",
|
|
677
678
|
"search.help.option.limit": "प्रिंट किए जाने वाले परिणामों की संख्या सेट करें। डिफ़ॉल्ट: 10, अधिकतम: 50",
|
package/dist/cli/i18n/ko.js
CHANGED
|
@@ -672,6 +672,7 @@ export const koMessages = {
|
|
|
672
672
|
"run.error.maxOutputBytesDetail": "출력 상한은 허용된 최댓값 안에 있어야 합니다.",
|
|
673
673
|
"run.error.conflictingPreviewModes": "--dry-run과 --plan-only 중 하나만 사용하세요",
|
|
674
674
|
"run.error.timedOut": '명령 "{intent}"가 {seconds}초 뒤 시간 초과되었습니다',
|
|
675
|
+
"run.error.outputLimitExceeded": '명령 "{intent}"가 max_output_bytes 제한을 넘었습니다: {message}',
|
|
675
676
|
"run.error.startFailed": '명령 "{intent}"를 시작하지 못했습니다: {message}',
|
|
676
677
|
"search.help.summary": "로컬 SQLite 색인에서 mustflow 워크플로우를 검색합니다.",
|
|
677
678
|
"search.help.option.limit": "출력할 검색 결과 수를 설정합니다. 기본값: 10, 최대: 50",
|
package/dist/cli/i18n/zh.js
CHANGED
|
@@ -672,6 +672,7 @@ export const zhMessages = {
|
|
|
672
672
|
"run.error.maxOutputBytesDetail": "输出限制必须保持在允许的最大值内。",
|
|
673
673
|
"run.error.conflictingPreviewModes": "只能使用 --dry-run 或 --plan-only,不能同时使用",
|
|
674
674
|
"run.error.timedOut": '命令 "{intent}" 在 {seconds} 秒后超时',
|
|
675
|
+
"run.error.outputLimitExceeded": '命令 "{intent}" 超过 max_output_bytes:{message}',
|
|
675
676
|
"run.error.startFailed": '命令 "{intent}" 启动失败:{message}',
|
|
676
677
|
"search.help.summary": "搜索本地 SQLite 索引中的 mustflow 工作流。",
|
|
677
678
|
"search.help.option.limit": "设置要输出的结果数量。默认值:10,最大:50",
|
|
@@ -4,6 +4,7 @@ import { createDashboardCompletionVerdict } from '../../core/completion-verdict.
|
|
|
4
4
|
import { createDashboardEvidenceModel } from '../../core/verification-evidence.js';
|
|
5
5
|
import { redactSecretLikeText } from '../../core/secret-redaction.js';
|
|
6
6
|
import { ensureFileTargetInsideWithoutSymlinks, ensureInside, toPosixPath, writeUtf8FileInsideWithoutSymlinks, } from './filesystem.js';
|
|
7
|
+
import { safeJsonForInlineScript } from './html-json.js';
|
|
7
8
|
export class DashboardExportPathError extends Error {
|
|
8
9
|
targetPath;
|
|
9
10
|
constructor(targetPath) {
|
|
@@ -634,7 +635,7 @@ export function renderDashboardExportHtml(snapshot) {
|
|
|
634
635
|
const graphSummary = asRecord(harnessVerification.decision_graph_summary);
|
|
635
636
|
const harnessRunHistory = asRecord(harnessReport.run_history);
|
|
636
637
|
const harnessDocsReview = asRecord(harnessReport.docs_review);
|
|
637
|
-
const embeddedJson =
|
|
638
|
+
const embeddedJson = safeJsonForInlineScript(snapshot);
|
|
638
639
|
return `<!doctype html>
|
|
639
640
|
<html lang="en">
|
|
640
641
|
<head>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { getDashboardLocaleBundle } from '../dashboard-locale.js';
|
|
2
|
+
import { safeJsonForInlineScript } from '../html-json.js';
|
|
2
3
|
export function createDashboardLocaleBootstrap() {
|
|
3
4
|
const localeBundle = getDashboardLocaleBundle();
|
|
4
5
|
return {
|
|
5
|
-
serializedLocaleBundle:
|
|
6
|
-
serializedAvailableLocales:
|
|
6
|
+
serializedLocaleBundle: safeJsonForInlineScript(localeBundle),
|
|
7
|
+
serializedAvailableLocales: safeJsonForInlineScript(localeBundle.locales),
|
|
7
8
|
};
|
|
8
9
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { safeJsonForInlineScript } from '../html-json.js';
|
|
1
2
|
import { renderDashboardClientScript } from './client-script.js';
|
|
2
3
|
import { createDashboardLocaleBootstrap } from './locale-bootstrap.js';
|
|
3
4
|
import { renderDashboardStyles } from './styles.js';
|
|
@@ -12,10 +13,10 @@ function escapeHtml(value) {
|
|
|
12
13
|
export function renderDashboardHtml(snapshot, token, statusSnapshot, docReviewSnapshot) {
|
|
13
14
|
const root = escapeHtml(snapshot.projectRoot);
|
|
14
15
|
const preferencesPath = escapeHtml(snapshot.preferencesPath);
|
|
15
|
-
const serializedSnapshot =
|
|
16
|
-
const serializedStatusSnapshot =
|
|
17
|
-
const serializedDocReviewSnapshot =
|
|
18
|
-
const serializedToken =
|
|
16
|
+
const serializedSnapshot = safeJsonForInlineScript(snapshot);
|
|
17
|
+
const serializedStatusSnapshot = safeJsonForInlineScript(statusSnapshot);
|
|
18
|
+
const serializedDocReviewSnapshot = safeJsonForInlineScript(docReviewSnapshot);
|
|
19
|
+
const serializedToken = safeJsonForInlineScript(token);
|
|
19
20
|
const { serializedLocaleBundle, serializedAvailableLocales } = createDashboardLocaleBootstrap();
|
|
20
21
|
return `<!doctype html>
|
|
21
22
|
<html lang="en">
|