claude-code-modes 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +304 -0
  3. package/package.json +35 -0
  4. package/prompts/axis/agency/autonomous.md +9 -0
  5. package/prompts/axis/agency/collaborative.md +9 -0
  6. package/prompts/axis/agency/surgical.md +9 -0
  7. package/prompts/axis/quality/architect.md +24 -0
  8. package/prompts/axis/quality/minimal.md +19 -0
  9. package/prompts/axis/quality/pragmatic.md +21 -0
  10. package/prompts/axis/scope/adjacent.md +9 -0
  11. package/prompts/axis/scope/narrow.md +9 -0
  12. package/prompts/axis/scope/unrestricted.md +8 -0
  13. package/prompts/base/actions.md +9 -0
  14. package/prompts/base/base.json +12 -0
  15. package/prompts/base/doing-tasks.md +12 -0
  16. package/prompts/base/env.md +16 -0
  17. package/prompts/base/intro.md +5 -0
  18. package/prompts/base/session-guidance.md +7 -0
  19. package/prompts/base/system.md +7 -0
  20. package/prompts/base/tone.md +5 -0
  21. package/prompts/base/tools.md +10 -0
  22. package/prompts/chill/actions.md +16 -0
  23. package/prompts/chill/base.json +8 -0
  24. package/prompts/chill/core.md +60 -0
  25. package/prompts/chill/env.md +15 -0
  26. package/prompts/chill/tools.md +12 -0
  27. package/prompts/modifiers/context-pacing.md +14 -0
  28. package/prompts/modifiers/readonly.md +10 -0
  29. package/src/args.ts +118 -0
  30. package/src/assemble.ts +187 -0
  31. package/src/build-prompt.ts +119 -0
  32. package/src/cli.ts +132 -0
  33. package/src/config-cli.ts +320 -0
  34. package/src/config.ts +300 -0
  35. package/src/embedded-prompts.ts +366 -0
  36. package/src/env.ts +64 -0
  37. package/src/inspect.ts +315 -0
  38. package/src/presets.ts +39 -0
  39. package/src/resolve.ts +278 -0
  40. package/src/types.ts +99 -0
  41. package/src/usage.ts +51 -0
