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
package/README.md CHANGED
@@ -322,6 +322,7 @@ mf run mustflow_update_apply
322
322
  | `mf script-pack run core/text-budget check <path...> --max <count>` | Check exact text length budgets for files using grapheme counts by default. |
323
323
  | `mf script-pack run core/text-budget check package.json --json-pointer /description --max <count> --json` | Check a JSON string field and print the stable report schema. |
324
324
  | `mf run <intent>` | Run an allowed one-shot command. |
325
+ | `mf run <intent> --repo <path>` | Run one delegated scoped workspace contract from the workspace root. |
325
326
  | `mf run <intent> --wait` | Wait for conflicting active run locks before executing the command. |
326
327
  | `mf run <intent> --dry-run --json` | Preview whether an intent is runnable and what command metadata would be used, without executing it. |
327
328
  | `mf index` | Build a SQLite index for mustflow docs, skill routes, command rules, command-effect locks, and file fingerprints. Use `--incremental` to reuse a compatible fresh index without rewriting it. |
@@ -12,6 +12,7 @@ const RUN_OPTIONS = [
12
12
  { name: ALLOW_UNTRUSTED_ROOT_OPTION, kind: 'boolean' },
13
13
  { name: '--wait-timeout', kind: 'string' },
14
14
  { name: '--allow-approval', kind: 'string' },
15
+ { name: '--repo', kind: 'string' },
15
16
  ];
