mustflow 2.115.15 → 2.115.17

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.
@@ -2,8 +2,8 @@ import { renderHelp } from '../../lib/cli-output.js';
2
2
  import { t } from '../../lib/i18n.js';
3
3
  import { getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../../lib/option-parser.js';
4
4
  import { ALLOW_UNTRUSTED_ROOT_OPTION } from '../../lib/run-root-trust.js';
5
+ import { APPROVAL_ACTION_TYPE_SET, APPROVAL_ACTION_TYPES } from '../../../core/approval-actions.js';
5
6
  const DEFAULT_ACTIVE_LOCK_WAIT_TIMEOUT_SECONDS = 300;
6
- const SUPPORTED_RUN_APPROVAL_ACTIONS = new Set(['network_access', 'destructive_command']);
7
7
  const RUN_OPTIONS = [
8
8
  { name: '--json', kind: 'boolean' },
9
9
  { name: '--dry-run', kind: 'boolean' },
@@ -17,7 +17,7 @@ export function hasRunHelpOption(args) {
17
17
  return hasCliOptionToken(args, '--help', ['-h']);
18
18
  }
19
19
  export function getSupportedRunApprovalActions() {
20
- return [...SUPPORTED_RUN_APPROVAL_ACTIONS].sort((left, right) => left.localeCompare(right));
20
+ return [...APPROVAL_ACTION_TYPES].sort((left, right) => left.localeCompare(right));
21
21
  }
22
22
  function getAllowApprovalValues(parsed) {
23
23
  const values = parsed.occurrences
@@ -27,7 +27,7 @@ function getAllowApprovalValues(parsed) {
27
27
  return [...new Set(values)];
28
28
  }
29
29
  function findInvalidApprovalAction(values) {
30
- return values.find((value) => !SUPPORTED_RUN_APPROVAL_ACTIONS.has(value)) ?? null;
30
+ return values.find((value) => !APPROVAL_ACTION_TYPE_SET.has(value)) ?? null;
31
31
  }
32
32
  export function parseRunArguments(args) {
33
33
  const parsed = parseCliOptions(args, RUN_OPTIONS, { allowPositionals: true });
@@ -93,7 +93,11 @@ export function getRunHelp(lang = 'en') {
93
93
  { label: ALLOW_UNTRUSTED_ROOT_OPTION, description: t(lang, 'run.help.option.allowUntrustedRoot') },
94
94
  { label: '-h, --help', description: t(lang, 'cli.option.help') },
95
95
  ],
96
- examples: ['mf run test', 'mf run lint --json', 'mf run release_npm_version_available --allow-approval network_access --json'],
96
+ examples: [
97
+ 'mf run test',
98
+ 'mf run lint --json',
99
+ 'mf run release_npm_publish --allow-approval release --allow-approval network_access --json',
100
+ ],
97
101
  exitCodes: [
98
102
  {
99
103
  label: '0',
@@ -72,6 +72,7 @@ function reportRunPlanFailure(plan, reporter, lang) {
72
72
  break;
73
73
  case 'network_requires_approval':
74
74
  case 'destructive_requires_approval':
75
+ case 'explicit_approval_required':
75
76
  case 'approval_policy_unreadable':
76
77
  message = t(lang, 'run.error.approvalRequired', {
77
78
  intent: plan.intentName,
@@ -10,7 +10,10 @@ export function executeRunPreviewCommand(input, reporter, lang, options) {
10
10
  const profiler = new RunProfiler();
11
11
  const projectRoot = profiler.measure('root_detection', () => resolveMustflowRoot());
12
12
  const contract = profiler.measure('command_contract', () => readCommandContract(projectRoot));
13
- const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, input.intentName, { testTargets: options.testTargets }));
13
+ const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, input.intentName, {
14
+ testTargets: options.testTargets,
15
+ approvedActions: input.allowApprovals,
16
+ }));
14
17
  profiler.measure('preview_render', () => {
15
18
  if (input.json) {
16
19
  reporter.stdout(JSON.stringify(createRunPreview(plan, input.previewMode), null, 2));
@@ -72,5 +72,5 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
72
72
  }, reporter, lang, options);
73
73
  return result.exitCode;
74
74
  }
75
- return executeRunPreviewCommand({ intentName, json, previewMode }, reporter, lang, options);
75
+ return executeRunPreviewCommand({ intentName, json, previewMode, allowApprovals: parsedArgs.allowApprovals }, reporter, lang, options);
76
76
  }
@@ -8,6 +8,7 @@ import { inspectActiveRunLocks, } from '../../core/active-run-locks.js';
8
8
  import { isRecord, readPositiveInteger, readString, readStringArray, } from '../../core/config-loading.js';
9
9
  import { DEFAULT_COMMAND_MAX_OUTPUT_BYTES, COMMAND_OUTPUT_LIMIT_SCOPE, } from '../../core/command-output-limits.js';
10
10
  import { checkRepoApprovalGate } from '../../core/repo-approval-gate.js';
11
+ import { inferCommandApprovalActions } from '../../core/approval-actions.js';
11
12
  import { normalizeSuccessExitCodes } from '../../core/success-exit-codes.js';
12
13
  import { normalizeSafeTestTargetPath, TEST_TARGET_PATH_ERROR } from '../../core/test-target-paths.js';
13
14
  import { evaluateCommandPreconditions, } from '../../core/command-preconditions.js';
@@ -123,6 +124,7 @@ function readRunIntentMetadata(contract, intent) {
123
124
  effects: readArray(intent, 'effects'),
124
125
  network: readBoolean(intent, 'network'),
125
126
  destructive: readBoolean(intent, 'destructive'),
127
+ approvalActions: readStringArray(intent, 'approval_actions') ?? [],
126
128
  envPolicy: env.policy,
127
129
  envAllowlist: env.allowlist,
128
130
  testTargets: [],
@@ -133,17 +135,21 @@ function readRunIntentMetadata(contract, intent) {
133
135
  };
134
136
  }
135
137
  function createApprovalBlock(projectRoot, metadata, approvedActions) {
136
- const actionTypes = [];
138
+ const actionTypes = [
139
+ ...metadata.approvalActions,
140
+ ...inferCommandApprovalActions(metadata.commandArgv ?? []),
141
+ ];
137
142
  if (metadata.network === true) {
138
143
  actionTypes.push('network_access');
139
144
  }
140
145
  if (metadata.destructive === true) {
141
146
  actionTypes.push('destructive_command');
142
147
  }
143
- if (actionTypes.length === 0) {
148
+ const uniqueActionTypes = [...new Set(actionTypes)];
149
+ if (uniqueActionTypes.length === 0) {
144
150
  return null;
145
151
  }
146
- const approvalReport = checkRepoApprovalGate(projectRoot, actionTypes);
152
+ const approvalReport = checkRepoApprovalGate(projectRoot, uniqueActionTypes);
147
153
  if (approvalReport.issues.length > 0) {
148
154
  return {
149
155
  reasonCode: 'approval_policy_unreadable',
@@ -159,7 +165,9 @@ function createApprovalBlock(projectRoot, metadata, approvedActions) {
159
165
  }
160
166
  const reasonCode = missingActions.includes('destructive_command')
161
167
  ? 'destructive_requires_approval'
162
- : 'network_requires_approval';
168
+ : missingActions.includes('network_access')
169
+ ? 'network_requires_approval'
170
+ : 'explicit_approval_required';
163
171
  return {
164
172
  reasonCode,
165
173
  detail: `Action ${missingActions.map((action) => JSON.stringify(action)).join(', ')} requires explicit approval before mf run can execute this intent.`,
@@ -193,6 +201,7 @@ function createBlockedRunPlan(contract, intentName, intent, eligibility, reasonC
193
201
  effects: metadata?.effects,
194
202
  network: metadata?.network,
195
203
  destructive: metadata?.destructive,
204
+ approvalActions: metadata?.approvalActions ?? [],
196
205
  envPolicy: metadata?.envPolicy ?? null,
197
206
  envAllowlist: metadata?.envAllowlist ?? [],
198
207
  testTargets: [],
@@ -277,6 +286,7 @@ export function createRunPlan(projectRoot, contract, intentName, options = {}) {
277
286
  effects: metadata.effects,
278
287
  network: metadata.network,
279
288
  destructive: metadata.destructive,
289
+ approvalActions: metadata.approvalActions,
280
290
  envPolicy: metadata.envPolicy,
281
291
  envAllowlist: metadata.envAllowlist,
282
292
  testTargets,
@@ -360,6 +370,7 @@ export function createRunPreview(plan, previewMode) {
360
370
  effects: plan.effects,
361
371
  network: plan.network,
362
372
  destructive: plan.destructive,
373
+ approval_actions: plan.approvalActions,
363
374
  env_policy: plan.envPolicy,
364
375
  env_allowlist: plan.envAllowlist,
365
376
  test_targets: plan.testTargets,
@@ -72,6 +72,9 @@ function shouldIncludeTemplatePath(relativePath, selectedSkills) {
72
72
  return selectedSkills.includes(skillName);
73
73
  }
74
74
  const SKILL_INDEX_SKILL_PATH_PATTERN = /`\.mustflow\/skills\/([^/]+)\/SKILL\.md`/u;
75
+ const SKILL_NAME_REFERENCE_PATTERN = /`([a-z][a-z0-9-]+)`/gu;
76
+ const SKILL_PATH_REFERENCE_PATTERN = /`\.mustflow\/skills\/([^/]+)\/SKILL\.md`/gu;
77
+ const UNAVAILABLE_SKILL_REFERENCE_FALLBACK = 'the closest installed route for this scope';
75
78
  const SKILL_INDEX_HEADING_PATTERN = /^(#{2,3})\s+(.+?)\s*$/u;
76
79
  const SKILL_INDEX_ROUTE_CATEGORY_NAMES = [
77
80
  'Bug and Failure',
@@ -190,6 +193,16 @@ function filterSkillIndexContent(content, selectedSkills) {
190
193
  }
191
194
  return filteredLines.join('\n').replace(/\n{3,}/gu, '\n\n');
192
195
  }
196
+ function filterUnavailableSkillReferences(content, selectedSkills, knownSkills) {
197
+ const selectedSkillSet = new Set(selectedSkills);
198
+ const knownSkillSet = new Set(knownSkills);
199
+ const replaceUnavailableReference = (reference, skillName) => knownSkillSet.has(skillName) && !selectedSkillSet.has(skillName)
200
+ ? UNAVAILABLE_SKILL_REFERENCE_FALLBACK
201
+ : reference;
202
+ return content
203
+ .replace(SKILL_PATH_REFERENCE_PATTERN, replaceUnavailableReference)
204
+ .replace(SKILL_NAME_REFERENCE_PATTERN, replaceUnavailableReference);
205
+ }
193
206
  function filterSkillRouteMetadataContent(content, selectedSkills) {
194
207
  const selectedSkillSet = new Set(selectedSkills);
195
208
  let keepCurrentRoute = true;
@@ -298,6 +311,7 @@ export function getTemplateFiles(template, locale = template.manifest.defaultLoc
298
311
  ? path.join(template.templateRoot, template.manifest.localesRoot, template.manifest.defaultLocale)
299
312
  : undefined;
300
313
  const selectedSkills = selectedSkillNames(template.manifest, profile, options);
314
+ const knownSkills = templateSkillNames(template.manifest.creates);
301
315
  return template.manifest.creates.filter((relativePath) => shouldIncludeTemplatePath(relativePath, selectedSkills)).map((relativePath) => {
302
316
  const localePath = localeRoot ? path.join(localeRoot, ...relativePath.split('/')) : undefined;
303
317
  const sourceLocalePath = sourceLocaleRoot ? path.join(sourceLocaleRoot, ...relativePath.split('/')) : undefined;
@@ -306,11 +320,16 @@ export function getTemplateFiles(template, locale = template.manifest.defaultLoc
306
320
  const fallbackLocalePath = sourceLocalePath && existsSync(sourceLocalePath) ? sourceLocalePath : undefined;
307
321
  const commonSourcePath = existsSync(commonPath) ? commonPath : undefined;
308
322
  const selectedSourcePath = localizedPath ?? fallbackLocalePath ?? commonSourcePath;
309
- const content = selectedSourcePath && relativePath === '.mustflow/skills/INDEX.md'
323
+ const selectedContent = selectedSourcePath && relativePath === '.mustflow/skills/INDEX.md'
310
324
  ? filterSkillIndexContent(readFileSync(selectedSourcePath, 'utf8'), selectedSkills)
311
325
  : selectedSourcePath && relativePath === '.mustflow/skills/routes.toml'
312
326
  ? filterSkillRouteMetadataContent(readFileSync(selectedSourcePath, 'utf8'), selectedSkills)
313
327
  : undefined;
328
+ const content = selectedSourcePath && relativePath === '.mustflow/skills/INDEX.md'
329
+ ? filterUnavailableSkillReferences(selectedContent ?? '', selectedSkills, knownSkills)
330
+ : selectedSourcePath && /^\.mustflow\/skills\/[^/]+\/SKILL\.md$/u.test(relativePath)
331
+ ? filterUnavailableSkillReferences(readFileSync(selectedSourcePath, 'utf8'), selectedSkills, knownSkills)
332
+ : selectedContent;
314
333
  if (localizedPath) {
315
334
  return {
316
335
  relativePath,
@@ -130,18 +130,7 @@ export const ALLOWED_PROMPT_CACHE_STRATEGIES = new Set(['stable_prefix']);
130
130
  export const ALLOWED_PROMPT_CACHE_STABLE_PREFIX_POLICIES = new Set(['hash_verified']);
131
131
  export const ALLOWED_PROMPT_CACHE_TASK_READ_POLICIES = new Set(['task_relevant_only']);
132
132
  export const ALLOWED_BUDGET_LIMIT_ACTIONS = new Set(['stop_and_handoff', 'stop_and_report']);
133
- export const ALLOWED_APPROVAL_GATES = new Set([
134
- 'git_commit',
135
- 'git_push',
136
- 'dependency_install',
137
- 'dependency_upgrade',
138
- 'network_access',
139
- 'database_migration',
140
- 'destructive_command',
141
- 'secret_access',
142
- 'release',
143
- 'cross_repository_change',
144
- ]);
133
+ export { APPROVAL_ACTION_TYPE_SET as ALLOWED_APPROVAL_GATES } from '../../../core/approval-actions.js';
145
134
  export const ALLOWED_APPROVAL_ACTIONS = new Set(['stop_and_request_approval']);
146
135
  export const ALLOWED_ISOLATION_PREFERENCES = new Set(['none', 'git_worktree', 'sandbox']);
147
136
  export const ALLOWED_REFRESH_MODES = new Set(['checkpoint']);
@@ -1066,7 +1066,7 @@ function readSkillRouteMetadataFromTomlContent(content, sourceLabel, issues) {
1066
1066
  }
1067
1067
  return metadata;
1068
1068
  }
1069
- function validateTemplateProfileSkillRoutes(profile, files, issues) {
1069
+ function validateTemplateProfileSkillRoutes(profile, files, knownSkillNames, issues) {
1070
1070
  const contentByPath = new Map(files.map((file) => [file.relativePath, templateFileContent(file)]));
1071
1071
  const indexContent = contentByPath.get(SKILL_INDEX_PATH);
1072
1072
  const routesContent = contentByPath.get(SKILL_ROUTES_METADATA_PATH);
@@ -1104,6 +1104,17 @@ function validateTemplateProfileSkillRoutes(profile, files, issues) {
1104
1104
  selectedSkillContents.set(skillName, content);
1105
1105
  }
1106
1106
  }
1107
+ for (const [relativePath, content] of contentByPath.entries()) {
1108
+ if (relativePath !== SKILL_INDEX_PATH && !skillRouteName(relativePath)) {
1109
+ continue;
1110
+ }
1111
+ for (const match of content.matchAll(/`([a-z][a-z0-9-]+)`/gu)) {
1112
+ const referencedSkill = match[1];
1113
+ if (referencedSkill && knownSkillNames.has(referencedSkill) && !selectedSkillContents.has(referencedSkill)) {
1114
+ pushStrictIssue(issues, `template profile "${profile}" ${relativePath} references skill "${referencedSkill}" not installed by that profile`);
1115
+ }
1116
+ }
1117
+ }
1107
1118
  for (const route of skillRoutes) {
1108
1119
  const routeSkillName = skillRouteName(route.skillPath);
1109
1120
  if (!routeSkillName) {
@@ -1178,8 +1189,11 @@ function validateStrictTemplateSkillProfiles(issues) {
1178
1189
  pushStrictIssue(issues, `default template skill profiles could not be loaded: ${message}`);
1179
1190
  return;
1180
1191
  }
1192
+ const knownSkillNames = new Set(template.manifest.creates
1193
+ .map((relativePath) => skillRouteName(relativePath))
1194
+ .filter((skillName) => Boolean(skillName)));
1181
1195
  for (const profile of template.manifest.profiles) {
1182
- validateTemplateProfileSkillRoutes(profile, getTemplateFiles(template, template.manifest.defaultLocale, profile), issues);
1196
+ validateTemplateProfileSkillRoutes(profile, getTemplateFiles(template, template.manifest.defaultLocale, profile), knownSkillNames, issues);
1183
1197
  }
1184
1198
  }
1185
1199
  function listManagedMarkdownDocuments(projectRoot) {
@@ -0,0 +1,41 @@
1
+ export const APPROVAL_ACTION_TYPES = [
2
+ 'git_commit',
3
+ 'git_push',
4
+ 'dependency_install',
5
+ 'dependency_upgrade',
6
+ 'network_access',
7
+ 'database_migration',
8
+ 'destructive_command',
9
+ 'secret_access',
10
+ 'release',
11
+ 'cross_repository_change',
12
+ ];
13
+ export const APPROVAL_ACTION_TYPE_SET = new Set(APPROVAL_ACTION_TYPES);
14
+ function readGitSubcommand(argv) {
15
+ if ((argv[0] ?? '').split(/[\\/]/u).at(-1)?.replace(/\.(?:cmd|exe|ps1)$/iu, '').toLowerCase() !== 'git') {
16
+ return null;
17
+ }
18
+ const optionsWithSeparateValue = new Set(['-C', '-c', '--exec-path', '--git-dir', '--work-tree', '--namespace']);
19
+ for (let index = 1; index < argv.length; index += 1) {
20
+ const argument = argv[index] ?? '';
21
+ if (optionsWithSeparateValue.has(argument)) {
22
+ index += 1;
23
+ continue;
24
+ }
25
+ if (argument.startsWith('-')) {
26
+ continue;
27
+ }
28
+ return argument;
29
+ }
30
+ return null;
31
+ }
32
+ export function inferCommandApprovalActions(argv) {
33
+ const gitSubcommand = readGitSubcommand(argv);
34
+ if (gitSubcommand === 'add' || gitSubcommand === 'commit') {
35
+ return ['git_commit'];
36
+ }
37
+ if (gitSubcommand === 'push') {
38
+ return ['git_push'];
39
+ }
40
+ return [];
41
+ }
@@ -6,6 +6,7 @@ import { COMMAND_PRECONDITION_KINDS } from './command-preconditions.js';
6
6
  import { commandIntentBlockedCommandPattern, commandIntentHasCommandSource, commandIntentNameIsSafe, } from './command-contract-rules.js';
7
7
  import { MAX_COMMAND_OUTPUT_BYTES, commandMaxOutputBytesLimitMessage } from './command-output-limits.js';
8
8
  import { SUCCESS_EXIT_CODES_CONTRACT_DESCRIPTION, successExitCodesAreValid } from './success-exit-codes.js';
9
+ import { APPROVAL_ACTION_TYPE_SET } from './approval-actions.js';
9
10
  const COMMAND_INPUT_TYPES = new Set(['path', 'enum', 'boolean', 'integer', 'literal']);
10
11
  const COMMAND_INPUT_NAME_PATTERN = /^[a-z][a-z0-9_]*$/u;
11
12
  const COMMAND_ARGV_PLACEHOLDER_PATTERN = /^\{([a-z][a-z0-9_]*)\}$/u;
@@ -59,6 +60,17 @@ function validateStringArrayField(table, key, label, issues) {
59
60
  issues.push(commandContractIssue(`${label} must be a string array`));
60
61
  }
61
62
  }
63
+ function validateApprovalActions(intentName, intent, issues) {
64
+ validateStringArrayField(intent, 'approval_actions', `[commands.intents.${intentName}].approval_actions`, issues);
65
+ const approvalActions = readStringArray(intent, 'approval_actions');
66
+ if (!approvalActions) {
67
+ return;
68
+ }
69
+ const unsupported = [...new Set(approvalActions.filter((action) => !APPROVAL_ACTION_TYPE_SET.has(action)))];
70
+ if (unsupported.length > 0) {
71
+ issues.push(commandContractIssue(`[commands.intents.${intentName}].approval_actions contains unsupported action types: ${unsupported.join(', ')}`));
72
+ }
73
+ }
62
74
  function validatePositiveIntegerField(table, key, label, issues) {
63
75
  if (hasOwn(table, key) && !isPositiveInteger(table[key])) {
64
76
  issues.push(commandContractIssue(`${label} must be a positive integer`));
@@ -339,6 +351,7 @@ function validateCommandIntent(intentName, intent, allIntents, issues) {
339
351
  validateAllowedStringField(intent, 'env_policy', `[commands.intents.${intentName}].env_policy`, COMMAND_ENV_POLICIES, issues);
340
352
  validateBooleanField(intent, 'allow_shell', `[commands.intents.${intentName}].allow_shell`, issues);
341
353
  validateStringArrayField(intent, 'env_allowlist', `[commands.intents.${intentName}].env_allowlist`, issues);
354
+ validateApprovalActions(intentName, intent, issues);
342
355
  validateBooleanField(intent, ALLOW_ENV_INHERITANCE_RISKS_KEY, `[commands.intents.${intentName}].${ALLOW_ENV_INHERITANCE_RISKS_KEY}`, issues);
343
356
  validateBooleanField(intent, ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY, `[commands.intents.${intentName}].${ALLOW_LONG_RUNNING_COMMAND_PATTERNS_KEY}`, issues);
344
357
  validateMaxOutputBytesField(intent, 'max_output_bytes', `[commands.intents.${intentName}].max_output_bytes`, issues);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.115.15",
3
+ "version": "2.115.17",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
@@ -210,6 +210,24 @@
210
210
  },
211
211
  "network": { "type": "boolean" },
212
212
  "destructive": { "type": "boolean" },
213
+ "approval_actions": {
214
+ "type": "array",
215
+ "uniqueItems": true,
216
+ "items": {
217
+ "enum": [
218
+ "git_commit",
219
+ "git_push",
220
+ "dependency_install",
221
+ "dependency_upgrade",
222
+ "network_access",
223
+ "database_migration",
224
+ "destructive_command",
225
+ "secret_access",
226
+ "release",
227
+ "cross_repository_change"
228
+ ]
229
+ }
230
+ },
213
231
  "required_after": {
214
232
  "type": "array",
215
233
  "items": { "type": "string" }
@@ -379,6 +379,14 @@ required_after = ["before_final_report", "commit_message_suggestion"]
379
379
 
380
380
  [intents.git_commit]
381
381
  status = "manual_only"
382
- description = "Create a Git commit."
383
- reason = "Creating a commit requires explicit user approval."
384
- agent_action = "do_not_commit_report_suggestion_only"
382
+ description = "Create an unscoped Git commit."
383
+ reason = "A generic commit cannot safely choose reviewed paths or a commit message."
384
+ agent_action = "author_bounded_repo_specific_stage_and_commit_intents_after_explicit_user_request"
385
+ approval_actions = ["git_commit"]
386
+
387
+ [intents.git_push]
388
+ status = "manual_only"
389
+ description = "Push an unspecified Git ref."
390
+ reason = "A generic push cannot safely choose the remote and ref."
391
+ agent_action = "author_bounded_repo_specific_push_intent_after_explicit_user_request"
392
+ approval_actions = ["git_push"]
@@ -1,6 +1,6 @@
1
1
  id = "default"
2
2
  name = "default"
3
- version = "2.115.15"
3
+ version = "2.115.17"
4
4
  description = "Minimal workflow for LLM agents to read, edit, and verify their work in a repository."
5
5
  common_root = "common"
6
6
  locales_root = "locales"