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,288 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Model Routing Verification Module
|
|
4
|
+
*
|
|
5
|
+
* Verifies that model routing configuration is actually working.
|
|
6
|
+
* Addresses the gap between "configuration exists" and "execution verified".
|
|
7
|
+
*
|
|
8
|
+
* @module bin/lib/model-verify
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if LiteLLM proxy is running
|
|
17
|
+
* @returns {Promise<object>} Proxy status: { running: boolean, latency: number|null }
|
|
18
|
+
*/
|
|
19
|
+
async function checkLiteLLMProxy() {
|
|
20
|
+
const startTime = Date.now();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// Try to connect to LiteLLM proxy health endpoint
|
|
24
|
+
const response = await fetch('http://localhost:4000/health', {
|
|
25
|
+
method: 'GET',
|
|
26
|
+
signal: AbortSignal.timeout(5000)
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const latency = Date.now() - startTime;
|
|
30
|
+
|
|
31
|
+
if (response.ok) {
|
|
32
|
+
return {
|
|
33
|
+
running: true,
|
|
34
|
+
latency,
|
|
35
|
+
status: response.status
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
running: false,
|
|
41
|
+
latency: null,
|
|
42
|
+
status: response.status
|
|
43
|
+
};
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return {
|
|
46
|
+
running: false,
|
|
47
|
+
latency: null,
|
|
48
|
+
error: error.message
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Verify model assignment for a caste using aether-utils.sh
|
|
55
|
+
* @param {string} caste - Caste name (prime, builder, oracle, etc.)
|
|
56
|
+
* @returns {object} Model assignment: { assigned: boolean, model: string|null, via: string }
|
|
57
|
+
*/
|
|
58
|
+
function verifyModelAssignment(caste) {
|
|
59
|
+
try {
|
|
60
|
+
// Check if aether-utils.sh exists
|
|
61
|
+
const utilsPath = path.join(process.cwd(), '.aether', 'aether-utils.sh');
|
|
62
|
+
if (!fs.existsSync(utilsPath)) {
|
|
63
|
+
return {
|
|
64
|
+
assigned: false,
|
|
65
|
+
model: null,
|
|
66
|
+
via: 'not_found',
|
|
67
|
+
error: 'aether-utils.sh not found'
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Try to get model profile
|
|
72
|
+
const result = execSync(`bash "${utilsPath}" model-profile get ${caste}`, {
|
|
73
|
+
encoding: 'utf8',
|
|
74
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Parse result - should be JSON
|
|
78
|
+
try {
|
|
79
|
+
const profile = JSON.parse(result.trim());
|
|
80
|
+
return {
|
|
81
|
+
assigned: !!profile.model,
|
|
82
|
+
model: profile.model || null,
|
|
83
|
+
via: profile.source || 'unknown',
|
|
84
|
+
profile
|
|
85
|
+
};
|
|
86
|
+
} catch (parseError) {
|
|
87
|
+
return {
|
|
88
|
+
assigned: false,
|
|
89
|
+
model: null,
|
|
90
|
+
via: 'parse_error',
|
|
91
|
+
error: `Failed to parse profile: ${parseError.message}`,
|
|
92
|
+
raw: result
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
assigned: false,
|
|
98
|
+
model: null,
|
|
99
|
+
via: 'error',
|
|
100
|
+
error: error.message
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check ANTHROPIC_MODEL and ANTHROPIC_BASE_URL environment variables
|
|
107
|
+
* @returns {object} Environment status: { model: string|null, baseUrl: string|null, routingActive: boolean }
|
|
108
|
+
*/
|
|
109
|
+
function checkAnthropicModelEnv() {
|
|
110
|
+
const model = process.env.ANTHROPIC_MODEL || null;
|
|
111
|
+
const baseUrl = process.env.ANTHROPIC_BASE_URL || null;
|
|
112
|
+
|
|
113
|
+
// Routing is active if both are set and baseUrl points to LiteLLM proxy
|
|
114
|
+
const routingActive = !!model && !!baseUrl && baseUrl.includes('localhost:4000');
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
model,
|
|
118
|
+
baseUrl,
|
|
119
|
+
routingActive
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Verify worker spawn environment
|
|
125
|
+
* Simulates what build.md does before spawning workers
|
|
126
|
+
* @returns {object} Spawn verification: { wouldRoute: boolean, model: string, issues: string[] }
|
|
127
|
+
*/
|
|
128
|
+
function verifyWorkerSpawnEnv() {
|
|
129
|
+
const issues = [];
|
|
130
|
+
|
|
131
|
+
// Check ANTHROPIC_MODEL
|
|
132
|
+
const model = process.env.ANTHROPIC_MODEL;
|
|
133
|
+
if (!model) {
|
|
134
|
+
issues.push('ANTHROPIC_MODEL not set');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check ANTHROPIC_BASE_URL
|
|
138
|
+
const baseUrl = process.env.ANTHROPIC_BASE_URL;
|
|
139
|
+
if (!baseUrl) {
|
|
140
|
+
issues.push('ANTHROPIC_BASE_URL not set');
|
|
141
|
+
} else if (!baseUrl.includes('localhost:4000')) {
|
|
142
|
+
issues.push('ANTHROPIC_BASE_URL does not point to LiteLLM proxy (localhost:4000)');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check WORKER_NAME
|
|
146
|
+
const workerName = process.env.WORKER_NAME;
|
|
147
|
+
if (!workerName) {
|
|
148
|
+
issues.push('WORKER_NAME not set (optional but recommended)');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check CASTE
|
|
152
|
+
const caste = process.env.CASTE;
|
|
153
|
+
if (!caste) {
|
|
154
|
+
issues.push('CASTE not set (optional but recommended)');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Would routing work?
|
|
158
|
+
const wouldRoute = !!model && !!baseUrl && baseUrl.includes('localhost:4000');
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
wouldRoute,
|
|
162
|
+
model: model || 'default (claude-opus-4-6)',
|
|
163
|
+
caste: caste || 'unknown',
|
|
164
|
+
workerName: workerName || 'unknown',
|
|
165
|
+
issues
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check model profile configuration file
|
|
171
|
+
* @param {string} repoPath - Path to repository root
|
|
172
|
+
* @returns {object} Profile file status: { exists: boolean, path: string, profiles: object }
|
|
173
|
+
*/
|
|
174
|
+
function checkModelProfilesFile(repoPath) {
|
|
175
|
+
const profilesPath = path.join(repoPath, '.aether', 'model-profiles.yaml');
|
|
176
|
+
|
|
177
|
+
if (!fs.existsSync(profilesPath)) {
|
|
178
|
+
return {
|
|
179
|
+
exists: false,
|
|
180
|
+
path: profilesPath,
|
|
181
|
+
profiles: {}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const content = fs.readFileSync(profilesPath, 'utf8');
|
|
187
|
+
|
|
188
|
+
// Simple YAML parsing for model profiles
|
|
189
|
+
// Looks for patterns like "prime: glm-5" or "builder: kimi-k2.5"
|
|
190
|
+
const profiles = {};
|
|
191
|
+
const lines = content.split('\n');
|
|
192
|
+
|
|
193
|
+
for (const line of lines) {
|
|
194
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
195
|
+
if (match) {
|
|
196
|
+
const [, caste, model] = match;
|
|
197
|
+
profiles[caste] = model.trim();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
exists: true,
|
|
203
|
+
path: profilesPath,
|
|
204
|
+
profiles
|
|
205
|
+
};
|
|
206
|
+
} catch (error) {
|
|
207
|
+
return {
|
|
208
|
+
exists: true,
|
|
209
|
+
path: profilesPath,
|
|
210
|
+
error: error.message,
|
|
211
|
+
profiles: {}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Create comprehensive verification report
|
|
218
|
+
* @param {string} repoPath - Path to repository root
|
|
219
|
+
* @returns {Promise<object>} Complete verification report
|
|
220
|
+
*/
|
|
221
|
+
async function createVerificationReport(repoPath) {
|
|
222
|
+
const issues = [];
|
|
223
|
+
|
|
224
|
+
// Check LiteLLM proxy
|
|
225
|
+
const proxy = await checkLiteLLMProxy();
|
|
226
|
+
if (!proxy.running) {
|
|
227
|
+
issues.push('LiteLLM proxy is not running on localhost:4000');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check environment
|
|
231
|
+
const env = checkAnthropicModelEnv();
|
|
232
|
+
if (!env.routingActive) {
|
|
233
|
+
issues.push('ANTHROPIC_MODEL/ANTHROPIC_BASE_URL not configured for proxy routing');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check model profiles file
|
|
237
|
+
const profilesFile = checkModelProfilesFile(repoPath);
|
|
238
|
+
if (!profilesFile.exists) {
|
|
239
|
+
issues.push('Model profiles file not found (.aether/model-profiles.yaml)');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Verify caste assignments
|
|
243
|
+
const castes = {};
|
|
244
|
+
const casteNames = ['prime', 'builder', 'oracle', 'scout'];
|
|
245
|
+
|
|
246
|
+
for (const caste of casteNames) {
|
|
247
|
+
const assignment = verifyModelAssignment(caste);
|
|
248
|
+
castes[caste] = assignment;
|
|
249
|
+
|
|
250
|
+
if (!assignment.assigned) {
|
|
251
|
+
issues.push(`Model not assigned for caste: ${caste}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check worker spawn environment
|
|
256
|
+
const spawnEnv = verifyWorkerSpawnEnv();
|
|
257
|
+
|
|
258
|
+
// Generate recommendation
|
|
259
|
+
let recommendation;
|
|
260
|
+
if (issues.length === 0) {
|
|
261
|
+
recommendation = 'All checks passed. Model routing is properly configured and verified.';
|
|
262
|
+
} else if (!proxy.running) {
|
|
263
|
+
recommendation = 'Start LiteLLM proxy: litellm --config /path/to/config.yaml';
|
|
264
|
+
} else if (!env.routingActive) {
|
|
265
|
+
recommendation = 'Set environment variables: export ANTHROPIC_MODEL=your-model && export ANTHROPIC_BASE_URL=http://localhost:4000';
|
|
266
|
+
} else {
|
|
267
|
+
recommendation = 'Review configuration files and environment variables';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
proxy,
|
|
272
|
+
env,
|
|
273
|
+
profilesFile,
|
|
274
|
+
castes,
|
|
275
|
+
spawnEnv,
|
|
276
|
+
issues,
|
|
277
|
+
recommendation
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = {
|
|
282
|
+
checkLiteLLMProxy,
|
|
283
|
+
verifyModelAssignment,
|
|
284
|
+
checkAnthropicModelEnv,
|
|
285
|
+
verifyWorkerSpawnEnv,
|
|
286
|
+
checkModelProfilesFile,
|
|
287
|
+
createVerificationReport
|
|
288
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Find nestmate colonies (sibling directories with .aether/)
|
|
6
|
+
* @param {string} currentRepoPath - Current repository path
|
|
7
|
+
* @returns {Array<{name: string, path: string, goal: string|null}>} Nestmate info
|
|
8
|
+
*/
|
|
9
|
+
function findNestmates(currentRepoPath) {
|
|
10
|
+
const parentDir = path.dirname(currentRepoPath);
|
|
11
|
+
const currentName = path.basename(currentRepoPath);
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const entries = fs.readdirSync(parentDir, { withFileTypes: true });
|
|
15
|
+
const nestmates = [];
|
|
16
|
+
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (!entry.isDirectory()) continue;
|
|
19
|
+
if (entry.name === currentName) continue;
|
|
20
|
+
if (entry.name.startsWith('.')) continue;
|
|
21
|
+
|
|
22
|
+
const siblingPath = path.join(parentDir, entry.name);
|
|
23
|
+
const aetherPath = path.join(siblingPath, '.aether');
|
|
24
|
+
|
|
25
|
+
if (fs.existsSync(aetherPath)) {
|
|
26
|
+
// Try to read colony goal from state
|
|
27
|
+
let goal = null;
|
|
28
|
+
try {
|
|
29
|
+
const statePath = path.join(aetherPath, 'data', 'COLONY_STATE.json');
|
|
30
|
+
if (fs.existsSync(statePath)) {
|
|
31
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
32
|
+
goal = state.goal || null;
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// Ignore read errors
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
nestmates.push({
|
|
39
|
+
name: entry.name,
|
|
40
|
+
path: siblingPath,
|
|
41
|
+
goal: goal
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return nestmates;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load TO-DOs from a nestmate
|
|
54
|
+
* @param {string} nestmatePath - Path to nestmate repository
|
|
55
|
+
* @returns {Array<{file: string, todos: string[]}>} TO-DO items
|
|
56
|
+
*/
|
|
57
|
+
function loadNestmateTodos(nestmatePath) {
|
|
58
|
+
const todos = [];
|
|
59
|
+
const planningPath = path.join(nestmatePath, '.planning');
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Look for TODO files in .planning/
|
|
63
|
+
if (fs.existsSync(planningPath)) {
|
|
64
|
+
const entries = fs.readdirSync(planningPath);
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (entry.toLowerCase().includes('todo')) {
|
|
67
|
+
const todoPath = path.join(planningPath, entry);
|
|
68
|
+
const content = fs.readFileSync(todoPath, 'utf8');
|
|
69
|
+
// Extract TODO items (lines starting with - [ ] or TODO:)
|
|
70
|
+
const items = content.split('\n')
|
|
71
|
+
.filter(line => line.match(/^\s*-\s*\[\s*\]|TODO:/i))
|
|
72
|
+
.map(line => line.trim());
|
|
73
|
+
|
|
74
|
+
if (items.length > 0) {
|
|
75
|
+
todos.push({ file: entry, items });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// Ignore errors
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return todos;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get colony state summary from a nestmate
|
|
89
|
+
* @param {string} nestmatePath - Path to nestmate repository
|
|
90
|
+
* @returns {Object|null} State summary
|
|
91
|
+
*/
|
|
92
|
+
function getNestmateState(nestmatePath) {
|
|
93
|
+
try {
|
|
94
|
+
const statePath = path.join(nestmatePath, '.aether', 'data', 'COLONY_STATE.json');
|
|
95
|
+
if (!fs.existsSync(statePath)) return null;
|
|
96
|
+
|
|
97
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
98
|
+
return {
|
|
99
|
+
goal: state.goal,
|
|
100
|
+
state: state.state,
|
|
101
|
+
currentPhase: state.current_phase,
|
|
102
|
+
milestone: state.milestone
|
|
103
|
+
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Format nestmate info for display
|
|
111
|
+
* @param {Array} nestmates - Nestmate array from findNestmates
|
|
112
|
+
* @returns {string} Formatted string
|
|
113
|
+
*/
|
|
114
|
+
function formatNestmates(nestmates) {
|
|
115
|
+
if (nestmates.length === 0) {
|
|
116
|
+
return 'No nestmates found.';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return nestmates.map(n => {
|
|
120
|
+
const goal = n.goal ? ` - ${n.goal.substring(0, 40)}${n.goal.length > 40 ? '...' : ''}` : '';
|
|
121
|
+
return ` • ${n.name}${goal}`;
|
|
122
|
+
}).join('\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
findNestmates,
|
|
127
|
+
loadNestmateTodos,
|
|
128
|
+
getNestmateState,
|
|
129
|
+
formatNestmates
|
|
130
|
+
};
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Proxy Health Library
|
|
4
|
+
*
|
|
5
|
+
* Health checking utilities for LiteLLM proxy.
|
|
6
|
+
* Provides functions to verify proxy status, model availability, and routing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { ConfigurationError } = require('./errors');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default timeout for health check requests (ms)
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_TIMEOUT = 5000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check proxy health endpoint
|
|
18
|
+
* @param {string} endpoint - Proxy endpoint URL (e.g., http://localhost:4000)
|
|
19
|
+
* @param {number} timeoutMs - Timeout in milliseconds (default: 5000)
|
|
20
|
+
* @returns {Promise<object>} Health status object
|
|
21
|
+
* - healthy: boolean - Whether proxy is healthy
|
|
22
|
+
* - status: number - HTTP status code
|
|
23
|
+
* - latency: number - Response time in milliseconds
|
|
24
|
+
* - error: string|null - Error message if unhealthy
|
|
25
|
+
* - models: string[]|null - Available model IDs if healthy
|
|
26
|
+
*/
|
|
27
|
+
async function checkProxyHealth(endpoint, timeoutMs = DEFAULT_TIMEOUT) {
|
|
28
|
+
const startTime = Date.now();
|
|
29
|
+
const healthUrl = `${endpoint.replace(/\/$/, '')}/health`;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Use native fetch with AbortSignal for timeout (Node 18+)
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
35
|
+
|
|
36
|
+
const response = await fetch(healthUrl, {
|
|
37
|
+
method: 'GET',
|
|
38
|
+
signal: controller.signal,
|
|
39
|
+
headers: {
|
|
40
|
+
'Accept': 'application/json',
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
clearTimeout(timeoutId);
|
|
45
|
+
const latency = Date.now() - startTime;
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
return {
|
|
49
|
+
healthy: false,
|
|
50
|
+
status: response.status,
|
|
51
|
+
latency,
|
|
52
|
+
error: `HTTP ${response.status}: ${response.statusText}`,
|
|
53
|
+
models: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Try to get models list
|
|
58
|
+
let models = null;
|
|
59
|
+
try {
|
|
60
|
+
models = await getProxyModels(endpoint, timeoutMs);
|
|
61
|
+
} catch {
|
|
62
|
+
// Models endpoint may fail even if health passes
|
|
63
|
+
models = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
healthy: true,
|
|
68
|
+
status: response.status,
|
|
69
|
+
latency,
|
|
70
|
+
error: null,
|
|
71
|
+
models,
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const latency = Date.now() - startTime;
|
|
75
|
+
|
|
76
|
+
if (error.name === 'AbortError') {
|
|
77
|
+
return {
|
|
78
|
+
healthy: false,
|
|
79
|
+
status: 0,
|
|
80
|
+
latency,
|
|
81
|
+
error: `Timeout after ${timeoutMs}ms`,
|
|
82
|
+
models: null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Handle network errors
|
|
87
|
+
const errorMessage = error.message || 'Unknown error';
|
|
88
|
+
return {
|
|
89
|
+
healthy: false,
|
|
90
|
+
status: 0,
|
|
91
|
+
latency,
|
|
92
|
+
error: errorMessage,
|
|
93
|
+
models: null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Verify a specific model is routable through the proxy
|
|
100
|
+
* @param {string} endpoint - Proxy endpoint URL
|
|
101
|
+
* @param {string} model - Model name to verify
|
|
102
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
103
|
+
* @returns {Promise<object>} Verification result
|
|
104
|
+
* - available: boolean - Whether model is available on proxy
|
|
105
|
+
* - found: boolean - Whether model was found in proxy's model list
|
|
106
|
+
* - model: string - The model name that was checked
|
|
107
|
+
*/
|
|
108
|
+
async function verifyModelRouting(endpoint, model, timeoutMs = DEFAULT_TIMEOUT) {
|
|
109
|
+
try {
|
|
110
|
+
const models = await getProxyModels(endpoint, timeoutMs);
|
|
111
|
+
|
|
112
|
+
if (!models) {
|
|
113
|
+
return {
|
|
114
|
+
available: false,
|
|
115
|
+
found: false,
|
|
116
|
+
model,
|
|
117
|
+
error: 'Could not fetch models from proxy',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const found = models.includes(model);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
available: found,
|
|
125
|
+
found,
|
|
126
|
+
model,
|
|
127
|
+
error: found ? null : `Model '${model}' not found in proxy model list`,
|
|
128
|
+
};
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return {
|
|
131
|
+
available: false,
|
|
132
|
+
found: false,
|
|
133
|
+
model,
|
|
134
|
+
error: error.message || 'Failed to verify model routing',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Fetch available models from proxy
|
|
141
|
+
* @param {string} endpoint - Proxy endpoint URL
|
|
142
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
143
|
+
* @returns {Promise<string[]|null>} Array of model IDs or null on error
|
|
144
|
+
*/
|
|
145
|
+
async function getProxyModels(endpoint, timeoutMs = DEFAULT_TIMEOUT) {
|
|
146
|
+
const modelsUrl = `${endpoint.replace(/\/$/, '')}/models`;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
151
|
+
|
|
152
|
+
const response = await fetch(modelsUrl, {
|
|
153
|
+
method: 'GET',
|
|
154
|
+
signal: controller.signal,
|
|
155
|
+
headers: {
|
|
156
|
+
'Accept': 'application/json',
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
clearTimeout(timeoutId);
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const data = await response.json();
|
|
167
|
+
|
|
168
|
+
// LiteLLM returns models in OpenAI format: { data: [{ id: 'model-name' }, ...] }
|
|
169
|
+
if (data && Array.isArray(data.data)) {
|
|
170
|
+
return data.data.map(m => m.id).filter(Boolean);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Fallback: try to extract models from various formats
|
|
174
|
+
if (Array.isArray(data)) {
|
|
175
|
+
return data.map(m => m.id || m.name || m).filter(Boolean);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return null;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Format proxy health status for display
|
|
186
|
+
* @param {object} health - Health status object from checkProxyHealth
|
|
187
|
+
* @returns {string} Formatted status string with colors/emoji
|
|
188
|
+
*/
|
|
189
|
+
function formatProxyStatus(health) {
|
|
190
|
+
if (!health) {
|
|
191
|
+
return 'Unknown';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (health.healthy) {
|
|
195
|
+
const latencyStr = health.latency ? `(${health.latency}ms)` : '';
|
|
196
|
+
return `✓ Healthy ${latencyStr}`.trim();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const errorStr = health.error || 'Unknown error';
|
|
200
|
+
return `✗ Unhealthy: ${errorStr}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Format proxy health status with ANSI colors
|
|
205
|
+
* @param {object} health - Health status object
|
|
206
|
+
* @param {object} colors - Color functions from colors.js
|
|
207
|
+
* @returns {string} Colored status string
|
|
208
|
+
*/
|
|
209
|
+
function formatProxyStatusColored(health, colors) {
|
|
210
|
+
if (!health) {
|
|
211
|
+
return colors.dim('Unknown');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (health.healthy) {
|
|
215
|
+
const latencyStr = health.latency ? `(${health.latency}ms)` : '';
|
|
216
|
+
return `${colors.success('✓')} ${colors.success('Healthy')} ${colors.dim(latencyStr)}`.trim();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const errorStr = health.error || 'Unknown error';
|
|
220
|
+
return `${colors.error('✗')} ${colors.error('Unhealthy')}: ${errorStr}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Verify caste model assignments against proxy
|
|
225
|
+
* @param {string} endpoint - Proxy endpoint URL
|
|
226
|
+
* @param {object} profiles - Model profiles object
|
|
227
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
228
|
+
* @returns {Promise<object>} Verification results for all castes
|
|
229
|
+
*/
|
|
230
|
+
async function verifyCasteModels(endpoint, profiles, timeoutMs = DEFAULT_TIMEOUT) {
|
|
231
|
+
const results = {};
|
|
232
|
+
const workerModels = profiles?.worker_models || {};
|
|
233
|
+
|
|
234
|
+
for (const [caste, model] of Object.entries(workerModels)) {
|
|
235
|
+
const verification = await verifyModelRouting(endpoint, model, timeoutMs);
|
|
236
|
+
results[caste] = {
|
|
237
|
+
model,
|
|
238
|
+
...verification,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return results;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = {
|
|
246
|
+
checkProxyHealth,
|
|
247
|
+
verifyModelRouting,
|
|
248
|
+
getProxyModels,
|
|
249
|
+
formatProxyStatus,
|
|
250
|
+
formatProxyStatusColored,
|
|
251
|
+
verifyCasteModels,
|
|
252
|
+
DEFAULT_TIMEOUT,
|
|
253
|
+
};
|