mustflow 2.117.0 → 2.118.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 +1 -0
- package/dist/cli/commands/run/args.js +5 -0
- package/dist/cli/commands/run/execution.js +7 -6
- package/dist/cli/commands/run/preview.js +6 -5
- package/dist/cli/commands/run/receipt.js +1 -0
- package/dist/cli/commands/run.js +2 -1
- package/dist/cli/commands/workspace.js +216 -81
- 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/manifest-lock.js +10 -1
- package/dist/cli/lib/repo-map.js +5 -0
- package/dist/cli/lib/run-context.js +136 -0
- package/dist/cli/lib/run-root-trust.js +22 -15
- package/dist/cli/lib/validation/index.js +8 -0
- package/dist/core/config-loading.js +107 -14
- package/dist/core/run-receipt.js +1 -0
- package/dist/core/workspace-command-authority.js +98 -0
- package/package.json +1 -1
- package/schemas/run-receipt.schema.json +10 -0
- package/schemas/workspace-command-catalog.schema.json +3 -0
- package/schemas/workspace-command-fragments.schema.json +2 -0
- package/schemas/workspace-status.schema.json +4 -1
- package/schemas/workspace-verification-plan.schema.json +3 -0
- package/templates/default/common/.mustflow/config/mustflow.toml +2 -0
- package/templates/default/i18n.toml +14 -2
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +12 -3
- package/templates/default/locales/en/.mustflow/skills/ai-game-asset-production/SKILL.md +209 -0
- package/templates/default/locales/en/.mustflow/skills/ai-game-asset-production/references/asset-contract-validation.md +106 -0
- package/templates/default/locales/en/.mustflow/skills/ai-game-asset-production/references/raster-alpha-atlas-checklist.md +109 -0
- package/templates/default/locales/en/.mustflow/skills/ai-game-asset-production/references/tile-animation-checklist.md +98 -0
- package/templates/default/locales/en/.mustflow/skills/connection-lifecycle-integrity-review/SKILL.md +302 -0
- package/templates/default/locales/en/.mustflow/skills/connection-lifecycle-integrity-review/references/connection-fault-injection-resource-lifetime-validation.md +277 -0
- package/templates/default/locales/en/.mustflow/skills/connection-lifecycle-integrity-review/references/node-stream-transport-lifecycle-checklist.md +381 -0
- package/templates/default/locales/en/.mustflow/skills/memory-lifetime-review/SKILL.md +94 -2
- package/templates/default/locales/en/.mustflow/skills/memory-lifetime-review/references/cross-runtime-memory-stream-reproduction-harness.md +289 -0
- package/templates/default/locales/en/.mustflow/skills/memory-lifetime-review/references/node-memory-resource-backpressure-diagnostics.md +369 -0
- package/templates/default/locales/en/.mustflow/skills/routes.toml +23 -0
- package/templates/default/manifest.toml +17 -1
package/dist/cli/i18n/en.js
CHANGED
|
@@ -1334,6 +1334,7 @@ Read these files before working:
|
|
|
1334
1334
|
"run.help.option.dryRun": "Print a non-executing command plan",
|
|
1335
1335
|
"run.help.option.planOnly": "Alias for --dry-run",
|
|
1336
1336
|
"run.help.option.json": "Print the run record or command plan as JSON",
|
|
1337
|
+
"run.help.option.repository": "Select a delegated workspace contract by repository-relative path",
|
|
1337
1338
|
"run.help.option.wait": "Wait for conflicting active run locks before executing",
|
|
1338
1339
|
"run.help.option.waitTimeout": "Maximum seconds to wait for active run locks. Default: 300",
|
|
1339
1340
|
"run.help.option.allowApproval": "Allow this execution to satisfy one approval-gated action such as network_access",
|
package/dist/cli/i18n/es.js
CHANGED
|
@@ -1334,6 +1334,7 @@ Lee estos archivos antes de trabajar:
|
|
|
1334
1334
|
"run.help.option.dryRun": "Imprime un plan de comando sin ejecutarlo",
|
|
1335
1335
|
"run.help.option.planOnly": "Alias de --dry-run",
|
|
1336
1336
|
"run.help.option.json": "Imprime el registro de ejecución o el plan de comando como JSON",
|
|
1337
|
+
"run.help.option.repository": "Selecciona un contrato delegado del workspace mediante una ruta relativa del repositorio",
|
|
1337
1338
|
"run.help.option.wait": "Espera bloqueos activos en conflicto antes de ejecutar",
|
|
1338
1339
|
"run.help.option.waitTimeout": "Segundos máximos para esperar bloqueos activos. Predeterminado: 300",
|
|
1339
1340
|
"run.help.option.allowApproval": "Permite que esta ejecución satisfaga una acción con aprobación como network_access",
|
package/dist/cli/i18n/fr.js
CHANGED
|
@@ -1334,6 +1334,7 @@ Lisez ces fichiers avant de travailler :
|
|
|
1334
1334
|
"run.help.option.dryRun": "Imprime un plan de commande sans l'exécuter",
|
|
1335
1335
|
"run.help.option.planOnly": "Alias de --dry-run",
|
|
1336
1336
|
"run.help.option.json": "Imprime l'enregistrement d'exécution ou le plan de commande en JSON",
|
|
1337
|
+
"run.help.option.repository": "Sélectionne un contrat workspace délégué par chemin relatif du dépôt",
|
|
1337
1338
|
"run.help.option.wait": "Attend les verrous actifs en conflit avant d'exécuter",
|
|
1338
1339
|
"run.help.option.waitTimeout": "Nombre maximal de secondes d'attente des verrous actifs. Par défaut : 300",
|
|
1339
1340
|
"run.help.option.allowApproval": "Autorise cette exécution à satisfaire une action soumise à approbation comme network_access",
|
package/dist/cli/i18n/hi.js
CHANGED
|
@@ -1334,6 +1334,7 @@ export const hiMessages = {
|
|
|
1334
1334
|
"run.help.option.dryRun": "कमांड चलाए बिना उसका plan प्रिंट करें",
|
|
1335
1335
|
"run.help.option.planOnly": "--dry-run का alias",
|
|
1336
1336
|
"run.help.option.json": "Run record या command plan को JSON के रूप में प्रिंट करें",
|
|
1337
|
+
"run.help.option.repository": "repository-relative path से delegated workspace contract चुनें",
|
|
1337
1338
|
"run.help.option.wait": "चलाने से पहले conflicting active run locks के लिए wait करें",
|
|
1338
1339
|
"run.help.option.waitTimeout": "active run locks के लिए wait करने की maximum seconds. Default: 300",
|
|
1339
1340
|
"run.help.option.allowApproval": "इस execution को network_access जैसे approval-gated action को satisfy करने दें",
|
package/dist/cli/i18n/ko.js
CHANGED
|
@@ -1334,6 +1334,7 @@ export const koMessages = {
|
|
|
1334
1334
|
"run.help.option.dryRun": "실행하지 않고 명령 계획을 출력합니다",
|
|
1335
1335
|
"run.help.option.planOnly": "--dry-run과 같은 동작입니다",
|
|
1336
1336
|
"run.help.option.json": "실행 결과 또는 명령 계획을 JSON으로 출력합니다",
|
|
1337
|
+
"run.help.option.repository": "저장소 상대 경로로 위임된 workspace 계약을 선택합니다",
|
|
1337
1338
|
"run.help.option.wait": "충돌하는 활성 실행 잠금이 풀릴 때까지 기다린 뒤 실행합니다",
|
|
1338
1339
|
"run.help.option.waitTimeout": "활성 실행 잠금을 기다릴 최대 초입니다. 기본값: 300",
|
|
1339
1340
|
"run.help.option.allowApproval": "이번 실행에서 network_access 같은 승인 게이트 action 하나를 허용합니다",
|
package/dist/cli/i18n/zh.js
CHANGED
|
@@ -1334,6 +1334,7 @@ export const zhMessages = {
|
|
|
1334
1334
|
"run.help.option.dryRun": "输出命令计划但不执行",
|
|
1335
1335
|
"run.help.option.planOnly": "--dry-run 的别名",
|
|
1336
1336
|
"run.help.option.json": "将运行记录或命令计划输出为 JSON",
|
|
1337
|
+
"run.help.option.repository": "通过仓库相对路径选择委派的工作区契约",
|
|
1337
1338
|
"run.help.option.wait": "执行前等待冲突的活动运行锁释放",
|
|
1338
1339
|
"run.help.option.waitTimeout": "等待活动运行锁的最大秒数。默认值:300",
|
|
1339
1340
|
"run.help.option.allowApproval": "允许本次执行满足一个需要审批的动作,例如 network_access",
|
|
@@ -107,7 +107,7 @@ export function readManifestLock(projectRoot) {
|
|
|
107
107
|
return { kind: 'invalid', lockPath, message };
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
-
|
|
110
|
+
function inspectManifestLockEntries(projectRoot, requiredPaths) {
|
|
111
111
|
const readResult = readManifestLock(projectRoot);
|
|
112
112
|
const changedFiles = [];
|
|
113
113
|
const missingFiles = [];
|
|
@@ -124,6 +124,9 @@ export function inspectManifestLock(projectRoot) {
|
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
126
|
for (const lockedFile of readResult.lock.files) {
|
|
127
|
+
if (requiredPaths && !requiredPaths.has(lockedFile.relativePath)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
127
130
|
const filePath = path.join(projectRoot, lockedFile.relativePath);
|
|
128
131
|
try {
|
|
129
132
|
ensureInside(projectRoot, filePath);
|
|
@@ -153,3 +156,9 @@ export function inspectManifestLock(projectRoot) {
|
|
|
153
156
|
}
|
|
154
157
|
return { readResult, changedFiles, missingFiles, issues };
|
|
155
158
|
}
|
|
159
|
+
export function inspectManifestLock(projectRoot) {
|
|
160
|
+
return inspectManifestLockEntries(projectRoot);
|
|
161
|
+
}
|
|
162
|
+
export function inspectManifestLockPaths(projectRoot, requiredPaths) {
|
|
163
|
+
return inspectManifestLockEntries(projectRoot, new Set(requiredPaths));
|
|
164
|
+
}
|
package/dist/cli/lib/repo-map.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { toPosixPath } from './filesystem.js';
|
|
5
5
|
import { getRepoMapSourceFingerprint, renderRepoMapFrontmatter, } from './repo-map-frontmatter.js';
|
|
6
6
|
import { writeUtf8FileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
|
|
7
|
+
import { readWorkspaceCommandAuthorityConfig, } from '../../core/workspace-command-authority.js';
|
|
7
8
|
import { isRecord } from './command-contract.js';
|
|
8
9
|
import { readMustflowTomlFile } from './toml.js';
|
|
9
10
|
const DEFAULT_DEPTH = 3;
|
|
@@ -291,6 +292,7 @@ export function getRepoMapConfig(projectRoot) {
|
|
|
291
292
|
const configuredPriorityPaths = [...getStringArray(parsed.read_order), ...getStringArray(parsed.optional_read_order)];
|
|
292
293
|
const map = isRecord(parsed.map) ? parsed.map : {};
|
|
293
294
|
const workspace = isRecord(parsed.workspace) ? parsed.workspace : {};
|
|
295
|
+
const workspaceAuthority = readWorkspaceCommandAuthorityConfig(parsed);
|
|
294
296
|
const anchorFiles = getStringArray(map.anchor_files);
|
|
295
297
|
return {
|
|
296
298
|
priorityPaths: configuredPriorityPaths.length > 0 ? configuredPriorityPaths : DEFAULT_PRIORITY_PATHS,
|
|
@@ -301,6 +303,9 @@ export function getRepoMapConfig(projectRoot) {
|
|
|
301
303
|
workspace: {
|
|
302
304
|
enabled: getBoolean(workspace.enabled, false),
|
|
303
305
|
roots: getStringArray(workspace.roots),
|
|
306
|
+
authorityMode: workspaceAuthority.authorityMode,
|
|
307
|
+
delegatedContracts: workspaceAuthority.contracts,
|
|
308
|
+
delegatedContractCount: workspaceAuthority.contracts.length,
|
|
304
309
|
maxDepth: getPositiveInteger(workspace.max_depth, 4),
|
|
305
310
|
maxRepositories: getPositiveInteger(workspace.max_repositories, 50),
|
|
306
311
|
followSymlinks: getBoolean(workspace.follow_symlinks, false),
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readCommandContract, readMustflowConfigIfExists, readScopedCommandContract, } from '../../core/config-loading.js';
|
|
4
|
+
import { readWorkspaceCommandAuthorityConfig, } from '../../core/workspace-command-authority.js';
|
|
5
|
+
import { resolveSafeProjectCwd } from '../../core/command-cwd.js';
|
|
6
|
+
import { normalizeCommandEffects } from '../../core/command-effects.js';
|
|
7
|
+
import { readEffectiveCommandCwd } from '../../core/command-run-constraints.js';
|
|
8
|
+
import { isRecord } from './command-contract.js';
|
|
9
|
+
import { resolveMustflowRoot } from './project-root.js';
|
|
10
|
+
const MUSTFLOW_CONFIG_RELATIVE_PATH = '.mustflow/config/mustflow.toml';
|
|
11
|
+
function toPosixPath(value) {
|
|
12
|
+
return value.split(path.sep).join('/');
|
|
13
|
+
}
|
|
14
|
+
function normalizeRepositoryOption(value) {
|
|
15
|
+
return value.trim().replace(/\\/gu, '/').replace(/^\.\//u, '').replace(/\/+$/gu, '');
|
|
16
|
+
}
|
|
17
|
+
function normalizeForContainment(value) {
|
|
18
|
+
const resolved = path.resolve(value);
|
|
19
|
+
let canonical = resolved;
|
|
20
|
+
try {
|
|
21
|
+
canonical = realpathSync.native(resolved);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Non-existent effect paths still inherit the canonical project root used to construct them.
|
|
25
|
+
}
|
|
26
|
+
return process.platform === 'win32' ? canonical.toLowerCase() : canonical;
|
|
27
|
+
}
|
|
28
|
+
function resolvedPathIsInside(candidate, parent) {
|
|
29
|
+
const relative = path.relative(normalizeForContainment(parent), normalizeForContainment(candidate));
|
|
30
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
31
|
+
}
|
|
32
|
+
function resolveProjectRelativePath(projectRoot, relativePath) {
|
|
33
|
+
const canonicalRoot = realpathSync.native(path.resolve(projectRoot));
|
|
34
|
+
return path.resolve(canonicalRoot, ...relativePath.split('/'));
|
|
35
|
+
}
|
|
36
|
+
function findScopeByRepository(contracts, repository) {
|
|
37
|
+
const normalized = normalizeRepositoryOption(repository);
|
|
38
|
+
return contracts.find((contract) => contract.repository === normalized);
|
|
39
|
+
}
|
|
40
|
+
function findScopeByStartPath(projectRoot, startPath, contracts) {
|
|
41
|
+
const resolvedStart = path.resolve(startPath);
|
|
42
|
+
return [...contracts]
|
|
43
|
+
.sort((left, right) => right.repository.length - left.repository.length)
|
|
44
|
+
.find((contract) => resolvedPathIsInside(resolvedStart, resolveProjectRelativePath(projectRoot, contract.repository)));
|
|
45
|
+
}
|
|
46
|
+
function startPathIsInsideWorkspaceRoot(projectRoot, startPath, workspaceRoots) {
|
|
47
|
+
const resolvedStart = path.resolve(startPath);
|
|
48
|
+
return workspaceRoots.some((workspaceRoot) => resolvedPathIsInside(resolvedStart, resolveProjectRelativePath(projectRoot, workspaceRoot)));
|
|
49
|
+
}
|
|
50
|
+
function assertScopeRepositoryExists(projectRoot, scope) {
|
|
51
|
+
const repositoryRoot = resolveProjectRelativePath(projectRoot, scope.repository);
|
|
52
|
+
if (!resolvedPathIsInside(repositoryRoot, projectRoot)) {
|
|
53
|
+
throw new Error(`Delegated workspace repository escapes the mustflow root: ${scope.repository}`);
|
|
54
|
+
}
|
|
55
|
+
if (!existsSync(repositoryRoot)) {
|
|
56
|
+
throw new Error(`Delegated workspace repository does not exist: ${scope.repository}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function assertScopedCommandIntentIsolation(projectRoot, scope, contract, intentName) {
|
|
60
|
+
if (!intentName) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const repositoryRoot = resolveProjectRelativePath(projectRoot, scope.repository);
|
|
64
|
+
const rawIntent = contract.intents[intentName];
|
|
65
|
+
if (!isRecord(rawIntent)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const intentCwd = resolveSafeProjectCwd(projectRoot, readEffectiveCommandCwd(contract, rawIntent));
|
|
69
|
+
if (!resolvedPathIsInside(intentCwd, repositoryRoot)) {
|
|
70
|
+
throw new Error(`Delegated workspace intent "${intentName}" cwd must stay inside ${scope.repository}`);
|
|
71
|
+
}
|
|
72
|
+
for (const effect of normalizeCommandEffects(projectRoot, contract, intentName)) {
|
|
73
|
+
if (effect.path === null) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const effectPath = resolveProjectRelativePath(projectRoot, effect.path);
|
|
77
|
+
if (!resolvedPathIsInside(effectPath, repositoryRoot)) {
|
|
78
|
+
throw new Error(`Delegated workspace intent "${intentName}" effect path must stay inside ${scope.repository}: ${effect.path}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function createScopedContext(projectRoot, mustflowConfig, scope, intentName) {
|
|
83
|
+
assertScopeRepositoryExists(projectRoot, scope);
|
|
84
|
+
const contractPath = `.mustflow/config/${scope.file}`;
|
|
85
|
+
const contract = readScopedCommandContract(projectRoot, scope.file, `workspace:${scope.repository}`, scope.repository);
|
|
86
|
+
assertScopedCommandIntentIsolation(projectRoot, scope, contract, intentName);
|
|
87
|
+
return {
|
|
88
|
+
projectRoot,
|
|
89
|
+
contract,
|
|
90
|
+
mustflowConfig,
|
|
91
|
+
workspaceScope: {
|
|
92
|
+
repository: scope.repository,
|
|
93
|
+
contract: contractPath,
|
|
94
|
+
},
|
|
95
|
+
trustPaths: [MUSTFLOW_CONFIG_RELATIVE_PATH, contractPath],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export function resolveRunCommandContext(options = {}) {
|
|
99
|
+
const startPath = path.resolve(options.startPath ?? process.cwd());
|
|
100
|
+
const projectRoot = resolveMustflowRoot(startPath);
|
|
101
|
+
const mustflowConfig = readMustflowConfigIfExists(projectRoot);
|
|
102
|
+
const workspaceAuthority = readWorkspaceCommandAuthorityConfig(mustflowConfig);
|
|
103
|
+
const requestedRepository = options.repository?.trim() ? normalizeRepositoryOption(options.repository) : null;
|
|
104
|
+
if (workspaceAuthority.authorityMode !== 'delegated_scoped') {
|
|
105
|
+
if (requestedRepository) {
|
|
106
|
+
throw new Error('--repo requires [workspace].authority_mode = "delegated_scoped"');
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
projectRoot,
|
|
110
|
+
contract: readCommandContract(projectRoot),
|
|
111
|
+
mustflowConfig,
|
|
112
|
+
workspaceScope: null,
|
|
113
|
+
trustPaths: undefined,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const scope = requestedRepository
|
|
117
|
+
? findScopeByRepository(workspaceAuthority.contracts, requestedRepository)
|
|
118
|
+
: findScopeByStartPath(projectRoot, startPath, workspaceAuthority.contracts);
|
|
119
|
+
if (requestedRepository && !scope) {
|
|
120
|
+
throw new Error(`No delegated workspace contract is mapped for repository: ${requestedRepository}`);
|
|
121
|
+
}
|
|
122
|
+
if (scope) {
|
|
123
|
+
return createScopedContext(projectRoot, mustflowConfig, scope, options.intentName);
|
|
124
|
+
}
|
|
125
|
+
if (startPathIsInsideWorkspaceRoot(projectRoot, startPath, workspaceAuthority.roots)) {
|
|
126
|
+
const relativeStart = toPosixPath(path.relative(projectRoot, startPath)) || '.';
|
|
127
|
+
throw new Error(`No delegated workspace contract is mapped for working directory: ${relativeStart}`);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
projectRoot,
|
|
131
|
+
contract: readCommandContract(projectRoot),
|
|
132
|
+
mustflowConfig,
|
|
133
|
+
workspaceScope: null,
|
|
134
|
+
trustPaths: [MUSTFLOW_CONFIG_RELATIVE_PATH],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { MANIFEST_LOCK_RELATIVE_PATH, inspectManifestLock } from './manifest-lock.js';
|
|
1
|
+
import { MANIFEST_LOCK_RELATIVE_PATH, inspectManifestLock, inspectManifestLockPaths } from './manifest-lock.js';
|
|
2
2
|
import { readCommandContractIncludePaths } from '../../core/config-loading.js';
|
|
3
3
|
export const ALLOW_UNTRUSTED_ROOT_OPTION = '--allow-untrusted-root';
|
|
4
4
|
const REQUIRED_RUN_TRUST_LOCK_PATHS = [
|
|
5
5
|
'AGENTS.md',
|
|
6
6
|
'.mustflow/config/commands.toml',
|
|
7
7
|
];
|
|
8
|
-
export function assessRunRootTrust(projectRoot) {
|
|
9
|
-
const
|
|
8
|
+
export function assessRunRootTrust(projectRoot, options = {}) {
|
|
9
|
+
const scopedRequiredPaths = options.requiredPaths
|
|
10
|
+
? [...new Set([...REQUIRED_RUN_TRUST_LOCK_PATHS, ...options.requiredPaths])]
|
|
11
|
+
: undefined;
|
|
12
|
+
const inspection = scopedRequiredPaths
|
|
13
|
+
? inspectManifestLockPaths(projectRoot, scopedRequiredPaths)
|
|
14
|
+
: inspectManifestLock(projectRoot);
|
|
10
15
|
const { readResult } = inspection;
|
|
11
16
|
if (readResult.kind === 'present') {
|
|
12
17
|
if (readResult.lock.files.length === 0) {
|
|
@@ -18,18 +23,20 @@ export function assessRunRootTrust(projectRoot) {
|
|
|
18
23
|
};
|
|
19
24
|
}
|
|
20
25
|
const trackedPaths = new Set(readResult.lock.files.map((file) => file.relativePath));
|
|
21
|
-
let requiredPaths = REQUIRED_RUN_TRUST_LOCK_PATHS;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
let requiredPaths = scopedRequiredPaths ?? REQUIRED_RUN_TRUST_LOCK_PATHS;
|
|
27
|
+
if (!scopedRequiredPaths) {
|
|
28
|
+
try {
|
|
29
|
+
requiredPaths = [...REQUIRED_RUN_TRUST_LOCK_PATHS, ...readCommandContractIncludePaths(projectRoot)];
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
return {
|
|
34
|
+
trusted: false,
|
|
35
|
+
reason: 'manifest_lock_invalid',
|
|
36
|
+
manifestLockPath: readResult.lockPath,
|
|
37
|
+
detail: message,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
33
40
|
}
|
|
34
41
|
const missingRequiredPath = requiredPaths.find((relativePath) => !trackedPaths.has(relativePath));
|
|
35
42
|
if (missingRequiredPath) {
|
|
@@ -8,6 +8,7 @@ import { formatManagedMarkdownLabel, getManagedMarkdownExpectation, } from '../.
|
|
|
8
8
|
import { SKILL_INDEX_ROUTE_COLUMN_COUNT, SKILL_INDEX_ROUTE_COLUMNS, SKILL_INDEX_SKILL_PATH_COLUMN_INDEX, findSkillRouteConflictWarnings, findSkillIndexRoutePathColumn, parseSkillIndexRoutes, readBacktickValues, } from '../../../core/skill-route-alignment.js';
|
|
9
9
|
import { validateTemplateVersionSync } from '../../../core/release-version-validation.js';
|
|
10
10
|
import { validateSourceAnchorsInProject } from '../../../core/source-anchor-validation.js';
|
|
11
|
+
import { readWorkspaceCommandAuthorityConfig, WORKSPACE_COMMAND_AUTHORITY_MODES, } from '../../../core/workspace-command-authority.js';
|
|
11
12
|
import { readLocalSourceAnchorCheckWarnings } from '../local-index/index.js';
|
|
12
13
|
import { listFilesRecursive, toPosixPath } from '../filesystem.js';
|
|
13
14
|
import { readGitChangedFiles } from '../git-changes.js';
|
|
@@ -85,6 +86,7 @@ function validateMustflowConfig(mustflowToml, issues) {
|
|
|
85
86
|
if (workspace) {
|
|
86
87
|
validateBooleanField(workspace, 'enabled', '[workspace].enabled', issues);
|
|
87
88
|
const roots = validateWorkspaceRoots(workspace, issues);
|
|
89
|
+
validateAllowedStringField(workspace, 'authority_mode', '[workspace].authority_mode', new Set(WORKSPACE_COMMAND_AUTHORITY_MODES), issues);
|
|
88
90
|
validatePositiveIntegerField(workspace, 'max_depth', '[workspace].max_depth', issues);
|
|
89
91
|
validatePositiveIntegerField(workspace, 'max_repositories', '[workspace].max_repositories', issues);
|
|
90
92
|
validateBooleanField(workspace, 'follow_symlinks', '[workspace].follow_symlinks', issues);
|
|
@@ -92,6 +94,12 @@ function validateMustflowConfig(mustflowToml, issues) {
|
|
|
92
94
|
if (workspace.enabled === true && roots?.length === 0) {
|
|
93
95
|
issues.push({ message: '[workspace].enabled requires at least one [workspace].roots entry' });
|
|
94
96
|
}
|
|
97
|
+
try {
|
|
98
|
+
readWorkspaceCommandAuthorityConfig(mustflowToml);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
issues.push({ message: error instanceof Error ? error.message : String(error) });
|
|
102
|
+
}
|
|
95
103
|
}
|
|
96
104
|
const capabilities = validateTable(mustflowToml, 'capabilities', issues);
|
|
97
105
|
if (capabilities) {
|
|
@@ -67,11 +67,15 @@ function readCommandIncludePathsFromParsed(root) {
|
|
|
67
67
|
return normalizedPaths;
|
|
68
68
|
}
|
|
69
69
|
export function readCommandContractIncludePaths(projectRoot) {
|
|
70
|
+
const parsed = readRootCommandContractToml(projectRoot);
|
|
71
|
+
return readCommandIncludePathsFromParsed(parsed).map((includePath) => `${COMMANDS_CONFIG_DIRECTORY_RELATIVE_PATH}/${includePath}`);
|
|
72
|
+
}
|
|
73
|
+
function readRootCommandContractToml(projectRoot) {
|
|
70
74
|
const parsed = readMustflowOwnedTomlFile(projectRoot, COMMANDS_CONFIG_RELATIVE_PATH);
|
|
71
75
|
if (!isRecord(parsed)) {
|
|
72
76
|
throw new Error(`${COMMANDS_CONFIG_RELATIVE_PATH} must contain a TOML table`);
|
|
73
77
|
}
|
|
74
|
-
return
|
|
78
|
+
return parsed;
|
|
75
79
|
}
|
|
76
80
|
function assertCommandIncludeTable(includePath, parsed) {
|
|
77
81
|
if (!isRecord(parsed)) {
|
|
@@ -84,6 +88,13 @@ function assertCommandIncludeTable(includePath, parsed) {
|
|
|
84
88
|
}
|
|
85
89
|
return parsed;
|
|
86
90
|
}
|
|
91
|
+
function readCommandIncludeTable(projectRoot, includePath) {
|
|
92
|
+
const normalized = normalizeCommandIncludePath(includePath);
|
|
93
|
+
if (commandIncludePathIsUnsafe(includePath)) {
|
|
94
|
+
throw new Error(`Command include path "${includePath}" must be a relative ${COMMAND_INCLUDE_DIRECTORY}/*.toml path under ${COMMANDS_CONFIG_DIRECTORY_RELATIVE_PATH}`);
|
|
95
|
+
}
|
|
96
|
+
return assertCommandIncludeTable(normalized, readMustflowOwnedTomlFile(projectRoot, `${COMMANDS_CONFIG_DIRECTORY_RELATIVE_PATH}/${normalized}`));
|
|
97
|
+
}
|
|
87
98
|
function mergeCommandSection(target, source, sectionName, sourceLabel, seenSources) {
|
|
88
99
|
if (source === undefined) {
|
|
89
100
|
return;
|
|
@@ -101,10 +112,7 @@ function mergeCommandSection(target, source, sectionName, sourceLabel, seenSourc
|
|
|
101
112
|
}
|
|
102
113
|
}
|
|
103
114
|
export function readResolvedCommandContractToml(projectRoot) {
|
|
104
|
-
const parsed =
|
|
105
|
-
if (!isRecord(parsed)) {
|
|
106
|
-
throw new Error(`${COMMANDS_CONFIG_RELATIVE_PATH} must contain a TOML table`);
|
|
107
|
-
}
|
|
115
|
+
const parsed = readRootCommandContractToml(projectRoot);
|
|
108
116
|
if (hasOwn(parsed, 'intents') && !isRecord(parsed.intents)) {
|
|
109
117
|
throw new Error(`[intents] table in ${COMMANDS_CONFIG_RELATIVE_PATH} must be a TOML table`);
|
|
110
118
|
}
|
|
@@ -138,6 +146,99 @@ export function readResolvedCommandContractToml(projectRoot) {
|
|
|
138
146
|
}
|
|
139
147
|
return merged;
|
|
140
148
|
}
|
|
149
|
+
function commandContractFromParsed(parsed) {
|
|
150
|
+
if (!isRecord(parsed.intents)) {
|
|
151
|
+
throw new Error(`Missing [intents] table in ${COMMANDS_CONFIG_RELATIVE_PATH}`);
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
defaults: isRecord(parsed.defaults) ? parsed.defaults : {},
|
|
155
|
+
intents: parsed.intents,
|
|
156
|
+
resources: isRecord(parsed.resources) ? parsed.resources : {},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function namespaceScopedLock(lockNamespace, lockName) {
|
|
160
|
+
return `${lockNamespace}::${lockName}`;
|
|
161
|
+
}
|
|
162
|
+
function namespaceScopedIntentLocks(rawIntents, lockNamespace) {
|
|
163
|
+
if (rawIntents === undefined) {
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
if (!isRecord(rawIntents)) {
|
|
167
|
+
throw new Error(`Scoped command contract must contain an [intents] table`);
|
|
168
|
+
}
|
|
169
|
+
const intents = {};
|
|
170
|
+
for (const [intentName, rawIntent] of Object.entries(rawIntents)) {
|
|
171
|
+
if (!isRecord(rawIntent) || !Array.isArray(rawIntent.effects)) {
|
|
172
|
+
intents[intentName] = rawIntent;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
intents[intentName] = {
|
|
176
|
+
...rawIntent,
|
|
177
|
+
effects: rawIntent.effects.map((rawEffect) => {
|
|
178
|
+
if (!isRecord(rawEffect) || typeof rawEffect.lock !== 'string') {
|
|
179
|
+
return rawEffect;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
...rawEffect,
|
|
183
|
+
lock: namespaceScopedLock(lockNamespace, rawEffect.lock),
|
|
184
|
+
};
|
|
185
|
+
}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return intents;
|
|
189
|
+
}
|
|
190
|
+
function rebaseScopedCwd(repository, rawCwd) {
|
|
191
|
+
const normalizedCwd = rawCwd.replaceAll('\\', '/');
|
|
192
|
+
if (path.posix.isAbsolute(normalizedCwd) || path.win32.isAbsolute(rawCwd)) {
|
|
193
|
+
return rawCwd;
|
|
194
|
+
}
|
|
195
|
+
return path.posix.normalize(path.posix.join(repository, normalizedCwd));
|
|
196
|
+
}
|
|
197
|
+
function rebaseScopedCommandContract(contract, repository) {
|
|
198
|
+
const defaultCwd = readString(contract.defaults, 'default_cwd') ?? '.';
|
|
199
|
+
const intents = Object.fromEntries(Object.entries(contract.intents).map(([intentName, rawIntent]) => {
|
|
200
|
+
if (!isRecord(rawIntent)) {
|
|
201
|
+
return [intentName, rawIntent];
|
|
202
|
+
}
|
|
203
|
+
const intentCwd = readString(rawIntent, 'cwd');
|
|
204
|
+
return [
|
|
205
|
+
intentName,
|
|
206
|
+
intentCwd === undefined ? rawIntent : { ...rawIntent, cwd: rebaseScopedCwd(repository, intentCwd) },
|
|
207
|
+
];
|
|
208
|
+
}));
|
|
209
|
+
return {
|
|
210
|
+
defaults: {
|
|
211
|
+
...contract.defaults,
|
|
212
|
+
default_cwd: rebaseScopedCwd(repository, defaultCwd),
|
|
213
|
+
},
|
|
214
|
+
intents,
|
|
215
|
+
resources: contract.resources,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function namespaceScopedResources(rawResources, lockNamespace) {
|
|
219
|
+
if (rawResources === undefined) {
|
|
220
|
+
return {};
|
|
221
|
+
}
|
|
222
|
+
if (!isRecord(rawResources)) {
|
|
223
|
+
throw new Error(`Scoped command contract [resources] must be a TOML table`);
|
|
224
|
+
}
|
|
225
|
+
return Object.fromEntries(Object.entries(rawResources).map(([resourceName, resource]) => [
|
|
226
|
+
namespaceScopedLock(lockNamespace, resourceName),
|
|
227
|
+
resource,
|
|
228
|
+
]));
|
|
229
|
+
}
|
|
230
|
+
export function readRootCommandContract(projectRoot) {
|
|
231
|
+
return commandContractFromParsed(readRootCommandContractToml(projectRoot));
|
|
232
|
+
}
|
|
233
|
+
export function readScopedCommandContract(projectRoot, includePath, lockNamespace, repository) {
|
|
234
|
+
const root = readRootCommandContractToml(projectRoot);
|
|
235
|
+
const fragment = readCommandIncludeTable(projectRoot, includePath);
|
|
236
|
+
return rebaseScopedCommandContract({
|
|
237
|
+
defaults: isRecord(root.defaults) ? root.defaults : {},
|
|
238
|
+
intents: namespaceScopedIntentLocks(fragment.intents, lockNamespace),
|
|
239
|
+
resources: namespaceScopedResources(fragment.resources, lockNamespace),
|
|
240
|
+
}, repository);
|
|
241
|
+
}
|
|
141
242
|
export function readMustflowConfig(projectRoot) {
|
|
142
243
|
const parsed = readMustflowOwnedTomlFile(projectRoot, MUSTFLOW_CONFIG_RELATIVE_PATH);
|
|
143
244
|
if (!isRecord(parsed)) {
|
|
@@ -150,15 +251,7 @@ export function readMustflowConfigIfExists(projectRoot) {
|
|
|
150
251
|
return existsSync(configPath) ? readMustflowConfig(projectRoot) : undefined;
|
|
151
252
|
}
|
|
152
253
|
export function readCommandContract(projectRoot) {
|
|
153
|
-
|
|
154
|
-
if (!isRecord(parsed.intents)) {
|
|
155
|
-
throw new Error(`Missing [intents] table in ${COMMANDS_CONFIG_RELATIVE_PATH}`);
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
defaults: isRecord(parsed.defaults) ? parsed.defaults : {},
|
|
159
|
-
intents: parsed.intents,
|
|
160
|
-
resources: isRecord(parsed.resources) ? parsed.resources : {},
|
|
161
|
-
};
|
|
254
|
+
return commandContractFromParsed(readResolvedCommandContractToml(projectRoot));
|
|
162
255
|
}
|
|
163
256
|
export function readString(table, key) {
|
|
164
257
|
const value = table[key];
|
package/dist/core/run-receipt.js
CHANGED
|
@@ -285,6 +285,7 @@ export function createRunReceipt(input) {
|
|
|
285
285
|
redaction_kinds: [...redactionState.kinds].sort(),
|
|
286
286
|
fields: [...redactionState.fields].sort(),
|
|
287
287
|
},
|
|
288
|
+
...(input.workspaceScope ? { workspace_scope: input.workspaceScope } : {}),
|
|
288
289
|
receipt_path: getReceiptRelativePath(input.receiptPath),
|
|
289
290
|
};
|
|
290
291
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export const WORKSPACE_COMMAND_AUTHORITY_MODES = ['repository_local', 'delegated_scoped'];
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function normalizeRelativePath(value) {
|
|
7
|
+
return value.trim().replace(/\\/gu, '/').replace(/\/+$/gu, '');
|
|
8
|
+
}
|
|
9
|
+
function relativePathIsUnsafe(value) {
|
|
10
|
+
const normalized = normalizeRelativePath(value);
|
|
11
|
+
const segments = normalized.split('/');
|
|
12
|
+
return (normalized.length === 0 ||
|
|
13
|
+
normalized.includes('\0') ||
|
|
14
|
+
normalized.startsWith('/') ||
|
|
15
|
+
path.win32.isAbsolute(value) ||
|
|
16
|
+
path.posix.isAbsolute(normalized) ||
|
|
17
|
+
segments.some((segment) => segment.length === 0 || segment === '.' || segment === '..'));
|
|
18
|
+
}
|
|
19
|
+
function commandFragmentPathIsUnsafe(value) {
|
|
20
|
+
const normalized = normalizeRelativePath(value);
|
|
21
|
+
return relativePathIsUnsafe(value) || !normalized.startsWith('commands/') || !normalized.endsWith('.toml');
|
|
22
|
+
}
|
|
23
|
+
function pathIsInside(candidate, parent) {
|
|
24
|
+
return candidate === parent || candidate.startsWith(`${parent}/`);
|
|
25
|
+
}
|
|
26
|
+
function readStringArray(value, label) {
|
|
27
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
|
|
28
|
+
throw new Error(`${label} must be a string array`);
|
|
29
|
+
}
|
|
30
|
+
return value.map((entry) => normalizeRelativePath(entry));
|
|
31
|
+
}
|
|
32
|
+
function readContracts(value) {
|
|
33
|
+
if (value === undefined) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
if (!Array.isArray(value)) {
|
|
37
|
+
throw new Error('[workspace].contracts must be an array of tables');
|
|
38
|
+
}
|
|
39
|
+
const contracts = [];
|
|
40
|
+
const repositories = new Set();
|
|
41
|
+
const files = new Set();
|
|
42
|
+
for (const [index, rawContract] of value.entries()) {
|
|
43
|
+
if (!isRecord(rawContract)) {
|
|
44
|
+
throw new Error(`[workspace].contracts[${index}] must be a table`);
|
|
45
|
+
}
|
|
46
|
+
const repository = typeof rawContract.repository === 'string' ? normalizeRelativePath(rawContract.repository) : '';
|
|
47
|
+
const file = typeof rawContract.file === 'string' ? normalizeRelativePath(rawContract.file) : '';
|
|
48
|
+
if (relativePathIsUnsafe(repository)) {
|
|
49
|
+
throw new Error(`[workspace].contracts[${index}].repository must be a normalized repository-relative path`);
|
|
50
|
+
}
|
|
51
|
+
if (commandFragmentPathIsUnsafe(file)) {
|
|
52
|
+
throw new Error(`[workspace].contracts[${index}].file must be a normalized commands/*.toml path`);
|
|
53
|
+
}
|
|
54
|
+
if (repositories.has(repository)) {
|
|
55
|
+
throw new Error(`[workspace].contracts contains duplicate repository "${repository}"`);
|
|
56
|
+
}
|
|
57
|
+
if (files.has(file)) {
|
|
58
|
+
throw new Error(`[workspace].contracts contains duplicate file "${file}"`);
|
|
59
|
+
}
|
|
60
|
+
repositories.add(repository);
|
|
61
|
+
files.add(file);
|
|
62
|
+
contracts.push({ repository, file });
|
|
63
|
+
}
|
|
64
|
+
return contracts;
|
|
65
|
+
}
|
|
66
|
+
export function readWorkspaceCommandAuthorityConfig(mustflowConfig) {
|
|
67
|
+
const root = isRecord(mustflowConfig) ? mustflowConfig : {};
|
|
68
|
+
const workspace = isRecord(root.workspace) ? root.workspace : {};
|
|
69
|
+
const enabled = workspace.enabled === true;
|
|
70
|
+
const roots = workspace.roots === undefined ? [] : readStringArray(workspace.roots, '[workspace].roots');
|
|
71
|
+
const authorityMode = workspace.authority_mode === undefined ? 'repository_local' : workspace.authority_mode;
|
|
72
|
+
if (!WORKSPACE_COMMAND_AUTHORITY_MODES.includes(authorityMode)) {
|
|
73
|
+
throw new Error(`[workspace].authority_mode must be one of: ${WORKSPACE_COMMAND_AUTHORITY_MODES.join(', ')}`);
|
|
74
|
+
}
|
|
75
|
+
if (roots.some((rootPath) => relativePathIsUnsafe(rootPath))) {
|
|
76
|
+
throw new Error('[workspace].roots entries must be normalized repository-relative paths');
|
|
77
|
+
}
|
|
78
|
+
const contracts = readContracts(workspace.contracts);
|
|
79
|
+
if (authorityMode === 'delegated_scoped') {
|
|
80
|
+
if (!enabled) {
|
|
81
|
+
throw new Error('[workspace].authority_mode = "delegated_scoped" requires [workspace].enabled = true');
|
|
82
|
+
}
|
|
83
|
+
if (roots.length === 0) {
|
|
84
|
+
throw new Error('[workspace].authority_mode = "delegated_scoped" requires at least one [workspace].roots entry');
|
|
85
|
+
}
|
|
86
|
+
for (const contract of contracts) {
|
|
87
|
+
if (!roots.some((rootPath) => pathIsInside(contract.repository, rootPath))) {
|
|
88
|
+
throw new Error(`[workspace].contracts repository "${contract.repository}" must stay inside one [workspace].roots entry`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
enabled,
|
|
94
|
+
roots,
|
|
95
|
+
authorityMode: authorityMode,
|
|
96
|
+
contracts,
|
|
97
|
+
};
|
|
98
|
+
}
|
package/package.json
CHANGED
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"write_drift": { "$ref": "#/$defs/writeDrift" },
|
|
77
77
|
"performance": { "$ref": "#/$defs/performance" },
|
|
78
78
|
"redaction": { "$ref": "#/$defs/redaction" },
|
|
79
|
+
"workspace_scope": { "$ref": "#/$defs/workspaceScope" },
|
|
79
80
|
"verification_plan_id": {
|
|
80
81
|
"type": "string",
|
|
81
82
|
"pattern": "^sha256:[0-9a-f]{64}$"
|
|
@@ -83,6 +84,15 @@
|
|
|
83
84
|
"receipt_path": { "type": "string" }
|
|
84
85
|
},
|
|
85
86
|
"$defs": {
|
|
87
|
+
"workspaceScope": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"additionalProperties": false,
|
|
90
|
+
"required": ["repository", "contract"],
|
|
91
|
+
"properties": {
|
|
92
|
+
"repository": { "type": "string" },
|
|
93
|
+
"contract": { "type": "string" }
|
|
94
|
+
}
|
|
95
|
+
},
|
|
86
96
|
"output": {
|
|
87
97
|
"type": "object",
|
|
88
98
|
"additionalProperties": false,
|
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
"properties": {
|
|
54
54
|
"enabled": { "type": "boolean" },
|
|
55
55
|
"roots": { "$ref": "#/$defs/stringArray" },
|
|
56
|
+
"authority_mode": { "enum": ["repository_local", "delegated_scoped"] },
|
|
57
|
+
"delegated_contract_count": { "type": "integer", "minimum": 0 },
|
|
56
58
|
"max_depth": { "type": "integer", "minimum": 1 },
|
|
57
59
|
"max_repositories": { "type": "integer", "minimum": 1 },
|
|
58
60
|
"follow_symlinks": { "type": "boolean" },
|
|
@@ -119,6 +121,7 @@
|
|
|
119
121
|
"properties": {
|
|
120
122
|
"relative_path": { "type": "string" },
|
|
121
123
|
"status": { "enum": ["available", "contract_missing", "contract_invalid"] },
|
|
124
|
+
"command_authority": { "enum": ["repository_local", "delegated_scoped", null] },
|
|
122
125
|
"command_contract": { "$ref": "#/$defs/commandSurface" },
|
|
123
126
|
"intent_count": { "type": "integer", "minimum": 0 },
|
|
124
127
|
"runnable_count": { "type": "integer", "minimum": 0 },
|