aether-colony 1.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 (207) hide show
  1. package/.aether/CONTEXT.md +160 -0
  2. package/.aether/QUEEN.md +84 -0
  3. package/.aether/aether-utils.sh +7749 -0
  4. package/.aether/docs/QUEEN-SYSTEM.md +211 -0
  5. package/.aether/docs/README.md +68 -0
  6. package/.aether/docs/caste-system.md +48 -0
  7. package/.aether/docs/disciplines/DISCIPLINES.md +93 -0
  8. package/.aether/docs/disciplines/coding-standards.md +197 -0
  9. package/.aether/docs/disciplines/debugging.md +207 -0
  10. package/.aether/docs/disciplines/learning.md +254 -0
  11. package/.aether/docs/disciplines/tdd.md +257 -0
  12. package/.aether/docs/disciplines/verification-loop.md +167 -0
  13. package/.aether/docs/disciplines/verification.md +116 -0
  14. package/.aether/docs/error-codes.md +268 -0
  15. package/.aether/docs/known-issues.md +233 -0
  16. package/.aether/docs/pheromones.md +205 -0
  17. package/.aether/docs/queen-commands.md +97 -0
  18. package/.aether/exchange/colony-registry.xml +11 -0
  19. package/.aether/exchange/pheromone-xml.sh +575 -0
  20. package/.aether/exchange/pheromones.xml +87 -0
  21. package/.aether/exchange/queen-wisdom.xml +14 -0
  22. package/.aether/exchange/registry-xml.sh +273 -0
  23. package/.aether/exchange/wisdom-xml.sh +319 -0
  24. package/.aether/midden/approach-changes.md +5 -0
  25. package/.aether/midden/build-failures.md +5 -0
  26. package/.aether/midden/test-failures.md +5 -0
  27. package/.aether/model-profiles.yaml +100 -0
  28. package/.aether/rules/aether-colony.md +134 -0
  29. package/.aether/schemas/aether-types.xsd +255 -0
  30. package/.aether/schemas/colony-registry.xsd +309 -0
  31. package/.aether/schemas/example-prompt-builder.xml +234 -0
  32. package/.aether/schemas/pheromone.xsd +163 -0
  33. package/.aether/schemas/prompt.xsd +416 -0
  34. package/.aether/schemas/queen-wisdom.xsd +325 -0
  35. package/.aether/schemas/worker-priming.xsd +276 -0
  36. package/.aether/templates/QUEEN.md.template +79 -0
  37. package/.aether/templates/colony-state-reset.jq.template +22 -0
  38. package/.aether/templates/colony-state.template.json +35 -0
  39. package/.aether/templates/constraints.template.json +9 -0
  40. package/.aether/templates/crowned-anthill.template.md +36 -0
  41. package/.aether/templates/handoff-build-error.template.md +30 -0
  42. package/.aether/templates/handoff-build-success.template.md +39 -0
  43. package/.aether/templates/handoff.template.md +40 -0
  44. package/.aether/templates/learning-observations.template.json +6 -0
  45. package/.aether/templates/midden.template.json +7 -0
  46. package/.aether/templates/pheromones.template.json +6 -0
  47. package/.aether/templates/session.template.json +9 -0
  48. package/.aether/utils/atomic-write.sh +219 -0
  49. package/.aether/utils/chamber-compare.sh +193 -0
  50. package/.aether/utils/chamber-utils.sh +297 -0
  51. package/.aether/utils/colorize-log.sh +132 -0
  52. package/.aether/utils/error-handler.sh +212 -0
  53. package/.aether/utils/file-lock.sh +158 -0
  54. package/.aether/utils/queen-to-md.xsl +395 -0
  55. package/.aether/utils/semantic-cli.sh +413 -0
  56. package/.aether/utils/spawn-tree.sh +428 -0
  57. package/.aether/utils/spawn-with-model.sh +56 -0
  58. package/.aether/utils/state-loader.sh +215 -0
  59. package/.aether/utils/swarm-display.sh +268 -0
  60. package/.aether/utils/watch-spawn-tree.sh +253 -0
  61. package/.aether/utils/xml-compose.sh +253 -0
  62. package/.aether/utils/xml-convert.sh +273 -0
  63. package/.aether/utils/xml-core.sh +186 -0
  64. package/.aether/utils/xml-query.sh +201 -0
  65. package/.aether/utils/xml-utils.sh +110 -0
  66. package/.aether/workers.md +765 -0
  67. package/.claude/agents/ant/aether-ambassador.md +264 -0
  68. package/.claude/agents/ant/aether-archaeologist.md +322 -0
  69. package/.claude/agents/ant/aether-auditor.md +266 -0
  70. package/.claude/agents/ant/aether-builder.md +187 -0
  71. package/.claude/agents/ant/aether-chaos.md +268 -0
  72. package/.claude/agents/ant/aether-chronicler.md +304 -0
  73. package/.claude/agents/ant/aether-gatekeeper.md +325 -0
  74. package/.claude/agents/ant/aether-includer.md +373 -0
  75. package/.claude/agents/ant/aether-keeper.md +271 -0
  76. package/.claude/agents/ant/aether-measurer.md +317 -0
  77. package/.claude/agents/ant/aether-probe.md +210 -0
  78. package/.claude/agents/ant/aether-queen.md +325 -0
  79. package/.claude/agents/ant/aether-route-setter.md +173 -0
  80. package/.claude/agents/ant/aether-sage.md +353 -0
  81. package/.claude/agents/ant/aether-scout.md +142 -0
  82. package/.claude/agents/ant/aether-surveyor-disciplines.md +416 -0
  83. package/.claude/agents/ant/aether-surveyor-nest.md +354 -0
  84. package/.claude/agents/ant/aether-surveyor-pathogens.md +288 -0
  85. package/.claude/agents/ant/aether-surveyor-provisions.md +359 -0
  86. package/.claude/agents/ant/aether-tracker.md +265 -0
  87. package/.claude/agents/ant/aether-watcher.md +244 -0
  88. package/.claude/agents/ant/aether-weaver.md +247 -0
  89. package/.claude/commands/ant/archaeology.md +341 -0
  90. package/.claude/commands/ant/build.md +1160 -0
  91. package/.claude/commands/ant/chaos.md +349 -0
  92. package/.claude/commands/ant/colonize.md +270 -0
  93. package/.claude/commands/ant/continue.md +1070 -0
  94. package/.claude/commands/ant/council.md +309 -0
  95. package/.claude/commands/ant/dream.md +265 -0
  96. package/.claude/commands/ant/entomb.md +487 -0
  97. package/.claude/commands/ant/feedback.md +78 -0
  98. package/.claude/commands/ant/flag.md +139 -0
  99. package/.claude/commands/ant/flags.md +155 -0
  100. package/.claude/commands/ant/focus.md +58 -0
  101. package/.claude/commands/ant/help.md +122 -0
  102. package/.claude/commands/ant/history.md +137 -0
  103. package/.claude/commands/ant/init.md +409 -0
  104. package/.claude/commands/ant/interpret.md +267 -0
  105. package/.claude/commands/ant/lay-eggs.md +201 -0
  106. package/.claude/commands/ant/maturity.md +102 -0
  107. package/.claude/commands/ant/memory-details.md +77 -0
  108. package/.claude/commands/ant/migrate-state.md +165 -0
  109. package/.claude/commands/ant/oracle.md +387 -0
  110. package/.claude/commands/ant/organize.md +227 -0
  111. package/.claude/commands/ant/pause-colony.md +247 -0
  112. package/.claude/commands/ant/phase.md +126 -0
  113. package/.claude/commands/ant/plan.md +544 -0
  114. package/.claude/commands/ant/redirect.md +58 -0
  115. package/.claude/commands/ant/resume-colony.md +182 -0
  116. package/.claude/commands/ant/resume.md +363 -0
  117. package/.claude/commands/ant/seal.md +306 -0
  118. package/.claude/commands/ant/status.md +272 -0
  119. package/.claude/commands/ant/swarm.md +361 -0
  120. package/.claude/commands/ant/tunnels.md +425 -0
  121. package/.claude/commands/ant/update.md +209 -0
  122. package/.claude/commands/ant/verify-castes.md +95 -0
  123. package/.claude/commands/ant/watch.md +238 -0
  124. package/.opencode/agents/aether-ambassador.md +140 -0
  125. package/.opencode/agents/aether-archaeologist.md +108 -0
  126. package/.opencode/agents/aether-auditor.md +144 -0
  127. package/.opencode/agents/aether-builder.md +184 -0
  128. package/.opencode/agents/aether-chaos.md +115 -0
  129. package/.opencode/agents/aether-chronicler.md +122 -0
  130. package/.opencode/agents/aether-gatekeeper.md +116 -0
  131. package/.opencode/agents/aether-includer.md +117 -0
  132. package/.opencode/agents/aether-keeper.md +177 -0
  133. package/.opencode/agents/aether-measurer.md +128 -0
  134. package/.opencode/agents/aether-probe.md +133 -0
  135. package/.opencode/agents/aether-queen.md +286 -0
  136. package/.opencode/agents/aether-route-setter.md +130 -0
  137. package/.opencode/agents/aether-sage.md +106 -0
  138. package/.opencode/agents/aether-scout.md +101 -0
  139. package/.opencode/agents/aether-surveyor-disciplines.md +386 -0
  140. package/.opencode/agents/aether-surveyor-nest.md +324 -0
  141. package/.opencode/agents/aether-surveyor-pathogens.md +259 -0
  142. package/.opencode/agents/aether-surveyor-provisions.md +329 -0
  143. package/.opencode/agents/aether-tracker.md +137 -0
  144. package/.opencode/agents/aether-watcher.md +174 -0
  145. package/.opencode/agents/aether-weaver.md +130 -0
  146. package/.opencode/commands/ant/archaeology.md +338 -0
  147. package/.opencode/commands/ant/build.md +1200 -0
  148. package/.opencode/commands/ant/chaos.md +346 -0
  149. package/.opencode/commands/ant/colonize.md +202 -0
  150. package/.opencode/commands/ant/continue.md +938 -0
  151. package/.opencode/commands/ant/council.md +305 -0
  152. package/.opencode/commands/ant/dream.md +262 -0
  153. package/.opencode/commands/ant/entomb.md +367 -0
  154. package/.opencode/commands/ant/feedback.md +80 -0
  155. package/.opencode/commands/ant/flag.md +137 -0
  156. package/.opencode/commands/ant/flags.md +153 -0
  157. package/.opencode/commands/ant/focus.md +56 -0
  158. package/.opencode/commands/ant/help.md +124 -0
  159. package/.opencode/commands/ant/history.md +127 -0
  160. package/.opencode/commands/ant/init.md +337 -0
  161. package/.opencode/commands/ant/interpret.md +256 -0
  162. package/.opencode/commands/ant/lay-eggs.md +141 -0
  163. package/.opencode/commands/ant/maturity.md +92 -0
  164. package/.opencode/commands/ant/memory-details.md +77 -0
  165. package/.opencode/commands/ant/migrate-state.md +153 -0
  166. package/.opencode/commands/ant/oracle.md +338 -0
  167. package/.opencode/commands/ant/organize.md +224 -0
  168. package/.opencode/commands/ant/pause-colony.md +220 -0
  169. package/.opencode/commands/ant/phase.md +123 -0
  170. package/.opencode/commands/ant/plan.md +531 -0
  171. package/.opencode/commands/ant/redirect.md +67 -0
  172. package/.opencode/commands/ant/resume-colony.md +178 -0
  173. package/.opencode/commands/ant/resume.md +363 -0
  174. package/.opencode/commands/ant/seal.md +247 -0
  175. package/.opencode/commands/ant/status.md +272 -0
  176. package/.opencode/commands/ant/swarm.md +357 -0
  177. package/.opencode/commands/ant/tunnels.md +406 -0
  178. package/.opencode/commands/ant/update.md +191 -0
  179. package/.opencode/commands/ant/verify-castes.md +85 -0
  180. package/.opencode/commands/ant/watch.md +220 -0
  181. package/.opencode/opencode.json +3 -0
  182. package/CHANGELOG.md +325 -0
  183. package/DISCLAIMER.md +74 -0
  184. package/LICENSE +21 -0
  185. package/README.md +258 -0
  186. package/bin/cli.js +2436 -0
  187. package/bin/generate-commands.sh +291 -0
  188. package/bin/lib/caste-colors.js +57 -0
  189. package/bin/lib/colors.js +76 -0
  190. package/bin/lib/errors.js +255 -0
  191. package/bin/lib/event-types.js +190 -0
  192. package/bin/lib/file-lock.js +695 -0
  193. package/bin/lib/init.js +454 -0
  194. package/bin/lib/logger.js +242 -0
  195. package/bin/lib/model-profiles.js +445 -0
  196. package/bin/lib/model-verify.js +288 -0
  197. package/bin/lib/nestmate-loader.js +130 -0
  198. package/bin/lib/proxy-health.js +253 -0
  199. package/bin/lib/spawn-logger.js +266 -0
  200. package/bin/lib/state-guard.js +602 -0
  201. package/bin/lib/state-sync.js +516 -0
  202. package/bin/lib/telemetry.js +441 -0
  203. package/bin/lib/update-transaction.js +1454 -0
  204. package/bin/npx-install.js +178 -0
  205. package/bin/sync-to-runtime.sh +6 -0
  206. package/bin/validate-package.sh +88 -0
  207. package/package.json +70 -0