package/src/inspect.ts ADDED
@@ -0,0 +1,315 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ import { parseCliArgs } from "./args.js";
4
+ import { loadConfig, resolveConfigPath, type LoadedConfig } from "./config.js";
5
+ import { resolveConfig } from "./resolve.js";
6
+ import { getFragmentOrder } from "./assemble.js";
7
+ import { detectEnv, buildTemplateVars } from "./env.js";
8
+ import type { TemplateVars } from "./types.js";
9
+
10
+ const PROVENANCE_VALUES = ["built-in", "config-defined", "cli-path"] as const;
11
+ type Provenance = (typeof PROVENANCE_VALUES)[number];
12
+
13
+ const WARNING_KINDS = ["outside-project", "non-md", "missing-file", "suspicious-path"] as const;
14
+ type WarningKind = (typeof WARNING_KINDS)[number];
15
+
16
+ interface FragmentEntry {
17
+ index: number;
18
+ path: string;
19
+ resolvedPath: string;
20
+ provenance: Provenance;
21
+ }
22
+
23
+ interface Warning {
24
+ fragmentIndex: number;
25
+ fragmentPath: string;
26
+ kind: WarningKind;
27
+ detail: string;
28
+ }
29
+
30
+ interface ConfigSource {
31
+ loaded: boolean;
32
+ path: string | null;
33
+ scope: "project" | "global" | null;
34
+ }
35
+
36
+ interface InspectResult {
37
+ baseName: string;
38
+ configSource: ConfigSource;
39
+ fragments: FragmentEntry[];
40
+ fragmentContents: Map<number, string | null>;
41
+ warnings: Warning[];
42
+ templateVars: TemplateVars;
43
+ verbose: boolean;
44
+ }
45
+
46
+ const SUSPICIOUS_PATTERN = /\.(ssh|env|gnupg|aws|kube|docker|pgpass)|\.npmrc|\.netrc|id_rsa|id_ed25519|id_ecdsa|credentials|secret|token|password|private/i;
47
+
48
+ /** Builds a Set of all absolute paths that config definitions could resolve to. */
49
+ function collectConfigDefinedPaths(loadedConfig: LoadedConfig | null): Set<string> {
50
+ const paths = new Set<string>();
51
+ if (!loadedConfig) return paths;
52
+
53
+ const { config, configDir } = loadedConfig;
54
+
55
+ // Modifier definitions
56
+ if (config.modifiers) {
57
+ for (const val of Object.values(config.modifiers)) {
58
+ paths.add(resolveConfigPath(configDir, val));
59
+ }
60
+ }
61
+
62
+ // Axis definitions
63
+ if (config.axes) {
64
+ for (const axisName of ["agency", "quality", "scope"] as const) {
65
+ const axisMap = config.axes[axisName];
66
+ if (axisMap) {
67
+ for (const val of Object.values(axisMap)) {
68
+ paths.add(resolveConfigPath(configDir, val));
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ // Preset modifier lists
75
+ if (config.presets) {
76
+ for (const preset of Object.values(config.presets)) {
77
+ if (preset.modifiers) {
78
+ for (const mod of preset.modifiers) {
79
+ // Modifier names resolve through config.modifiers; raw paths resolve against configDir
80
+ if (config.modifiers && mod in config.modifiers) {
81
+ paths.add(resolveConfigPath(configDir, config.modifiers[mod]));
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ // defaultModifiers
89
+ if (config.defaultModifiers) {
90
+ for (const mod of config.defaultModifiers) {
91
+ if (config.modifiers && mod in config.modifiers) {
92
+ paths.add(resolveConfigPath(configDir, config.modifiers[mod]));
93
+ }
94
+ }
95
+ }
96
+
97
+ return paths;
98
+ }
99
+
100
+ /** Classifies a single fragment path as built-in, config-defined, or cli-path. */
101
+ function classifyProvenance(
102
+ fragPath: string,
103
+ configDefinedPaths: Set<string>,
104
+ ): Provenance {
105
+ if (!isAbsolute(fragPath)) return "built-in";
106
+ if (configDefinedPaths.has(fragPath)) return "config-defined";
107
+ return "cli-path";
108
+ }
109
+
110
+ /** Detects warnings for the fragment manifest. */
111
+ function detectWarnings(
112
+ fragments: FragmentEntry[],
113
+ promptsDir: string,
114
+ ): Warning[] {
115
+ const warnings: Warning[] = [];
116
+ const cwd = process.cwd();
117
+ const resolvedPromptsDir = resolve(promptsDir);
118
+
119
+ for (const frag of fragments) {
120
+ if (frag.provenance === "built-in") {
121
+ // Only check existence for built-in fragments
122
+ if (!existsSync(frag.resolvedPath)) {
123
+ warnings.push({
124
+ fragmentIndex: frag.index,
125
+ fragmentPath: frag.path,
126
+ kind: "missing-file",
127
+ detail: `file not found: ${frag.resolvedPath}`,
128
+ });
129
+ }
130
+ continue;
131
+ }
132
+
133
+ // Custom fragments: check all warning types
134
+ // Use cwd + "/" to prevent prefix attacks (e.g. /project-evil matching /project)
135
+ const inProject = frag.resolvedPath === cwd || frag.resolvedPath.startsWith(cwd + "/");
136
+ const inPrompts = frag.resolvedPath === resolvedPromptsDir || frag.resolvedPath.startsWith(resolvedPromptsDir + "/");
137
+ if (!inProject && !inPrompts) {
138
+ warnings.push({
139
+ fragmentIndex: frag.index,
140
+ fragmentPath: frag.resolvedPath,
141
+ kind: "outside-project",
142
+ detail: `resolves outside project directory`,
143
+ });
144
+ }
145
+
146
+ if (!frag.resolvedPath.endsWith(".md")) {
147
+ warnings.push({
148
+ fragmentIndex: frag.index,
149
+ fragmentPath: frag.resolvedPath,
150
+ kind: "non-md",
151
+ detail: `non-.md extension`,
152
+ });
153
+ }
154
+
155
+ if (!existsSync(frag.resolvedPath)) {
156
+ warnings.push({
157
+ fragmentIndex: frag.index,
158
+ fragmentPath: frag.resolvedPath,
159
+ kind: "missing-file",
160
+ detail: `file not found`,
161
+ });
162
+ }
163
+
164
+ if (SUSPICIOUS_PATTERN.test(frag.resolvedPath)) {
165
+ warnings.push({
166
+ fragmentIndex: frag.index,
167
+ fragmentPath: frag.resolvedPath,
168
+ kind: "suspicious-path",
169
+ detail: `references potentially sensitive path`,
170
+ });
171
+ }
172
+ }
173
+
174
+ return warnings;
175
+ }
176
+
177
+ /** Determines config source info from LoadedConfig. */
178
+ function describeConfigSource(loadedConfig: LoadedConfig | null): ConfigSource {
179
+ if (!loadedConfig) return { loaded: false, path: null, scope: null };
180
+
181
+ const { configDir } = loadedConfig;
182
+ const cwd = process.cwd();
183
+
184
+ if (configDir === cwd) {
185
+ return { loaded: true, path: resolve(configDir, ".claude-mode.json"), scope: "project" };
186
+ }
187
+ return { loaded: true, path: resolve(configDir, "config.json"), scope: "global" };
188
+ }
189
+
190
+ /** Formats an InspectResult as a human-readable string. */
191
+ function formatInspectOutput(result: InspectResult): string {
192
+ const lines: string[] = [];
193
+
194
+ // Warning banner — top of output when warnings exist
195
+ if (result.warnings.length > 0) {
196
+ const count = result.warnings.length;
197
+ lines.push(`!! ${count} warning${count > 1 ? "s" : ""} found -- review before running`);
198
+ lines.push("");
199
+ }
200
+
201
+ // Config section
202
+ lines.push("=== Config ===");
203
+ if (!result.configSource.loaded) {
204
+ lines.push("No config file found.");
205
+ } else {
206
+ const label = result.configSource.scope === "project"
207
+ ? ".claude-mode.json (project)"
208
+ : "config.json (global)";
209
+ lines.push(`Loaded: ${label}`);
210
+ lines.push(` Path: ${result.configSource.path}`);
211
+ }
212
+ lines.push("");
213
+
214
+ // Base section
215
+ lines.push("=== Base ===");
216
+ lines.push(`Active: ${result.baseName}`);
217
+ lines.push("");
218
+
219
+ // Warnings section — before fragments so issues surface early
220
+ lines.push("=== Warnings ===");
221
+ if (result.warnings.length === 0) {
222
+ lines.push("(none)");
223
+ } else {
224
+ for (const w of result.warnings) {
225
+ lines.push(`[!] #${w.fragmentIndex} ${w.detail}: ${w.fragmentPath}`);
226
+ }
227
+ }
228
+ lines.push("");
229
+
230
+ // Fragments section
231
+ lines.push("=== Fragments ===");
232
+ if (result.verbose) {
233
+ for (const frag of result.fragments) {
234
+ const displayPath = frag.provenance === "built-in" ? frag.path : frag.resolvedPath;
235
+ lines.push("");
236
+ lines.push(`--- #${frag.index} [${frag.provenance}] ${displayPath} ---`);
237
+ const content = result.fragmentContents.get(frag.index);
238
+ if (content === null || content === undefined) {
239
+ lines.push("(file not found)");
240
+ } else {
241
+ lines.push(content.trim());
242
+ }
243
+ }
244
+ } else {
245
+ const indexWidth = String(result.fragments.length).length;
246
+ lines.push(`${" ".repeat(indexWidth + 1)} Provenance Path`);
247
+ for (const frag of result.fragments) {
248
+ const idx = String(frag.index).padStart(indexWidth);
249
+ const prov = frag.provenance.padEnd(16);
250
+ const displayPath = frag.provenance === "built-in" ? frag.path : frag.resolvedPath;
251
+ lines.push(`${idx} ${prov} ${displayPath}`);
252
+ }
253
+ }
254
+ lines.push("");
255
+
256
+ // Template variables section
257
+ lines.push("=== Template Variables ===");
258
+ const maxKeyLen = Math.max(...Object.keys(result.templateVars).map((k) => k.length));
259
+ for (const [key, value] of Object.entries(result.templateVars)) {
260
+ const displayValue = value.includes("\n") ? "(multiline)" : value;
261
+ lines.push(`${key.padEnd(maxKeyLen)} = ${displayValue}`);
262
+ }
263
+
264
+ return lines.join("\n") + "\n";
265
+ }
266
+
267
+ /** Reads a fragment file, returning its content or null if missing. */
268
+ function readFragmentContent(resolvedPath: string): string | null {
269
+ try {
270
+ return readFileSync(resolvedPath, "utf8");
271
+ } catch {
272
+ return null;
273
+ }
274
+ }
275
+
276
+ /** Runs the inspect subcommand. Accepts argv after "inspect" has been stripped. */
277
+ export function runInspectCommand(argv: string[], promptsDir: string): void {
278
+ const verbose = argv.includes("--print");
279
+ const filteredArgv = argv.filter((a) => a !== "--print");
280
+
281
+ const parsed = parseCliArgs(filteredArgv);
282
+ const loadedConfig = loadConfig();
283
+ const config = resolveConfig(parsed, loadedConfig);
284
+
285
+ const env = detectEnv();
286
+ const templateVars = buildTemplateVars(env);
287
+
288
+ const fragmentPaths = getFragmentOrder(config, promptsDir);
289
+ const configDefinedPaths = collectConfigDefinedPaths(loadedConfig);
290
+
291
+ const fragments: FragmentEntry[] = fragmentPaths.map((fragPath, i) => {
292
+ const resolvedPath = isAbsolute(fragPath)
293
+ ? fragPath
294
+ : resolve(promptsDir, fragPath);
295
+ return {
296
+ index: i + 1,
297
+ path: fragPath,
298
+ resolvedPath,
299
+ provenance: classifyProvenance(fragPath, configDefinedPaths),
300
+ };
301
+ });
302
+
303
+ const fragmentContents = new Map<number, string | null>();
304
+ if (verbose) {
305
+ for (const frag of fragments) {
306
+ fragmentContents.set(frag.index, readFragmentContent(frag.resolvedPath));
307
+ }
308
+ }
309
+
310
+ const warnings = detectWarnings(fragments, promptsDir);
311
+ const configSource = describeConfigSource(loadedConfig);
312
+
313
+ const result: InspectResult = { baseName: config.base, configSource, fragments, fragmentContents, warnings, templateVars, verbose };
314
+ process.stdout.write(formatInspectOutput(result));
315
+ }
package/src/presets.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { AxisConfig, PresetName } from "./types.js";
2
+ export { isPresetName } from "./types.js";
3
+
4
+ export interface PresetDefinition {
5
+ axes: AxisConfig | null;
6
+ readonly: boolean;
7
+ }
8
+
9
+ const PRESETS: Record<PresetName, PresetDefinition> = {
10
+ "create": {
11
+ axes: { agency: "autonomous", quality: "architect", scope: "unrestricted" },
12
+ readonly: false,
13
+ },
14
+ "extend": {
15
+ axes: { agency: "autonomous", quality: "pragmatic", scope: "adjacent" },
16
+ readonly: false,
17
+ },
18
+ "safe": {
19
+ axes: { agency: "collaborative", quality: "minimal", scope: "narrow" },
20
+ readonly: false,
21
+ },
22
+ "refactor": {
23
+ axes: { agency: "autonomous", quality: "pragmatic", scope: "unrestricted" },
24
+ readonly: false,
25
+ },
26
+ "explore": {
27
+ axes: { agency: "collaborative", quality: "architect", scope: "narrow" },
28
+ readonly: true,
29
+ },
30
+ "none": {
31
+ axes: null,
32
+ readonly: false,
33
+ },
34
+ };
35
+
36
+ export function getPreset(name: PresetName): PresetDefinition {
37
+ return PRESETS[name];
38
+ }
39
+
package/src/resolve.ts ADDED
@@ -0,0 +1,278 @@
1
+ import type { ModeConfig } from "./types.js";
2
+ import type { ParsedArgs } from "./args.js";
3
+ import type { LoadedConfig } from "./config.js";
4
+ import { resolveConfigPath } from "./config.js";
5
+ import { getPreset, isPresetName } from "./presets.js";
6
+ import {
7
+ AGENCY_VALUES,
8
+ QUALITY_VALUES,
9
+ SCOPE_VALUES,
10
+ BUILTIN_MODIFIER_NAMES,
11
+ BUILTIN_BASE_NAMES,
12
+ PRESET_NAMES,
13
+ isBuiltinModifier,
14
+ isBuiltinBase,
15
+ } from "./types.js";
16
+ import { resolve as pathResolve, isAbsolute } from "node:path";
17
+
18
+ const DEFAULT_AGENCY = "collaborative";
19
+ const DEFAULT_QUALITY = "pragmatic";
20
+ const DEFAULT_SCOPE = "adjacent";
21
+
22
+ /**
23
+ * Returns true if a string looks like a file path rather than a name.
24
+ * Paths contain "/" or "\" or end with ".md".
25
+ */
26
+ function looksLikeFilePath(value: string): boolean {
27
+ return value.includes("/") || value.includes("\\") || value.endsWith(".md");
28
+ }
29
+
30
+ /**
31
+ * Resolves an axis value to either a built-in name or an absolute path.
32
+ * Resolution order: built-in values → config-defined names → file path heuristic.
33
+ * Throws with descriptive error if unresolvable.
34
+ */
35
+ function resolveAxisValue(
36
+ raw: string,
37
+ axisName: "agency" | "quality" | "scope",
38
+ builtinValues: readonly string[],
39
+ loadedConfig: LoadedConfig | null,
40
+ ): string {
41
+ // 1. Built-in value
42
+ if (builtinValues.includes(raw)) return raw;
43
+
44
+ // 2. Config-defined custom name
45
+ const configAxes = loadedConfig?.config.axes?.[axisName];
46
+ if (configAxes && raw in configAxes) {
47
+ return resolveConfigPath(loadedConfig!.configDir, configAxes[raw]);
48
+ }
49
+
50
+ // 3. File path
51
+ if (looksLikeFilePath(raw)) {
52
+ return isAbsolute(raw) ? raw : pathResolve(raw);
53
+ }
54
+
55
+ // 4. Unknown
56
+ const configHint = loadedConfig
57
+ ? ` Config loaded from: ${loadedConfig.configDir}`
58
+ : " No config file found.";
59
+ throw new Error(
60
+ `Unknown --${axisName} value: "${raw}". ` +
61
+ `Must be one of: ${builtinValues.join(", ")}, ` +
62
+ `a name defined in your config, or a file path.${configHint}`
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Resolves a modifier reference to either a built-in flag name or an absolute path.
68
+ * Resolution order: built-in modifier name → config-defined name → file path.
69
+ */
70
+ function resolveModifier(
71
+ raw: string,
72
+ loadedConfig: LoadedConfig | null,
73
+ ): { kind: "builtin"; name: string } | { kind: "custom"; path: string } {
74
+ // 1. Built-in modifier
75
+ if (isBuiltinModifier(raw)) {
76
+ return { kind: "builtin", name: raw };
77
+ }
78
+
79
+ // 2. Config-defined custom modifier
80
+ const configModifiers = loadedConfig?.config.modifiers;
81
+ if (configModifiers && raw in configModifiers) {
82
+ return {
83
+ kind: "custom",
84
+ path: resolveConfigPath(loadedConfig!.configDir, configModifiers[raw]),
85
+ };
86
+ }
87
+
88
+ // 3. File path
89
+ if (looksLikeFilePath(raw)) {
90
+ const absPath = isAbsolute(raw) ? raw : pathResolve(raw);
91
+ return { kind: "custom", path: absPath };
92
+ }
93
+
94
+ // 4. Unknown
95
+ const configHint = loadedConfig
96
+ ? ` Config loaded from: ${loadedConfig.configDir}`
97
+ : " No config file found.";
98
+ throw new Error(
99
+ `Unknown modifier: "${raw}". ` +
100
+ `Must be a built-in modifier (${BUILTIN_MODIFIER_NAMES.join(", ")}), ` +
101
+ `a name defined in your config, or a file path.${configHint}`
102
+ );
103
+ }
104
+
105
+ /** Resolves a list of modifier references, updating flags and custom paths in place. */
106
+ function applyModifiers(
107
+ modifiers: string[],
108
+ loadedConfig: LoadedConfig | null,
109
+ flags: { readonly: boolean; contextPacing: boolean },
110
+ customPaths: string[],
111
+ position: "append" | "prepend",
112
+ ): void {
113
+ for (const raw of modifiers) {
114
+ const resolved = resolveModifier(raw, loadedConfig);
115
+ if (resolved.kind === "builtin") {
116
+ if (resolved.name === "readonly") flags.readonly = true;
117
+ if (resolved.name === "context-pacing") flags.contextPacing = true;
118
+ } else {
119
+ if (!customPaths.includes(resolved.path)) {
120
+ if (position === "prepend") customPaths.unshift(resolved.path);
121
+ else customPaths.push(resolved.path);
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Resolves a base reference to a built-in name or absolute directory path.
129
+ * Priority: CLI --base > preset base > config defaultBase > "standard"
130
+ */
131
+ function resolveBase(
132
+ raw: string | undefined,
133
+ loadedConfig: LoadedConfig | null,
134
+ presetBase: string | undefined,
135
+ ): string {
136
+ const config = loadedConfig?.config ?? null;
137
+
138
+ // Priority: CLI --base > preset base > config defaultBase > "standard"
139
+ const value = raw ?? presetBase ?? config?.defaultBase ?? "standard";
140
+
141
+ // 1. Built-in name
142
+ if (isBuiltinBase(value)) return value;
143
+
144
+ // 2. Config-defined name
145
+ const configBases = config?.bases;
146
+ if (configBases && value in configBases) {
147
+ return resolveConfigPath(loadedConfig!.configDir, configBases[value]);
148
+ }
149
+
150
+ // 3. Directory path
151
+ if (looksLikeFilePath(value)) {
152
+ return isAbsolute(value) ? value : pathResolve(value);
153
+ }
154
+
155
+ // 4. Unknown
156
+ const configHint = loadedConfig
157
+ ? ` Config loaded from: ${loadedConfig.configDir}`
158
+ : " No config file found.";
159
+ throw new Error(
160
+ `Unknown --base value: "${value}". ` +
161
+ `Must be one of: ${BUILTIN_BASE_NAMES.join(", ")}, ` +
162
+ `a name defined in your config, or a directory path.${configHint}`
163
+ );
164
+ }
165
+
166
+ export function resolveConfig(
167
+ parsed: ParsedArgs,
168
+ loadedConfig: LoadedConfig | null,
169
+ ): ModeConfig {
170
+ const config = loadedConfig?.config ?? null;
171
+
172
+ // Resolve modifiers: defaultModifiers (config) → --modifier flags (CLI)
173
+ const flags = { readonly: parsed.modifiers.readonly, contextPacing: parsed.modifiers.contextPacing };
174
+ const customModifierPaths: string[] = [];
175
+
176
+ // 1. Config defaultModifiers — always applied first
177
+ if (config?.defaultModifiers) {
178
+ applyModifiers(config.defaultModifiers, loadedConfig, flags, customModifierPaths, "append");
179
+ }
180
+
181
+ // 2. CLI --modifier flags — appended after defaults
182
+ applyModifiers(parsed.customModifiers, loadedConfig, flags, customModifierPaths, "append");
183
+
184
+ // Handle "none" preset — resolve base before early return
185
+ if (parsed.preset === "none") {
186
+ const base = resolveBase(parsed.base, loadedConfig, undefined);
187
+ return {
188
+ base,
189
+ axes: null,
190
+ modifiers: {
191
+ readonly: flags.readonly,
192
+ contextPacing: flags.contextPacing,
193
+ custom: customModifierPaths,
194
+ },
195
+ };
196
+ }
197
+
198
+ let agency: string;
199
+ let quality: string;
200
+ let scope: string;
201
+ let presetBase: string | undefined;
202
+
203
+ if (parsed.preset) {
204
+ // Check built-in presets first, then config presets
205
+ if (isPresetName(parsed.preset)) {
206
+ const preset = getPreset(parsed.preset);
207
+ if (preset.axes === null) throw new Error(`Preset "${parsed.preset}" has null axes`);
208
+ agency = parsed.overrides.agency
209
+ ? resolveAxisValue(parsed.overrides.agency, "agency", AGENCY_VALUES, loadedConfig)
210
+ : preset.axes.agency;
211
+ quality = parsed.overrides.quality
212
+ ? resolveAxisValue(parsed.overrides.quality, "quality", QUALITY_VALUES, loadedConfig)
213
+ : preset.axes.quality;
214
+ scope = parsed.overrides.scope
215
+ ? resolveAxisValue(parsed.overrides.scope, "scope", SCOPE_VALUES, loadedConfig)
216
+ : preset.axes.scope;
217
+ flags.readonly = flags.readonly || preset.readonly;
218
+ // Built-in presets don't specify a base — presetBase stays undefined
219
+ } else if (config?.presets && parsed.preset in config.presets) {
220
+ // Config-defined preset
221
+ const customPreset = config.presets[parsed.preset];
222
+ presetBase = customPreset.base;
223
+ agency = parsed.overrides.agency
224
+ ? resolveAxisValue(parsed.overrides.agency, "agency", AGENCY_VALUES, loadedConfig)
225
+ : customPreset.agency
226
+ ? resolveAxisValue(customPreset.agency, "agency", AGENCY_VALUES, loadedConfig)
227
+ : DEFAULT_AGENCY;
228
+ quality = parsed.overrides.quality
229
+ ? resolveAxisValue(parsed.overrides.quality, "quality", QUALITY_VALUES, loadedConfig)
230
+ : customPreset.quality
231
+ ? resolveAxisValue(customPreset.quality, "quality", QUALITY_VALUES, loadedConfig)
232
+ : DEFAULT_QUALITY;
233
+ scope = parsed.overrides.scope
234
+ ? resolveAxisValue(parsed.overrides.scope, "scope", SCOPE_VALUES, loadedConfig)
235
+ : customPreset.scope
236
+ ? resolveAxisValue(customPreset.scope, "scope", SCOPE_VALUES, loadedConfig)
237
+ : DEFAULT_SCOPE;
238
+ if (customPreset.readonly) flags.readonly = true;
239
+ if (customPreset.contextPacing) flags.contextPacing = true;
240
+
241
+ // Resolve preset's modifiers list — inserted before CLI modifiers
242
+ if (customPreset.modifiers) {
243
+ applyModifiers(customPreset.modifiers, loadedConfig, flags, customModifierPaths, "prepend");
244
+ }
245
+ } else {
246
+ throw new Error(
247
+ `Unknown preset: "${parsed.preset}". ` +
248
+ `Built-in presets: ${PRESET_NAMES.join(", ")}. ` +
249
+ (config?.presets
250
+ ? `Config presets: ${Object.keys(config.presets).join(", ")}.`
251
+ : "No config file found.")
252
+ );
253
+ }
254
+ } else {
255
+ // No preset: use overrides with defaults
256
+ agency = parsed.overrides.agency
257
+ ? resolveAxisValue(parsed.overrides.agency, "agency", AGENCY_VALUES, loadedConfig)
258
+ : DEFAULT_AGENCY;
259
+ quality = parsed.overrides.quality
260
+ ? resolveAxisValue(parsed.overrides.quality, "quality", QUALITY_VALUES, loadedConfig)
261
+ : DEFAULT_QUALITY;
262
+ scope = parsed.overrides.scope
263
+ ? resolveAxisValue(parsed.overrides.scope, "scope", SCOPE_VALUES, loadedConfig)
264
+ : DEFAULT_SCOPE;
265
+ }
266
+
267
+ const base = resolveBase(parsed.base, loadedConfig, presetBase);
268
+
269
+ return {
270
+ base,
271
+ axes: { agency, quality, scope },
272
+ modifiers: {
273
+ readonly: flags.readonly,
274
+ contextPacing: flags.contextPacing,
275
+ custom: customModifierPaths,
276
+ },
277
+ };
278
+ }