mustflow 2.22.4 → 2.22.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 (72) hide show
  1. package/README.md +17 -75
  2. package/dist/cli/commands/classify.js +2 -0
  3. package/dist/cli/commands/contract-lint.js +2 -2
  4. package/dist/cli/commands/dashboard.js +23 -75
  5. package/dist/cli/commands/help.js +8 -9
  6. package/dist/cli/commands/impact.js +2 -3
  7. package/dist/cli/commands/init.js +61 -5
  8. package/dist/cli/commands/run/receipt.js +1 -0
  9. package/dist/cli/commands/run.js +14 -1
  10. package/dist/cli/commands/update.js +2 -2
  11. package/dist/cli/commands/verify/evidence-input.js +269 -0
  12. package/dist/cli/commands/verify/input.js +212 -0
  13. package/dist/cli/commands/verify.js +23 -482
  14. package/dist/cli/commands/version-sources.js +2 -3
  15. package/dist/cli/i18n/en.js +5 -0
  16. package/dist/cli/i18n/es.js +5 -0
  17. package/dist/cli/i18n/fr.js +5 -0
  18. package/dist/cli/i18n/hi.js +5 -0
  19. package/dist/cli/i18n/ko.js +5 -0
  20. package/dist/cli/i18n/zh.js +5 -0
  21. package/dist/cli/lib/agent-context.js +6 -11
  22. package/dist/cli/lib/dashboard-export.js +2 -0
  23. package/dist/cli/lib/dashboard-mutations.js +79 -0
  24. package/dist/cli/lib/local-index/command-effect-index.js +25 -0
  25. package/dist/cli/lib/local-index/hashing.js +7 -0
  26. package/dist/cli/lib/local-index/index.js +127 -823
  27. package/dist/cli/lib/local-index/source-index.js +137 -0
  28. package/dist/cli/lib/local-index/verification-evidence.js +451 -0
  29. package/dist/cli/lib/local-index/workflow-documents.js +204 -0
  30. package/dist/cli/lib/mustflow-read.js +41 -0
  31. package/dist/cli/lib/project-root.js +1 -2
  32. package/dist/cli/lib/repo-map.js +65 -16
  33. package/dist/cli/lib/run-root-trust.js +27 -0
  34. package/dist/cli/lib/templates.js +124 -8
  35. package/dist/cli/lib/toml.js +6 -1
  36. package/dist/cli/lib/validation/constants.js +2 -0
  37. package/dist/cli/lib/validation/index.js +291 -22
  38. package/dist/cli/lib/validation/primitives.js +2 -2
  39. package/dist/cli/lib/validation/test-selection.js +2 -2
  40. package/dist/core/bounded-output.js +32 -7
  41. package/dist/core/change-classification-policy.js +47 -0
  42. package/dist/core/change-classification.js +10 -43
  43. package/dist/core/check-issues.js +7 -1
  44. package/dist/core/command-contract-validation.js +28 -4
  45. package/dist/core/command-env.js +1 -1
  46. package/dist/core/config-loading.js +9 -3
  47. package/dist/core/contract-lint.js +8 -3
  48. package/dist/core/correlation-id.js +16 -0
  49. package/dist/core/run-receipt.js +1 -0
  50. package/dist/core/safe-filesystem.js +11 -4
  51. package/dist/core/skill-route-alignment.js +1 -0
  52. package/dist/core/skill-route-explanation.js +9 -3
  53. package/dist/core/test-selection.js +2 -3
  54. package/dist/core/verification-scheduler.js +7 -6
  55. package/dist/core/version-sources.js +2 -3
  56. package/package.json +4 -1
  57. package/schemas/README.md +4 -0
  58. package/schemas/change-verification-report.schema.json +4 -0
  59. package/schemas/classify-report.schema.json +4 -0
  60. package/schemas/commands.schema.json +1 -0
  61. package/schemas/dashboard-export.schema.json +4 -0
  62. package/schemas/latest-run-pointer.schema.json +4 -0
  63. package/schemas/run-receipt.schema.json +4 -0
  64. package/schemas/verify-report.schema.json +4 -0
  65. package/schemas/verify-run-manifest.schema.json +4 -0
  66. package/templates/default/i18n.toml +3 -3
  67. package/templates/default/locales/en/.mustflow/skills/INDEX.md +10 -6
  68. package/templates/default/locales/en/.mustflow/skills/architecture-deepening-review/SKILL.md +25 -2
  69. package/templates/default/locales/en/.mustflow/skills/routes.toml +2 -2
  70. package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +9 -1
  71. package/templates/default/locales/en/.mustflow/skills/test-design-guard/SKILL.md +9 -1
  72. package/templates/default/manifest.toml +1 -1
