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.
Files changed (42) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/commands/run/args.js +5 -0
  3. package/dist/cli/commands/run/execution.js +7 -6
  4. package/dist/cli/commands/run/preview.js +6 -5
  5. package/dist/cli/commands/run/receipt.js +1 -0
  6. package/dist/cli/commands/run.js +2 -1
  7. package/dist/cli/commands/workspace.js +216 -81
  8. package/dist/cli/i18n/en.js +1 -0
  9. package/dist/cli/i18n/es.js +1 -0
  10. package/dist/cli/i18n/fr.js +1 -0
  11. package/dist/cli/i18n/hi.js +1 -0
  12. package/dist/cli/i18n/ko.js +1 -0
  13. package/dist/cli/i18n/zh.js +1 -0
  14. package/dist/cli/lib/manifest-lock.js +10 -1
  15. package/dist/cli/lib/repo-map.js +5 -0
  16. package/dist/cli/lib/run-context.js +136 -0
  17. package/dist/cli/lib/run-root-trust.js +22 -15
  18. package/dist/cli/lib/validation/index.js +8 -0
  19. package/dist/core/config-loading.js +107 -14
  20. package/dist/core/run-receipt.js +1 -0
  21. package/dist/core/workspace-command-authority.js +98 -0
  22. package/package.json +1 -1
  23. package/schemas/run-receipt.schema.json +10 -0
  24. package/schemas/workspace-command-catalog.schema.json +3 -0
  25. package/schemas/workspace-command-fragments.schema.json +2 -0
  26. package/schemas/workspace-status.schema.json +4 -1
  27. package/schemas/workspace-verification-plan.schema.json +3 -0
  28. package/templates/default/common/.mustflow/config/mustflow.toml +2 -0
  29. package/templates/default/i18n.toml +14 -2
  30. package/templates/default/locales/en/.mustflow/skills/INDEX.md +12 -3
  31. package/templates/default/locales/en/.mustflow/skills/ai-game-asset-production/SKILL.md +209 -0
  32. package/templates/default/locales/en/.mustflow/skills/ai-game-asset-production/references/asset-contract-validation.md +106 -0
  33. package/templates/default/locales/en/.mustflow/skills/ai-game-asset-production/references/raster-alpha-atlas-checklist.md +109 -0
  34. package/templates/default/locales/en/.mustflow/skills/ai-game-asset-production/references/tile-animation-checklist.md +98 -0
  35. package/templates/default/locales/en/.mustflow/skills/connection-lifecycle-integrity-review/SKILL.md +302 -0
  36. package/templates/default/locales/en/.mustflow/skills/connection-lifecycle-integrity-review/references/connection-fault-injection-resource-lifetime-validation.md +277 -0
  37. package/templates/default/locales/en/.mustflow/skills/connection-lifecycle-integrity-review/references/node-stream-transport-lifecycle-checklist.md +381 -0
  38. package/templates/default/locales/en/.mustflow/skills/memory-lifetime-review/SKILL.md +94 -2
  39. package/templates/default/locales/en/.mustflow/skills/memory-lifetime-review/references/cross-runtime-memory-stream-reproduction-harness.md +289 -0
  40. package/templates/default/locales/en/.mustflow/skills/memory-lifetime-review/references/node-memory-resource-backpressure-diagnostics.md +369 -0
  41. package/templates/default/locales/en/.mustflow/skills/routes.toml +23 -0
  42. package/templates/default/manifest.toml +17 -1
@@ -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",
@@ -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",
@@ -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",
@@ -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 करने दें",
@@ -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 하나를 허용합니다",
@@ -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
- export function inspectManifestLock(projectRoot) {
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
+ }
@@ -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 inspection = inspectManifestLock(projectRoot);
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
- try {
23
- requiredPaths = [...REQUIRED_RUN_TRUST_LOCK_PATHS, ...readCommandContractIncludePaths(projectRoot)];
24
- }
25
- catch (error) {
26
- const message = error instanceof Error ? error.message : String(error);
27
- return {
28
- trusted: false,
29
- reason: 'manifest_lock_invalid',
30
- manifestLockPath: readResult.lockPath,
31
- detail: message,
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 readCommandIncludePathsFromParsed(parsed).map((includePath) => `${COMMANDS_CONFIG_DIRECTORY_RELATIVE_PATH}/${includePath}`);
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 = readMustflowOwnedTomlFile(projectRoot, COMMANDS_CONFIG_RELATIVE_PATH);
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
- const parsed = readResolvedCommandContractToml(projectRoot);
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];
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.117.0",
3
+ "version": "2.118.0",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
@@ -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 },