mustflow 2.107.1 → 2.107.9

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 (30) hide show
  1. package/README.md +6 -1
  2. package/dist/cli/commands/init.js +49 -1
  3. package/dist/cli/commands/run/execution.js +7 -0
  4. package/dist/cli/commands/run/executor.js +7 -0
  5. package/dist/cli/commands/verify.js +14 -0
  6. package/dist/cli/commands/workspace.js +106 -16
  7. package/dist/cli/i18n/en.js +6 -1
  8. package/dist/cli/i18n/es.js +6 -1
  9. package/dist/cli/i18n/fr.js +6 -1
  10. package/dist/cli/i18n/hi.js +6 -1
  11. package/dist/cli/i18n/ko.js +6 -1
  12. package/dist/cli/i18n/zh.js +6 -1
  13. package/dist/cli/index.js +8 -0
  14. package/dist/cli/lib/agent-context.js +7 -0
  15. package/dist/cli/lib/repo-map.js +14 -0
  16. package/dist/cli/lib/run-plan.js +7 -0
  17. package/dist/core/change-verification.js +7 -0
  18. package/dist/core/verification-scheduler.js +7 -0
  19. package/package.json +1 -1
  20. package/schemas/README.md +3 -3
  21. package/schemas/workspace-status.schema.json +4 -2
  22. package/templates/default/common/.mustflow/config/mustflow.toml +3 -3
  23. package/templates/default/i18n.toml +19 -1
  24. package/templates/default/locales/en/.mustflow/docs/agent-workflow.md +4 -3
  25. package/templates/default/locales/en/.mustflow/skills/INDEX.md +11 -0
  26. package/templates/default/locales/en/.mustflow/skills/cli-option-contract-review/SKILL.md +147 -0
  27. package/templates/default/locales/en/.mustflow/skills/routes.toml +18 -0
  28. package/templates/default/locales/en/.mustflow/skills/third-party-api-integration-review/SKILL.md +188 -0
  29. package/templates/default/locales/en/.mustflow/skills/website-task-friction-review/SKILL.md +139 -0
  30. package/templates/default/manifest.toml +18 -1
package/README.md CHANGED
@@ -269,6 +269,7 @@ mf run mustflow_update_apply
269
269
  | `mf onboard commands` | Suggest review-only command-intent snippets from package.json, Makefile, or justfile without writing files or granting command authority. |
270
270
  | `mf next` | Inspect install state, changed files, verification coverage, and command-contract gaps, then print the next safe mustflow action without running commands. |
271
271
  | `mf evidence` | Summarize changed-file verification requirements, risk-priced evidence assessment, latest failure replay capsule, conflict ledger, receipts, remaining risks, and gaps without running commands. |
272
+ | `mf workspace scan` | Scan a `projects/` directory for nested repositories without requiring workspace configuration or granting command authority. |
272
273
  | `mf workspace status` | Inspect configured workspace roots and nested repository contract readiness without granting parent-to-child command authority. |
273
274
  | `mf workspace command-catalog` | Aggregate per-repository command intent availability with safe `mf run` entrypoints and no raw command strings. |
274
275
  | `mf workspace verify --changed --plan-only` | Aggregate per-repository changed-file verification plans without running commands or granting parent-to-child command authority. |
@@ -369,6 +370,9 @@ commands when a repository uses another runner or has a faster related-test entr
369
370
  - `lifecycle = "oneshot"`
370
371
  - `run_policy = "agent_allowed"`
371
372
  - `stdin = "closed"`
373
+ - `timeout_seconds` is a positive integer
374
+ - a command is declared with `argv`, or with `mode = "shell"` plus `cmd` and `allow_shell = true`
375
+ - `cwd` resolves inside the current mustflow root
372
376
 
373
377
  Development servers, watch modes, browser UIs, interactive commands, and background processes do not run directly. `mf run` also rejects obvious long-running `argv` shapes, such as shell-wrapper background payloads, interpreter loops, package-manager development scripts, watchers, and development servers declared as one-shot commands. If a bounded one-shot command has a name that matches a common long-running pattern, the intent can explicitly acknowledge that with `allow_long_running_command_patterns = true`; background shell patterns remain blocked.
374
378
 
@@ -468,6 +472,7 @@ mf run docs_validate_fast
468
472
  mf run docs_validate
469
473
  mf run mustflow_check
470
474
  mf run release_npm_version_available
475
+ mf run release_npm_publish
471
476
  mf run release_npm_published_verify
472
477
  ```
473
478
 
@@ -481,7 +486,7 @@ Run the full release check before publishing:
481
486
  bun run release:check
482
487
  ```
483
488
 
484
- `release:check` validates the CLI, builds the documentation site, packs the npm tarball, installs it into a temporary project, and runs the public `mf` workflow. Maintainer npm publishing uses the `Publish npm package` GitHub Actions workflow from a published GitHub Release. The release tag must match the `package.json` version, with an optional leading `v`. Run `mf run release_npm_version_available` before creating the tag and `mf run release_npm_published_verify` after the publish workflow completes. npm Trusted Publishing must be configured for the workflow before maintainers publish through it.
489
+ `release:check` validates the CLI, builds the documentation site, packs the npm tarball, installs it into a temporary project, and runs the public `mf` workflow. Maintainer npm publishing uses the `Publish npm package` GitHub Actions workflow from a release tag. The release tag must match the `package.json` version, with an optional leading `v`. Run `mf run release_npm_version_available` before creating the tag, `mf run release_npm_publish` to push the release tag that triggers trusted publishing and GitHub Release creation, and `mf run release_npm_published_verify` after the publish workflow completes. npm Trusted Publishing must be configured for the workflow before maintainers publish through it.
485
490
 
486
491
  ## Documentation site
487
492
 
