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.
- package/.aether/CONTEXT.md +160 -0
- package/.aether/QUEEN.md +84 -0
- package/.aether/aether-utils.sh +7749 -0
- package/.aether/docs/QUEEN-SYSTEM.md +211 -0
- package/.aether/docs/README.md +68 -0
- package/.aether/docs/caste-system.md +48 -0
- package/.aether/docs/disciplines/DISCIPLINES.md +93 -0
- package/.aether/docs/disciplines/coding-standards.md +197 -0
- package/.aether/docs/disciplines/debugging.md +207 -0
- package/.aether/docs/disciplines/learning.md +254 -0
- package/.aether/docs/disciplines/tdd.md +257 -0
- package/.aether/docs/disciplines/verification-loop.md +167 -0
- package/.aether/docs/disciplines/verification.md +116 -0
- package/.aether/docs/error-codes.md +268 -0
- package/.aether/docs/known-issues.md +233 -0
- package/.aether/docs/pheromones.md +205 -0
- package/.aether/docs/queen-commands.md +97 -0
- package/.aether/exchange/colony-registry.xml +11 -0
- package/.aether/exchange/pheromone-xml.sh +575 -0
- package/.aether/exchange/pheromones.xml +87 -0
- package/.aether/exchange/queen-wisdom.xml +14 -0
- package/.aether/exchange/registry-xml.sh +273 -0
- package/.aether/exchange/wisdom-xml.sh +319 -0
- package/.aether/midden/approach-changes.md +5 -0
- package/.aether/midden/build-failures.md +5 -0
- package/.aether/midden/test-failures.md +5 -0
- package/.aether/model-profiles.yaml +100 -0
- package/.aether/rules/aether-colony.md +134 -0
- package/.aether/schemas/aether-types.xsd +255 -0
- package/.aether/schemas/colony-registry.xsd +309 -0
- package/.aether/schemas/example-prompt-builder.xml +234 -0
- package/.aether/schemas/pheromone.xsd +163 -0
- package/.aether/schemas/prompt.xsd +416 -0
- package/.aether/schemas/queen-wisdom.xsd +325 -0
- package/.aether/schemas/worker-priming.xsd +276 -0
- package/.aether/templates/QUEEN.md.template +79 -0
- package/.aether/templates/colony-state-reset.jq.template +22 -0
- package/.aether/templates/colony-state.template.json +35 -0
- package/.aether/templates/constraints.template.json +9 -0
- package/.aether/templates/crowned-anthill.template.md +36 -0
- package/.aether/templates/handoff-build-error.template.md +30 -0
- package/.aether/templates/handoff-build-success.template.md +39 -0
- package/.aether/templates/handoff.template.md +40 -0
- package/.aether/templates/learning-observations.template.json +6 -0
- package/.aether/templates/midden.template.json +7 -0
- package/.aether/templates/pheromones.template.json +6 -0
- package/.aether/templates/session.template.json +9 -0
- package/.aether/utils/atomic-write.sh +219 -0
- package/.aether/utils/chamber-compare.sh +193 -0
- package/.aether/utils/chamber-utils.sh +297 -0
- package/.aether/utils/colorize-log.sh +132 -0
- package/.aether/utils/error-handler.sh +212 -0
- package/.aether/utils/file-lock.sh +158 -0
- package/.aether/utils/queen-to-md.xsl +395 -0
- package/.aether/utils/semantic-cli.sh +413 -0
- package/.aether/utils/spawn-tree.sh +428 -0
- package/.aether/utils/spawn-with-model.sh +56 -0
- package/.aether/utils/state-loader.sh +215 -0
- package/.aether/utils/swarm-display.sh +268 -0
- package/.aether/utils/watch-spawn-tree.sh +253 -0
- package/.aether/utils/xml-compose.sh +253 -0
- package/.aether/utils/xml-convert.sh +273 -0
- package/.aether/utils/xml-core.sh +186 -0
- package/.aether/utils/xml-query.sh +201 -0
- package/.aether/utils/xml-utils.sh +110 -0
- package/.aether/workers.md +765 -0
- package/.claude/agents/ant/aether-ambassador.md +264 -0
- package/.claude/agents/ant/aether-archaeologist.md +322 -0
- package/.claude/agents/ant/aether-auditor.md +266 -0
- package/.claude/agents/ant/aether-builder.md +187 -0
- package/.claude/agents/ant/aether-chaos.md +268 -0
- package/.claude/agents/ant/aether-chronicler.md +304 -0
- package/.claude/agents/ant/aether-gatekeeper.md +325 -0
- package/.claude/agents/ant/aether-includer.md +373 -0
- package/.claude/agents/ant/aether-keeper.md +271 -0
- package/.claude/agents/ant/aether-measurer.md +317 -0
- package/.claude/agents/ant/aether-probe.md +210 -0
- package/.claude/agents/ant/aether-queen.md +325 -0
- package/.claude/agents/ant/aether-route-setter.md +173 -0
- package/.claude/agents/ant/aether-sage.md +353 -0
- package/.claude/agents/ant/aether-scout.md +142 -0
- package/.claude/agents/ant/aether-surveyor-disciplines.md +416 -0
- package/.claude/agents/ant/aether-surveyor-nest.md +354 -0
- package/.claude/agents/ant/aether-surveyor-pathogens.md +288 -0
- package/.claude/agents/ant/aether-surveyor-provisions.md +359 -0
- package/.claude/agents/ant/aether-tracker.md +265 -0
- package/.claude/agents/ant/aether-watcher.md +244 -0
- package/.claude/agents/ant/aether-weaver.md +247 -0
- package/.claude/commands/ant/archaeology.md +341 -0
- package/.claude/commands/ant/build.md +1160 -0
- package/.claude/commands/ant/chaos.md +349 -0
- package/.claude/commands/ant/colonize.md +270 -0
- package/.claude/commands/ant/continue.md +1070 -0
- package/.claude/commands/ant/council.md +309 -0
- package/.claude/commands/ant/dream.md +265 -0
- package/.claude/commands/ant/entomb.md +487 -0
- package/.claude/commands/ant/feedback.md +78 -0
- package/.claude/commands/ant/flag.md +139 -0
- package/.claude/commands/ant/flags.md +155 -0
- package/.claude/commands/ant/focus.md +58 -0
- package/.claude/commands/ant/help.md +122 -0
- package/.claude/commands/ant/history.md +137 -0
- package/.claude/commands/ant/init.md +409 -0
- package/.claude/commands/ant/interpret.md +267 -0
- package/.claude/commands/ant/lay-eggs.md +201 -0
- package/.claude/commands/ant/maturity.md +102 -0
- package/.claude/commands/ant/memory-details.md +77 -0
- package/.claude/commands/ant/migrate-state.md +165 -0
- package/.claude/commands/ant/oracle.md +387 -0
- package/.claude/commands/ant/organize.md +227 -0
- package/.claude/commands/ant/pause-colony.md +247 -0
- package/.claude/commands/ant/phase.md +126 -0
- package/.claude/commands/ant/plan.md +544 -0
- package/.claude/commands/ant/redirect.md +58 -0
- package/.claude/commands/ant/resume-colony.md +182 -0
- package/.claude/commands/ant/resume.md +363 -0
- package/.claude/commands/ant/seal.md +306 -0
- package/.claude/commands/ant/status.md +272 -0
- package/.claude/commands/ant/swarm.md +361 -0
- package/.claude/commands/ant/tunnels.md +425 -0
- package/.claude/commands/ant/update.md +209 -0
- package/.claude/commands/ant/verify-castes.md +95 -0
- package/.claude/commands/ant/watch.md +238 -0
- package/.opencode/agents/aether-ambassador.md +140 -0
- package/.opencode/agents/aether-archaeologist.md +108 -0
- package/.opencode/agents/aether-auditor.md +144 -0
- package/.opencode/agents/aether-builder.md +184 -0
- package/.opencode/agents/aether-chaos.md +115 -0
- package/.opencode/agents/aether-chronicler.md +122 -0
- package/.opencode/agents/aether-gatekeeper.md +116 -0
- package/.opencode/agents/aether-includer.md +117 -0
- package/.opencode/agents/aether-keeper.md +177 -0
- package/.opencode/agents/aether-measurer.md +128 -0
- package/.opencode/agents/aether-probe.md +133 -0
- package/.opencode/agents/aether-queen.md +286 -0
- package/.opencode/agents/aether-route-setter.md +130 -0
- package/.opencode/agents/aether-sage.md +106 -0
- package/.opencode/agents/aether-scout.md +101 -0
- package/.opencode/agents/aether-surveyor-disciplines.md +386 -0
- package/.opencode/agents/aether-surveyor-nest.md +324 -0
- package/.opencode/agents/aether-surveyor-pathogens.md +259 -0
- package/.opencode/agents/aether-surveyor-provisions.md +329 -0
- package/.opencode/agents/aether-tracker.md +137 -0
- package/.opencode/agents/aether-watcher.md +174 -0
- package/.opencode/agents/aether-weaver.md +130 -0
- package/.opencode/commands/ant/archaeology.md +338 -0
- package/.opencode/commands/ant/build.md +1200 -0
- package/.opencode/commands/ant/chaos.md +346 -0
- package/.opencode/commands/ant/colonize.md +202 -0
- package/.opencode/commands/ant/continue.md +938 -0
- package/.opencode/commands/ant/council.md +305 -0
- package/.opencode/commands/ant/dream.md +262 -0
- package/.opencode/commands/ant/entomb.md +367 -0
- package/.opencode/commands/ant/feedback.md +80 -0
- package/.opencode/commands/ant/flag.md +137 -0
- package/.opencode/commands/ant/flags.md +153 -0
- package/.opencode/commands/ant/focus.md +56 -0
- package/.opencode/commands/ant/help.md +124 -0
- package/.opencode/commands/ant/history.md +127 -0
- package/.opencode/commands/ant/init.md +337 -0
- package/.opencode/commands/ant/interpret.md +256 -0
- package/.opencode/commands/ant/lay-eggs.md +141 -0
- package/.opencode/commands/ant/maturity.md +92 -0
- package/.opencode/commands/ant/memory-details.md +77 -0
- package/.opencode/commands/ant/migrate-state.md +153 -0
- package/.opencode/commands/ant/oracle.md +338 -0
- package/.opencode/commands/ant/organize.md +224 -0
- package/.opencode/commands/ant/pause-colony.md +220 -0
- package/.opencode/commands/ant/phase.md +123 -0
- package/.opencode/commands/ant/plan.md +531 -0
- package/.opencode/commands/ant/redirect.md +67 -0
- package/.opencode/commands/ant/resume-colony.md +178 -0
- package/.opencode/commands/ant/resume.md +363 -0
- package/.opencode/commands/ant/seal.md +247 -0
- package/.opencode/commands/ant/status.md +272 -0
- package/.opencode/commands/ant/swarm.md +357 -0
- package/.opencode/commands/ant/tunnels.md +406 -0
- package/.opencode/commands/ant/update.md +191 -0
- package/.opencode/commands/ant/verify-castes.md +85 -0
- package/.opencode/commands/ant/watch.md +220 -0
- package/.opencode/opencode.json +3 -0
- package/CHANGELOG.md +325 -0
- package/DISCLAIMER.md +74 -0
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/bin/cli.js +2436 -0
- package/bin/generate-commands.sh +291 -0
- package/bin/lib/caste-colors.js +57 -0
- package/bin/lib/colors.js +76 -0
- package/bin/lib/errors.js +255 -0
- package/bin/lib/event-types.js +190 -0
- package/bin/lib/file-lock.js +695 -0
- package/bin/lib/init.js +454 -0
- package/bin/lib/logger.js +242 -0
- package/bin/lib/model-profiles.js +445 -0
- package/bin/lib/model-verify.js +288 -0
- package/bin/lib/nestmate-loader.js +130 -0
- package/bin/lib/proxy-health.js +253 -0
- package/bin/lib/spawn-logger.js +266 -0
- package/bin/lib/state-guard.js +602 -0
- package/bin/lib/state-sync.js +516 -0
- package/bin/lib/telemetry.js +441 -0
- package/bin/lib/update-transaction.js +1454 -0
- package/bin/npx-install.js +178 -0
- package/bin/sync-to-runtime.sh +6 -0
- package/bin/validate-package.sh +88 -0
- 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
|
+
};
|