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,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
+ };