@@ -0,0 +1,204 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { listFilesRecursive, toPosixPath } from '../filesystem.js';
4
+ import { readMustflowTextFile } from '../mustflow-read.js';
5
+ import { MAX_SNIPPET_BYTES_PER_DOCUMENT } from './constants.js';
6
+ import { sha256Text } from './hashing.js';
7
+ export function getExistingIndexablePaths(projectRoot) {
8
+ const paths = new Set();
9
+ const addIfExists = (relativePath) => {
10
+ if (existsSync(path.join(projectRoot, ...relativePath.split('/')))) {
11
+ paths.add(relativePath);
12
+ }
13
+ };
14
+ addIfExists('AGENTS.md');
15
+ for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'docs'))) {
16
+ if (relativePath.endsWith('.md')) {
17
+ paths.add(toPosixPath(path.join('.mustflow', 'docs', relativePath)));
18
+ }
19
+ }
20
+ for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'context'))) {
21
+ if (relativePath.endsWith('.md')) {
22
+ paths.add(toPosixPath(path.join('.mustflow', 'context', relativePath)));
23
+ }
24
+ }
25
+ for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'skills'))) {
26
+ if (relativePath === 'INDEX.md' || relativePath.endsWith('/SKILL.md')) {
27
+ paths.add(toPosixPath(path.join('.mustflow', 'skills', relativePath)));
28
+ }
29
+ }
30
+ for (const relativePath of listFilesRecursive(path.join(projectRoot, '.mustflow', 'config'))) {
31
+ if (relativePath.endsWith('.toml')) {
32
+ paths.add(toPosixPath(path.join('.mustflow', 'config', relativePath)));
33
+ }
34
+ }
35
+ return Array.from(paths).sort((left, right) => left.localeCompare(right));
36
+ }
37
+ export function readText(projectRoot, relativePath) {
38
+ return readMustflowTextFile(projectRoot, relativePath);
39
+ }
40
+ function getDocumentType(relativePath) {
41
+ if (relativePath === 'AGENTS.md') {
42
+ return 'agent_rules';
43
+ }
44
+ if (relativePath.startsWith('.mustflow/config/')) {
45
+ return 'config';
46
+ }
47
+ if (relativePath === '.mustflow/skills/INDEX.md') {
48
+ return 'skill_index';
49
+ }
50
+ if (relativePath === '.mustflow/context/INDEX.md') {
51
+ return 'context_index';
52
+ }
53
+ if (relativePath.startsWith('.mustflow/context/')) {
54
+ return 'context';
55
+ }
56
+ if (relativePath.endsWith('/SKILL.md')) {
57
+ return 'skill';
58
+ }
59
+ if (relativePath.startsWith('.mustflow/docs/')) {
60
+ return 'workflow_doc';
61
+ }
62
+ return 'document';
63
+ }
64
+ function parseFrontmatter(content) {
65
+ if (!content.startsWith('---')) {
66
+ return {};
67
+ }
68
+ const end = content.indexOf('\n---', 3);
69
+ if (end === -1) {
70
+ return {};
71
+ }
72
+ const result = {};
73
+ const rawFrontmatter = content.slice(3, end);
74
+ for (const line of rawFrontmatter.split(/\r?\n/)) {
75
+ const separatorIndex = line.indexOf(':');
76
+ if (separatorIndex === -1) {
77
+ continue;
78
+ }
79
+ const key = line.slice(0, separatorIndex).trim();
80
+ const value = line.slice(separatorIndex + 1).trim();
81
+ if (key.length > 0 && value.length > 0) {
82
+ result[key] = value;
83
+ }
84
+ }
85
+ return result;
86
+ }
87
+ function getTitle(relativePath, content) {
88
+ const heading = content.match(/^#\s+(.+)$/mu)?.[1]?.trim();
89
+ return heading && heading.length > 0 ? heading : path.posix.basename(relativePath);
90
+ }
91
+ function getSections(content) {
92
+ return [...content.matchAll(/^##\s+(.+)$/gmu)].map((match) => match[1]?.trim()).filter((value) => Boolean(value));
93
+ }
94
+ function truncateUtf8(value, maxBytes) {
95
+ const buffer = Buffer.from(value, 'utf8');
96
+ if (buffer.byteLength <= maxBytes) {
97
+ return value;
98
+ }
99
+ return buffer.subarray(0, maxBytes).toString('utf8').replace(/\uFFFD$/u, '');
100
+ }
101
+ export function collectDocumentsFromPaths(projectRoot, relativePaths) {
102
+ return relativePaths.map((relativePath) => {
103
+ const content = readText(projectRoot, relativePath);
104
+ const frontmatter = parseFrontmatter(content);
105
+ const revision = Number.parseInt(frontmatter.revision ?? '', 10);
106
+ return {
107
+ path: relativePath,
108
+ type: getDocumentType(relativePath),
109
+ title: getTitle(relativePath, content),
110
+ locale: frontmatter.locale ?? null,
111
+ revision: Number.isInteger(revision) ? revision : null,
112
+ contentHash: sha256Text(content),
113
+ contentSnippet: truncateUtf8(content, MAX_SNIPPET_BYTES_PER_DOCUMENT),
114
+ sections: getSections(content),
115
+ };
116
+ });
117
+ }
118
+ export function collectDocuments(projectRoot) {
119
+ return collectDocumentsFromPaths(projectRoot, getExistingIndexablePaths(projectRoot));
120
+ }
121
+ export function collectSkills(documents) {
122
+ return documents
123
+ .filter((document) => document.type === 'skill')
124
+ .map((document) => ({
125
+ name: document.path.split('/').at(-2) ?? document.title,
126
+ path: document.path,
127
+ title: document.title,
128
+ }))
129
+ .sort((left, right) => left.name.localeCompare(right.name));
130
+ }
131
+ function normalizeMarkdownCell(value) {
132
+ return value
133
+ .replace(/<br\s*\/?>/giu, ' ')
134
+ .replace(/`([^`]+)`/gu, '$1')
135
+ .replace(/\s+/gu, ' ')
136
+ .trim();
137
+ }
138
+ function parseMarkdownTableRow(line) {
139
+ return line
140
+ .trim()
141
+ .replace(/^\|/u, '')
142
+ .replace(/\|$/u, '')
143
+ .split('|')
144
+ .map((cell) => normalizeMarkdownCell(cell));
145
+ }
146
+ function isMarkdownSeparatorRow(cells) {
147
+ return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/u.test(cell));
148
+ }
149
+ function skillNameFromPath(skillPath) {
150
+ return skillPath.split('/').at(-2) ?? path.posix.basename(skillPath, '.md');
151
+ }
152
+ export function splitVerificationIntents(value) {
153
+ return value
154
+ .split(',')
155
+ .map((item) => item.trim())
156
+ .filter(Boolean)
157
+ .sort((left, right) => left.localeCompare(right));
158
+ }
159
+ export function skillRouteKey(route) {
160
+ return `${route.skillName}\u0000${route.trigger}`;
161
+ }
162
+ export function collectSkillRoutes(projectRoot) {
163
+ const indexPath = path.join(projectRoot, '.mustflow', 'skills', 'INDEX.md');
164
+ if (!existsSync(indexPath)) {
165
+ return [];
166
+ }
167
+ const content = readMustflowTextFile(projectRoot, '.mustflow/skills/INDEX.md');
168
+ const routes = [];
169
+ let inRouteTable = false;
170
+ for (const line of content.split(/\r?\n/u)) {
171
+ if (!line.trim().startsWith('|')) {
172
+ if (inRouteTable && line.trim() === '') {
173
+ inRouteTable = false;
174
+ }
175
+ continue;
176
+ }
177
+ const cells = parseMarkdownTableRow(line);
178
+ if (cells.includes('Skill Document') && cells.includes('Trigger')) {
179
+ inRouteTable = true;
180
+ continue;
181
+ }
182
+ if (!inRouteTable || isMarkdownSeparatorRow(cells) || cells.length < 7) {
183
+ continue;
184
+ }
185
+ const [trigger, skillPath, requiredInput, editScope, risk, verificationIntents, expectedOutput] = cells;
186
+ if (!skillPath?.startsWith('.mustflow/skills/') || !skillPath.endsWith('/SKILL.md')) {
187
+ continue;
188
+ }
189
+ routes.push({
190
+ skillName: skillNameFromPath(skillPath),
191
+ skillPath,
192
+ trigger: trigger ?? '',
193
+ requiredInput: requiredInput ?? '',
194
+ editScope: editScope ?? '',
195
+ risk: risk ?? '',
196
+ verificationIntents: splitVerificationIntents(verificationIntents ?? ''),
197
+ expectedOutput: expectedOutput ?? '',
198
+ });
199
+ }
200
+ return routes.sort((left, right) => {
201
+ const skillOrder = left.skillName.localeCompare(right.skillName);
202
+ return skillOrder === 0 ? left.trigger.localeCompare(right.trigger) : skillOrder;
203
+ });
204
+ }
@@ -0,0 +1,41 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readUtf8FileInsideWithoutSymlinks } from './filesystem.js';
4
+ export const MUSTFLOW_TEXT_MAX_BYTES = 1024 * 1024;
5
+ export const MUSTFLOW_TOML_MAX_BYTES = 256 * 1024;
6
+ export const MUSTFLOW_JSON_MAX_BYTES = 1024 * 1024;
7
+ export function mustflowProjectPath(projectRoot, relativePath) {
8
+ return path.join(projectRoot, ...relativePath.split('/'));
9
+ }
10
+ function missingPath(error) {
11
+ return error instanceof Error && 'code' in error && error.code === 'ENOENT';
12
+ }
13
+ export function readMustflowTextFile(projectRoot, relativePath, options = {}) {
14
+ return readUtf8FileInsideWithoutSymlinks(projectRoot, mustflowProjectPath(projectRoot, relativePath), { maxBytes: options.maxBytes ?? MUSTFLOW_TEXT_MAX_BYTES });
15
+ }
16
+ export function readMustflowTextFileResult(projectRoot, relativePath, options = {}) {
17
+ const filePath = mustflowProjectPath(projectRoot, relativePath);
18
+ if (!existsSync(filePath)) {
19
+ return { ok: false, exists: false, error: null };
20
+ }
21
+ try {
22
+ return {
23
+ ok: true,
24
+ content: readMustflowTextFile(projectRoot, relativePath, options),
25
+ };
26
+ }
27
+ catch (error) {
28
+ if (missingPath(error)) {
29
+ return { ok: false, exists: false, error: null };
30
+ }
31
+ return {
32
+ ok: false,
33
+ exists: true,
34
+ error: error instanceof Error ? error.message : String(error),
35
+ };
36
+ }
37
+ }
38
+ export function readMustflowTextFileIfExists(projectRoot, relativePath, options = {}) {
39
+ const result = readMustflowTextFileResult(projectRoot, relativePath, options);
40
+ return result.ok ? result.content : null;
41
+ }
@@ -2,8 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  function hasMustflowMarker(directoryPath) {
4
4
  return (existsSync(path.join(directoryPath, '.mustflow', 'config', 'mustflow.toml')) ||
5
- existsSync(path.join(directoryPath, '.mustflow', 'config', 'commands.toml')) ||
6
- existsSync(path.join(directoryPath, '.mustflow')));
5
+ existsSync(path.join(directoryPath, '.mustflow', 'config', 'commands.toml')));
7
6
  }
8
7
  export function findMustflowRoot(startPath = process.cwd()) {
9
8
  let current = path.resolve(startPath);
@@ -4,7 +4,7 @@ import { existsSync, lstatSync, readdirSync, realpathSync, statSync } from 'node
4
4
  import path from 'node:path';
5
5
  import { toPosixPath } from './filesystem.js';
6
6
  import { writeUtf8FileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
7
- import { readTomlFile } from './toml.js';
7
+ import { readMustflowTomlFile } from './toml.js';
8
8
  const DEFAULT_DEPTH = 3;
9
9
  const REPO_MAP_DOC_ID = 'repo-map';
10
10
  const REPO_MAP_LIFECYCLE = 'generated';
@@ -214,7 +214,7 @@ function readMustflowConfig(projectRoot) {
214
214
  return {};
215
215
  }
216
216
  try {
217
- const parsed = readTomlFile(configPath);
217
+ const parsed = readMustflowTomlFile(projectRoot, '.mustflow/config/mustflow.toml');
218
218
  return isRecord(parsed) ? parsed : {};
219
219
  }
220
220
  catch {
@@ -243,7 +243,19 @@ function getRepoMapConfig(projectRoot) {
243
243
  },
244
244
  };
245
245
  }
246
- export function listGitFilesForRepoMap(projectRoot, options = {}) {
246
+ function classifyGitLsFilesFailure(result) {
247
+ const errorRecord = result.error;
248
+ const errorCode = typeof errorRecord?.code === 'string' ? errorRecord.code : undefined;
249
+ const errorMessage = result.error?.message ?? '';
250
+ if (errorCode === 'ETIMEDOUT' || /timed?\s*out|ETIMEDOUT/iu.test(errorMessage)) {
251
+ return 'timeout';
252
+ }
253
+ if (errorCode === 'ENOBUFS' || /maxBuffer|ENOBUFS|buffer/iu.test(errorMessage)) {
254
+ return 'max_buffer';
255
+ }
256
+ return 'error';
257
+ }
258
+ export function discoverGitFilesForRepoMap(projectRoot, options = {}) {
247
259
  const spawnGit = options.spawnGit ??
248
260
  ((command, args, spawnOptions) => spawnSync(command, [...args], spawnOptions));
249
261
  const result = spawnGit('git', ['ls-files', '-z'], {
@@ -254,12 +266,21 @@ export function listGitFilesForRepoMap(projectRoot, options = {}) {
254
266
  windowsHide: true,
255
267
  });
256
268
  if (result.status !== 0 || result.error) {
257
- return [];
269
+ return {
270
+ files: [],
271
+ status: classifyGitLsFilesFailure(result),
272
+ };
258
273
  }
259
- return result.stdout
260
- .split('\0')
261
- .map((line) => toPosixPath(line))
262
- .filter(Boolean);
274
+ return {
275
+ files: result.stdout
276
+ .split('\0')
277
+ .map((line) => toPosixPath(line))
278
+ .filter(Boolean),
279
+ status: 'ok',
280
+ };
281
+ }
282
+ export function listGitFilesForRepoMap(projectRoot, options = {}) {
283
+ return discoverGitFilesForRepoMap(projectRoot, options).files;
263
284
  }
264
285
  function isAnchorCandidatePath(relativePath, priorityPaths) {
265
286
  return priorityPaths.has(relativePath) || Boolean(getAnchorDescription(relativePath));
@@ -290,13 +311,17 @@ function listAnchorCandidateFilesRecursive(rootPath, depth, priorityPaths) {
290
311
  }
291
312
  function getRepositoryFiles(projectRoot, depth, priorityPaths) {
292
313
  const files = new Set();
293
- for (const relativePath of listGitFilesForRepoMap(projectRoot)) {
314
+ const gitFiles = discoverGitFilesForRepoMap(projectRoot);
315
+ for (const relativePath of gitFiles.files) {
294
316
  files.add(relativePath);
295
317
  }
296
318
  for (const relativePath of listAnchorCandidateFilesRecursive(projectRoot, depth, priorityPaths)) {
297
319
  files.add(relativePath);
298
320
  }
299
- return Array.from(files);
321
+ return {
322
+ files: Array.from(files),
323
+ gitLsFilesStatus: gitFiles.status,
324
+ };
300
325
  }
301
326
  function shouldIncludePath(relativePath) {
302
327
  if (GENERATED_FILES.has(relativePath)) {
@@ -341,7 +366,8 @@ function isUnderExcludedPrefix(relativePath, excludedPrefixes) {
341
366
  return excludedPrefixes.some((prefix) => relativePath === prefix.slice(0, -1) || relativePath.startsWith(prefix));
342
367
  }
343
368
  function discoverAnchors(projectRoot, depth, priorityPaths, nestedRepositories, excludedPrefixes) {
344
- return getRepositoryFiles(projectRoot, depth, priorityPaths)
369
+ const repositoryFiles = getRepositoryFiles(projectRoot, depth, priorityPaths);
370
+ const anchors = repositoryFiles.files
345
371
  .filter(shouldIncludePath)
346
372
  .filter((relativePath) => !isUnderNestedRepository(relativePath, nestedRepositories))
347
373
  .filter((relativePath) => !isUnderExcludedPrefix(relativePath, excludedPrefixes))
@@ -352,6 +378,10 @@ function discoverAnchors(projectRoot, depth, priorityPaths, nestedRepositories,
352
378
  .filter((anchor) => Boolean(anchor))
353
379
  .filter((anchor) => priorityPaths.has(anchor.relativePath) || getDirectoryDepth(anchor.relativePath) <= depth)
354
380
  .sort((left, right) => left.relativePath.localeCompare(right.relativePath));
381
+ return {
382
+ anchors,
383
+ gitLsFilesStatus: repositoryFiles.gitLsFilesStatus,
384
+ };
355
385
  }
356
386
  function renderAnchorList(anchors) {
357
387
  return anchors.map((anchor) => `- \`${anchor.relativePath}\`: ${anchor.description}`);
@@ -594,10 +624,11 @@ function countNestedEntrypoints(repository) {
594
624
  ...repository.editingPolicies,
595
625
  ].filter(Boolean).length;
596
626
  }
597
- function getSourceFingerprint(depth, includeNested, configuredPriorityPaths, anchors, nestedRepositories) {
627
+ function getSourceFingerprint(depth, includeNested, configuredPriorityPaths, gitLsFilesStatus, anchors, nestedRepositories) {
598
628
  const payload = {
599
629
  depth,
600
630
  includeNested,
631
+ gitLsFilesStatus,
601
632
  priorityPaths: [...configuredPriorityPaths].sort(),
602
633
  anchors: anchors.map((anchor) => anchor.relativePath).sort(),
603
634
  nestedRepositories: nestedRepositories
@@ -621,7 +652,8 @@ function getSourceFingerprint(depth, includeNested, configuredPriorityPaths, anc
621
652
  const digest = createHash('sha256').update(JSON.stringify(payload)).digest('hex');
622
653
  return `sha256:${digest}`;
623
654
  }
624
- function renderRepoMapFrontmatter(anchorCount, sourceFingerprint) {
655
+ function renderRepoMapFrontmatter(anchorCount, sourceFingerprint, gitLsFilesStatus) {
656
+ const degraded = gitLsFilesStatus !== 'ok';
625
657
  return [
626
658
  '---',
627
659
  `mustflow_doc: ${REPO_MAP_DOC_ID}`,
@@ -631,11 +663,25 @@ function renderRepoMapFrontmatter(anchorCount, sourceFingerprint) {
631
663
  `source_policy: ${REPO_MAP_SOURCE_POLICY}`,
632
664
  `privacy_mode: ${REPO_MAP_PRIVACY_MODE}`,
633
665
  `anchor_count: ${anchorCount}`,
666
+ `degraded: ${degraded ? 'true' : 'false'}`,
667
+ `git_ls_files_status: ${gitLsFilesStatus}`,
634
668
  `source_fingerprint: "${sourceFingerprint}"`,
635
669
  '---',
636
670
  '',
637
671
  ];
638
672
  }
673
+ function renderSourceQuality(gitLsFilesStatus) {
674
+ if (gitLsFilesStatus === 'ok') {
675
+ return [];
676
+ }
677
+ return [
678
+ '## Source Quality',
679
+ '',
680
+ `- \`git ls-files\` status: \`${gitLsFilesStatus}\``,
681
+ '- Anchor discovery used the bounded recursive fallback. Treat this map as incomplete until regenerated after Git file discovery succeeds.',
682
+ '',
683
+ ];
684
+ }
639
685
  export function generateRepoMap(projectRoot, options = {}) {
640
686
  const depth = options.depth ?? DEFAULT_DEPTH;
641
687
  const config = getRepoMapConfig(projectRoot);
@@ -647,15 +693,17 @@ export function generateRepoMap(projectRoot, options = {}) {
647
693
  };
648
694
  const nestedRepositories = discoverNestedRepositories(projectRoot, mapConfig, config.workspace);
649
695
  const workspaceRootPrefixes = getWorkspaceRootPrefixes(projectRoot, config.workspace);
650
- const anchors = discoverAnchors(projectRoot, depth, priorityPathSet, nestedRepositories, workspaceRootPrefixes);
696
+ const anchorDiscovery = discoverAnchors(projectRoot, depth, priorityPathSet, nestedRepositories, workspaceRootPrefixes);
697
+ const anchors = anchorDiscovery.anchors;
698
+ const gitLsFilesStatus = anchorDiscovery.gitLsFilesStatus;
651
699
  const priorityAnchors = configuredPriorityPaths
652
700
  .map((relativePath) => anchors.find((anchor) => anchor.relativePath === relativePath))
653
701
  .filter((anchor) => Boolean(anchor));
654
702
  const otherAnchors = anchors.filter((anchor) => !priorityPathSet.has(anchor.relativePath));
655
703
  const anchorCount = anchors.length + nestedRepositories.reduce((total, repository) => total + countNestedEntrypoints(repository), 0);
656
- const sourceFingerprint = getSourceFingerprint(depth, mapConfig.includeNested, configuredPriorityPaths, anchors, nestedRepositories);
704
+ const sourceFingerprint = getSourceFingerprint(depth, mapConfig.includeNested, configuredPriorityPaths, gitLsFilesStatus, anchors, nestedRepositories);
657
705
  return [
658
- ...renderRepoMapFrontmatter(anchorCount, sourceFingerprint),
706
+ ...renderRepoMapFrontmatter(anchorCount, sourceFingerprint, gitLsFilesStatus),
659
707
  '# REPO_MAP.md',
660
708
  '',
661
709
  'This file is an agent navigation map for the current mustflow root. It is not a full file listing.',
@@ -670,6 +718,7 @@ export function generateRepoMap(projectRoot, options = {}) {
670
718
  : []),
671
719
  '- Use `git ls-files` or your editor when you need the complete file list.',
672
720
  '',
721
+ ...renderSourceQuality(gitLsFilesStatus),
673
722
  '## Priority Anchors',
674
723
  '',
675
724
  ...(priorityAnchors.length > 0 ? renderAnchorList(priorityAnchors) : ['No mustflow priority anchors were found.']),
@@ -0,0 +1,27 @@
1
+ import { MANIFEST_LOCK_RELATIVE_PATH, readManifestLock } from './manifest-lock.js';
2
+ export const ALLOW_UNTRUSTED_ROOT_OPTION = '--allow-untrusted-root';
3
+ export function assessRunRootTrust(projectRoot) {
4
+ const readResult = readManifestLock(projectRoot);
5
+ if (readResult.kind === 'present') {
6
+ return {
7
+ trusted: true,
8
+ reason: 'manifest_lock_present',
9
+ manifestLockPath: readResult.lockPath,
10
+ detail: null,
11
+ };
12
+ }
13
+ if (readResult.kind === 'invalid') {
14
+ return {
15
+ trusted: false,
16
+ reason: 'manifest_lock_invalid',
17
+ manifestLockPath: readResult.lockPath,
18
+ detail: readResult.message,
19
+ };
20
+ }
21
+ return {
22
+ trusted: false,
23
+ reason: 'manifest_lock_missing',
24
+ manifestLockPath: readResult.lockPath,
25
+ detail: MANIFEST_LOCK_RELATIVE_PATH,
26
+ };
27
+ }
@@ -2,6 +2,8 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { parse } from 'smol-toml';
5
+ const DEV_TEMPLATE_ROOT_ENV = 'MUSTFLOW_DEV_TEMPLATE_ROOT';
6
+ const ALLOW_DEV_TEMPLATE_ROOT_ENV = 'MUSTFLOW_ALLOW_DEV_TEMPLATE_ROOT';
5
7
  function readStringGroup(raw, label) {
6
8
  if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
7
9
  throw new Error(`Template manifest is missing table: ${label}`);
@@ -69,15 +71,124 @@ function shouldIncludeTemplatePath(relativePath, selectedSkills) {
69
71
  }
70
72
  return selectedSkills.includes(skillName);
71
73
  }
74
+ const SKILL_INDEX_SKILL_PATH_PATTERN = /`\.mustflow\/skills\/([^/]+)\/SKILL\.md`/u;
75
+ const SKILL_INDEX_HEADING_PATTERN = /^(#{2,3})\s+(.+?)\s*$/u;
76
+ const SKILL_INDEX_ROUTE_CATEGORY_NAMES = [
77
+ 'Bug and Failure',
78
+ 'General Code Change',
79
+ 'Tests and Regression',
80
+ 'Documentation and Release',
81
+ 'Security and Privacy',
82
+ 'Data and External Systems',
83
+ 'UI and Assets',
84
+ 'Architecture Patterns',
85
+ 'Workflow and Contract Maintenance',
86
+ ];
87
+ function skillNameFromSkillIndexLine(line) {
88
+ return SKILL_INDEX_SKILL_PATH_PATTERN.exec(line)?.[1];
89
+ }
90
+ function parseMarkdownTableCells(line) {
91
+ const trimmedLine = line.trim();
92
+ if (!trimmedLine.startsWith('|') || !trimmedLine.endsWith('|')) {
93
+ return undefined;
94
+ }
95
+ return trimmedLine
96
+ .slice(1, -1)
97
+ .split('|')
98
+ .map((cell) => cell.trim());
99
+ }
100
+ function isMarkdownTableDivider(cells) {
101
+ return cells.every((cell) => /^:?-{3,}:?$/u.test(cell));
102
+ }
103
+ function isMarkdownTableHeading(cells) {
104
+ const firstCell = cells[0] ?? '';
105
+ return [
106
+ 'Category',
107
+ 'Classification evidence',
108
+ 'Trigger',
109
+ ].includes(firstCell);
110
+ }
111
+ function referencedSkillIndexCategories(text) {
112
+ return SKILL_INDEX_ROUTE_CATEGORY_NAMES.filter((categoryName) => text.includes(categoryName));
113
+ }
114
+ function collectSelectedSkillIndexCategories(lines, selectedSkillSet) {
115
+ const selectedCategories = new Set();
116
+ let inSpecificRoutes = false;
117
+ let currentCategory;
118
+ for (const line of lines) {
119
+ const heading = SKILL_INDEX_HEADING_PATTERN.exec(line);
120
+ if (heading) {
121
+ const [, level, title] = heading;
122
+ if (level === '##') {
123
+ inSpecificRoutes = title === 'Specific Routes';
124
+ currentCategory = undefined;
125
+ continue;
126
+ }
127
+ if (inSpecificRoutes && level === '###') {
128
+ currentCategory = title;
129
+ }
130
+ }
131
+ const skillName = skillNameFromSkillIndexLine(line);
132
+ if (inSpecificRoutes && currentCategory && skillName && selectedSkillSet.has(skillName)) {
133
+ selectedCategories.add(currentCategory);
134
+ }
135
+ }
136
+ return selectedCategories;
137
+ }
138
+ function shouldKeepSkillIndexTableRow(line, currentSection, selectedCategories) {
139
+ const cells = parseMarkdownTableCells(line);
140
+ if (!cells || isMarkdownTableDivider(cells) || isMarkdownTableHeading(cells)) {
141
+ return true;
142
+ }
143
+ if (currentSection === 'Route Category Gate') {
144
+ const categoryName = cells[0] ?? '';
145
+ return !SKILL_INDEX_ROUTE_CATEGORY_NAMES.includes(categoryName) ||
146
+ selectedCategories.has(categoryName);
147
+ }
148
+ if (currentSection === 'Classification Prefilter') {
149
+ const categoryNames = referencedSkillIndexCategories(cells.join(' '));
150
+ return categoryNames.length === 0 || categoryNames.some((categoryName) => selectedCategories.has(categoryName));
151
+ }
152
+ return true;
153
+ }
72
154
  function filterSkillIndexContent(content, selectedSkills) {
73
155
  const selectedSkillSet = new Set(selectedSkills);
74
- return content
75
- .split(/\r?\n/u)
76
- .filter((line) => {
77
- const match = /`\.mustflow\/skills\/([^/]+)\/SKILL\.md`/u.exec(line);
78
- return !match || selectedSkillSet.has(match[1] ?? '');
79
- })
80
- .join('\n');
156
+ const lines = content.split(/\r?\n/u);
157
+ const selectedCategories = collectSelectedSkillIndexCategories(lines, selectedSkillSet);
158
+ const filteredLines = [];
159
+ let currentSection;
160
+ let skipCurrentSpecificCategory = false;
161
+ for (const line of lines) {
162
+ const heading = SKILL_INDEX_HEADING_PATTERN.exec(line);
163
+ if (heading) {
164
+ const [, level, title] = heading;
165
+ if (level === '##') {
166
+ currentSection = title;
167
+ skipCurrentSpecificCategory = false;
168
+ filteredLines.push(line);
169
+ continue;
170
+ }
171
+ if (currentSection === 'Specific Routes' && level === '###') {
172
+ skipCurrentSpecificCategory = !selectedCategories.has(title);
173
+ if (!skipCurrentSpecificCategory) {
174
+ filteredLines.push(line);
175
+ }
176
+ continue;
177
+ }
178
+ }
179
+ if (skipCurrentSpecificCategory) {
180
+ continue;
181
+ }
182
+ const skillName = skillNameFromSkillIndexLine(line);
183
+ if (skillName && !selectedSkillSet.has(skillName)) {
184
+ continue;
185
+ }
186
+ if (!shouldKeepSkillIndexTableRow(line, currentSection, selectedCategories)) {
187
+ continue;
188
+ }
189
+ filteredLines.push(line);
190
+ }
191
+ return filteredLines.join('\n').replace(/\n{3,}/gu, '\n\n');
81
192
  }
82
193
  function filterSkillRouteMetadataContent(content, selectedSkills) {
83
194
  const selectedSkillSet = new Set(selectedSkills);
@@ -166,7 +277,12 @@ function readManifest(manifestPath) {
166
277
  };
167
278
  }
168
279
  export function getDefaultTemplate() {
169
- const templateRoot = path.resolve(process.env.MUSTFLOW_DEV_TEMPLATE_ROOT ?? fileURLToPath(new URL('../../../templates/default', import.meta.url)));
280
+ const packagedTemplateRoot = fileURLToPath(new URL('../../../templates/default', import.meta.url));
281
+ const templateRootOverride = process.env[DEV_TEMPLATE_ROOT_ENV];
282
+ const selectedTemplateRoot = templateRootOverride && process.env[ALLOW_DEV_TEMPLATE_ROOT_ENV] === '1'
283
+ ? templateRootOverride
284
+ : packagedTemplateRoot;
285
+ const templateRoot = path.resolve(selectedTemplateRoot);
170
286
  const manifestPath = path.join(templateRoot, 'manifest.toml');
171
287
  const manifest = readManifest(manifestPath);
172
288
  return {
@@ -1 +1,6 @@
1
- export { parseTomlText, readTomlFile, stringifyToml } from '../../core/toml.js';
1
+ import { parseTomlText, readTomlFile, stringifyToml } from '../../core/toml.js';
2
+ import { MUSTFLOW_TOML_MAX_BYTES, readMustflowTextFile } from './mustflow-read.js';
3
+ export { parseTomlText, readTomlFile, stringifyToml };
4
+ export function readMustflowTomlFile(projectRoot, relativePath) {
5
+ return parseTomlText(readMustflowTextFile(projectRoot, relativePath, { maxBytes: MUSTFLOW_TOML_MAX_BYTES }));
6
+ }
@@ -198,6 +198,8 @@ export const REPO_MAP_RELATIVE_ROOT = '.';
198
198
  export const REPO_MAP_SOURCE_POLICY = 'anchors_only';
199
199
  export const REPO_MAP_PRIVACY_MODE = 'minimal';
200
200
  export const REPO_MAP_SOURCE_FINGERPRINT_PATTERN = /^sha256:[a-f0-9]{64}$/u;
201
+ export const ALLOWED_REPO_MAP_DEGRADED_VALUES = new Set(['true', 'false']);
202
+ export const ALLOWED_REPO_MAP_GIT_LS_FILES_STATUSES = new Set(['ok', 'timeout', 'max_buffer', 'error']);
201
203
  export const SKILL_RESOURCE_MANIFEST = 'resources.toml';
202
204
  export const SKILL_RESOURCE_ROOTS = new Set(['references', 'assets', 'scripts']);
203
205
  export const ALLOWED_SKILL_RESOURCE_TYPES = new Set(['reference', 'asset', 'script']);