@@ -1,4 +1,4 @@
1
- import { existsSync, readSync } from 'node:fs';
1
+ import { existsSync, lstatSync, readSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { stdin as processStdin, stdout as processStdout } from 'node:process';
4
4
  import { createInterface } from 'node:readline/promises';
@@ -9,6 +9,7 @@ import { isLocaleTag } from '../lib/locale-tags.js';
9
9
  import { MANIFEST_LOCK_RELATIVE_PATH, sha256File } from '../lib/manifest-lock.js';
10
10
  import { formatCliOptionParseError, hasCliOptionToken, parseCliOptions } from '../lib/option-parser.js';
11
11
  import { isCommitMessageStyle, isTestAuthoringPolicy } from '../lib/preferences-options.js';
12
+ import { discoverNestedRepositories, getRepoMapConfig } from '../lib/repo-map.js';
12
13
  import { getDefaultTemplate, getTemplateFiles } from '../lib/templates.js';
13
14
  const MUSTFLOW_BLOCK_START = '<!-- mustflow:start schema=1 -->';
14
15
  const MUSTFLOW_BLOCK_END = '<!-- mustflow:end -->';
@@ -17,6 +18,8 @@ const GITIGNORE_FRAGMENT_RELATIVE_PATH = 'gitignore.mustflow';
17
18
  const NON_INTERACTIVE_PROMPT_MAX_BYTES = 16 * 1024;
18
19
  const NON_INTERACTIVE_PROMPT_MAX_RESPONSES = 64;
19
20
  const NON_INTERACTIVE_PROMPT_READ_CHUNK_BYTES = 4096;
21
+ const WORKSPACE_INIT_REPOSITORY_THRESHOLD = 2;
22
+ const DEFAULT_WORKSPACE_SCAN_ROOT = 'projects';
20
23
  const LOCALE_LABELS = {
21
24
  en: 'English',
22
25
  ko: 'Korean',
@@ -540,6 +543,47 @@ function shouldPromptForInit(args, options) {
540
543
  }
541
544
  return Boolean(processStdin.isTTY && processStdout.isTTY);
542
545
  }
546
+ function hasGitMarker(directoryPath) {
547
+ try {
548
+ const marker = lstatSync(path.join(directoryPath, '.git'));
549
+ return marker.isDirectory() || marker.isFile();
550
+ }
551
+ catch {
552
+ return false;
553
+ }
554
+ }
555
+ function hasInstalledMustflowMarker(directoryPath) {
556
+ return existsSync(path.join(directoryPath, 'AGENTS.md')) || existsSync(path.join(directoryPath, '.mustflow'));
557
+ }
558
+ function countDefaultWorkspaceRepositories(targetRoot) {
559
+ const config = getRepoMapConfig(targetRoot);
560
+ const repositories = discoverNestedRepositories(targetRoot, { ...config.map, includeNested: true }, {
561
+ ...config.workspace,
562
+ enabled: true,
563
+ roots: [DEFAULT_WORKSPACE_SCAN_ROOT],
564
+ });
565
+ return repositories.length;
566
+ }
567
+ /**
568
+ * mf:anchor cli.init.workspace-root-guard
569
+ * purpose: Detect multi-repository workspace roots before installing repository-scoped mustflow files.
570
+ * search: mf init, workspace root, projects directory, nested repositories, install guard
571
+ * invariant: Non-git workspace folders with multiple nested repositories should not receive repo-scoped AGENTS.md by default.
572
+ * risk: config, state
573
+ */
574
+ function shouldRefuseWorkspaceRootInit(targetRoot) {
575
+ if (hasGitMarker(targetRoot) || hasInstalledMustflowMarker(targetRoot)) {
576
+ return false;
577
+ }
578
+ return countDefaultWorkspaceRepositories(targetRoot) >= WORKSPACE_INIT_REPOSITORY_THRESHOLD;
579
+ }
580
+ function printWorkspaceRootInitRefusal(targetRoot, reporter, lang) {
581
+ reporter.stderr(t(lang, 'init.error.workspaceRootDetected', {
582
+ count: countDefaultWorkspaceRepositories(targetRoot),
583
+ projectsDir: DEFAULT_WORKSPACE_SCAN_ROOT,
584
+ }));
585
+ reporter.stderr(t(lang, 'init.error.workspaceRootGuidance'));
586
+ }
543
587
  async function promptChoice(reader, reporter, lang, question, choices, defaultValue) {
544
588
  const defaultIndex = Math.max(0, choices.findIndex((choice) => choice.value === defaultValue));
545
589
  reporter.stdout('');
@@ -944,6 +988,10 @@ export async function runInit(args, reporter, lang = 'en') {
944
988
  }
945
989
  const targetRoot = process.cwd();
946
990
  let template;
991
+ if (shouldRefuseWorkspaceRootInit(targetRoot)) {
992
+ printWorkspaceRootInitRefusal(targetRoot, reporter, lang);
993
+ return 1;
994
+ }
947
995
  try {
948
996
  template = getDefaultTemplate();
949
997
  }
@@ -174,6 +174,13 @@ function createRunProgressReporter(input) {
174
174
  }
175
175
  };
176
176
  }
177
+ /**
178
+ * mf:anchor cli.run.execute
179
+ * purpose: Coordinate approved run-plan execution with active locks, environment policy, output limits, and write drift.
180
+ * search: run plan execution, active lock, environment policy, write drift, receipt
181
+ * invariant: Trusted root, runnable plan, active lock, and receipt tracking remain one preflight chain.
182
+ * risk: config, security, state
183
+ */
177
184
  export async function executeRunCommand(request, reporter, lang = 'en', options = {}) {
178
185
  const executorStartedAtMs = performance.now();
179
186
  const profiler = new RunProfiler();
@@ -27,6 +27,13 @@ function normalizeSpawnedCommandInput(command) {
27
27
  windowsVerbatimArguments: false,
28
28
  };
29
29
  }