@@ -0,0 +1,445 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Model Profiles Library
4
+ *
5
+ * Reads and validates caste-to-model assignments from model-profiles.yaml.
6
+ * Provides utilities for model routing and profile management.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const yaml = require('js-yaml');
12
+ const { ConfigurationError } = require('./errors');
13
+
14
+ /**
15
+ * Default model to use when caste is not found
16
+ */
17
+ const DEFAULT_MODEL = 'kimi-k2.5';
18
+
19
+ /**
20
+ * Load and parse model profiles from YAML file
21
+ * @param {string} repoPath - Path to repository root
22
+ * @returns {object} Parsed model profiles
23
+ * @throws {ConfigurationError} If file not found or invalid YAML
24
+ */
25
+ function loadModelProfiles(repoPath) {
26
+ const profilePath = path.join(repoPath, '.aether', 'model-profiles.yaml');
27
+
28
+ if (!fs.existsSync(profilePath)) {
29
+ throw new ConfigurationError(
30
+ `Model profiles file not found: ${profilePath}`,
31
+ { path: profilePath }
32
+ );
33
+ }
34
+
35
+ let content;
36
+ try {
37
+ content = fs.readFileSync(profilePath, 'utf8');
38
+ } catch (error) {
39
+ throw new ConfigurationError(
40
+ `Failed to read model profiles file: ${error.message}`,
41
+ { path: profilePath, originalError: error.message }
42
+ );
43
+ }
44
+
45
+ try {
46
+ const config = yaml.load(content);
47
+
48
+ // Substitute environment variables in proxy config
49
+ if (config.proxy) {
50
+ if (config.proxy.auth_token) {
51
+ config.proxy.auth_token = substituteEnvVars(config.proxy.auth_token);
52
+ }
53
+ if (config.proxy.endpoint) {
54
+ config.proxy.endpoint = substituteEnvVars(config.proxy.endpoint);
55
+ }
56
+ }
57
+
58
+ return config;
59
+ } catch (error) {
60
+ throw new ConfigurationError(
61
+ `Invalid YAML in model profiles file: ${error.message}`,
62
+ { path: profilePath, originalError: error.message }
63
+ );
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Substitute environment variables in a string
69
+ * Supports ${VAR} and ${VAR:-default} syntax
70
+ * @param {string} str - String with potential env vars
71
+ * @returns {string} String with env vars substituted
72
+ */
73
+ function substituteEnvVars(str) {
74
+ if (typeof str !== 'string') return str;
75
+
76
+ // Match ${VAR:-default} or ${VAR}
77
+ return str.replace(/\$\{([^}:]+)(?::-([^}]*))?\}/g, (match, varName, defaultValue) => {
78
+ const envValue = process.env[varName];
79
+ if (envValue !== undefined && envValue !== '') {
80
+ return envValue;
81
+ }
82
+ return defaultValue !== undefined ? defaultValue : '';
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Get the assigned model for a specific caste
88
+ * @param {object} profiles - Parsed model profiles
89
+ * @param {string} caste - Caste name (e.g., 'builder', 'watcher')
90
+ * @returns {string} Model name for the caste, or default if not found
91
+ */
92
+ function getModelForCaste(profiles, caste) {
93
+ if (!profiles || typeof profiles !== 'object') {
94
+ console.warn(`[WARN] Invalid profiles object, using default model: ${DEFAULT_MODEL}`);
95
+ return DEFAULT_MODEL;
96
+ }
97
+
98
+ const model = profiles.worker_models?.[caste];
99
+
100
+ if (!model) {
101
+ console.warn(`[WARN] Unknown caste '${caste}', using default model: ${DEFAULT_MODEL}`);
102
+ return DEFAULT_MODEL;
103
+ }
104
+
105
+ return model;
106
+ }
107
+
108
+ /**
109
+ * Validate if a caste name is valid
110
+ * @param {object} profiles - Parsed model profiles
111
+ * @param {string} caste - Caste name to validate
112
+ * @returns {object} { valid: boolean, castes: string[] }
113
+ */
114
+ function validateCaste(profiles, caste) {
115
+ if (!profiles || typeof profiles !== 'object') {
116
+ return { valid: false, castes: [] };
117
+ }
118
+
119
+ const validCastes = Object.keys(profiles.worker_models || {});
120
+ const valid = validCastes.includes(caste);
121
+
122
+ return { valid, castes: validCastes };
123
+ }
124
+
125
+ /**
126
+ * Validate if a model name is valid
127
+ * @param {object} profiles - Parsed model profiles
128
+ * @param {string} model - Model name to validate
129
+ * @returns {object} { valid: boolean, models: string[] }
130
+ */
131
+ function validateModel(profiles, model) {
132
+ if (!profiles || typeof profiles !== 'object') {
133
+ return { valid: false, models: [] };
134
+ }
135
+
136
+ const validModels = Object.keys(profiles.model_metadata || {});
137
+ const valid = validModels.includes(model);
138
+
139
+ return { valid, models: validModels };
140
+ }
141
+
142
+ /**
143
+ * Get the provider for a specific model
144
+ * @param {object} profiles - Parsed model profiles
145
+ * @param {string} model - Model name
146
+ * @returns {string|null} Provider name, or null if not found
147
+ */
148
+ function getProviderForModel(profiles, model) {
149
+ if (!profiles || typeof profiles !== 'object') {
150
+ return null;
151
+ }
152
+
153
+ return profiles.model_metadata?.[model]?.provider || null;
154
+ }
155
+
156
+ /**
157
+ * Get all caste-to-model assignments with provider info
158
+ * @param {object} profiles - Parsed model profiles
159
+ * @returns {Array<{caste: string, model: string, provider: string|null}>} Array of assignments
160
+ */
161
+ function getAllAssignments(profiles) {
162
+ if (!profiles || typeof profiles !== 'object') {
163
+ return [];
164
+ }
165
+
166
+ const workerModels = profiles.worker_models || {};
167
+
168
+ return Object.entries(workerModels).map(([caste, model]) => ({
169
+ caste,
170
+ model,
171
+ provider: getProviderForModel(profiles, model),
172
+ }));
173
+ }
174
+
175
+ /**
176
+ * Get model metadata for a specific model
177
+ * @param {object} profiles - Parsed model profiles
178
+ * @param {string} model - Model name
179
+ * @returns {object|null} Model metadata, or null if not found
180
+ */
181
+ function getModelMetadata(profiles, model) {
182
+ if (!profiles || typeof profiles !== 'object') {
183
+ return null;
184
+ }
185
+
186
+ return profiles.model_metadata?.[model] || null;
187
+ }
188
+
189
+ /**
190
+ * Get proxy configuration from profiles
191
+ * @param {object} profiles - Parsed model profiles
192
+ * @returns {object|null} Proxy configuration, or null if not found
193
+ */
194
+ function getProxyConfig(profiles) {
195
+ if (!profiles || typeof profiles !== 'object') {
196
+ return null;
197
+ }
198
+
199
+ return profiles.proxy || null;
200
+ }
201
+
202
+ /**
203
+ * Set user override for a caste's model
204
+ * @param {string} repoPath - Path to repository root
205
+ * @param {string} caste - Caste name to override
206
+ * @param {string} model - Model name to assign
207
+ * @returns {object} {success: true, previous: string|null}
208
+ * @throws {ValidationError} If caste or model is invalid
209
+ */
210
+ function setModelOverride(repoPath, caste, model) {
211
+ const profiles = loadModelProfiles(repoPath);
212
+
213
+ // Validate caste exists
214
+ const casteValidation = validateCaste(profiles, caste);
215
+ if (!casteValidation.valid) {
216
+ const { ValidationError } = require('./errors');
217
+ throw new ValidationError(
218
+ `Invalid caste '${caste}'. Valid castes: ${casteValidation.castes.join(', ')}`,
219
+ { caste, validCastes: casteValidation.castes }
220
+ );
221
+ }
222
+
223
+ // Validate model exists
224
+ const modelValidation = validateModel(profiles, model);
225
+ if (!modelValidation.valid) {
226
+ const { ValidationError } = require('./errors');
227
+ throw new ValidationError(
228
+ `Invalid model '${model}'. Valid models: ${modelValidation.models.join(', ')}`,
229
+ { model, validModels: modelValidation.models }
230
+ );
231
+ }
232
+
233
+ // Get previous override if exists
234
+ const previous = profiles.user_overrides?.[caste] || null;
235
+
236
+ // Read current YAML content
237
+ const profilePath = path.join(repoPath, '.aether', 'model-profiles.yaml');
238
+ const content = fs.readFileSync(profilePath, 'utf8');
239
+ const data = yaml.load(content);
240
+
241
+ // Ensure user_overrides section exists
242
+ if (!data.user_overrides) {
243
+ data.user_overrides = {};
244
+ }
245
+
246
+ // Set the override
247
+ data.user_overrides[caste] = model;
248
+
249
+ // Write back with proper YAML formatting
250
+ const yamlContent = yaml.dump(data, {
251
+ indent: 2,
252
+ lineWidth: -1,
253
+ noRefs: true,
254
+ sortKeys: false,
255
+ });
256
+
257
+ fs.writeFileSync(profilePath, yamlContent, 'utf8');
258
+
259
+ return { success: true, previous };
260
+ }
261
+
262
+ /**
263
+ * Reset user override for a caste (remove override)
264
+ * @param {string} repoPath - Path to repository root
265
+ * @param {string} caste - Caste name to reset
266
+ * @returns {object} {success: true, hadOverride: boolean}
267
+ * @throws {ValidationError} If caste is invalid
268
+ */
269
+ function resetModelOverride(repoPath, caste) {
270
+ const profiles = loadModelProfiles(repoPath);
271
+
272
+ // Validate caste exists
273
+ const casteValidation = validateCaste(profiles, caste);
274
+ if (!casteValidation.valid) {
275
+ const { ValidationError } = require('./errors');
276
+ throw new ValidationError(
277
+ `Invalid caste '${caste}'. Valid castes: ${casteValidation.castes.join(', ')}`,
278
+ { caste, validCastes: casteValidation.castes }
279
+ );
280
+ }
281
+
282
+ // Check if override exists
283
+ const hadOverride = profiles.user_overrides?.[caste] !== undefined;
284
+
285
+ if (hadOverride) {
286
+ // Read current YAML content
287
+ const profilePath = path.join(repoPath, '.aether', 'model-profiles.yaml');
288
+ const content = fs.readFileSync(profilePath, 'utf8');
289
+ const data = yaml.load(content);
290
+
291
+ // Remove the override
292
+ if (data.user_overrides) {
293
+ delete data.user_overrides[caste];
294
+
295
+ // Clean up empty user_overrides section
296
+ if (Object.keys(data.user_overrides).length === 0) {
297
+ delete data.user_overrides;
298
+ }
299
+
300
+ // Write back with proper YAML formatting
301
+ const yamlContent = yaml.dump(data, {
302
+ indent: 2,
303
+ lineWidth: -1,
304
+ noRefs: true,
305
+ sortKeys: false,
306
+ });
307
+
308
+ fs.writeFileSync(profilePath, yamlContent, 'utf8');
309
+ }
310
+ }
311
+
312
+ return { success: true, hadOverride };
313
+ }
314
+
315
+ /**
316
+ * Get effective model for a caste (respecting overrides)
317
+ * @param {object} profiles - Parsed model profiles
318
+ * @param {string} caste - Caste name
319
+ * @returns {object} {model: string, source: 'override'|'default'|'fallback'}
320
+ */
321
+ function getEffectiveModel(profiles, caste) {
322
+ if (!profiles || typeof profiles !== 'object') {
323
+ return { model: DEFAULT_MODEL, source: 'fallback' };
324
+ }
325
+
326
+ // Check user overrides first
327
+ const override = profiles.user_overrides?.[caste];
328
+ if (override) {
329
+ return { model: override, source: 'override' };
330
+ }
331
+
332
+ // Fall back to worker_models default
333
+ const defaultModel = profiles.worker_models?.[caste];
334
+ if (defaultModel) {
335
+ return { model: defaultModel, source: 'default' };
336
+ }
337
+
338
+ // Final fallback
339
+ return { model: DEFAULT_MODEL, source: 'fallback' };
340
+ }
341
+
342
+ /**
343
+ * Get current user overrides
344
+ * @param {object} profiles - Parsed model profiles
345
+ * @returns {object} User overrides object (empty if none)
346
+ */
347
+ function getUserOverrides(profiles) {
348
+ if (!profiles || typeof profiles !== 'object') {
349
+ return {};
350
+ }
351
+
352
+ return profiles.user_overrides || {};
353
+ }
354
+
355
+ /**
356
+ * Get the appropriate model for a task based on keyword matching
357
+ * @param {object} taskRouting - Task routing configuration from profiles
358
+ * @param {string} taskDescription - Description of the task to route
359
+ * @returns {string|null} Model name for the task, or null if no match
360
+ */
361
+ function getModelForTask(taskRouting, taskDescription) {
362
+ if (!taskRouting || !taskDescription) {
363
+ return null;
364
+ }
365
+
366
+ const normalizedTask = taskDescription.toLowerCase();
367
+ const complexityIndicators = taskRouting.complexity_indicators || {};
368
+
369
+ // Iterate through complexity indicators (complex, simple, validate)
370
+ for (const [complexity, config] of Object.entries(complexityIndicators)) {
371
+ if (!config || !config.keywords || !Array.isArray(config.keywords)) {
372
+ continue;
373
+ }
374
+
375
+ // Check if any keyword is a substring of the task description
376
+ const hasMatch = config.keywords.some(keyword =>
377
+ normalizedTask.includes(keyword.toLowerCase())
378
+ );
379
+
380
+ if (hasMatch) {
381
+ return config.model;
382
+ }
383
+ }
384
+
385
+ // Return default model if no keywords match
386
+ return taskRouting.default_model || null;
387
+ }
388
+
389
+ /**
390
+ * Select the appropriate model for a task with full precedence chain
391
+ * Precedence: CLI override > user override > task routing > caste default > fallback
392
+ * @param {object} profiles - Parsed model profiles
393
+ * @param {string} caste - Caste name (e.g., 'builder', 'watcher')
394
+ * @param {string} taskDescription - Description of the task
395
+ * @param {string|null} cliOverride - Optional CLI-provided model override
396
+ * @returns {object} { model: string, source: string } with source tracking
397
+ */
398
+ function selectModelForTask(profiles, caste, taskDescription, cliOverride = null) {
399
+ // 1. CLI override (highest precedence)
400
+ if (cliOverride) {
401
+ const validation = validateModel(profiles, cliOverride);
402
+ if (validation.valid) {
403
+ return { model: cliOverride, source: 'cli-override' };
404
+ }
405
+ }
406
+
407
+ // 2. User override
408
+ if (profiles && profiles.user_overrides && profiles.user_overrides[caste]) {
409
+ return { model: profiles.user_overrides[caste], source: 'user-override' };
410
+ }
411
+
412
+ // 3. Task-based routing
413
+ if (taskDescription && profiles && profiles.task_routing) {
414
+ const taskModel = getModelForTask(profiles.task_routing, taskDescription);
415
+ if (taskModel) {
416
+ return { model: taskModel, source: 'task-routing' };
417
+ }
418
+ }
419
+
420
+ // 4. Caste default
421
+ if (profiles && profiles.worker_models && profiles.worker_models[caste]) {
422
+ return { model: profiles.worker_models[caste], source: 'caste-default' };
423
+ }
424
+
425
+ // 5. Fallback (lowest precedence)
426
+ return { model: DEFAULT_MODEL, source: 'fallback' };
427
+ }
428
+
429
+ module.exports = {
430
+ loadModelProfiles,
431
+ getModelForCaste,
432
+ validateCaste,
433
+ validateModel,
434
+ getProviderForModel,
435
+ getAllAssignments,
436
+ getModelMetadata,
437
+ getProxyConfig,
438
+ setModelOverride,
439
+ resetModelOverride,
440
+ getEffectiveModel,
441
+ getUserOverrides,
442
+ getModelForTask,
443
+ selectModelForTask,
444
+ DEFAULT_MODEL,
445
+ };