16
17
  export function hasRunHelpOption(args) {
17
18
  return hasCliOptionToken(args, '--help', ['-h']);
@@ -43,6 +44,7 @@ export function parseRunArguments(args) {
43
44
  allowApprovals,
44
45
  wait: hasParsedCliOption(parsed, '--wait'),
45
46
  waitTimeoutSeconds,
47
+ repository: getParsedCliStringOption(parsed, '--repo'),
46
48
  intentName: intentName ?? null,
47
49
  extra,
48
50
  invalidApprovalAction: null,
@@ -73,6 +75,7 @@ export function parseRunArguments(args) {
73
75
  allowApprovals,
74
76
  wait: hasParsedCliOption(parsed, '--wait'),
75
77
  waitTimeoutSeconds,
78
+ repository: getParsedCliStringOption(parsed, '--repo'),
76
79
  intentName: intentName ?? null,
77
80
  extra,
78
81
  invalidApprovalAction,
@@ -90,12 +93,14 @@ export function getRunHelp(lang = 'en') {
90
93
  { label: '--wait', description: t(lang, 'run.help.option.wait') },
91
94
  { label: '--wait-timeout <seconds>', description: t(lang, 'run.help.option.waitTimeout') },
92
95
  { label: '--allow-approval <action>', description: t(lang, 'run.help.option.allowApproval') },
96
+ { label: '--repo <path>', description: t(lang, 'run.help.option.repository') },
93
97
  { label: ALLOW_UNTRUSTED_ROOT_OPTION, description: t(lang, 'run.help.option.allowUntrustedRoot') },
94
98
  { label: '-h, --help', description: t(lang, 'cli.option.help') },
95
99
  ],
96
100
  examples: [
97
101
  'mf run test',
98
102
  'mf run lint --json',
103
+ 'mf run test --repo projects/example --json',
99
104
  'mf run release_npm_publish --allow-approval release --allow-approval network_access --json',
100
105
  ],
101
106
  exitCodes: [
@@ -2,7 +2,6 @@ import { createHash } from 'node:crypto';
2
2
  import { performance } from 'node:perf_hooks';
3
3
  import { ACTIVE_RUN_LOCK_ID_ENV, acquireActiveRunLock } from '../../../core/active-run-locks.js';
4
4
  import { createCommandEnv } from '../../../core/command-env.js';
5
- import { readCommandContract, readMustflowConfigIfExists } from '../../../core/config-loading.js';
6
5
  import { createCorrelationId } from '../../../core/correlation-id.js';
7
6
  import { recordRunPerformanceHistory } from '../../../core/run-performance-history.js';
8
7
  import { RunProfiler } from '../../../core/run-profile.js';
@@ -11,8 +10,8 @@ import { finishRunWriteTracking, startRunWriteTracking } from '../../../core/run
11
10
  import { resolveRunReceiptRetentionPolicy } from '../../../core/retention-policy.js';
12
11
  import { renderCliError } from '../../lib/cli-output.js';
13
12
  import { t } from '../../lib/i18n.js';
14
- import { resolveMustflowRoot } from '../../lib/project-root.js';
15
13
  import { assessRunRootTrust } from '../../lib/run-root-trust.js';
14
+ import { resolveRunCommandContext } from '../../lib/run-context.js';
16
15
  import { createRunPlan, createRunPreview, } from '../../lib/run-plan.js';
17
16
  import { getRunStatus, runArgvCommandStreaming, runShellCommandStreaming } from './executor.js';
18
17
  import { emitOutput, isOutputLimitExceededError } from './output.js';
@@ -185,8 +184,9 @@ function createRunProgressReporter(input) {
185
184
  export async function executeRunCommand(request, reporter, lang = 'en', options = {}) {
186
185
  const executorStartedAtMs = performance.now();
187
186
  const profiler = new RunProfiler();
188
- const projectRoot = profiler.measure('root_detection', () => resolveMustflowRoot());
189
- const rootTrust = profiler.measure('root_trust', () => assessRunRootTrust(projectRoot));
187
+ const runContext = profiler.measure('root_detection', () => resolveRunCommandContext({ repository: request.repository, intentName: request.intentName }));
188
+ const projectRoot = runContext.projectRoot;
189
+ const rootTrust = profiler.measure('root_trust', () => assessRunRootTrust(projectRoot, { requiredPaths: runContext.trustPaths }));
190
190
  const jsonLikeOutput = request.outputMode !== 'text';
191
191
  if (!request.allowUntrustedRoot && !rootTrust.trusted) {
192
192
  const message = rootTrust.reason === 'manifest_lock_invalid'
@@ -195,7 +195,7 @@ export async function executeRunCommand(request, reporter, lang = 'en', options
195
195
  reporter.stderr(renderCliError(message, 'mf run --help', lang));
196
196
  return { exitCode: 1, receipt: null };
197
197
  }
198
- const contract = profiler.measure('command_contract', () => readCommandContract(projectRoot));
198
+ const contract = profiler.measure('command_contract', () => runContext.contract);
199
199
  const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, request.intentName, {
200
200
  testTargets: options.testTargets,
201
201
  approvedActions: request.allowApprovals,
@@ -235,7 +235,7 @@ export async function executeRunCommand(request, reporter, lang = 'en', options
235
235
  return { exitCode: 1, receipt: null };
236
236
  }
237
237
  try {
238
- const runReceiptPolicy = profiler.measure('retention_policy', () => resolveRunReceiptRetentionPolicy(readMustflowConfigIfExists(projectRoot)));
238
+ const runReceiptPolicy = profiler.measure('retention_policy', () => resolveRunReceiptRetentionPolicy(runContext.mustflowConfig));
239
239
  const env = profiler.measure('environment', () => createCommandEnv(projectRoot, { policy: plan.envPolicy, allowlist: plan.envAllowlist }));
240
240
  env[ACTIVE_RUN_LOCK_ID_ENV] = activeRunLock.handle.record.run_id;
241
241
  const writeTracker = profiler.measure('write_drift_before', () => startRunWriteTracking(projectRoot, contract, request.intentName, {
@@ -299,6 +299,7 @@ export async function executeRunCommand(request, reporter, lang = 'en', options
299
299
  phaseTimings: profiler.getReceiptPhases(),
300
300
  stdoutTailBytes: runReceiptPolicy.stdoutTailBytes,
301
301
  stderrTailBytes: runReceiptPolicy.stderrTailBytes,
302
+ workspaceScope: runContext.workspaceScope,
302
303
  }));
303
304
  if (options.writeLatestReceipt !== false) {
304
305
  profiler.measure('receipt_write', () => writeRunReceipt(projectRoot, receipt, runReceiptPolicy));
@@ -1,6 +1,5 @@
1
- import { readCommandContract } from '../../../core/config-loading.js';
2
1
  import { RunProfiler } from '../../../core/run-profile.js';
3
- import { resolveMustflowRoot } from '../../lib/project-root.js';
2
+ import { resolveRunCommandContext } from '../../lib/run-context.js';
4
3
  import { createRunPlan, createRunPreview, renderRunPreviewText, } from '../../lib/run-plan.js';
5
4
  import { writeLatestRunProfile } from './profile.js';
6
5
  export function getRunPreviewMode(input) {
@@ -8,15 +7,17 @@ export function getRunPreviewMode(input) {
8
7
  }
9
8
  export function executeRunPreviewCommand(input, reporter, lang, options) {
10
9
  const profiler = new RunProfiler();
11
- const projectRoot = profiler.measure('root_detection', () => resolveMustflowRoot());
12
- const contract = profiler.measure('command_contract', () => readCommandContract(projectRoot));
10
+ const runContext = profiler.measure('root_detection', () => resolveRunCommandContext({ repository: input.repository, intentName: input.intentName }));
11
+ const projectRoot = runContext.projectRoot;
12
+ const contract = profiler.measure('command_contract', () => runContext.contract);
13
13
  const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, input.intentName, {
14
14
  testTargets: options.testTargets,
15
15
  approvedActions: input.allowApprovals,
16
16
  }));
17
17
  profiler.measure('preview_render', () => {
18
18
  if (input.json) {
19
- reporter.stdout(JSON.stringify(createRunPreview(plan, input.previewMode), null, 2));
19
+ const preview = createRunPreview(plan, input.previewMode);
20
+ reporter.stdout(JSON.stringify(runContext.workspaceScope ? { ...preview, workspace_scope: runContext.workspaceScope } : preview, null, 2));
20
21
  }
21
22
  else {
22
23
  reporter.stdout(renderRunPreviewText(plan, input.previewMode, lang));
@@ -37,6 +37,7 @@ export function assembleRunReceipt(input) {
37
37
  selected_target_count: Math.max(1, input.plan.testTargets.length),
38
38
  fallback_used: false,
39
39
  },
40
+ workspaceScope: input.workspaceScope,
40
41
  stdoutTailBytes: input.stdoutTailBytes,
41
42
  stderrTailBytes: input.stderrTailBytes,
42
43
  receiptPath: createRunReceiptRelativePath(),
@@ -69,8 +69,9 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
69
69
  allowApprovals: parsedArgs.allowApprovals,
70
70
  wait: parsedArgs.wait,
71
71
  waitTimeoutSeconds: parsedArgs.waitTimeoutSeconds,
72
+ repository: parsedArgs.repository,
72
73
  }, reporter, lang, options);
73
74
  return result.exitCode;
74
75
  }
75
- return executeRunPreviewCommand({ intentName, json, previewMode, allowApprovals: parsedArgs.allowApprovals }, reporter, lang, options);
76
+ return executeRunPreviewCommand({ intentName, json, previewMode, allowApprovals: parsedArgs.allowApprovals, repository: parsedArgs.repository }, reporter, lang, options);
76
77
  }
@@ -1,3 +1,4 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import path from 'node:path';
2
3
  import { createClassifyOutput } from './classify.js';
3
4
  import { createChangeVerificationReport, } from '../../core/change-verification.js';
@@ -8,7 +9,8 @@ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parse
8
9
  import { resolveMustflowRoot } from '../lib/project-root.js';
9
10
  import { getRepoMapConfig, discoverNestedRepositories } from '../lib/repo-map.js';
10
11
  import { createRunPlan } from '../lib/run-plan.js';
11
- import { readCommandContract, readString, readStringArray } from '../../core/config-loading.js';
12
+ import { assertScopedCommandIntentIsolation } from '../lib/run-context.js';
13
+ import { readCommandContract, readScopedCommandContract, readString, readStringArray, } from '../../core/config-loading.js';
12
14
  const DEFAULT_WORKSPACE_SCAN_ROOT = 'projects';
13
15
  const WORKSPACE_SCAN_SCHEMA_VERSION = '1';
14
16
  const WORKSPACE_STATUS_SCHEMA_VERSION = '1';
@@ -95,6 +97,9 @@ function createAdHocWorkspaceConfig(base, projectsDir) {
95
97
  ...base,
96
98
  enabled: true,
97
99
  roots: [projectsDir],
100
+ authorityMode: 'repository_local',
101
+ delegatedContracts: [],
102
+ delegatedContractCount: 0,
98
103
  };
99
104
  }
100
105
  function slugCommandFragmentSegment(segment) {
@@ -195,16 +200,19 @@ function commandFragmentNextActions(repositoryCount) {
195
200
  function getIntentNames(intents) {
196
201
  return Object.keys(intents).sort((left, right) => left.localeCompare(right));
197
202
  }
198
- function summarizeCommandSurface(repositoryRoot, repository) {
203
+ function summarizeLocalCommand(repositoryRoot, repository) {
199
204
  if (!repository.commandContract) {
200
205
  return {
201
- path: null,
202
- exists: false,
203
- parse_error: null,
204
- total_intents: null,
205
- runnable_count: null,
206
- runnable_intents: [],
207
- blocked_count: null,
206
+ surface: {
207
+ path: null,
208
+ exists: false,
209
+ parse_error: null,
210
+ total_intents: null,
211
+ runnable_count: null,
212
+ runnable_intents: [],
213
+ blocked_count: null,
214
+ },
215
+ contract: null,
208
216
  };
209
217
  }
210
218
  try {
@@ -212,45 +220,129 @@ function summarizeCommandSurface(repositoryRoot, repository) {
212
220
  const intentNames = getIntentNames(contract.intents);
213
221
  const runnableIntents = intentNames.filter((intentName) => createRunPlan(repositoryRoot, contract, intentName).ok);
214
222
  return {
215
- path: repository.commandContract,
216
- exists: true,
217
- parse_error: null,
218
- total_intents: intentNames.length,
219
- runnable_count: runnableIntents.length,
220
- runnable_intents: runnableIntents,
221
- blocked_count: Math.max(0, intentNames.length - runnableIntents.length),
223
+ surface: {
224
+ path: repository.commandContract,
225
+ exists: true,
226
+ parse_error: null,
227
+ total_intents: intentNames.length,
228
+ runnable_count: runnableIntents.length,
229
+ runnable_intents: runnableIntents,
230
+ blocked_count: Math.max(0, intentNames.length - runnableIntents.length),
231
+ },
232
+ contract,
222
233
  };
223
234
  }
224
235
  catch (error) {
225
236
  return {
226
- path: repository.commandContract,
227
- exists: true,
228
- parse_error: error instanceof Error ? error.message : String(error),
229
- total_intents: null,
230
- runnable_count: null,
231
- runnable_intents: [],
232
- blocked_count: null,
237
+ surface: {
238
+ path: repository.commandContract,
239
+ exists: true,
240
+ parse_error: error instanceof Error ? error.message : String(error),
241
+ total_intents: null,
242
+ runnable_count: null,
243
+ runnable_intents: [],
244
+ blocked_count: null,
245
+ },
246
+ contract: null,
233
247
  };
234
248
  }
235
249
  }
236
- function repositoryStatus(commandSurface) {
237
- if (!commandSurface.exists) {
238
- return 'contract_missing';
250
+ function normalizeRepositoryPath(value) {
251
+ return value.replace(/\\/gu, '/').replace(/\/+$/gu, '');
252
+ }
253
+ function findDelegatedContract(workspace, repository) {
254
+ if (workspace.authorityMode !== 'delegated_scoped' || repository.commandContract) {
255
+ return undefined;
256
+ }
257
+ const repositoryPath = normalizeRepositoryPath(repository.relativePath);
258
+ return workspace.delegatedContracts.find((contract) => contract.repository === repositoryPath);
259
+ }
260
+ function delegatedIntentIsRunnable(projectRoot, contract, scope, intentName) {
261
+ try {
262
+ assertScopedCommandIntentIsolation(projectRoot, scope, contract, intentName);
263
+ return createRunPlan(projectRoot, contract, intentName).ok;
264
+ }
265
+ catch {
266
+ return false;
267
+ }
268
+ }
269
+ function summarizeDelegatedCommand(projectRoot, scope) {
270
+ const contractPath = `.mustflow/config/${scope.file}`;
271
+ const contractExists = existsSync(path.resolve(projectRoot, '.mustflow', 'config', ...scope.file.split('/')));
272
+ try {
273
+ const contract = readScopedCommandContract(projectRoot, scope.file, `workspace:${scope.repository}`, scope.repository);
274
+ const intentNames = getIntentNames(contract.intents);
275
+ const runnableIntents = intentNames.filter((intentName) => delegatedIntentIsRunnable(projectRoot, contract, scope, intentName));
276
+ return {
277
+ surface: {
278
+ path: contractPath,
279
+ exists: true,
280
+ parse_error: null,
281
+ total_intents: intentNames.length,
282
+ runnable_count: runnableIntents.length,
283
+ runnable_intents: runnableIntents,
284
+ blocked_count: Math.max(0, intentNames.length - runnableIntents.length),
285
+ },
286
+ contract,
287
+ };
288
+ }
289
+ catch (error) {
290
+ return {
291
+ surface: {
292
+ path: contractPath,
293
+ exists: contractExists,
294
+ parse_error: error instanceof Error ? error.message : String(error),
295
+ total_intents: null,
296
+ runnable_count: null,
297
+ runnable_intents: [],
298
+ blocked_count: null,
299
+ },
300
+ contract: null,
301
+ };
302
+ }
303
+ }
304
+ function summarizeCommandSelection(projectRoot, repositoryRoot, repository, workspace) {
305
+ const delegatedContract = findDelegatedContract(workspace, repository);
306
+ if (delegatedContract) {
307
+ const summary = summarizeDelegatedCommand(projectRoot, delegatedContract);
308
+ return {
309
+ authority: 'delegated_scoped',
310
+ ...summary,
311
+ scope: delegatedContract,
312
+ planningRoot: projectRoot,
313
+ };
239
314
  }
315
+ const summary = summarizeLocalCommand(repositoryRoot, repository);
316
+ return {
317
+ authority: repository.commandContract ? 'repository_local' : null,
318
+ ...summary,
319
+ scope: null,
320
+ planningRoot: repositoryRoot,
321
+ };
322
+ }
323
+ function repositoryStatus(commandSurface, authority) {
240
324
  if (commandSurface.parse_error) {
241
325
  return 'contract_invalid';
242
326
  }
327
+ if (!commandSurface.exists) {
328
+ return 'contract_missing';
329
+ }
330
+ if (authority === 'delegated_scoped') {
331
+ return 'delegated_ready';
332
+ }
243
333
  return 'mustflow_ready';
244
334
  }
245
- function summarizeRepository(projectRoot, repository) {
335
+ function summarizeRepository(projectRoot, repository, workspace) {
246
336
  const repositoryRoot = path.resolve(projectRoot, repository.relativePath);
247
- const commandSurface = summarizeCommandSurface(repositoryRoot, repository);
337
+ const commandSelection = summarizeCommandSelection(projectRoot, repositoryRoot, repository, workspace);
338
+ const commandSurface = commandSelection.surface;
248
339
  const issues = commandSurface.parse_error ? [commandSurface.parse_error] : [];
249
340
  return {
250
341
  relative_path: repository.relativePath,
251
- status: repositoryStatus(commandSurface),
342
+ status: repositoryStatus(commandSurface, commandSelection.authority),
252
343
  git_repository: true,
253
344
  mustflow: repository.mustflow,
345
+ command_authority: commandSelection.authority,
254
346
  agent_rules: repository.agentRules ?? null,
255
347
  repo_map: repository.repoMap ?? null,
256
348
  mustflow_config: repository.mustflowConfig ?? null,
@@ -276,7 +368,7 @@ function createWorkspaceStatusOutput() {
276
368
  const projectRoot = resolveMustflowRoot();
277
369
  const config = getRepoMapConfig(projectRoot);
278
370
  const nestedRepositories = discoverNestedRepositories(projectRoot, { ...config.map, includeNested: true }, config.workspace);
279
- const repositories = nestedRepositories.map((repository) => summarizeRepository(projectRoot, repository));
371
+ const repositories = nestedRepositories.map((repository) => summarizeRepository(projectRoot, repository, config.workspace));
280
372
  const issues = config.workspace.enabled && config.workspace.roots.length > 0 && repositories.length === 0
281
373
  ? ['No nested git repositories were discovered under configured workspace roots.']
282
374
  : [];
@@ -295,7 +387,7 @@ function createWorkspaceScanOutput(projectsDir) {
295
387
  const projectRoot = resolveMustflowRoot();
296
388
  const config = getRepoMapConfig(projectRoot);
297
389
  const workspace = createAdHocWorkspaceConfig(config.workspace, projectsDir);
298
- const repositories = discoverNestedRepositories(projectRoot, { ...config.map, includeNested: true }, workspace).map((repository) => summarizeRepository(projectRoot, repository));
390
+ const repositories = discoverNestedRepositories(projectRoot, { ...config.map, includeNested: true }, workspace).map((repository) => summarizeRepository(projectRoot, repository, workspace));
299
391
  const issues = repositories.length === 0
300
392
  ? [t('en', 'workspace.scan.issue.noneDiscovered', { projectsDir })]
301
393
  : [];
@@ -326,6 +418,8 @@ function workspaceConfigOutput(config) {
326
418
  return {
327
419
  enabled: config.enabled,
328
420
  roots: config.roots,
421
+ authority_mode: config.authorityMode,
422
+ delegated_contract_count: config.delegatedContractCount,
329
423
  max_depth: config.maxDepth,
330
424
  max_repositories: config.maxRepositories,
331
425
  follow_symlinks: config.followSymlinks,
@@ -353,10 +447,13 @@ function readIntentString(rawIntent, key) {
353
447
  function safeRunCommand(intentName) {
354
448
  return /^[A-Za-z0-9_-]+$/u.test(intentName) ? `mf run ${intentName}` : null;
355
449
  }
356
- function createCatalogIntent(repositoryRoot, repositoryPath, contract, intentName) {
450
+ function createCatalogIntent(planningRoot, scope, repositoryPath, contract, intentName) {
357
451
  const rawIntent = contract.intents[intentName];
358
452
  try {
359
- const plan = createRunPlan(repositoryRoot, contract, intentName);
453
+ if (scope) {
454
+ assertScopedCommandIntentIsolation(planningRoot, scope, contract, intentName);
455
+ }
456
+ const plan = createRunPlan(planningRoot, contract, intentName);
360
457
  return {
361
458
  name: intentName,
362
459
  description: readIntentString(rawIntent, 'description'),
@@ -395,43 +492,31 @@ function createCatalogIntent(repositoryRoot, repositoryPath, contract, intentNam
395
492
  };
396
493
  }
397
494
  }
398
- function createCatalogRepository(projectRoot, repository) {
495
+ function createCatalogRepository(projectRoot, repository, workspace) {
399
496
  const repositoryRoot = path.resolve(projectRoot, repository.relativePath);
400
- const commandSurface = summarizeCommandSurface(repositoryRoot, repository);
401
- if (!repository.commandContract) {
402
- return {
403
- relative_path: repository.relativePath,
404
- status: 'contract_missing',
405
- command_contract: commandSurface,
406
- intent_count: 0,
407
- runnable_count: 0,
408
- blocked_count: 0,
409
- intents: [],
410
- issues: ['Command contract is missing.'],
411
- };
412
- }
413
- let contract;
414
- try {
415
- contract = readCommandContract(repositoryRoot);
416
- }
417
- catch (error) {
418
- const message = error instanceof Error ? error.message : String(error);
497
+ const commandSelection = summarizeCommandSelection(projectRoot, repositoryRoot, repository, workspace);
498
+ const commandSurface = commandSelection.surface;
499
+ if (!commandSelection.contract) {
500
+ const contractInvalid = commandSurface.parse_error !== null;
419
501
  return {
420
502
  relative_path: repository.relativePath,
421
- status: 'contract_invalid',
503
+ status: contractInvalid ? 'contract_invalid' : 'contract_missing',
504
+ command_authority: commandSelection.authority,
422
505
  command_contract: commandSurface,
423
506
  intent_count: 0,
424
507
  runnable_count: 0,
425
508
  blocked_count: 0,
426
509
  intents: [],
427
- issues: [message],
510
+ issues: [commandSurface.parse_error ?? 'Command contract is missing.'],
428
511
  };
429
512
  }
430
- const intents = getIntentNames(contract.intents).map((intentName) => createCatalogIntent(repositoryRoot, repository.relativePath, contract, intentName));
513
+ const contract = commandSelection.contract;
514
+ const intents = getIntentNames(contract.intents).map((intentName) => createCatalogIntent(commandSelection.planningRoot, commandSelection.scope, repository.relativePath, contract, intentName));
431
515
  const runnableCount = intents.filter((intent) => intent.runnable).length;
432
516
  return {
433
517
  relative_path: repository.relativePath,
434
518
  status: 'available',
519
+ command_authority: commandSelection.authority,
435
520
  command_contract: commandSurface,
436
521
  intent_count: intents.length,
437
522
  runnable_count: runnableCount,
@@ -443,7 +528,7 @@ function createCatalogRepository(projectRoot, repository) {
443
528
  function createWorkspaceCommandCatalogOutput() {
444
529
  const projectRoot = resolveMustflowRoot();
445
530
  const { config, repositories: nestedRepositories } = readWorkspaceRepositories(projectRoot);
446
- const repositories = nestedRepositories.map((repository) => createCatalogRepository(projectRoot, repository));
531
+ const repositories = nestedRepositories.map((repository) => createCatalogRepository(projectRoot, repository, config.workspace));
447
532
  return {
448
533
  schema_version: WORKSPACE_COMMAND_CATALOG_SCHEMA_VERSION,
449
534
  command: 'workspace command-catalog',
@@ -462,7 +547,13 @@ function createWorkspaceCommandFragmentsOutput(projectsDir) {
462
547
  const config = getRepoMapConfig(projectRoot);
463
548
  const workspace = projectsDir ? createAdHocWorkspaceConfig(config.workspace, projectsDir) : config.workspace;
464
549
  const nestedRepositories = discoverNestedRepositories(projectRoot, { ...config.map, includeNested: true }, workspace);
465
- const repositories = nestedRepositories.map((repository) => summarizeRepository(projectRoot, repository));
550
+ const repositoryLocalWorkspace = {
551
+ ...workspace,
552
+ authorityMode: 'repository_local',
553
+ delegatedContracts: [],
554
+ delegatedContractCount: 0,
555
+ };
556
+ const repositories = nestedRepositories.map((repository) => summarizeRepository(projectRoot, repository, repositoryLocalWorkspace));
466
557
  const includePaths = createCommandFragmentPaths(repositories);
467
558
  const suggestions = repositories.map((repository) => createCommandFragmentSuggestion(repository, includePaths.get(repository.relative_path) ?? `${COMMAND_FRAGMENT_INCLUDE_PREFIX}/repository.toml`));
468
559
  const includeEntries = suggestions.map((suggestion) => suggestion.include_entry);
@@ -487,10 +578,11 @@ function createWorkspaceCommandFragmentsOutput(projectsDir) {
487
578
  next_actions: commandFragmentNextActions(repositories.length),
488
579
  };
489
580
  }
490
- function createUnavailableVerificationRepository(repository, commandSurface, status, classification, issue) {
581
+ function createUnavailableVerificationRepository(repository, authority, commandSurface, status, classification, issue) {
491
582
  return {
492
583
  relative_path: repository.relativePath,
493
584
  status,
585
+ command_authority: authority,
494
586
  command_contract: commandSurface,
495
587
  changed_file_count: classification ? classification.summary.fileCount : null,
496
588
  changed_files: classification ? classification.files : [],
@@ -505,6 +597,40 @@ function createUnavailableVerificationRepository(repository, commandSurface, sta
505
597
  issues: [issue],
506
598
  };
507
599
  }
600
+ function workspaceRelativeChangePath(repositoryPath, filePath) {
601
+ const repository = normalizeRepositoryPath(repositoryPath);
602
+ const file = filePath.replace(/\\/gu, '/').replace(/^\.\//u, '');
603
+ return `${repository}/${file}`;
604
+ }
605
+ function repositoryRelativeChangePath(repositoryPath, filePath) {
606
+ const prefix = `${normalizeRepositoryPath(repositoryPath)}/`;
607
+ return filePath.startsWith(prefix) ? filePath.slice(prefix.length) : filePath;
608
+ }
609
+ function rebaseClassificationForWorkspaceRoot(classification, repositoryPath) {
610
+ return {
611
+ source: classification.source,
612
+ files: classification.files.map((filePath) => workspaceRelativeChangePath(repositoryPath, filePath)),
613
+ classifications: classification.classifications.map((entry) => ({
614
+ ...entry,
615
+ path: workspaceRelativeChangePath(repositoryPath, entry.path),
616
+ })),
617
+ summary: classification.summary,
618
+ };
619
+ }
620
+ function scopedPlanningContract(projectRoot, contract, scope) {
621
+ return {
622
+ ...contract,
623
+ intents: Object.fromEntries(Object.entries(contract.intents).filter(([intentName]) => {
624
+ try {
625
+ assertScopedCommandIntentIsolation(projectRoot, scope, contract, intentName);
626
+ return true;
627
+ }
628
+ catch {
629
+ return false;
630
+ }
631
+ })),
632
+ };
633
+ }
508
634
  function selectedIntentsForVerificationReport(repositoryPath, report) {
509
635
  return report.schedule.entries.map((entry) => ({
510
636
  intent: entry.intent,
@@ -516,44 +642,45 @@ function selectedIntentsForVerificationReport(repositoryPath, report) {
516
642
  }
517
643
  /**
518
644
  * mf:anchor cli.workspace.verify-plan
519
- * purpose: Build per-repository verification plans from each child repository's own command contract.
520
- * search: workspace verify, changed files, plan only, command contract, child repository
521
- * invariant: Workspace verification output selects intents per repository and does not run raw commands from the parent root.
645
+ * purpose: Build per-repository verification plans from each repository's effective local or delegated scoped command contract.
646
+ * search: workspace verify, changed files, plan only, command contract, delegated repository
647
+ * invariant: Workspace verification output selects only repository-scoped intents and never runs their raw commands.
522
648
  * risk: config, state
523
649
  */
524
- function createVerificationRepository(projectRoot, repository) {
650
+ function createVerificationRepository(projectRoot, repository, workspace) {
525
651
  const repositoryRoot = path.resolve(projectRoot, repository.relativePath);
526
- const commandSurface = summarizeCommandSurface(repositoryRoot, repository);
652
+ const commandSelection = summarizeCommandSelection(projectRoot, repositoryRoot, repository, workspace);
653
+ const commandSurface = commandSelection.surface;
527
654
  let classification = null;
528
655
  try {
529
656
  classification = createClassifyOutput(repositoryRoot, 'changed', []);
530
657
  }
531
658
  catch (error) {
532
659
  const message = error instanceof Error ? error.message : String(error);
533
- return createUnavailableVerificationRepository(repository, commandSurface, 'git_unavailable', null, message);
534
- }
535
- if (!repository.commandContract) {
536
- return createUnavailableVerificationRepository(repository, commandSurface, 'contract_missing', classification, 'Command contract is missing.');
537
- }
538
- let contract;
539
- try {
540
- contract = readCommandContract(repositoryRoot);
541
- }
542
- catch (error) {
543
- const message = error instanceof Error ? error.message : String(error);
544
- return createUnavailableVerificationRepository(repository, commandSurface, 'contract_invalid', classification, message);
545
- }
660
+ return createUnavailableVerificationRepository(repository, commandSelection.authority, commandSurface, 'git_unavailable', null, message);
661
+ }
662
+ if (!commandSelection.contract) {
663
+ const contractInvalid = commandSurface.parse_error !== null;
664
+ return createUnavailableVerificationRepository(repository, commandSelection.authority, commandSurface, contractInvalid ? 'contract_invalid' : 'contract_missing', classification, commandSurface.parse_error ?? 'Command contract is missing.');
665
+ }
666
+ const contract = commandSelection.scope
667
+ ? scopedPlanningContract(projectRoot, commandSelection.contract, commandSelection.scope)
668
+ : commandSelection.contract;
669
+ const planningClassification = commandSelection.scope
670
+ ? rebaseClassificationForWorkspaceRoot(classification, repository.relativePath)
671
+ : classification;
546
672
  let report;
547
673
  try {
548
- report = createChangeVerificationReport(classification, contract, repositoryRoot);
674
+ report = createChangeVerificationReport(planningClassification, contract, commandSelection.planningRoot);
549
675
  }
550
676
  catch (error) {
551
677
  const message = error instanceof Error ? error.message : String(error);
552
- return createUnavailableVerificationRepository(repository, commandSurface, 'plan_unavailable', classification, message);
678
+ return createUnavailableVerificationRepository(repository, commandSelection.authority, commandSurface, 'plan_unavailable', classification, message);
553
679
  }
554
680
  return {
555
681
  relative_path: repository.relativePath,
556
682
  status: 'available',
683
+ command_authority: commandSelection.authority,
557
684
  command_contract: commandSurface,
558
685
  changed_file_count: classification.summary.fileCount,
559
686
  changed_files: classification.files,
@@ -566,7 +693,9 @@ function createVerificationRepository(projectRoot, repository) {
566
693
  selected_intents: selectedIntentsForVerificationReport(repository.relativePath, report),
567
694
  gaps: report.gaps.map((gap) => ({
568
695
  reason: gap.reason,
569
- files: gap.files,
696
+ files: commandSelection.scope
697
+ ? gap.files.map((filePath) => repositoryRelativeChangePath(repository.relativePath, filePath))
698
+ : gap.files,
570
699
  surfaces: gap.surfaces,
571
700
  detail: gap.detail,
572
701
  })),
@@ -576,7 +705,7 @@ function createVerificationRepository(projectRoot, repository) {
576
705
  function createWorkspaceVerificationPlanOutput() {
577
706
  const projectRoot = resolveMustflowRoot();
578
707
  const { config, repositories: nestedRepositories } = readWorkspaceRepositories(projectRoot);
579
- const repositories = nestedRepositories.map((repository) => createVerificationRepository(projectRoot, repository));
708
+ const repositories = nestedRepositories.map((repository) => createVerificationRepository(projectRoot, repository, config.workspace));
580
709
  return {
581
710
  schema_version: WORKSPACE_VERIFICATION_PLAN_SCHEMA_VERSION,
582
711
  command: 'workspace verify',
@@ -607,6 +736,7 @@ function renderWorkspaceScan(output) {
607
736
  for (const repository of output.repositories) {
608
737
  lines.push(`- ${repository.relative_path} (${repository.status})`);
609
738
  lines.push(` mustflow: ${repository.mustflow ? 'yes' : 'no'}`);
739
+ lines.push(` command authority: ${repository.command_authority ?? 'none'}`);
610
740
  lines.push(` command contract: ${repository.command_contract.path ?? 'missing'}`);
611
741
  }
612
742
  lines.push('', 'Next actions:');
@@ -631,6 +761,7 @@ function renderWorkspaceStatus(output) {
631
761
  for (const repository of output.repositories) {
632
762
  lines.push(`- ${repository.relative_path} (${repository.status})`);
633
763
  lines.push(` mustflow: ${repository.mustflow ? 'yes' : 'no'}`);
764
+ lines.push(` command authority: ${repository.command_authority ?? 'none'}`);
634
765
  lines.push(` command contract: ${repository.command_contract.path ?? 'missing'}`);
635
766
  if (repository.command_contract.runnable_count !== null) {
636
767
  lines.push(` runnable intents: ${repository.command_contract.runnable_count}`);
@@ -657,6 +788,8 @@ function renderWorkspaceCommandCatalog(output) {
657
788
  }
658
789
  for (const repository of output.repositories) {
659
790
  lines.push(`- ${repository.relative_path} (${repository.status})`);
791
+ lines.push(` command authority: ${repository.command_authority ?? 'none'}`);
792
+ lines.push(` command contract: ${repository.command_contract.path ?? 'missing'}`);
660
793
  if (repository.intents.length === 0) {
661
794
  lines.push(` intents: none`);
662
795
  }
@@ -718,6 +851,8 @@ function renderWorkspaceVerificationPlan(output) {
718
851
  }
719
852
  for (const repository of output.repositories) {
720
853
  lines.push(`- ${repository.relative_path} (${repository.status})`);
854
+ lines.push(` command authority: ${repository.command_authority ?? 'none'}`);
855
+ lines.push(` command contract: ${repository.command_contract.path ?? 'missing'}`);
721
856
  lines.push(` changed files: ${repository.changed_file_count ?? 'unknown'}`);
722
857
  lines.push(` selected intents: ${repository.selected_intent_count}`);
723
858
  for (const selected of repository.selected_intents) {