30
+ /**
31
+ * mf:anchor cli.run.process-lifecycle
32
+ * purpose: Spawn configured commands with bounded output and deterministic timeout termination.
33
+ * search: child process, timeout, process tree, output limit, kill after
34
+ * invariant: Timed-out or output-limited commands must stop the process tree and return bounded stdout and stderr snapshots.
35
+ * risk: state, security
36
+ */
30
37
  function runSpawnedCommandStreaming(command, cwd, env, timeoutSeconds, killAfterSeconds, maxOutputBytes, stdoutTailBytes, stderrTailBytes, reporter, streamOutput, enforceOutputLimit) {
31
38
  if (command.executable.trim().length === 0) {
32
39
  return Promise.resolve({
@@ -600,6 +600,13 @@ function toParallelismReport(settings) {
600
600
  note: settings.note,
601
601
  };
602
602
  }
603
+ /**
604
+ * mf:anchor cli.verify.receipt-manifest
605
+ * purpose: Persist verify receipts, latest summary, failure evidence, and completion verdict in one state update boundary.
606
+ * search: mf verify, receipt manifest, latest run, completion verdict, failure fingerprint
607
+ * invariant: Receipt paths and verdict evidence must share the same verification_plan_id.
608
+ * risk: state, data_consistency
609
+ */
603
610
  function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks, scopeDiffRisks, validationRatchetRisks, reproEvidence, externalChecks) {
604
611
  const statePaths = createVerifyRunStatePaths(projectRoot);
605
612
  const receipts = [];
@@ -770,6 +777,13 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
770
777
  updateRunReceiptState(projectRoot, resolveRunReceiptRetentionPolicy(readMustflowConfigIfExists(projectRoot)));
771
778
  return outputWithReceiptPaths;
772
779
  }
780
+ /**
781
+ * mf:anchor cli.verify.output-model
782
+ * purpose: Create the verification output from classification, command contract, selected intents, and evidence risk models.
783
+ * search: verification output, risk assessment, source anchors, validation ratchet, external evidence
784
+ * invariant: Completion verdict risk counts must be derived from the same report and results that produce receipts.
785
+ * risk: state, data_consistency
786
+ */
773
787
  async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvidence = null, externalChecks = [], parallelism = DEFAULT_VERIFY_PARALLELISM, parallelismReport = null) {
774
788
  const contract = readCommandContract(projectRoot);
775
789
  const report = createChangeVerificationReport(input.classificationReport, contract, projectRoot);
@@ -9,9 +9,15 @@ import { resolveMustflowRoot } from '../lib/project-root.js';
9
9
  import { getRepoMapConfig, discoverNestedRepositories } from '../lib/repo-map.js';
10
10
  import { createRunPlan } from '../lib/run-plan.js';
11
11
  import { readCommandContract, readString, readStringArray } from '../../core/config-loading.js';
12
+ const DEFAULT_WORKSPACE_SCAN_ROOT = 'projects';
13
+ const WORKSPACE_SCAN_SCHEMA_VERSION = '1';
12
14
  const WORKSPACE_STATUS_SCHEMA_VERSION = '1';
13
15
  const WORKSPACE_COMMAND_CATALOG_SCHEMA_VERSION = '1';
14
16
  const WORKSPACE_VERIFICATION_PLAN_SCHEMA_VERSION = '1';
17
+ const WORKSPACE_SCAN_OPTIONS = [
18
+ { name: '--json', kind: 'boolean' },
19
+ { name: '--projects-dir', kind: 'string' },
20
+ ];
15
21
  const WORKSPACE_STATUS_OPTIONS = [{ name: '--json', kind: 'boolean' }];
16
22
  const WORKSPACE_COMMAND_CATALOG_OPTIONS = [{ name: '--json', kind: 'boolean' }];
17
23
  const WORKSPACE_VERIFY_OPTIONS = [
@@ -24,6 +30,7 @@ function getWorkspaceHelp(lang = 'en') {
24
30
  usage: 'mf workspace <action> [options]',
25
31
  summary: t(lang, 'workspace.help.summary'),
26
32
  commands: [
33
+ { label: 'scan', description: t(lang, 'workspace.help.action.scan') },
27
34
  { label: 'status', description: t(lang, 'workspace.help.action.status') },
28
35
  { label: 'command-catalog', description: t(lang, 'workspace.help.action.commandCatalog') },
29
36
  { label: 'verify', description: t(lang, 'workspace.help.action.verify') },
@@ -31,10 +38,13 @@ function getWorkspaceHelp(lang = 'en') {
31
38
  options: [
32
39
  { label: '--changed', description: t(lang, 'classify.help.option.changed') },
33
40
  { label: '--plan-only', description: t(lang, 'verify.help.option.planOnly') },
41
+ { label: '--projects-dir <path>', description: t(lang, 'workspace.help.option.projectsDir') },
34
42
  { label: '--json', description: t(lang, 'cli.option.json') },
35
43
  { label: '-h, --help', description: t(lang, 'cli.option.help') },
36
44
  ],
37
45
  examples: [
46
+ 'mf workspace scan --json',
47
+ 'mf workspace scan --projects-dir projects --json',
38
48
  'mf workspace status --json',
39
49
  'mf workspace command-catalog --json',
40
50
  'mf workspace verify --changed --plan-only --json',
@@ -63,6 +73,13 @@ function createWorkspaceVerificationPlanPolicy() {
63
73
  selected_intents_run_via: 'mf run <intent>',
64
74
  };
65
75
  }
76
+ function createAdHocWorkspaceConfig(base, projectsDir) {
77
+ return {
78
+ ...base,
79
+ enabled: true,
80
+ roots: [projectsDir],
81
+ };
82
+ }
66
83
  function getIntentNames(intents) {
67
84
  return Object.keys(intents).sort((left, right) => left.localeCompare(right));
68
85
  }
@@ -136,6 +153,13 @@ function summarizeRepository(projectRoot, repository) {
136
153
  issues,
137
154
  };
138
155
  }
156
+ /**
157
+ * mf:anchor cli.workspace.status-read-model
158
+ * purpose: Summarize nested repositories and command contracts without granting parent-root command authority.
159
+ * search: workspace status, nested repositories, command contract, read only, runnable intents
160
+ * invariant: Workspace status reports observable repo facts and never executes or authorizes child-repository commands.
161
+ * risk: config, state
162
+ */
139
163
  function createWorkspaceStatusOutput() {
140
164
  const projectRoot = resolveMustflowRoot();
141
165
  const config = getRepoMapConfig(projectRoot);
@@ -148,18 +172,35 @@ function createWorkspaceStatusOutput() {
148
172
  schema_version: WORKSPACE_STATUS_SCHEMA_VERSION,
149
173
  command: 'workspace status',
150
174
  mustflow_root: projectRoot,
151
- workspace: {
152
- enabled: config.workspace.enabled,
153
- roots: config.workspace.roots,
154
- max_depth: config.workspace.maxDepth,
155
- max_repositories: config.workspace.maxRepositories,
156
- follow_symlinks: config.workspace.followSymlinks,
157
- stop_at_repository_root: config.workspace.stopAtRepositoryRoot,
158
- },
175
+ workspace: workspaceConfigOutput(config.workspace),
176
+ policy: createWorkspaceStatusPolicy(),
177
+ repository_count: repositories.length,
178
+ repositories,
179
+ issues,
180
+ };
181
+ }
182
+ function createWorkspaceScanOutput(projectsDir) {
183
+ const projectRoot = resolveMustflowRoot();
184
+ const config = getRepoMapConfig(projectRoot);
185
+ const workspace = createAdHocWorkspaceConfig(config.workspace, projectsDir);
186
+ const repositories = discoverNestedRepositories(projectRoot, { ...config.map, includeNested: true }, workspace).map((repository) => summarizeRepository(projectRoot, repository));
187
+ const issues = repositories.length === 0
188
+ ? [t('en', 'workspace.scan.issue.noneDiscovered', { projectsDir })]
189
+ : [];
190
+ return {
191
+ schema_version: WORKSPACE_SCAN_SCHEMA_VERSION,
192
+ command: 'workspace scan',
193
+ mustflow_root: projectRoot,
194
+ workspace: workspaceConfigOutput(workspace),
159
195
  policy: createWorkspaceStatusPolicy(),
160
196
  repository_count: repositories.length,
161
197
  repositories,
162
198
  issues,
199
+ projects_dir: projectsDir,
200
+ next_actions: [
201
+ 'mf init inside one target repository',
202
+ 'mf workspace status --json after configuring workspace roots',
203
+ ],
163
204
  };
164
205
  }
165
206
  function readWorkspaceRepositories(projectRoot) {
@@ -171,12 +212,12 @@ function readWorkspaceRepositories(projectRoot) {
171
212
  }
172
213
  function workspaceConfigOutput(config) {
173
214
  return {
174
- enabled: config.workspace.enabled,
175
- roots: config.workspace.roots,
176
- max_depth: config.workspace.maxDepth,
177
- max_repositories: config.workspace.maxRepositories,
178
- follow_symlinks: config.workspace.followSymlinks,
179
- stop_at_repository_root: config.workspace.stopAtRepositoryRoot,
215
+ enabled: config.enabled,
216
+ roots: config.roots,
217
+ max_depth: config.maxDepth,
218
+ max_repositories: config.maxRepositories,
219
+ follow_symlinks: config.followSymlinks,
220
+ stop_at_repository_root: config.stopAtRepositoryRoot,
180
221
  };
181
222
  }
182
223
  function createWorkspaceIssues(config, repositoryCount) {
@@ -295,7 +336,7 @@ function createWorkspaceCommandCatalogOutput() {
295
336
  schema_version: WORKSPACE_COMMAND_CATALOG_SCHEMA_VERSION,
296
337
  command: 'workspace command-catalog',
297
338
  mustflow_root: projectRoot,
298
- workspace: workspaceConfigOutput(config),
339
+ workspace: workspaceConfigOutput(config.workspace),
299
340
  policy: createWorkspaceStatusPolicy(),
300
341
  repository_count: repositories.length,
301
342
  total_intent_count: repositories.reduce((total, repository) => total + repository.intent_count, 0),
@@ -331,6 +372,13 @@ function selectedIntentsForVerificationReport(repositoryPath, report) {
331
372
  conflict_count: entry.conflicts.length,
332
373
  }));
333
374
  }
375
+ /**
376
+ * mf:anchor cli.workspace.verify-plan
377
+ * purpose: Build per-repository verification plans from each child repository's own command contract.
378
+ * search: workspace verify, changed files, plan only, command contract, child repository
379
+ * invariant: Workspace verification output selects intents per repository and does not run raw commands from the parent root.
380
+ * risk: config, state
381
+ */
334
382
  function createVerificationRepository(projectRoot, repository) {
335
383
  const repositoryRoot = path.resolve(projectRoot, repository.relativePath);
336
384
  const commandSurface = summarizeCommandSurface(repositoryRoot, repository);
@@ -391,7 +439,7 @@ function createWorkspaceVerificationPlanOutput() {
391
439
  schema_version: WORKSPACE_VERIFICATION_PLAN_SCHEMA_VERSION,
392
440
  command: 'workspace verify',
393
441
  mustflow_root: projectRoot,
394
- workspace: workspaceConfigOutput(config),
442
+ workspace: workspaceConfigOutput(config.workspace),
395
443
  policy: createWorkspaceVerificationPlanPolicy(),
396
444
  repository_count: repositories.length,
397
445
  total_changed_file_count: repositories.reduce((total, repository) => total + (repository.changed_file_count ?? 0), 0),
@@ -402,6 +450,29 @@ function createWorkspaceVerificationPlanOutput() {
402
450
  issues: createWorkspaceIssues(config, repositories.length),
403
451
  };
404
452
  }
453
+ function renderWorkspaceScan(output) {
454
+ const lines = [
455
+ 'mustflow workspace scan',
456
+ `mustflow root: ${output.mustflow_root}`,
457
+ `projects dir: ${output.projects_dir}`,
458
+ `repositories: ${output.repository_count}`,
459
+ '',
460
+ ];
461
+ if (output.repositories.length === 0) {
462
+ lines.push(`No nested repositories discovered under ${output.projects_dir}.`);
463
+ return lines.join('\n');
464
+ }
465
+ for (const repository of output.repositories) {
466
+ lines.push(`- ${repository.relative_path} (${repository.status})`);
467
+ lines.push(` mustflow: ${repository.mustflow ? 'yes' : 'no'}`);
468
+ lines.push(` command contract: ${repository.command_contract.path ?? 'missing'}`);
469
+ }
470
+ lines.push('', 'Next actions:');
471
+ for (const action of output.next_actions) {
472
+ lines.push(`- ${action}`);
473
+ }
474
+ return lines.join('\n');
475
+ }
405
476
  function renderWorkspaceStatus(output) {
406
477
  const lines = [
407
478
  'mustflow workspace status',
@@ -488,6 +559,22 @@ function renderWorkspaceVerificationPlan(output) {
488
559
  }
489
560
  return lines.join('\n');
490
561
  }
562
+ function runWorkspaceScan(args, reporter, lang) {
563
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
564
+ reporter.stdout(getWorkspaceHelp(lang));
565
+ return 0;
566
+ }
567
+ const parsed = parseCliOptions(args, WORKSPACE_SCAN_OPTIONS);
568
+ if (parsed.error) {
569
+ printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf workspace --help', getWorkspaceHelp(lang), lang);
570
+ return 1;
571
+ }
572
+ const projectsDirValue = parsed.values.get('--projects-dir');
573
+ const projectsDir = typeof projectsDirValue === 'string' ? projectsDirValue : DEFAULT_WORKSPACE_SCAN_ROOT;
574
+ const output = createWorkspaceScanOutput(projectsDir);
575
+ reporter.stdout(hasParsedCliOption(parsed, '--json') ? JSON.stringify(output, null, 2) : renderWorkspaceScan(output));
576
+ return 0;
577
+ }
491
578
  function runWorkspaceStatus(args, reporter, lang) {
492
579
  if (hasCliOptionToken(args, '--help', ['-h'])) {
493
580
  reporter.stdout(getWorkspaceHelp(lang));
@@ -552,6 +639,9 @@ export function runWorkspace(args, reporter, lang = 'en') {
552
639
  printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: action }), 'mf workspace --help', getWorkspaceHelp(lang), lang);
553
640
  return 1;
554
641
  }
642
+ if (action === 'scan') {
643
+ return runWorkspaceScan(rest, reporter, lang);
644
+ }
555
645
  if (action === 'status') {
556
646
  return runWorkspaceStatus(rest, reporter, lang);
557
647
  }
@@ -135,14 +135,17 @@ export const enMessages = {
135
135
  "evidence.section.gaps": "Gaps",
136
136
  "evidence.section.remainingRisks": "Remaining risks",
137
137
  "workspace.help.summary": "Inspect configured workspace roots and nested repository contract readiness without granting command authority.",
138
+ "workspace.help.action.scan": "Scan a projects directory for nested repositories without requiring workspace configuration",
138
139
  "workspace.help.action.status": "Print discovered nested repositories and their local mustflow contract status",
139
140
  "workspace.help.action.commandCatalog": "Print per-repository command intent availability without raw command strings",
140
141
  "workspace.help.action.verify": "Print per-repository changed-file verification plans without running commands",
142
+ "workspace.help.option.projectsDir": "Select the child directory to scan; default: projects",
141
143
  "workspace.help.exit.ok": "Workspace status was inspected",
142
- "workspace.error.missingAction": "Specify a workspace action: status, command-catalog, or verify",
144
+ "workspace.error.missingAction": "Specify a workspace action: scan, status, command-catalog, or verify",
143
145
  "workspace.error.unknownAction": "Unknown workspace action: {action}",
144
146
  "workspace.error.verifyRequiresChanged": "workspace verify requires --changed",
145
147
  "workspace.error.verifyRequiresPlanOnly": "workspace verify requires --plan-only",
148
+ "workspace.scan.issue.noneDiscovered": "No nested git repositories were discovered under {projectsDir}.",
146
149
  "context.help.summary": "Print the agent context for the current mustflow root.",
147
150
  "context.help.option.json": "Print machine-readable context JSON",
148
151
  "context.help.option.cacheProfile": "Print a prompt-cache profile: stable, task, volatile, or all",
@@ -680,6 +683,8 @@ Read these files before working:
680
683
  "init.error.unsupportedPreference": "Unsupported init preference setting: {key}",
681
684
  "init.error.promptInputTooLarge": "Interactive init stdin input is too large; expected at most {maxBytes} bytes.",
682
685
  "init.error.promptInputTooManyResponses": "Interactive init stdin input has too many responses; expected at most {maxResponses} lines.",
686
+ "init.error.workspaceRootDetected": "This directory looks like a multi-repository workspace, not a single repository: found {count} git repositories under {projectsDir}.",
687
+ "init.error.workspaceRootGuidance": "Run mf workspace scan --json here, or run mf init inside one target repository.",
683
688
  "init.prompt.locale": "Which language should mustflow documents use?",
684
689
  "init.prompt.profile": "Which project profile should mustflow use?",
685
690
  "init.prompt.agentLang": "Which language should agents use for final reports?",
@@ -135,14 +135,17 @@ export const esMessages = {
135
135
  "evidence.section.gaps": "Brechas",
136
136
  "evidence.section.remainingRisks": "Riesgos restantes",
137
137
  "workspace.help.summary": "Inspecciona raíces workspace configuradas y preparación de contratos anidados sin conceder autoridad de comandos.",
138
+ "workspace.help.action.scan": "Escanea un directorio projects en busca de repositorios anidados sin requerir configuración de workspace",
138
139
  "workspace.help.action.status": "Imprime repositorios anidados descubiertos y su estado de contrato mustflow local",
139
140
  "workspace.help.action.commandCatalog": "Imprime disponibilidad de intents por repositorio sin cadenas de comando sin procesar",
140
141
  "workspace.help.action.verify": "Imprime planes de verificación de cambios por repositorio sin ejecutar comandos",
142
+ "workspace.help.option.projectsDir": "Selecciona el directorio hijo que se escaneará; predeterminado: projects",
141
143
  "workspace.help.exit.ok": "Se inspeccionó el estado del workspace",
142
- "workspace.error.missingAction": "Especifica una acción workspace: status, command-catalog o verify",
144
+ "workspace.error.missingAction": "Especifica una acción workspace: scan, status, command-catalog o verify",
143
145
  "workspace.error.unknownAction": "Acción workspace desconocida: {action}",
144
146
  "workspace.error.verifyRequiresChanged": "workspace verify requiere --changed",
145
147
  "workspace.error.verifyRequiresPlanOnly": "workspace verify requiere --plan-only",
148
+ "workspace.scan.issue.noneDiscovered": "No se descubrieron repositorios git anidados bajo {projectsDir}.",
146
149
  "context.help.summary": "Imprime el contexto de agente para la raíz mustflow actual.",
147
150
  "context.help.option.json": "Imprime JSON de contexto legible por máquinas",
148
151
  "context.help.option.cacheProfile": "Imprime un perfil de caché de prompt: stable, task, volatile o all",
@@ -680,6 +683,8 @@ Lee estos archivos antes de trabajar:
680
683
  "init.error.unsupportedPreference": "Ajuste de preferencia de inicio no admitido: {key}",
681
684
  "init.error.promptInputTooLarge": "La entrada estándar de init interactivo es demasiado grande; se esperaban como máximo {maxBytes} bytes.",
682
685
  "init.error.promptInputTooManyResponses": "La entrada estándar de init interactivo tiene demasiadas respuestas; se esperaban como máximo {maxResponses} líneas.",
686
+ "init.error.workspaceRootDetected": "Este directorio parece un workspace de varios repositorios, no un solo repositorio: se encontraron {count} repositorios git bajo {projectsDir}.",
687
+ "init.error.workspaceRootGuidance": "Ejecuta mf workspace scan --json aquí, o ejecuta mf init dentro de un repositorio de destino.",
683
688
  "init.prompt.locale": "¿Qué idioma deben usar los documentos mustflow?",
684
689
  "init.prompt.profile": "¿Qué perfil de proyecto debe usar mustflow?",
685
690
  "init.prompt.agentLang": "¿Qué idioma deben usar los agentes en los informes finales?",
@@ -135,14 +135,17 @@ export const frMessages = {
135
135
  "evidence.section.gaps": "Écarts",
136
136
  "evidence.section.remainingRisks": "Risques restants",
137
137
  "workspace.help.summary": "Inspecte les racines workspace configurées et l'état des contrats imbriqués sans accorder d'autorité de commande.",
138
+ "workspace.help.action.scan": "Scanne un répertoire projects pour trouver des dépôts imbriqués sans exiger de configuration workspace",
138
139
  "workspace.help.action.status": "Affiche les dépôts imbriqués découverts et leur état de contrat mustflow local",
139
140
  "workspace.help.action.commandCatalog": "Affiche la disponibilité des intents par dépôt sans chaînes de commande brutes",
140
141
  "workspace.help.action.verify": "Affiche les plans de vérification des changements par dépôt sans exécuter de commandes",
142
+ "workspace.help.option.projectsDir": "Sélectionne le répertoire enfant à scanner ; par défaut : projects",
141
143
  "workspace.help.exit.ok": "L'état du workspace a été inspecté",
142
- "workspace.error.missingAction": "Indiquez une action workspace : status, command-catalog ou verify",
144
+ "workspace.error.missingAction": "Indiquez une action workspace : scan, status, command-catalog ou verify",
143
145
  "workspace.error.unknownAction": "Action workspace inconnue : {action}",
144
146
  "workspace.error.verifyRequiresChanged": "workspace verify nécessite --changed",
145
147
  "workspace.error.verifyRequiresPlanOnly": "workspace verify nécessite --plan-only",
148
+ "workspace.scan.issue.noneDiscovered": "Aucun dépôt git imbriqué n'a été découvert sous {projectsDir}.",
146
149
  "context.help.summary": "Imprime le contexte d'agent pour la racine mustflow actuelle.",
147
150
  "context.help.option.json": "Imprime le JSON de contexte lisible par machine",
148
151
  "context.help.option.cacheProfile": "Imprime un profil de cache de prompt : stable, task, volatile ou all",
@@ -680,6 +683,8 @@ Lisez ces fichiers avant de travailler :
680
683
  "init.error.unsupportedPreference": "Paramètre de préférence d'initialisation non pris en charge : {key}",
681
684
  "init.error.promptInputTooLarge": "L'entrée standard de l'init interactif est trop volumineuse ; {maxBytes} octets maximum sont attendus.",
682
685
  "init.error.promptInputTooManyResponses": "L'entrée standard de l'init interactif contient trop de réponses ; {maxResponses} lignes maximum sont attendues.",
686
+ "init.error.workspaceRootDetected": "Ce répertoire ressemble à un workspace multi-dépôts, pas à un dépôt unique : {count} dépôts git ont été trouvés sous {projectsDir}.",
687
+ "init.error.workspaceRootGuidance": "Exécutez mf workspace scan --json ici, ou exécutez mf init dans un dépôt cible.",
683
688
  "init.prompt.locale": "Quelle langue les documents mustflow doivent-ils utiliser ?",
684
689
  "init.prompt.profile": "Quel profil de projet mustflow doit-il utiliser ?",
685
690
  "init.prompt.agentLang": "Quelle langue les agents doivent-ils utiliser pour les rapports finaux ?",
@@ -135,14 +135,17 @@ export const hiMessages = {
135
135
  "evidence.section.gaps": "Gaps",
136
136
  "evidence.section.remainingRisks": "Remaining risks",
137
137
  "workspace.help.summary": "command authority दिए बिना configured workspace roots और nested repository contract readiness inspect करें.",
138
+ "workspace.help.action.scan": "workspace configuration के बिना projects directory में nested repositories scan करें",
138
139
  "workspace.help.action.status": "discovered nested repositories और उनके local mustflow contract status प्रिंट करें",
139
140
  "workspace.help.action.commandCatalog": "raw command strings के बिना per-repository command intent availability प्रिंट करें",
140
141
  "workspace.help.action.verify": "commands चलाए बिना per-repository changed-file verification plans प्रिंट करें",
142
+ "workspace.help.option.projectsDir": "scan की जाने वाली child directory चुनें; default: projects",
141
143
  "workspace.help.exit.ok": "Workspace status inspect हुआ",
142
- "workspace.error.missingAction": "workspace action बताएँ: status, command-catalog या verify",
144
+ "workspace.error.missingAction": "workspace action बताएँ: scan, status, command-catalog या verify",
143
145
  "workspace.error.unknownAction": "अज्ञात workspace action: {action}",
144
146
  "workspace.error.verifyRequiresChanged": "workspace verify के लिए --changed चाहिए",
145
147
  "workspace.error.verifyRequiresPlanOnly": "workspace verify के लिए --plan-only चाहिए",
148
+ "workspace.scan.issue.noneDiscovered": "{projectsDir} के नीचे कोई nested git repository नहीं मिला.",
146
149
  "context.help.summary": "वर्तमान mustflow रूट के लिए एजेंट संदर्भ प्रिंट करें।",
147
150
  "context.help.option.json": "मशीन-पठनीय संदर्भ JSON प्रिंट करें",
148
151
  "context.help.option.cacheProfile": "prompt cache profile प्रिंट करें: stable, task, volatile, या all",
@@ -680,6 +683,8 @@ export const hiMessages = {
680
683
  "init.error.unsupportedPreference": "असमर्थित init preference setting: {key}",
681
684
  "init.error.promptInputTooLarge": "Interactive init stdin input बहुत बड़ा है; अधिकतम {maxBytes} bytes अपेक्षित हैं।",
682
685
  "init.error.promptInputTooManyResponses": "Interactive init stdin input में बहुत अधिक responses हैं; अधिकतम {maxResponses} lines अपेक्षित हैं।",
686
+ "init.error.workspaceRootDetected": "यह directory single repository नहीं बल्कि multi-repository workspace जैसी दिखती है: {projectsDir} के नीचे {count} git repositories मिले.",
687
+ "init.error.workspaceRootGuidance": "यहाँ mf workspace scan --json चलाएँ, या target repository के अंदर mf init चलाएँ.",
683
688
  "init.prompt.locale": "mustflow दस्तावेज़ कौन सी भाषा उपयोग करें?",
684
689
  "init.prompt.profile": "mustflow कौन सा project profile उपयोग करे?",
685
690
  "init.prompt.agentLang": "एजेंट final reports के लिए कौन सी भाषा उपयोग करें?",
@@ -135,14 +135,17 @@ export const koMessages = {
135
135
  "evidence.section.gaps": "구멍",
136
136
  "evidence.section.remainingRisks": "남은 리스크",
137
137
  "workspace.help.summary": "명령 권한을 부여하지 않고 설정된 workspace root와 nested repository 계약 준비 상태를 확인합니다.",
138
+ "workspace.help.action.scan": "workspace 설정 없이 projects 디렉터리의 nested repository를 스캔합니다",
138
139
  "workspace.help.action.status": "발견된 nested repository와 각 로컬 mustflow 계약 상태를 출력합니다",
139
140
  "workspace.help.action.commandCatalog": "raw command string 없이 repository별 command intent 사용 가능 상태를 출력합니다",
140
141
  "workspace.help.action.verify": "명령을 실행하지 않고 repository별 changed-file verification plan을 출력합니다",
142
+ "workspace.help.option.projectsDir": "스캔할 하위 디렉터리를 지정합니다. 기본값: projects",
141
143
  "workspace.help.exit.ok": "workspace 상태를 확인했습니다",
142
- "workspace.error.missingAction": "workspace 작업을 지정하세요: status, command-catalog 또는 verify",
144
+ "workspace.error.missingAction": "workspace 작업을 지정하세요: scan, status, command-catalog 또는 verify",
143
145
  "workspace.error.unknownAction": "알 수 없는 workspace 작업: {action}",
144
146
  "workspace.error.verifyRequiresChanged": "workspace verify에는 --changed가 필요합니다",
145
147
  "workspace.error.verifyRequiresPlanOnly": "workspace verify에는 --plan-only가 필요합니다",
148
+ "workspace.scan.issue.noneDiscovered": "{projectsDir} 아래에서 nested git repository를 찾지 못했습니다.",
146
149
  "context.help.summary": "현재 mustflow 루트의 에이전트 작업 맥락을 출력합니다.",
147
150
  "context.help.option.json": "맥락을 JSON 형식으로 출력합니다",
148
151
  "context.help.option.cacheProfile": "프롬프트 캐시 프로필을 출력합니다: stable, task, volatile, all",
@@ -680,6 +683,8 @@ export const koMessages = {
680
683
  "init.error.unsupportedPreference": "지원하지 않는 초기 설정 항목입니다: {key}",
681
684
  "init.error.promptInputTooLarge": "대화형 init 표준입력이 너무 큽니다. 최대 {maxBytes}바이트까지만 허용됩니다.",
682
685
  "init.error.promptInputTooManyResponses": "대화형 init 표준입력 응답이 너무 많습니다. 최대 {maxResponses}줄까지만 허용됩니다.",
686
+ "init.error.workspaceRootDetected": "이 디렉터리는 단일 저장소가 아니라 여러 저장소를 담은 workspace처럼 보입니다: {projectsDir} 아래에서 git repository {count}개를 찾았습니다.",
687
+ "init.error.workspaceRootGuidance": "여기서는 mf workspace scan --json을 실행하거나, 대상 저장소 안으로 이동해서 mf init을 실행하세요.",
683
688
  "init.prompt.locale": "mustflow 문서는 어떤 언어로 설치할까요?",
684
689
  "init.prompt.profile": "이 저장소에는 어떤 프로젝트 유형을 사용할까요?",
685
690
  "init.prompt.agentLang": "에이전트 최종 응답은 어떤 언어로 받을까요?",
@@ -135,14 +135,17 @@ export const zhMessages = {
135
135
  "evidence.section.gaps": "缺口",
136
136
  "evidence.section.remainingRisks": "剩余风险",
137
137
  "workspace.help.summary": "检查已配置的 workspace 根目录和嵌套仓库合同状态,但不授予命令权限。",
138
+ "workspace.help.action.scan": "无需 workspace 配置即可扫描 projects 目录中的嵌套仓库",
138
139
  "workspace.help.action.status": "输出发现的嵌套仓库及其本地 mustflow 合同状态",
139
140
  "workspace.help.action.commandCatalog": "输出各仓库的命令 intent 可用性,不包含原始命令字符串",
140
141
  "workspace.help.action.verify": "输出各仓库的变更文件验证计划,但不运行命令",
142
+ "workspace.help.option.projectsDir": "选择要扫描的子目录;默认值:projects",
141
143
  "workspace.help.exit.ok": "已检查 workspace 状态",
142
- "workspace.error.missingAction": "请指定 workspace 操作:status、command-catalog 或 verify",
144
+ "workspace.error.missingAction": "请指定 workspace 操作:scan、status、command-catalog 或 verify",
143
145
  "workspace.error.unknownAction": "未知 workspace 操作:{action}",
144
146
  "workspace.error.verifyRequiresChanged": "workspace verify 需要 --changed",
145
147
  "workspace.error.verifyRequiresPlanOnly": "workspace verify 需要 --plan-only",
148
+ "workspace.scan.issue.noneDiscovered": "未在 {projectsDir} 下发现嵌套 git 仓库。",
146
149
  "context.help.summary": "输出当前 mustflow 根目录的代理上下文。",
147
150
  "context.help.option.json": "输出机器可读的上下文 JSON",
148
151
  "context.help.option.cacheProfile": "输出提示缓存配置文件:stable、task、volatile 或 all",
@@ -680,6 +683,8 @@ export const zhMessages = {
680
683
  "init.error.unsupportedPreference": "不支持的初始化偏好设置:{key}",
681
684
  "init.error.promptInputTooLarge": "交互式 init 的标准输入过大;最多允许 {maxBytes} 字节。",
682
685
  "init.error.promptInputTooManyResponses": "交互式 init 的标准输入响应过多;最多允许 {maxResponses} 行。",
686
+ "init.error.workspaceRootDetected": "此目录看起来是多仓库 workspace,而不是单个仓库:在 {projectsDir} 下发现了 {count} 个 git 仓库。",
687
+ "init.error.workspaceRootGuidance": "请在此处运行 mf workspace scan --json,或进入目标仓库后运行 mf init。",
683
688
  "init.prompt.locale": "mustflow 文档应使用哪种语言?",
684
689
  "init.prompt.profile": "mustflow 应使用哪个项目配置?",
685
690
  "init.prompt.agentLang": "代理最终报告应使用哪种语言?",
package/dist/cli/index.js CHANGED
@@ -40,6 +40,7 @@ function getTopLevelHelp(lang) {
40
40
  'mf onboard commands --json',
41
41
  'mf next --json',
42
42
  'mf evidence --changed --json',
43
+ 'mf workspace scan --json',
43
44
  'mf workspace status --json',
44
45
  'mf workspace verify --changed --plan-only --json',
45
46
  'mf context --json',
@@ -99,6 +100,13 @@ function parseGlobalOptions(argv) {
99
100
  }
100
101
  return { lang, args: parsed.positionals };
101
102
  }
103
+ /**
104
+ * mf:anchor cli.entrypoint.dispatch
105
+ * purpose: Resolve top-level CLI options and dispatch commands without bypassing per-command validation.
106
+ * search: CLI dispatch, global options, command registry, version alias, help
107
+ * invariant: Unknown commands and global option errors must stop before any command runner is loaded.
108
+ * risk: config
109
+ */
102
110
  export async function runCli(argv, reporter = consoleReporter) {
103
111
  const parsed = parseGlobalOptions(argv);
104
112
  const [command, ...args] = parsed.args;
@@ -598,6 +598,13 @@ function readTaskPromptCacheRepoMapReadPlan() {
598
598
  },
599
599
  };
600
600
  }
601
+ /**
602
+ * mf:anchor cli.context.prompt-cache-task-layer
603
+ * purpose: Build the task-layer context plan that prefers selected routes, local-index anchors, and bounded repo-map spans.
604
+ * search: mf context, prompt cache, task context, route read plan, repo map, local index
605
+ * invariant: Task context may recommend what to read next but cannot replace AGENTS.md or command contract authority.
606
+ * risk: config, cache
607
+ */
601
608
  async function readTaskPromptCacheLayer(projectRoot, mustflow) {
602
609
  const layer = readPromptCacheLayer(mustflow, 'task');
603
610
  const localIndex = await readLocalIndexPromptContext(projectRoot);