mustflow 2.27.0 → 2.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli/commands/context.js +1 -0
- package/dist/cli/commands/help.js +55 -1
- package/dist/cli/commands/tech.js +346 -0
- 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/index.js +1 -0
- package/dist/cli/lib/agent-context.js +16 -0
- package/dist/cli/lib/command-registry.js +6 -0
- package/dist/cli/lib/run-plan.js +11 -3
- package/dist/cli/lib/validation/index.js +11 -0
- package/dist/cli/lib/validation/primitives.js +5 -0
- package/dist/core/command-contract-validation.js +15 -12
- package/dist/core/command-env.js +43 -0
- package/dist/core/command-intent-eligibility.js +2 -1
- package/dist/core/contract-lint.js +2 -1
- package/dist/core/technology-preferences.js +189 -0
- package/package.json +1 -1
- package/schemas/commands.schema.json +4 -1
- package/schemas/context-report.schema.json +61 -0
- package/templates/default/common/.mustflow/config/commands.toml +1 -0
- package/templates/default/common/.mustflow/config/mustflow.toml +8 -0
- package/templates/default/common/.mustflow/config/technology.toml +20 -0
- package/templates/default/i18n.toml +78 -12
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +33 -1
- package/templates/default/locales/en/.mustflow/skills/code-review/SKILL.md +15 -5
- package/templates/default/locales/en/.mustflow/skills/codebase-orientation/SKILL.md +15 -8
- package/templates/default/locales/en/.mustflow/skills/command-intent-mapping-gate/SKILL.md +124 -0
- package/templates/default/locales/en/.mustflow/skills/completion-evidence-gate/SKILL.md +178 -0
- package/templates/default/locales/en/.mustflow/skills/contract-sync-check/SKILL.md +9 -3
- package/templates/default/locales/en/.mustflow/skills/dependency-reality-check/SKILL.md +6 -3
- package/templates/default/locales/en/.mustflow/skills/evidence-stall-breaker/SKILL.md +166 -0
- package/templates/default/locales/en/.mustflow/skills/external-prompt-injection-defense/SKILL.md +8 -6
- package/templates/default/locales/en/.mustflow/skills/provenance-license-gate/SKILL.md +131 -0
- package/templates/default/locales/en/.mustflow/skills/public-json-contract-change/SKILL.md +133 -0
- package/templates/default/locales/en/.mustflow/skills/restricted-handoff-resume/SKILL.md +122 -0
- package/templates/default/locales/en/.mustflow/skills/routes.toml +60 -0
- package/templates/default/locales/en/.mustflow/skills/runtime-target-selection/SKILL.md +203 -0
- package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +55 -18
- package/templates/default/locales/en/.mustflow/skills/secret-exposure-response/SKILL.md +125 -0
- package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +10 -1
- package/templates/default/locales/en/.mustflow/skills/skill-authoring/SKILL.md +9 -5
- package/templates/default/locales/en/.mustflow/skills/source-freshness-check/SKILL.md +3 -2
- package/templates/default/locales/en/.mustflow/skills/structure-first-engineering/SKILL.md +205 -0
- package/templates/default/locales/en/.mustflow/skills/template-install-surface-sync/SKILL.md +131 -0
- package/templates/default/locales/en/AGENTS.md +8 -7
- package/templates/default/locales/ko/AGENTS.md +8 -7
- package/templates/default/manifest.toml +66 -1
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { isRecord } from '../command-contract.js';
|
|
4
4
|
import { readMustflowTomlFile } from '../toml.js';
|
|
5
5
|
import { REQUIRED_FILES, } from './constants.js';
|
|
6
|
+
import { TECHNOLOGY_CONFIG_RELATIVE_PATH } from '../../../core/technology-preferences.js';
|
|
6
7
|
import { VERSIONING_CONFIG_PATH } from '../../../core/version-sources.js';
|
|
7
8
|
export function hasOwn(table, key) {
|
|
8
9
|
return Object.prototype.hasOwnProperty.call(table, key);
|
|
@@ -34,6 +35,7 @@ export function validateToml(projectRoot, issues) {
|
|
|
34
35
|
'.mustflow/config/mustflow.toml',
|
|
35
36
|
'.mustflow/config/commands.toml',
|
|
36
37
|
'.mustflow/config/preferences.toml',
|
|
38
|
+
TECHNOLOGY_CONFIG_RELATIVE_PATH,
|
|
37
39
|
VERSIONING_CONFIG_PATH,
|
|
38
40
|
]) {
|
|
39
41
|
const filePath = path.join(projectRoot, relativePath);
|
|
@@ -55,6 +57,9 @@ export function validateToml(projectRoot, issues) {
|
|
|
55
57
|
if (relativePath.endsWith('preferences.toml')) {
|
|
56
58
|
parsedFiles.preferencesToml = parsed;
|
|
57
59
|
}
|
|
60
|
+
if (relativePath.endsWith('technology.toml')) {
|
|
61
|
+
parsedFiles.technologyToml = parsed;
|
|
62
|
+
}
|
|
58
63
|
if (relativePath.endsWith('versioning.toml')) {
|
|
59
64
|
parsedFiles.versioningToml = parsed;
|
|
60
65
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import { COMMAND_LIFECYCLES, COMMAND_RUN_POLICIES, LONG_RUNNING_LIFECYCLES, isRecord, readStringArray, } from './config-loading.js';
|
|
4
|
-
import { COMMAND_ENV_POLICIES, DEFAULT_COMMAND_ENV_POLICY } from './command-env.js';
|
|
3
|
+
import { COMMAND_ENV_POLICIES, DEFAULT_COMMAND_ENV_POLICY, PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST_KEY, normalizeCommandExecutableName, readProjectLocalBinBareExecutableAllowlist, resolveAllowedProjectLocalBinExecutable, } from './command-env.js';
|
|
5
4
|
import { COMMAND_EFFECT_CONCURRENCY, COMMAND_EFFECT_MODES, COMMAND_EFFECT_TYPES, validateCommandEffectLockWarnings, validateCommandEffects, } from './command-effects.js';
|
|
6
5
|
import { COMMAND_PRECONDITION_KINDS } from './command-preconditions.js';
|
|
7
6
|
import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
|
|
@@ -13,6 +12,8 @@ const COMMAND_ARGV_PLACEHOLDER_PATTERN = /^\{([a-z][a-z0-9_]*)\}$/u;
|
|
|
13
12
|
const COMMAND_ARGV_MIXED_PLACEHOLDER_PATTERN = /\{([a-z][a-z0-9_]*)\}/u;
|
|
14
13
|
const WINDOWS_RESERVED_PATH_SEGMENTS = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu;
|
|
15
14
|
const SAFE_PATH_EXTENSION_PATTERN = /^\.[A-Za-z0-9][A-Za-z0-9._-]*$/u;
|
|
15
|
+
const ALLOW_ENV_INHERITANCE_RISKS_KEY = 'allow_env_inheritance_risks';
|
|
16
|
+
const ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY = 'allow_long_running_command_patterns';
|
|
16
17
|
function commandContractIssue(message, id) {
|
|
17
18
|
return id ? { id, message } : { message };
|
|
18
19
|
}
|
|
@@ -257,6 +258,7 @@ function validateCommandDefaults(commandsToml, issues) {
|
|
|
257
258
|
validateStringField(defaults, 'on_timeout', '[commands.defaults].on_timeout', issues);
|
|
258
259
|
validateAllowedStringField(defaults, 'env_policy', '[commands.defaults].env_policy', COMMAND_ENV_POLICIES, issues);
|
|
259
260
|
validateStringArrayField(defaults, 'env_allowlist', '[commands.defaults].env_allowlist', issues);
|
|
261
|
+
validateStringArrayField(defaults, PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST_KEY, `[commands.defaults].${PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST_KEY}`, issues);
|
|
260
262
|
validatePositiveIntegerField(defaults, 'default_timeout_seconds', '[commands.defaults].default_timeout_seconds', issues);
|
|
261
263
|
validateMaxOutputBytesField(defaults, 'max_output_bytes', '[commands.defaults].max_output_bytes', issues);
|
|
262
264
|
validatePositiveIntegerField(defaults, 'kill_after_seconds', '[commands.defaults].kill_after_seconds', issues);
|
|
@@ -337,6 +339,8 @@ function validateCommandIntent(intentName, intent, allIntents, issues) {
|
|
|
337
339
|
validateAllowedStringField(intent, 'env_policy', `[commands.intents.${intentName}].env_policy`, COMMAND_ENV_POLICIES, issues);
|
|
338
340
|
validateBooleanField(intent, 'allow_shell', `[commands.intents.${intentName}].allow_shell`, issues);
|
|
339
341
|
validateStringArrayField(intent, 'env_allowlist', `[commands.intents.${intentName}].env_allowlist`, issues);
|
|
342
|
+
validateBooleanField(intent, ALLOW_ENV_INHERITANCE_RISKS_KEY, `[commands.intents.${intentName}].${ALLOW_ENV_INHERITANCE_RISKS_KEY}`, issues);
|
|
343
|
+
validateBooleanField(intent, ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY, `[commands.intents.${intentName}].${ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY}`, issues);
|
|
340
344
|
validateMaxOutputBytesField(intent, 'max_output_bytes', `[commands.intents.${intentName}].max_output_bytes`, issues);
|
|
341
345
|
validatePositiveIntegerField(intent, 'kill_after_seconds', `[commands.intents.${intentName}].kill_after_seconds`, issues);
|
|
342
346
|
validateCommandIntentSelection(intentName, intent, issues);
|
|
@@ -375,7 +379,7 @@ function validateCommandIntent(intentName, intent, allIntents, issues) {
|
|
|
375
379
|
if (blockedCommandPattern?.code === 'shell_background_pattern') {
|
|
376
380
|
issues.push(commandContractIssue(`Shell intent ${intentName} contains a blocked long-running or background pattern`, 'mustflow.command_contract.shell_background_pattern'));
|
|
377
381
|
}
|
|
378
|
-
if (blockedCommandPattern?.code === 'long_running_command_pattern') {
|
|
382
|
+
if (blockedCommandPattern?.code === 'long_running_command_pattern' && intent[ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY] !== true) {
|
|
379
383
|
issues.push(commandContractIssue(`Intent ${intentName} contains a blocked long-running or background command pattern`, 'mustflow.command_contract.long_running_command_pattern'));
|
|
380
384
|
}
|
|
381
385
|
if (hasOwn(intent, 'success_exit_codes')) {
|
|
@@ -419,6 +423,9 @@ export function findCommandEnvInheritanceWarnings(commandsToml) {
|
|
|
419
423
|
if (envPolicy.policy !== 'inherit') {
|
|
420
424
|
continue;
|
|
421
425
|
}
|
|
426
|
+
if (intent[ALLOW_ENV_INHERITANCE_RISKS_KEY] === true) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
422
429
|
const reasons = readCommandEnvInheritanceRiskReasons(intent);
|
|
423
430
|
warnings.push({
|
|
424
431
|
intentName,
|
|
@@ -465,21 +472,14 @@ function validateCommandEnvInheritanceWarnings(commandsToml) {
|
|
|
465
472
|
return issues;
|
|
466
473
|
}
|
|
467
474
|
function projectLocalBinExecutableExists(projectRoot, executable) {
|
|
468
|
-
|
|
469
|
-
const executableName = path.basename(executable).replace(/\.(?:cmd|exe|ps1)$/iu, '');
|
|
470
|
-
const candidates = [
|
|
471
|
-
executableName,
|
|
472
|
-
`${executableName}.cmd`,
|
|
473
|
-
`${executableName}.exe`,
|
|
474
|
-
`${executableName}.ps1`,
|
|
475
|
-
];
|
|
476
|
-
return candidates.some((candidate) => existsSync(path.join(localBinPath, candidate)));
|
|
475
|
+
return resolveAllowedProjectLocalBinExecutable(projectRoot, executable, new Set([normalizeCommandExecutableName(executable)])) !== null;
|
|
477
476
|
}
|
|
478
477
|
function validateProjectLocalBinWarnings(projectRoot, commandsToml) {
|
|
479
478
|
const issues = [];
|
|
480
479
|
if (!isRecord(commandsToml?.intents)) {
|
|
481
480
|
return issues;
|
|
482
481
|
}
|
|
482
|
+
const allowedBareExecutables = readProjectLocalBinBareExecutableAllowlist(commandsToml);
|
|
483
483
|
for (const [intentName, intent] of Object.entries(commandsToml.intents)) {
|
|
484
484
|
if (!isRecord(intent)) {
|
|
485
485
|
continue;
|
|
@@ -492,6 +492,9 @@ function validateProjectLocalBinWarnings(projectRoot, commandsToml) {
|
|
|
492
492
|
if (!executable || executable.includes('/') || executable.includes('\\')) {
|
|
493
493
|
continue;
|
|
494
494
|
}
|
|
495
|
+
if (allowedBareExecutables.has(normalizeCommandExecutableName(executable))) {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
495
498
|
if (!projectLocalBinExecutableExists(projectRoot, executable)) {
|
|
496
499
|
continue;
|
|
497
500
|
}
|
package/dist/core/command-env.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import { readString, readStringArray } from './config-loading.js';
|
|
3
4
|
export const COMMAND_ENV_POLICIES = new Set(['inherit', 'minimal', 'allowlist']);
|
|
@@ -25,6 +26,8 @@ const BASE_MINIMAL_ENV_KEYS = [
|
|
|
25
26
|
'WINDIR',
|
|
26
27
|
'windir',
|
|
27
28
|
];
|
|
29
|
+
export const PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST_KEY = 'allow_project_local_bin_bare_executables';
|
|
30
|
+
export const DEFAULT_PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST = new Set(['mf', 'mustflow']);
|
|
28
31
|
function getPathEnvKey(env) {
|
|
29
32
|
return Object.keys(env).find((key) => key.toLowerCase() === 'path') ?? 'PATH';
|
|
30
33
|
}
|
|
@@ -48,6 +51,46 @@ function readEnvPolicy(table) {
|
|
|
48
51
|
function readEnvAllowlist(table) {
|
|
49
52
|
return table ? (readStringArray(table, 'env_allowlist') ?? []) : [];
|
|
50
53
|
}
|
|
54
|
+
export function normalizeCommandExecutableName(executable) {
|
|
55
|
+
return path.basename(executable).replace(/\.(?:cmd|exe|ps1)$/iu, '').toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
export function readProjectLocalBinBareExecutableAllowlist(contractOrCommands) {
|
|
58
|
+
const defaults = contractOrCommands && typeof contractOrCommands === 'object' && 'defaults' in contractOrCommands
|
|
59
|
+
? contractOrCommands.defaults
|
|
60
|
+
: undefined;
|
|
61
|
+
const configuredAllowlist = defaults && typeof defaults === 'object'
|
|
62
|
+
? defaults[PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST_KEY]
|
|
63
|
+
: undefined;
|
|
64
|
+
if (!Array.isArray(configuredAllowlist)) {
|
|
65
|
+
return DEFAULT_PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST;
|
|
66
|
+
}
|
|
67
|
+
return new Set(configuredAllowlist
|
|
68
|
+
.filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
|
|
69
|
+
.map((entry) => normalizeCommandExecutableName(entry)));
|
|
70
|
+
}
|
|
71
|
+
export function resolveAllowedProjectLocalBinExecutable(projectRoot, executable, allowlist) {
|
|
72
|
+
if (executable.includes('/') || executable.includes('\\')) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const executableName = normalizeCommandExecutableName(executable);
|
|
76
|
+
if (!allowlist.has(executableName)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const localBinPath = path.join(projectRoot, 'node_modules', '.bin');
|
|
80
|
+
const candidates = [
|
|
81
|
+
executableName,
|
|
82
|
+
`${executableName}.cmd`,
|
|
83
|
+
`${executableName}.exe`,
|
|
84
|
+
`${executableName}.ps1`,
|
|
85
|
+
];
|
|
86
|
+
for (const candidate of candidates) {
|
|
87
|
+
const candidatePath = path.join(localBinPath, candidate);
|
|
88
|
+
if (existsSync(candidatePath)) {
|
|
89
|
+
return candidatePath;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
51
94
|
function pickEnv(source, names) {
|
|
52
95
|
const output = {};
|
|
53
96
|
for (const name of names) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isRecord, readString } from './config-loading.js';
|
|
2
2
|
import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
|
|
3
|
+
const ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY = 'allow_long_running_command_patterns';
|
|
3
4
|
export const COMMAND_INTENT_INELIGIBILITY_CODES = [
|
|
4
5
|
'intent_not_table',
|
|
5
6
|
'status_not_configured',
|
|
@@ -83,7 +84,7 @@ export function evaluateCommandIntentEligibility(intentName, rawIntent) {
|
|
|
83
84
|
detail: blockedPattern.detail,
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
|
-
if (blockedPattern?.code === 'long_running_command_pattern') {
|
|
87
|
+
if (blockedPattern?.code === 'long_running_command_pattern' && rawIntent[ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY] !== true) {
|
|
87
88
|
return {
|
|
88
89
|
ok: false,
|
|
89
90
|
code: 'blocked_long_running_command_pattern',
|
|
@@ -58,6 +58,7 @@ const AGENT_WORKFLOW_PATH = '.mustflow/docs/agent-workflow.md';
|
|
|
58
58
|
const PACKAGE_SCRIPT_RUNNERS = new Set(['bun', 'npm', 'pnpm', 'yarn']);
|
|
59
59
|
const MAKEFILE_CANDIDATES = ['Makefile', 'makefile'];
|
|
60
60
|
const JUSTFILE_CANDIDATES = ['justfile', 'Justfile'];
|
|
61
|
+
const ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY = 'allow_long_running_command_patterns';
|
|
61
62
|
function uniqueSorted(values) {
|
|
62
63
|
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
63
64
|
}
|
|
@@ -333,7 +334,7 @@ function lintIntent(name, value, issues) {
|
|
|
333
334
|
if (blockedCommandPattern?.code === 'shell_background_pattern') {
|
|
334
335
|
pushIssue(issues, 'error', 'shell_background_pattern', name, `Shell intent ${name} contains a blocked long-running or background pattern.`);
|
|
335
336
|
}
|
|
336
|
-
if (blockedCommandPattern?.code === 'long_running_command_pattern') {
|
|
337
|
+
if (blockedCommandPattern?.code === 'long_running_command_pattern' && value[ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY] !== true) {
|
|
337
338
|
pushIssue(issues, 'error', 'long_running_command_pattern', name, `Intent ${name} contains a blocked long-running or background command pattern.`);
|
|
338
339
|
}
|
|
339
340
|
if (!successExitCodesAreValid(value)) {
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isRecord } from './config-loading.js';
|
|
4
|
+
export const TECHNOLOGY_CONFIG_RELATIVE_PATH = '.mustflow/config/technology.toml';
|
|
5
|
+
export const TECHNOLOGY_SCHEMA_VERSION = '1';
|
|
6
|
+
export const TECHNOLOGY_AUTHORITY = 'hint';
|
|
7
|
+
export const TECHNOLOGY_GUIDANCE = [
|
|
8
|
+
'Technology preferences are hints only; inspect the current repository stack before proposing changes.',
|
|
9
|
+
'They do not authorize dependency installation, framework migration, command execution, or ignoring existing style.',
|
|
10
|
+
'When adding packages, verify package reality and use the configured command contract or direct user approval.',
|
|
11
|
+
];
|
|
12
|
+
export const TECHNOLOGY_KINDS = [
|
|
13
|
+
'language',
|
|
14
|
+
'runtime',
|
|
15
|
+
'framework',
|
|
16
|
+
'library',
|
|
17
|
+
'tool',
|
|
18
|
+
'service',
|
|
19
|
+
'platform',
|
|
20
|
+
];
|
|
21
|
+
export const TECHNOLOGY_STATUSES = ['preferred', 'allowed', 'avoid'];
|
|
22
|
+
function isTechnologyKind(value) {
|
|
23
|
+
return TECHNOLOGY_KINDS.includes(value);
|
|
24
|
+
}
|
|
25
|
+
function isTechnologyStatus(value) {
|
|
26
|
+
return TECHNOLOGY_STATUSES.includes(value);
|
|
27
|
+
}
|
|
28
|
+
function readString(value) {
|
|
29
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
30
|
+
}
|
|
31
|
+
function readStringArray(value) {
|
|
32
|
+
if (!Array.isArray(value)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const entries = value
|
|
36
|
+
.filter((entry) => typeof entry === 'string')
|
|
37
|
+
.map((entry) => entry.trim())
|
|
38
|
+
.filter((entry) => entry.length > 0);
|
|
39
|
+
return entries.length === value.length ? entries : null;
|
|
40
|
+
}
|
|
41
|
+
function normalizeStringList(values) {
|
|
42
|
+
return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))];
|
|
43
|
+
}
|
|
44
|
+
export function normalizeTechnologyName(value) {
|
|
45
|
+
return value.trim().replace(/\s+/gu, ' ');
|
|
46
|
+
}
|
|
47
|
+
export function normalizeTechnologyKey(value) {
|
|
48
|
+
return normalizeTechnologyName(value).toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
function slug(value) {
|
|
51
|
+
const normalized = normalizeTechnologyKey(value)
|
|
52
|
+
.replace(/[^a-z0-9]+/gu, '-')
|
|
53
|
+
.replace(/^-+|-+$/gu, '');
|
|
54
|
+
return normalized.length > 0 ? normalized : 'item';
|
|
55
|
+
}
|
|
56
|
+
export function createTechnologyPreferenceId(kind, name, scope) {
|
|
57
|
+
const scopePart = scope.length > 0 ? `${slug(scope[0])}.` : '';
|
|
58
|
+
return `${kind}.${scopePart}${slug(name)}`;
|
|
59
|
+
}
|
|
60
|
+
function normalizePreference(raw, index, issues) {
|
|
61
|
+
const rawKind = readString(raw.kind);
|
|
62
|
+
if (!rawKind || !isTechnologyKind(rawKind)) {
|
|
63
|
+
issues.push(`[preferences.${index}].kind must be one of ${TECHNOLOGY_KINDS.join(', ')}`);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const rawName = readString(raw.name);
|
|
67
|
+
if (!rawName) {
|
|
68
|
+
issues.push(`[preferences.${index}].name must be a non-empty string`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const rawStatus = readString(raw.status) ?? 'preferred';
|
|
72
|
+
if (!isTechnologyStatus(rawStatus)) {
|
|
73
|
+
issues.push(`[preferences.${index}].status must be preferred, allowed, or avoid`);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const rawAuthority = readString(raw.authority) ?? TECHNOLOGY_AUTHORITY;
|
|
77
|
+
if (rawAuthority !== TECHNOLOGY_AUTHORITY) {
|
|
78
|
+
issues.push(`[preferences.${index}].authority must be "${TECHNOLOGY_AUTHORITY}"`);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const scope = normalizeStringList(readStringArray(raw.scope) ?? []);
|
|
82
|
+
const packages = normalizeStringList(readStringArray(raw.packages) ?? []);
|
|
83
|
+
const constraints = normalizeStringList(readStringArray(raw.constraints) ?? []);
|
|
84
|
+
const id = readString(raw.id) ?? createTechnologyPreferenceId(rawKind, rawName, scope);
|
|
85
|
+
return {
|
|
86
|
+
id,
|
|
87
|
+
kind: rawKind,
|
|
88
|
+
name: normalizeTechnologyName(rawName),
|
|
89
|
+
status: rawStatus,
|
|
90
|
+
authority: TECHNOLOGY_AUTHORITY,
|
|
91
|
+
scope,
|
|
92
|
+
ecosystem: readString(raw.ecosystem),
|
|
93
|
+
packages,
|
|
94
|
+
rationale: readString(raw.rationale),
|
|
95
|
+
constraints,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export function normalizeTechnologyPreferencesTable(table, exists) {
|
|
99
|
+
const issues = [];
|
|
100
|
+
const schemaVersion = table ? readString(table.schema_version) ?? '' : TECHNOLOGY_SCHEMA_VERSION;
|
|
101
|
+
if (table && schemaVersion !== TECHNOLOGY_SCHEMA_VERSION) {
|
|
102
|
+
issues.push(`[technology].schema_version must be "${TECHNOLOGY_SCHEMA_VERSION}"`);
|
|
103
|
+
}
|
|
104
|
+
const rawPreferences = table?.preferences;
|
|
105
|
+
const preferences = [];
|
|
106
|
+
if (rawPreferences !== undefined) {
|
|
107
|
+
if (!Array.isArray(rawPreferences)) {
|
|
108
|
+
issues.push('[technology].preferences must be an array of tables');
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
for (const [index, rawPreference] of rawPreferences.entries()) {
|
|
112
|
+
if (!isRecord(rawPreference)) {
|
|
113
|
+
issues.push(`[preferences.${index}] must be a TOML table`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const preference = normalizePreference(rawPreference, index, issues);
|
|
117
|
+
if (preference) {
|
|
118
|
+
preferences.push(preference);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const seenIds = new Set();
|
|
124
|
+
for (const preference of preferences) {
|
|
125
|
+
if (seenIds.has(preference.id)) {
|
|
126
|
+
issues.push(`[technology].preferences contains duplicate id "${preference.id}"`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
seenIds.add(preference.id);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
path: TECHNOLOGY_CONFIG_RELATIVE_PATH,
|
|
133
|
+
exists,
|
|
134
|
+
schema_version: schemaVersion || TECHNOLOGY_SCHEMA_VERSION,
|
|
135
|
+
authority: TECHNOLOGY_AUTHORITY,
|
|
136
|
+
guidance: TECHNOLOGY_GUIDANCE,
|
|
137
|
+
preferences,
|
|
138
|
+
issues,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
export function technologyPreferenceMatches(preference, filters) {
|
|
142
|
+
if (filters.kind && preference.kind !== filters.kind) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if (filters.status && preference.status !== filters.status) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (filters.scope) {
|
|
149
|
+
const expectedScope = normalizeTechnologyKey(filters.scope);
|
|
150
|
+
if (!preference.scope.some((scope) => normalizeTechnologyKey(scope) === expectedScope)) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
function quoteTomlString(value) {
|
|
157
|
+
return JSON.stringify(value);
|
|
158
|
+
}
|
|
159
|
+
function renderStringArray(values) {
|
|
160
|
+
return `[${values.map((value) => quoteTomlString(value)).join(', ')}]`;
|
|
161
|
+
}
|
|
162
|
+
export function serializeTechnologyPreferences(preferences) {
|
|
163
|
+
const lines = [
|
|
164
|
+
`schema_version = ${quoteTomlString(TECHNOLOGY_SCHEMA_VERSION)}`,
|
|
165
|
+
'',
|
|
166
|
+
'# Repository-local technology preferences for agents.',
|
|
167
|
+
'# Entries here are low-authority hints. They do not authorize dependency installation,',
|
|
168
|
+
'# migration, command execution, or ignoring current project style.',
|
|
169
|
+
];
|
|
170
|
+
for (const preference of [...preferences].sort((left, right) => left.id.localeCompare(right.id))) {
|
|
171
|
+
lines.push('', '[[preferences]]', `id = ${quoteTomlString(preference.id)}`, `kind = ${quoteTomlString(preference.kind)}`, `name = ${quoteTomlString(preference.name)}`, `status = ${quoteTomlString(preference.status)}`, `authority = ${quoteTomlString(TECHNOLOGY_AUTHORITY)}`, `scope = ${renderStringArray(preference.scope)}`);
|
|
172
|
+
if (preference.ecosystem) {
|
|
173
|
+
lines.push(`ecosystem = ${quoteTomlString(preference.ecosystem)}`);
|
|
174
|
+
}
|
|
175
|
+
if (preference.packages.length > 0) {
|
|
176
|
+
lines.push(`packages = ${renderStringArray(preference.packages)}`);
|
|
177
|
+
}
|
|
178
|
+
if (preference.rationale) {
|
|
179
|
+
lines.push(`rationale = ${quoteTomlString(preference.rationale)}`);
|
|
180
|
+
}
|
|
181
|
+
if (preference.constraints.length > 0) {
|
|
182
|
+
lines.push(`constraints = ${renderStringArray(preference.constraints)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return `${lines.join('\n')}\n`;
|
|
186
|
+
}
|
|
187
|
+
export function technologyConfigExists(projectRoot) {
|
|
188
|
+
return existsSync(path.join(projectRoot, ...TECHNOLOGY_CONFIG_RELATIVE_PATH.split('/')));
|
|
189
|
+
}
|
package/package.json
CHANGED
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"on_timeout": { "type": "string" },
|
|
37
37
|
"kill_after_seconds": { "type": "integer" },
|
|
38
38
|
"env_policy": { "$ref": "#/$defs/envPolicy" },
|
|
39
|
-
"env_allowlist": { "$ref": "#/$defs/stringArray" }
|
|
39
|
+
"env_allowlist": { "$ref": "#/$defs/stringArray" },
|
|
40
|
+
"allow_project_local_bin_bare_executables": { "$ref": "#/$defs/stringArray" }
|
|
40
41
|
}
|
|
41
42
|
},
|
|
42
43
|
"intents": {
|
|
@@ -172,6 +173,7 @@
|
|
|
172
173
|
},
|
|
173
174
|
"cmd": { "type": "string" },
|
|
174
175
|
"allow_shell": { "type": "boolean" },
|
|
176
|
+
"allow_long_running_command_patterns": { "type": "boolean" },
|
|
175
177
|
"cwd": { "type": "string" },
|
|
176
178
|
"timeout_seconds": { "type": "integer" },
|
|
177
179
|
"kill_after_seconds": { "type": "integer" },
|
|
@@ -185,6 +187,7 @@
|
|
|
185
187
|
},
|
|
186
188
|
"env_policy": { "$ref": "#/$defs/envPolicy" },
|
|
187
189
|
"env_allowlist": { "$ref": "#/$defs/stringArray" },
|
|
190
|
+
"allow_env_inheritance_risks": { "type": "boolean" },
|
|
188
191
|
"manual_start_hint": { "type": "string" },
|
|
189
192
|
"health_check_url": { "type": "string" },
|
|
190
193
|
"stop_instruction": { "type": "string" },
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"items": { "$ref": "#/$defs/pathContext" }
|
|
48
48
|
},
|
|
49
49
|
"command_contract": { "$ref": "#/$defs/commandContractContext" },
|
|
50
|
+
"technology_preferences": { "$ref": "#/$defs/technologyPreferencesContext" },
|
|
50
51
|
"effective_policy": { "$ref": "#/$defs/effectivePolicy" },
|
|
51
52
|
"state_policy": { "$ref": "#/$defs/statePolicy" },
|
|
52
53
|
"blocked_actions": {
|
|
@@ -149,6 +150,66 @@
|
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
152
|
},
|
|
153
|
+
"technologyPreference": {
|
|
154
|
+
"type": "object",
|
|
155
|
+
"additionalProperties": false,
|
|
156
|
+
"required": [
|
|
157
|
+
"id",
|
|
158
|
+
"kind",
|
|
159
|
+
"name",
|
|
160
|
+
"status",
|
|
161
|
+
"authority",
|
|
162
|
+
"scope",
|
|
163
|
+
"ecosystem",
|
|
164
|
+
"packages",
|
|
165
|
+
"rationale",
|
|
166
|
+
"constraints"
|
|
167
|
+
],
|
|
168
|
+
"properties": {
|
|
169
|
+
"id": { "type": "string" },
|
|
170
|
+
"kind": { "enum": ["language", "runtime", "framework", "library", "tool", "service", "platform"] },
|
|
171
|
+
"name": { "type": "string" },
|
|
172
|
+
"status": { "enum": ["preferred", "allowed", "avoid"] },
|
|
173
|
+
"authority": { "const": "hint" },
|
|
174
|
+
"scope": {
|
|
175
|
+
"type": "array",
|
|
176
|
+
"items": { "type": "string" }
|
|
177
|
+
},
|
|
178
|
+
"ecosystem": { "type": ["string", "null"] },
|
|
179
|
+
"packages": {
|
|
180
|
+
"type": "array",
|
|
181
|
+
"items": { "type": "string" }
|
|
182
|
+
},
|
|
183
|
+
"rationale": { "type": ["string", "null"] },
|
|
184
|
+
"constraints": {
|
|
185
|
+
"type": "array",
|
|
186
|
+
"items": { "type": "string" }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
"technologyPreferencesContext": {
|
|
191
|
+
"type": "object",
|
|
192
|
+
"additionalProperties": false,
|
|
193
|
+
"required": ["path", "exists", "authority", "guidance", "count", "preferences", "issues"],
|
|
194
|
+
"properties": {
|
|
195
|
+
"path": { "type": "string" },
|
|
196
|
+
"exists": { "type": "boolean" },
|
|
197
|
+
"authority": { "const": "hint" },
|
|
198
|
+
"guidance": {
|
|
199
|
+
"type": "array",
|
|
200
|
+
"items": { "type": "string" }
|
|
201
|
+
},
|
|
202
|
+
"count": { "type": "integer", "minimum": 0 },
|
|
203
|
+
"preferences": {
|
|
204
|
+
"type": "array",
|
|
205
|
+
"items": { "$ref": "#/$defs/technologyPreference" }
|
|
206
|
+
},
|
|
207
|
+
"issues": {
|
|
208
|
+
"type": "array",
|
|
209
|
+
"items": { "type": "string" }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
},
|
|
152
213
|
"effectivePolicy": {
|
|
153
214
|
"type": "object",
|
|
154
215
|
"additionalProperties": false,
|
|
@@ -18,6 +18,7 @@ on_timeout = "terminate_process_tree"
|
|
|
18
18
|
kill_after_seconds = 5
|
|
19
19
|
env_policy = "minimal"
|
|
20
20
|
env_allowlist = []
|
|
21
|
+
allow_project_local_bin_bare_executables = ["mf", "mustflow"]
|
|
21
22
|
|
|
22
23
|
[resources.local_index_cache]
|
|
23
24
|
description = "Generated mustflow SQLite local index under .mustflow/cache/."
|
|
@@ -7,6 +7,7 @@ read_order = [
|
|
|
7
7
|
".mustflow/config/mustflow.toml",
|
|
8
8
|
".mustflow/config/commands.toml",
|
|
9
9
|
".mustflow/config/preferences.toml",
|
|
10
|
+
".mustflow/config/technology.toml",
|
|
10
11
|
".mustflow/skills/INDEX.md",
|
|
11
12
|
]
|
|
12
13
|
optional_read_order = [
|
|
@@ -20,6 +21,7 @@ repository_map = "REPO_MAP.md"
|
|
|
20
21
|
skill_index = ".mustflow/skills/INDEX.md"
|
|
21
22
|
command_contract = ".mustflow/config/commands.toml"
|
|
22
23
|
workflow_preferences = ".mustflow/config/preferences.toml"
|
|
24
|
+
technology_preferences = ".mustflow/config/technology.toml"
|
|
23
25
|
workflow_reference = ".mustflow/docs/agent-workflow.md"
|
|
24
26
|
context_index = ".mustflow/context/INDEX.md"
|
|
25
27
|
project_context = ".mustflow/context/PROJECT.md"
|
|
@@ -35,6 +37,7 @@ anchor_files = [
|
|
|
35
37
|
".mustflow/config/mustflow.toml",
|
|
36
38
|
".mustflow/config/commands.toml",
|
|
37
39
|
".mustflow/config/preferences.toml",
|
|
40
|
+
".mustflow/config/technology.toml",
|
|
38
41
|
".mustflow/skills/INDEX.md",
|
|
39
42
|
"README.md",
|
|
40
43
|
"PROJECT.md",
|
|
@@ -98,6 +101,7 @@ command_contract = true
|
|
|
98
101
|
skills = true
|
|
99
102
|
repo_map = "generated_optional"
|
|
100
103
|
preferences = "optional"
|
|
104
|
+
technology_preferences = "optional"
|
|
101
105
|
context = "optional"
|
|
102
106
|
local_index = "generated_optional"
|
|
103
107
|
work_items = "disabled"
|
|
@@ -136,6 +140,7 @@ read = [
|
|
|
136
140
|
".mustflow/docs/agent-workflow.md",
|
|
137
141
|
".mustflow/config/mustflow.toml",
|
|
138
142
|
".mustflow/config/commands.toml",
|
|
143
|
+
".mustflow/config/technology.toml",
|
|
139
144
|
".mustflow/skills/INDEX.md",
|
|
140
145
|
]
|
|
141
146
|
|
|
@@ -230,6 +235,7 @@ read = [
|
|
|
230
235
|
"AGENTS.md",
|
|
231
236
|
".mustflow/config/mustflow.toml",
|
|
232
237
|
".mustflow/config/preferences.toml",
|
|
238
|
+
".mustflow/config/technology.toml",
|
|
233
239
|
]
|
|
234
240
|
|
|
235
241
|
[refresh.levels.skill]
|
|
@@ -247,6 +253,7 @@ read = [
|
|
|
247
253
|
".mustflow/config/mustflow.toml",
|
|
248
254
|
".mustflow/config/commands.toml",
|
|
249
255
|
".mustflow/config/preferences.toml",
|
|
256
|
+
".mustflow/config/technology.toml",
|
|
250
257
|
".mustflow/skills/INDEX.md",
|
|
251
258
|
]
|
|
252
259
|
|
|
@@ -369,6 +376,7 @@ canonical = [
|
|
|
369
376
|
".mustflow/config/commands.toml",
|
|
370
377
|
".mustflow/config/mustflow.toml",
|
|
371
378
|
".mustflow/config/preferences.toml",
|
|
379
|
+
".mustflow/config/technology.toml",
|
|
372
380
|
".mustflow/context/**",
|
|
373
381
|
".mustflow/docs/**",
|
|
374
382
|
".mustflow/skills/**",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
schema_version = "1"
|
|
2
|
+
|
|
3
|
+
# Repository-local technology preferences for agents.
|
|
4
|
+
# Entries here are low-authority hints. They do not authorize dependency installation,
|
|
5
|
+
# migration, command execution, or ignoring current project style.
|
|
6
|
+
#
|
|
7
|
+
# [[preferences]]
|
|
8
|
+
# id = "framework.frontend.nextjs"
|
|
9
|
+
# kind = "framework"
|
|
10
|
+
# name = "nextjs"
|
|
11
|
+
# status = "preferred"
|
|
12
|
+
# authority = "hint"
|
|
13
|
+
# scope = ["frontend", "web", "react"]
|
|
14
|
+
# ecosystem = "npm"
|
|
15
|
+
# packages = ["next", "react", "react-dom"]
|
|
16
|
+
# rationale = "Preferred React full-stack framework for product web apps."
|
|
17
|
+
# constraints = [
|
|
18
|
+
# "Check existing project stack before proposing migration.",
|
|
19
|
+
# "Do not install packages without direct user approval or a configured command intent.",
|
|
20
|
+
# ]
|