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
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isRecord } from './command-contract.js';
|
|
4
|
+
import { readUtf8FileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks } from './filesystem.js';
|
|
4
5
|
import { isLocaleTag } from './locale-tags.js';
|
|
5
|
-
import { markManifestLockFileCustomized } from './manifest-lock.js';
|
|
6
|
+
import { ensureManifestLockTargetSafe, markManifestLockFileCustomized } from './manifest-lock.js';
|
|
6
7
|
import { COMMIT_MESSAGE_STYLES, TEST_AUTHORING_POLICIES } from './preferences-options.js';
|
|
7
|
-
import {
|
|
8
|
+
import { parseTomlText } from './toml.js';
|
|
8
9
|
const PREFERENCES_RELATIVE_PATH = '.mustflow/config/preferences.toml';
|
|
9
10
|
export const DASHBOARD_PREFERENCE_SETTINGS = [
|
|
10
11
|
{
|
|
@@ -281,7 +282,7 @@ export function readDashboardPreferences(projectRoot) {
|
|
|
281
282
|
if (!existsSync(preferencesPath)) {
|
|
282
283
|
throw new Error('Missing .mustflow/config/preferences.toml. Run mf init first or switch to a mustflow root.');
|
|
283
284
|
}
|
|
284
|
-
const parsed =
|
|
285
|
+
const parsed = parseTomlText(readUtf8FileInsideWithoutSymlinks(projectRoot, preferencesPath));
|
|
285
286
|
if (!isRecord(parsed)) {
|
|
286
287
|
throw new Error('.mustflow/config/preferences.toml must contain a TOML table.');
|
|
287
288
|
}
|
|
@@ -390,7 +391,7 @@ function coerceUpdateValue(definition, value) {
|
|
|
390
391
|
export function updateDashboardPreferences(projectRoot, updates) {
|
|
391
392
|
const preferencesPath = getPreferencesPath(projectRoot);
|
|
392
393
|
const definitionsById = new Map(DASHBOARD_PREFERENCE_SETTINGS.map((definition) => [definition.id, definition]));
|
|
393
|
-
let content =
|
|
394
|
+
let content = readUtf8FileInsideWithoutSymlinks(projectRoot, preferencesPath);
|
|
394
395
|
for (const update of updates) {
|
|
395
396
|
const definition = definitionsById.get(update.id);
|
|
396
397
|
if (!definition) {
|
|
@@ -399,7 +400,8 @@ export function updateDashboardPreferences(projectRoot, updates) {
|
|
|
399
400
|
const value = coerceUpdateValue(definition, update.value);
|
|
400
401
|
content = setTomlScalar(content, definition.path, value);
|
|
401
402
|
}
|
|
402
|
-
|
|
403
|
+
ensureManifestLockTargetSafe(projectRoot);
|
|
404
|
+
writeUtf8FileInsideWithoutSymlinks(projectRoot, preferencesPath, content);
|
|
403
405
|
markManifestLockFileCustomized(projectRoot, PREFERENCES_RELATIVE_PATH);
|
|
404
406
|
return readDashboardPreferences(projectRoot);
|
|
405
407
|
}
|
|
@@ -48,11 +48,14 @@ export function ensureInsideWithoutSymlinks(parentPath, childPath, options = {})
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
export function readUtf8FileInsideWithoutSymlinks(parentPath, childPath) {
|
|
51
|
+
return readFileInsideWithoutSymlinks(parentPath, childPath).toString('utf8');
|
|
52
|
+
}
|
|
53
|
+
export function readFileInsideWithoutSymlinks(parentPath, childPath) {
|
|
51
54
|
const absoluteChildPath = path.resolve(childPath);
|
|
52
55
|
ensureInsideWithoutSymlinks(parentPath, absoluteChildPath);
|
|
53
56
|
const fileDescriptor = openSync(absoluteChildPath, constants.O_RDONLY | NOFOLLOW_FLAG);
|
|
54
57
|
try {
|
|
55
|
-
return readFileSync(fileDescriptor
|
|
58
|
+
return readFileSync(fileDescriptor);
|
|
56
59
|
}
|
|
57
60
|
finally {
|
|
58
61
|
closeSync(fileDescriptor);
|
|
@@ -79,6 +82,9 @@ export function ensureFileTargetInsideWithoutSymlinks(parentPath, childPath, opt
|
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
export function writeUtf8FileInsideWithoutSymlinks(parentPath, childPath, content) {
|
|
85
|
+
writeFileInsideWithoutSymlinks(parentPath, childPath, content);
|
|
86
|
+
}
|
|
87
|
+
export function writeFileInsideWithoutSymlinks(parentPath, childPath, content) {
|
|
82
88
|
const absoluteChildPath = path.resolve(childPath);
|
|
83
89
|
const directoryPath = path.dirname(absoluteChildPath);
|
|
84
90
|
ensureInsideWithoutSymlinks(parentPath, directoryPath, { allowMissingLeaf: true });
|
|
@@ -92,6 +98,10 @@ export function writeUtf8FileInsideWithoutSymlinks(parentPath, childPath, conten
|
|
|
92
98
|
closeSync(fileDescriptor);
|
|
93
99
|
}
|
|
94
100
|
}
|
|
101
|
+
export function copyFileInsideWithoutSymlinks(sourceParentPath, sourcePath, targetParentPath, targetPath) {
|
|
102
|
+
const content = readFileInsideWithoutSymlinks(sourceParentPath, sourcePath);
|
|
103
|
+
writeFileInsideWithoutSymlinks(targetParentPath, targetPath, content);
|
|
104
|
+
}
|
|
95
105
|
export function copyFileIfMissing(sourcePath, targetPath, relativePath) {
|
|
96
106
|
if (existsSync(targetPath)) {
|
|
97
107
|
return { status: 'skipped', relativePath };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const INLINE_SCRIPT_JSON_ESCAPES = {
|
|
2
|
+
'<': '\\u003C',
|
|
3
|
+
'>': '\\u003E',
|
|
4
|
+
'&': '\\u0026',
|
|
5
|
+
'\u2028': '\\u2028',
|
|
6
|
+
'\u2029': '\\u2029',
|
|
7
|
+
};
|
|
8
|
+
export function safeJsonForInlineScript(value) {
|
|
9
|
+
const json = JSON.stringify(value);
|
|
10
|
+
return (json ?? 'null').replace(/[<>&\u2028\u2029]/gu, (character) => INLINE_SCRIPT_JSON_ESCAPES[character]);
|
|
11
|
+
}
|
|
@@ -5,6 +5,7 @@ import { isRecord, readCommandContract, readString, readStringArray } from '../c
|
|
|
5
5
|
import { listFilesRecursive, toPosixPath } from '../filesystem.js';
|
|
6
6
|
import { readTomlFile } from '../toml.js';
|
|
7
7
|
import { collectSourceAnchorIndexRecords, hasHighRiskSourceAnchorRiskTags, } from '../../../core/source-anchor-status.js';
|
|
8
|
+
import { listSourceAnchorFiles } from '../../../core/source-anchors.js';
|
|
8
9
|
import { normalizeCommandEffects } from '../../../core/command-effects.js';
|
|
9
10
|
import { listChangeClassificationRuleDescriptors } from '../../../core/change-classification.js';
|
|
10
11
|
import { DEFAULT_DATABASE_RELATIVE_PATH, DEFAULT_PROMPT_CACHE_STABLE_READ, DEFAULT_PROMPT_CACHE_TASK_SOURCES, DEFAULT_PROMPT_CACHE_VOLATILE_SOURCES, INDEX_CONFIG_RELATIVE_PATH, LOCAL_INDEX_CONTENT_MODE, LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS, LOCAL_INDEX_PARSER_VERSION, LOCAL_INDEX_SCHEMA_VERSION, LOCAL_INDEX_STORE_FULL_CONTENT, LATEST_RUN_STATE_RELATIVE_PATH, MAX_SEARCH_MATCH_SNIPPET_CHARS, MAX_SNIPPET_BYTES_PER_DOCUMENT, MUSTFLOW_RELATIVE_PATH, SEARCH_BACKEND_FTS5, SEARCH_BACKEND_TABLE_SCAN, SEARCH_MATCH_CONTEXT_AFTER_CHARS, SEARCH_MATCH_CONTEXT_BEFORE_CHARS, SEARCH_MATCH_TRUNCATION_MARKER, SEARCH_NGRAM_MAX_GRAMS_PER_TARGET, SEARCH_NGRAM_MAX_LENGTH, SEARCH_NGRAM_MAX_TOKEN_CHARS, SEARCH_NGRAM_MIN_LENGTH, SOURCE_INDEX_MAX_FILE_BYTES, TEST_DISABLE_FTS5_ENV, } from './constants.js';
|
|
@@ -284,7 +285,21 @@ function collectCommandIntents(projectRoot) {
|
|
|
284
285
|
}
|
|
285
286
|
return intents;
|
|
286
287
|
}
|
|
288
|
+
function normalizeIndexedFileSourceScope(value) {
|
|
289
|
+
const sourceScope = toSearchString(value);
|
|
290
|
+
if (sourceScope === 'source_anchor' || sourceScope === 'state') {
|
|
291
|
+
return sourceScope;
|
|
292
|
+
}
|
|
293
|
+
return 'workflow';
|
|
294
|
+
}
|
|
287
295
|
function readIndexedFileRecord(projectRoot, relativePath, sourceScope, contentHash = null) {
|
|
296
|
+
const metadata = readIndexedFileMetadataRecord(projectRoot, relativePath, sourceScope);
|
|
297
|
+
return {
|
|
298
|
+
...metadata,
|
|
299
|
+
contentHash: contentHash ?? sha256Bytes(readFileSync(path.join(projectRoot, ...relativePath.split('/')))),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function readIndexedFileMetadataRecord(projectRoot, relativePath, sourceScope) {
|
|
288
303
|
const fullPath = path.join(projectRoot, ...relativePath.split('/'));
|
|
289
304
|
const stats = statSync(fullPath);
|
|
290
305
|
return {
|
|
@@ -292,19 +307,50 @@ function readIndexedFileRecord(projectRoot, relativePath, sourceScope, contentHa
|
|
|
292
307
|
sourceScope,
|
|
293
308
|
sizeBytes: stats.size,
|
|
294
309
|
mtimeMs: Math.round(stats.mtimeMs),
|
|
295
|
-
contentHash: contentHash ?? sha256Bytes(readFileSync(fullPath)),
|
|
296
310
|
};
|
|
297
311
|
}
|
|
298
|
-
function collectIndexedFileRecords(projectRoot, documents, sourceAnchors) {
|
|
312
|
+
function collectIndexedFileRecords(projectRoot, documents, sourceAnchors, sourceAnchorCandidatePaths = []) {
|
|
299
313
|
const records = new Map();
|
|
300
314
|
for (const document of documents) {
|
|
301
315
|
records.set(document.path, readIndexedFileRecord(projectRoot, document.path, 'workflow', document.contentHash));
|
|
302
316
|
}
|
|
303
|
-
|
|
317
|
+
const sourcePaths = new Set([...sourceAnchorCandidatePaths, ...sourceAnchors.map((anchor) => anchor.path)]);
|
|
318
|
+
for (const anchorPath of [...sourcePaths].sort((left, right) => left.localeCompare(right))) {
|
|
304
319
|
if (!records.has(anchorPath)) {
|
|
305
320
|
records.set(anchorPath, readIndexedFileRecord(projectRoot, anchorPath, 'source_anchor'));
|
|
306
321
|
}
|
|
307
322
|
}
|
|
323
|
+
if (existsSync(path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/')))) {
|
|
324
|
+
records.set(LATEST_RUN_STATE_RELATIVE_PATH, readIndexedFileRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
|
|
325
|
+
}
|
|
326
|
+
return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
327
|
+
}
|
|
328
|
+
function collectSourceAnchorCandidatePaths(projectRoot, sourceConfig) {
|
|
329
|
+
return listSourceAnchorFiles(projectRoot, {
|
|
330
|
+
...sourceConfig,
|
|
331
|
+
excludeGeneratedOrVendor: true,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
function collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource, sourceConfig) {
|
|
335
|
+
const records = new Map();
|
|
336
|
+
for (const relativePath of getExistingIndexablePaths(projectRoot)) {
|
|
337
|
+
records.set(relativePath, readIndexedFileMetadataRecord(projectRoot, relativePath, 'workflow'));
|
|
338
|
+
}
|
|
339
|
+
if (includeSource) {
|
|
340
|
+
try {
|
|
341
|
+
for (const sourcePath of collectSourceAnchorCandidatePaths(projectRoot, sourceConfig)) {
|
|
342
|
+
if (!records.has(sourcePath)) {
|
|
343
|
+
records.set(sourcePath, readIndexedFileMetadataRecord(projectRoot, sourcePath, 'source_anchor'));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (existsSync(path.join(projectRoot, ...LATEST_RUN_STATE_RELATIVE_PATH.split('/')))) {
|
|
352
|
+
records.set(LATEST_RUN_STATE_RELATIVE_PATH, readIndexedFileMetadataRecord(projectRoot, LATEST_RUN_STATE_RELATIVE_PATH, 'state'));
|
|
353
|
+
}
|
|
308
354
|
return [...records.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
309
355
|
}
|
|
310
356
|
function normalizeSearchText(value) {
|
|
@@ -1948,6 +1994,86 @@ function populateDatabase(database, capabilities, documents, skills, skillRoutes
|
|
|
1948
1994
|
populatePathSurfaceReadModel(database);
|
|
1949
1995
|
populateSearchTables(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors);
|
|
1950
1996
|
}
|
|
1997
|
+
function readCount(database, tableName) {
|
|
1998
|
+
if (!hasTable(database, tableName)) {
|
|
1999
|
+
return 0;
|
|
2000
|
+
}
|
|
2001
|
+
const [row] = queryRows(database, `SELECT COUNT(*) AS count FROM ${tableName}`);
|
|
2002
|
+
const count = row?.count;
|
|
2003
|
+
return typeof count === 'number' && Number.isFinite(count) ? count : 0;
|
|
2004
|
+
}
|
|
2005
|
+
function readStoredIndexedPaths(database) {
|
|
2006
|
+
if (!hasTable(database, 'documents')) {
|
|
2007
|
+
return [];
|
|
2008
|
+
}
|
|
2009
|
+
return queryRows(database, 'SELECT path FROM documents ORDER BY path')
|
|
2010
|
+
.map((row) => toSearchString(row.path))
|
|
2011
|
+
.filter(Boolean);
|
|
2012
|
+
}
|
|
2013
|
+
function createStoredLocalIndexResult(projectRoot, databasePath, dryRun, indexMode, database, capabilities) {
|
|
2014
|
+
return {
|
|
2015
|
+
schema_version: LOCAL_INDEX_SCHEMA_VERSION,
|
|
2016
|
+
command: 'index',
|
|
2017
|
+
ok: true,
|
|
2018
|
+
mustflow_root: path.resolve(projectRoot),
|
|
2019
|
+
database_path: databasePath,
|
|
2020
|
+
dry_run: dryRun,
|
|
2021
|
+
wrote_files: false,
|
|
2022
|
+
index_mode: indexMode,
|
|
2023
|
+
reused_existing: true,
|
|
2024
|
+
rebuild_reason: null,
|
|
2025
|
+
document_count: readCount(database, 'documents'),
|
|
2026
|
+
skill_count: readCount(database, 'skills'),
|
|
2027
|
+
skill_route_count: readCount(database, 'skill_routes'),
|
|
2028
|
+
command_intent_count: readCount(database, 'command_intents'),
|
|
2029
|
+
command_effect_count: readCount(database, 'command_effects'),
|
|
2030
|
+
verification_evidence_summary_count: readCount(database, 'verification_evidence_summaries'),
|
|
2031
|
+
verification_plan_count: readCount(database, 'verification_plans'),
|
|
2032
|
+
acceptance_criteria_count: readCount(database, 'acceptance_criteria'),
|
|
2033
|
+
criterion_coverage_count: readCount(database, 'criterion_coverage'),
|
|
2034
|
+
verification_receipt_summary_count: readCount(database, 'verification_receipt_summaries'),
|
|
2035
|
+
command_receipt_summary_count: readCount(database, 'command_receipt_summaries'),
|
|
2036
|
+
verification_coverage_state_count: readCount(database, 'verification_coverage_states'),
|
|
2037
|
+
verification_risk_signal_count: readCount(database, 'verification_risk_signals'),
|
|
2038
|
+
validation_ratchet_signal_count: readCount(database, 'validation_ratchet_signals'),
|
|
2039
|
+
completion_verdict_summary_count: readCount(database, 'completion_verdict_summaries'),
|
|
2040
|
+
repro_route_count: readCount(database, 'repro_routes'),
|
|
2041
|
+
repro_observation_count: readCount(database, 'repro_observations'),
|
|
2042
|
+
failure_fingerprint_count: readCount(database, 'verification_failure_fingerprints'),
|
|
2043
|
+
source_index_enabled: readMetadataValue(database, 'source_index_enabled') === 'true',
|
|
2044
|
+
source_anchor_count: readCount(database, 'source_anchors'),
|
|
2045
|
+
source_anchor_risk_signal_count: readCount(database, 'source_anchor_risk_signals'),
|
|
2046
|
+
search_backend: capabilities.backend,
|
|
2047
|
+
search_fts5_available: capabilities.fts5Available,
|
|
2048
|
+
content_mode: LOCAL_INDEX_CONTENT_MODE,
|
|
2049
|
+
store_full_content: LOCAL_INDEX_STORE_FULL_CONTENT,
|
|
2050
|
+
max_snippet_bytes_per_document: MAX_SNIPPET_BYTES_PER_DOCUMENT,
|
|
2051
|
+
excluded_raw_data_kinds: [...LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS],
|
|
2052
|
+
indexed_file_count: readCount(database, 'indexed_files'),
|
|
2053
|
+
indexed_paths: readStoredIndexedPaths(database),
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
function indexedFileMetadataMatch(database, currentFiles) {
|
|
2057
|
+
const rows = queryRows(database, 'SELECT path, source_scope, size_bytes, mtime_ms, parser_version FROM indexed_files ORDER BY path');
|
|
2058
|
+
if (rows.length !== currentFiles.length) {
|
|
2059
|
+
return false;
|
|
2060
|
+
}
|
|
2061
|
+
const currentByPath = new Map(currentFiles.map((file) => [file.path, file]));
|
|
2062
|
+
for (const row of rows) {
|
|
2063
|
+
const storedPath = toSearchString(row.path);
|
|
2064
|
+
const current = currentByPath.get(storedPath);
|
|
2065
|
+
if (!current) {
|
|
2066
|
+
return false;
|
|
2067
|
+
}
|
|
2068
|
+
if (normalizeIndexedFileSourceScope(row.source_scope) !== current.sourceScope ||
|
|
2069
|
+
row.size_bytes !== current.sizeBytes ||
|
|
2070
|
+
row.mtime_ms !== current.mtimeMs ||
|
|
2071
|
+
toSearchString(row.parser_version) !== LOCAL_INDEX_PARSER_VERSION) {
|
|
2072
|
+
return false;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
return true;
|
|
2076
|
+
}
|
|
1951
2077
|
function indexedFilesMatch(database, currentFiles) {
|
|
1952
2078
|
const rows = queryRows(database, 'SELECT path, source_scope, content_hash, parser_version FROM indexed_files ORDER BY path');
|
|
1953
2079
|
if (rows.length !== currentFiles.length) {
|
|
@@ -1960,7 +2086,7 @@ function indexedFilesMatch(database, currentFiles) {
|
|
|
1960
2086
|
if (!current) {
|
|
1961
2087
|
return false;
|
|
1962
2088
|
}
|
|
1963
|
-
if (
|
|
2089
|
+
if (normalizeIndexedFileSourceScope(row.source_scope) !== current.sourceScope ||
|
|
1964
2090
|
toSearchString(row.content_hash) !== current.contentHash ||
|
|
1965
2091
|
toSearchString(row.parser_version) !== LOCAL_INDEX_PARSER_VERSION) {
|
|
1966
2092
|
return false;
|
|
@@ -1968,6 +2094,44 @@ function indexedFilesMatch(database, currentFiles) {
|
|
|
1968
2094
|
}
|
|
1969
2095
|
return true;
|
|
1970
2096
|
}
|
|
2097
|
+
async function readIncrementalPreflightReuse(SQL, databasePath, projectRoot, currentFiles, sourceScopeHash, dryRun, indexMode) {
|
|
2098
|
+
if (!currentFiles) {
|
|
2099
|
+
return { result: null, rebuildReason: null };
|
|
2100
|
+
}
|
|
2101
|
+
if (!existsSync(databasePath)) {
|
|
2102
|
+
return { result: null, rebuildReason: 'missing_index' };
|
|
2103
|
+
}
|
|
2104
|
+
let database;
|
|
2105
|
+
try {
|
|
2106
|
+
database = new SQL.Database(readFileSync(databasePath));
|
|
2107
|
+
if (readStoredSchemaVersion(database) !== LOCAL_INDEX_SCHEMA_VERSION) {
|
|
2108
|
+
return { result: null, rebuildReason: 'schema_version_mismatch' };
|
|
2109
|
+
}
|
|
2110
|
+
if (readMetadataValue(database, 'parser_version') !== LOCAL_INDEX_PARSER_VERSION) {
|
|
2111
|
+
return { result: null, rebuildReason: 'parser_version_mismatch' };
|
|
2112
|
+
}
|
|
2113
|
+
if (readMetadataValue(database, 'source_scope_hash') !== sourceScopeHash) {
|
|
2114
|
+
return { result: null, rebuildReason: 'source_scope_mismatch' };
|
|
2115
|
+
}
|
|
2116
|
+
if (!hasTable(database, 'indexed_files')) {
|
|
2117
|
+
return { result: null, rebuildReason: 'indexed_files_missing' };
|
|
2118
|
+
}
|
|
2119
|
+
if (!indexedFileMetadataMatch(database, currentFiles)) {
|
|
2120
|
+
return { result: null, rebuildReason: 'file_fingerprint_mismatch' };
|
|
2121
|
+
}
|
|
2122
|
+
const capabilities = readStoredSearchCapabilities(database);
|
|
2123
|
+
return {
|
|
2124
|
+
result: createStoredLocalIndexResult(projectRoot, databasePath, dryRun, indexMode, database, capabilities),
|
|
2125
|
+
rebuildReason: null,
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
catch {
|
|
2129
|
+
return { result: null, rebuildReason: 'unreadable_index' };
|
|
2130
|
+
}
|
|
2131
|
+
finally {
|
|
2132
|
+
database?.close();
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
1971
2135
|
async function readIncrementalReuseDecision(SQL, databasePath, currentFiles, sourceScopeHash) {
|
|
1972
2136
|
if (!existsSync(databasePath)) {
|
|
1973
2137
|
return { reusable: false, rebuildReason: 'missing_index', capabilities: null };
|
|
@@ -2015,16 +2179,32 @@ export async function createLocalIndex(projectRoot, options = {}) {
|
|
|
2015
2179
|
const dryRun = options.dryRun === true;
|
|
2016
2180
|
const incremental = options.incremental === true;
|
|
2017
2181
|
const indexMode = incremental ? 'incremental' : 'full';
|
|
2182
|
+
const sourceConfig = readLocalIndexSourceConfig(projectRoot);
|
|
2183
|
+
const includeSource = options.includeSource === true || sourceConfig.enabledByDefault;
|
|
2184
|
+
const sourceScopeHash = getSourceScopeHash(includeSource, sourceConfig);
|
|
2185
|
+
let capabilities = searchCapabilities(false);
|
|
2186
|
+
let reusedExisting = false;
|
|
2187
|
+
let rebuildReason = null;
|
|
2188
|
+
const SQL = await loadSqlJs();
|
|
2189
|
+
const capabilityDatabase = new SQL.Database();
|
|
2190
|
+
capabilities = detectLocalSearchCapabilities(capabilityDatabase);
|
|
2191
|
+
capabilityDatabase.close();
|
|
2192
|
+
if (incremental) {
|
|
2193
|
+
const preflightFiles = collectFastPreflightIndexedFileMetadataRecords(projectRoot, includeSource, sourceConfig);
|
|
2194
|
+
const preflightReuse = await readIncrementalPreflightReuse(SQL, databasePath, projectRoot, preflightFiles, sourceScopeHash, dryRun, indexMode);
|
|
2195
|
+
if (preflightReuse.result) {
|
|
2196
|
+
return preflightReuse.result;
|
|
2197
|
+
}
|
|
2198
|
+
rebuildReason = preflightReuse.rebuildReason;
|
|
2199
|
+
}
|
|
2018
2200
|
const documents = collectDocuments(projectRoot);
|
|
2019
2201
|
const skills = collectSkills(documents);
|
|
2020
2202
|
const skillRoutes = collectSkillRoutes(projectRoot);
|
|
2021
2203
|
const commandIntents = collectCommandIntents(projectRoot);
|
|
2022
|
-
const sourceConfig = readLocalIndexSourceConfig(projectRoot);
|
|
2023
|
-
const includeSource = options.includeSource === true || sourceConfig.enabledByDefault;
|
|
2024
|
-
const sourceScopeHash = getSourceScopeHash(includeSource, sourceConfig);
|
|
2025
2204
|
const previousSourceAnchors = includeSource
|
|
2026
2205
|
? await readPreviousSourceAnchorSnapshots(databasePath).catch(() => [])
|
|
2027
2206
|
: [];
|
|
2207
|
+
const sourceAnchorCandidatePaths = includeSource ? collectSourceAnchorCandidatePaths(projectRoot, sourceConfig) : [];
|
|
2028
2208
|
const sourceAnchors = includeSource
|
|
2029
2209
|
? collectSourceAnchorIndexRecords(projectRoot, previousSourceAnchors, {
|
|
2030
2210
|
...sourceConfig,
|
|
@@ -2032,18 +2212,11 @@ export async function createLocalIndex(projectRoot, options = {}) {
|
|
|
2032
2212
|
})
|
|
2033
2213
|
: [];
|
|
2034
2214
|
const verificationEvidence = createVerificationEvidenceIndex(projectRoot);
|
|
2035
|
-
const indexedFiles = collectIndexedFileRecords(projectRoot, documents, sourceAnchors);
|
|
2036
|
-
let capabilities = searchCapabilities(false);
|
|
2037
|
-
let reusedExisting = false;
|
|
2038
|
-
let rebuildReason = null;
|
|
2039
|
-
const SQL = await loadSqlJs();
|
|
2040
|
-
const capabilityDatabase = new SQL.Database();
|
|
2041
|
-
capabilities = detectLocalSearchCapabilities(capabilityDatabase);
|
|
2042
|
-
capabilityDatabase.close();
|
|
2215
|
+
const indexedFiles = collectIndexedFileRecords(projectRoot, documents, sourceAnchors, sourceAnchorCandidatePaths);
|
|
2043
2216
|
if (incremental) {
|
|
2044
2217
|
const reuseDecision = await readIncrementalReuseDecision(SQL, databasePath, indexedFiles, sourceScopeHash);
|
|
2045
2218
|
reusedExisting = reuseDecision.reusable;
|
|
2046
|
-
rebuildReason = reuseDecision.rebuildReason;
|
|
2219
|
+
rebuildReason = reuseDecision.rebuildReason ?? rebuildReason;
|
|
2047
2220
|
capabilities = reuseDecision.capabilities ?? capabilities;
|
|
2048
2221
|
}
|
|
2049
2222
|
if (!dryRun && !reusedExisting) {
|
|
@@ -2110,7 +2283,7 @@ function getStalePaths(projectRoot, database) {
|
|
|
2110
2283
|
const indexedPaths = new Set(indexedRows.map((row) => toSearchString(row.path)));
|
|
2111
2284
|
for (const row of indexedRows) {
|
|
2112
2285
|
const indexedPath = toSearchString(row.path);
|
|
2113
|
-
const sourceScope =
|
|
2286
|
+
const sourceScope = normalizeIndexedFileSourceScope(row.source_scope);
|
|
2114
2287
|
try {
|
|
2115
2288
|
const current = readIndexedFileRecord(projectRoot, indexedPath, sourceScope);
|
|
2116
2289
|
if (current.contentHash !== toSearchString(row.content_hash)) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { ensureInside } from './filesystem.js';
|
|
5
|
-
import {
|
|
4
|
+
import { ensureFileTargetInsideWithoutSymlinks, ensureInside, readFileInsideWithoutSymlinks, readUtf8FileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks, } from './filesystem.js';
|
|
5
|
+
import { parseTomlText, stringifyToml } from './toml.js';
|
|
6
6
|
export const MANIFEST_LOCK_RELATIVE_PATH = '.mustflow/config/manifest.lock.toml';
|
|
7
7
|
function isRecord(value) {
|
|
8
8
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -48,20 +48,31 @@ function parseManifestLock(raw) {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
export function sha256File(filePath) {
|
|
51
|
-
return `sha256:${createHash('sha256')
|
|
51
|
+
return `sha256:${createHash('sha256')
|
|
52
|
+
.update(readFileInsideWithoutSymlinks(path.dirname(filePath), filePath))
|
|
53
|
+
.digest('hex')}`;
|
|
54
|
+
}
|
|
55
|
+
function sha256ProjectFile(projectRoot, filePath) {
|
|
56
|
+
return `sha256:${createHash('sha256').update(readFileInsideWithoutSymlinks(projectRoot, filePath)).digest('hex')}`;
|
|
57
|
+
}
|
|
58
|
+
export function ensureManifestLockTargetSafe(projectRoot) {
|
|
59
|
+
const lockPath = path.join(projectRoot, MANIFEST_LOCK_RELATIVE_PATH);
|
|
60
|
+
ensureInside(projectRoot, lockPath);
|
|
61
|
+
ensureFileTargetInsideWithoutSymlinks(projectRoot, lockPath, { allowMissingLeaf: true });
|
|
62
|
+
return existsSync(lockPath);
|
|
52
63
|
}
|
|
53
64
|
export function markManifestLockFileCustomized(projectRoot, relativePath) {
|
|
54
65
|
const lockPath = path.join(projectRoot, MANIFEST_LOCK_RELATIVE_PATH);
|
|
55
66
|
const filePath = path.join(projectRoot, relativePath);
|
|
56
|
-
ensureInside(projectRoot, lockPath);
|
|
57
67
|
ensureInside(projectRoot, filePath);
|
|
58
|
-
if (!
|
|
68
|
+
if (!ensureManifestLockTargetSafe(projectRoot)) {
|
|
59
69
|
return false;
|
|
60
70
|
}
|
|
71
|
+
ensureFileTargetInsideWithoutSymlinks(projectRoot, filePath, { allowMissingLeaf: true });
|
|
61
72
|
if (!existsSync(filePath)) {
|
|
62
73
|
throw new Error(`Cannot refresh manifest lock for missing file: ${relativePath}`);
|
|
63
74
|
}
|
|
64
|
-
const parsed =
|
|
75
|
+
const parsed = parseTomlText(readUtf8FileInsideWithoutSymlinks(projectRoot, lockPath));
|
|
65
76
|
if (!isRecord(parsed)) {
|
|
66
77
|
throw new Error(`Invalid manifest lock: ${MANIFEST_LOCK_RELATIVE_PATH} must contain a TOML table`);
|
|
67
78
|
}
|
|
@@ -71,20 +82,27 @@ export function markManifestLockFileCustomized(projectRoot, relativePath) {
|
|
|
71
82
|
filesTable[relativePath] = {
|
|
72
83
|
source: typeof existingTable.source === 'string' ? existingTable.source : 'template_common',
|
|
73
84
|
last_action: 'customized',
|
|
74
|
-
content_hash:
|
|
85
|
+
content_hash: sha256ProjectFile(projectRoot, filePath),
|
|
75
86
|
};
|
|
76
87
|
parsed.files = filesTable;
|
|
77
|
-
|
|
88
|
+
writeUtf8FileInsideWithoutSymlinks(projectRoot, lockPath, stringifyToml(parsed));
|
|
78
89
|
return true;
|
|
79
90
|
}
|
|
80
91
|
export function readManifestLock(projectRoot) {
|
|
81
92
|
const lockPath = path.join(projectRoot, MANIFEST_LOCK_RELATIVE_PATH);
|
|
82
|
-
|
|
93
|
+
try {
|
|
94
|
+
ensureInside(projectRoot, lockPath);
|
|
95
|
+
ensureFileTargetInsideWithoutSymlinks(projectRoot, lockPath, { allowMissingLeaf: true });
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
+
return { kind: 'invalid', lockPath, message };
|
|
100
|
+
}
|
|
83
101
|
if (!existsSync(lockPath)) {
|
|
84
102
|
return { kind: 'missing', lockPath };
|
|
85
103
|
}
|
|
86
104
|
try {
|
|
87
|
-
return { kind: 'present', lockPath, lock: parseManifestLock(
|
|
105
|
+
return { kind: 'present', lockPath, lock: parseManifestLock(parseTomlText(readUtf8FileInsideWithoutSymlinks(projectRoot, lockPath))) };
|
|
88
106
|
}
|
|
89
107
|
catch (error) {
|
|
90
108
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -121,7 +139,15 @@ export function inspectManifestLock(projectRoot) {
|
|
|
121
139
|
issues.push(`Locked file missing: ${lockedFile.relativePath}`);
|
|
122
140
|
continue;
|
|
123
141
|
}
|
|
124
|
-
|
|
142
|
+
let actualHash;
|
|
143
|
+
try {
|
|
144
|
+
actualHash = sha256ProjectFile(projectRoot, filePath);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
148
|
+
issues.push(`Locked file cannot be read safely: ${lockedFile.relativePath}: ${message}`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
125
151
|
if (actualHash !== lockedFile.contentHash) {
|
|
126
152
|
changedFiles.push(lockedFile.relativePath);
|
|
127
153
|
issues.push(`Lock hash mismatch: ${lockedFile.relativePath}`);
|
package/dist/cli/lib/run-plan.js
CHANGED
|
@@ -78,6 +78,9 @@ function readEffectiveMaxOutputBytes(contract, intent) {
|
|
|
78
78
|
readPositiveInteger(contract.defaults, 'max_output_bytes') ??
|
|
79
79
|
DEFAULT_COMMAND_MAX_OUTPUT_BYTES;
|
|
80
80
|
}
|
|
81
|
+
function readEffectiveKillAfterSeconds(contract) {
|
|
82
|
+
return readPositiveInteger(contract.defaults, 'kill_after_seconds') ?? 5;
|
|
83
|
+
}
|
|
81
84
|
function getMaxOutputBytesLimitDetail(contract, intent) {
|
|
82
85
|
const intentValue = readPositiveInteger(intent, 'max_output_bytes');
|
|
83
86
|
if (intentValue !== undefined) {
|
|
@@ -103,6 +106,7 @@ function readRunIntentMetadata(contract, intent) {
|
|
|
103
106
|
kind: readString(intent, 'kind') ?? null,
|
|
104
107
|
configuredCwd,
|
|
105
108
|
timeoutSeconds: readPositiveInteger(intent, 'timeout_seconds') ?? null,
|
|
109
|
+
killAfterSeconds: readEffectiveKillAfterSeconds(contract),
|
|
106
110
|
maxOutputBytes: readEffectiveMaxOutputBytes(contract, intent),
|
|
107
111
|
successExitCodes: getSuccessExitCodes(intent),
|
|
108
112
|
commandArgv,
|
|
@@ -138,6 +142,7 @@ function createBlockedRunPlan(contract, intentName, intent, eligibility, reasonC
|
|
|
138
142
|
cwd: null,
|
|
139
143
|
relativeCwd: null,
|
|
140
144
|
timeoutSeconds: metadata?.timeoutSeconds ?? null,
|
|
145
|
+
killAfterSeconds: metadata?.killAfterSeconds ?? null,
|
|
141
146
|
maxOutputBytes: metadata?.maxOutputBytes ?? null,
|
|
142
147
|
successExitCodes: metadata?.successExitCodes ?? null,
|
|
143
148
|
commandArgv: metadata?.commandArgv,
|
|
@@ -199,6 +204,7 @@ export function createRunPlan(projectRoot, contract, intentName, options = {}) {
|
|
|
199
204
|
cwd,
|
|
200
205
|
relativeCwd: getRelativeProjectPath(projectRoot, cwd),
|
|
201
206
|
timeoutSeconds: metadata.timeoutSeconds,
|
|
207
|
+
killAfterSeconds: metadata.killAfterSeconds,
|
|
202
208
|
maxOutputBytes: metadata.maxOutputBytes,
|
|
203
209
|
successExitCodes: metadata.successExitCodes,
|
|
204
210
|
commandArgv,
|
|
@@ -15,6 +15,7 @@ const CHECK_ISSUE_ID_RULES = [
|
|
|
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
17
|
['mustflow.command_contract.broad_env_inheritance', /^Strict warning: configured agent-runnable intent [^\s]+ (?:implicitly inherits the host environment|uses env_policy = "inherit")/u],
|
|
18
|
+
['mustflow.command_contract.project_local_bin_bare_executable', /^Strict warning: configured agent-runnable intent [^\s]+ uses bare executable "[^"]+" that matches project-local node_modules\/\.bin/u],
|
|
18
19
|
['mustflow.prompt_cache.required', /^Strict: \[prompt_cache\] table is required$/u],
|
|
19
20
|
['mustflow.prompt_cache.volatile_in_stable', /^Strict: \[prompt_cache\.layers\.stable\]\.read must not include volatile path /u],
|
|
20
21
|
['mustflow.refresh.hash_method_required', /^Strict: \[refresh\]\.default_method should be "hash_check" for cache-friendly refresh$/u],
|
|
@@ -1,20 +1,4 @@
|
|
|
1
1
|
const MUSTFLOW_BIN_NAMES = new Set(['mf', 'mustflow']);
|
|
2
|
-
const IN_PROCESS_MUSTFLOW_BUILTIN_COMMANDS = new Set([
|
|
3
|
-
'check',
|
|
4
|
-
'classify',
|
|
5
|
-
'context',
|
|
6
|
-
'doctor',
|
|
7
|
-
'help',
|
|
8
|
-
'impact',
|
|
9
|
-
'line-endings',
|
|
10
|
-
'map',
|
|
11
|
-
'status',
|
|
12
|
-
'update',
|
|
13
|
-
'version-sources',
|
|
14
|
-
]);
|
|
15
2
|
export function isMustflowBinName(command) {
|
|
16
3
|
return MUSTFLOW_BIN_NAMES.has(command.toLowerCase());
|
|
17
4
|
}
|
|
18
|
-
export function canRunMustflowBuiltinInProcess(command) {
|
|
19
|
-
return command !== undefined && IN_PROCESS_MUSTFLOW_BUILTIN_COMMANDS.has(command);
|
|
20
|
-
}
|
|
@@ -14,6 +14,7 @@ const INTERPRETER_EVALUATION_FLAGS = new Map([
|
|
|
14
14
|
const PACKAGE_SCRIPT_RUNNERS = new Set(['bun', 'npm', 'pnpm', 'yarn']);
|
|
15
15
|
const LONG_RUNNING_PACKAGE_SCRIPTS = new Set(['dev', 'start', 'serve', 'watch', 'preview']);
|
|
16
16
|
const LONG_RUNNING_EXECUTABLES = new Set(['nodemon', 'pm2', 'serve', 'http-server', 'live-server', 'webpack-dev-server']);
|
|
17
|
+
const ATTACHED_EVALUATION_FLAGS = new Set(['-command', '-commandwithargs']);
|
|
17
18
|
export const BACKGROUND_SHELL_PATTERNS = [
|
|
18
19
|
/(?:^|[^&])&(?!&)\s*$/u,
|
|
19
20
|
/\bnohup\b/iu,
|
|
@@ -45,16 +46,26 @@ export function commandIntentHasCommandSource(intent) {
|
|
|
45
46
|
export function shellCommandHasBlockedBackgroundPattern(command) {
|
|
46
47
|
return BACKGROUND_SHELL_PATTERNS.some((pattern) => pattern.test(command));
|
|
47
48
|
}
|
|
48
|
-
export function commandIntentHasBlockedShellBackgroundPattern(intent) {
|
|
49
|
-
return intent.mode === 'shell' && typeof intent.cmd === 'string' && shellCommandHasBlockedBackgroundPattern(intent.cmd);
|
|
50
|
-
}
|
|
51
49
|
function normalizeExecutableName(value) {
|
|
52
50
|
return path.basename(value).replace(/\.(?:cmd|exe|ps1)$/iu, '').toLowerCase();
|
|
53
51
|
}
|
|
52
|
+
function flagAllowsAttachedPayload(flag) {
|
|
53
|
+
return (flag.startsWith('-') && !flag.startsWith('--') && flag.length === 2) || flag === '/c' || ATTACHED_EVALUATION_FLAGS.has(flag);
|
|
54
|
+
}
|
|
54
55
|
function findFlagPayload(argv, flags) {
|
|
55
|
-
for (let index = 1; index < argv.length
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
57
|
+
const argument = argv[index] ?? '';
|
|
58
|
+
const normalizedArgument = argument.toLowerCase();
|
|
59
|
+
if (flags.has(normalizedArgument)) {
|
|
60
|
+
return argv[index + 1] ?? null;
|
|
61
|
+
}
|
|
62
|
+
for (const flag of flags) {
|
|
63
|
+
if (normalizedArgument.startsWith(`${flag}=`)) {
|
|
64
|
+
return argument.slice(flag.length + 1);
|
|
65
|
+
}
|
|
66
|
+
if (flagAllowsAttachedPayload(flag) && normalizedArgument.startsWith(flag) && argument.length > flag.length) {
|
|
67
|
+
return argument.slice(flag.length);
|
|
68
|
+
}
|
|
58
69
|
}
|
|
59
70
|
}
|
|
60
71
|
return null;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { COMMAND_LIFECYCLES, COMMAND_RUN_POLICIES, LONG_RUNNING_LIFECYCLES, isRecord, readStringArray, } from './config-loading.js';
|
|
2
4
|
import { COMMAND_ENV_POLICIES, DEFAULT_COMMAND_ENV_POLICY } from './command-env.js';
|
|
3
5
|
import { COMMAND_EFFECT_CONCURRENCY, COMMAND_EFFECT_MODES, COMMAND_EFFECT_TYPES, validateCommandEffectLockWarnings, validateCommandEffects, } from './command-effects.js';
|
|
4
|
-
import { commandIntentBlockedCommandPattern,
|
|
6
|
+
import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
|
|
5
7
|
import { MAX_COMMAND_OUTPUT_BYTES, commandMaxOutputBytesLimitMessage } from './command-output-limits.js';
|
|
6
8
|
function commandContractIssue(message) {
|
|
7
9
|
return { message };
|
|
@@ -186,10 +188,10 @@ function validateCommandIntent(intentName, intent, issues) {
|
|
|
186
188
|
if (!commandIntentHasCommandSource(intent)) {
|
|
187
189
|
issues.push(commandContractIssue(`Configured intent ${intentName} must define argv or mode = "shell" with cmd`));
|
|
188
190
|
}
|
|
189
|
-
|
|
191
|
+
const blockedCommandPattern = commandIntentBlockedCommandPattern(intent);
|
|
192
|
+
if (blockedCommandPattern?.code === 'shell_background_pattern') {
|
|
190
193
|
issues.push(commandContractIssue(`Shell intent ${intentName} contains a blocked long-running or background pattern`));
|
|
191
194
|
}
|
|
192
|
-
const blockedCommandPattern = commandIntentBlockedCommandPattern(intent);
|
|
193
195
|
if (blockedCommandPattern?.code === 'long_running_command_pattern') {
|
|
194
196
|
issues.push(commandContractIssue(`Intent ${intentName} contains a blocked long-running or background command pattern`));
|
|
195
197
|
}
|
|
@@ -245,6 +247,41 @@ function validateCommandEnvInheritanceWarnings(commandsToml) {
|
|
|
245
247
|
}
|
|
246
248
|
return issues;
|
|
247
249
|
}
|
|
250
|
+
function projectLocalBinExecutableExists(projectRoot, executable) {
|
|
251
|
+
const localBinPath = path.join(projectRoot, 'node_modules', '.bin');
|
|
252
|
+
const executableName = path.basename(executable).replace(/\.(?:cmd|exe|ps1)$/iu, '');
|
|
253
|
+
const candidates = [
|
|
254
|
+
executableName,
|
|
255
|
+
`${executableName}.cmd`,
|
|
256
|
+
`${executableName}.exe`,
|
|
257
|
+
`${executableName}.ps1`,
|
|
258
|
+
];
|
|
259
|
+
return candidates.some((candidate) => existsSync(path.join(localBinPath, candidate)));
|
|
260
|
+
}
|
|
261
|
+
function validateProjectLocalBinWarnings(projectRoot, commandsToml) {
|
|
262
|
+
const issues = [];
|
|
263
|
+
if (!isRecord(commandsToml?.intents)) {
|
|
264
|
+
return issues;
|
|
265
|
+
}
|
|
266
|
+
for (const [intentName, intent] of Object.entries(commandsToml.intents)) {
|
|
267
|
+
if (!isRecord(intent)) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (intent.status !== 'configured' || intent.lifecycle !== 'oneshot' || intent.run_policy !== 'agent_allowed') {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const argv = readStringArray(intent, 'argv');
|
|
274
|
+
const executable = argv?.[0];
|
|
275
|
+
if (!executable || executable.includes('/') || executable.includes('\\')) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (!projectLocalBinExecutableExists(projectRoot, executable)) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
issues.push(commandContractWarning(`configured agent-runnable intent ${intentName} uses bare executable "${executable}" that matches project-local node_modules/.bin; use a package-manager mediated command such as npm exec, pnpm exec, bun x, or yarn exec`));
|
|
282
|
+
}
|
|
283
|
+
return issues;
|
|
284
|
+
}
|
|
248
285
|
/**
|
|
249
286
|
* mf:anchor core.command-contract-validation
|
|
250
287
|
* purpose: Validate command intent declarations that gate agent-executable repository commands.
|
|
@@ -287,6 +324,7 @@ export function validateCommandContractStrictDefaults(projectRoot, commandsToml)
|
|
|
287
324
|
}
|
|
288
325
|
}
|
|
289
326
|
issues.push(...validateCommandEnvInheritanceWarnings(commandsToml));
|
|
327
|
+
issues.push(...validateProjectLocalBinWarnings(projectRoot, commandsToml));
|
|
290
328
|
issues.push(...validateCommandEffects(projectRoot, commandsToml));
|
|
291
329
|
issues.push(...validateCommandEffectLockWarnings(commandsToml));
|
|
292
330
|
return issues;
|