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,441 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Telemetry Module
4
+ *
5
+ * Tracks model performance and routing decisions for data-driven model selection.
6
+ * Records every spawn with model, caste, task, and routing source.
7
+ * Tracks success/failure rates per model-caste combination.
8
+ * Rotates at 1000 routing decisions to prevent unbounded growth.
9
+ * Uses atomic writes (temp file + rename) for data integrity.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const TELEMETRY_VERSION = '1.0';
16
+ const MAX_ROUTING_DECISIONS = 1000;
17
+ const DEFAULT_TELEMETRY_FILE = 'telemetry.json';
18
+
19
+ /**
20
+ * Get the telemetry file path
21
+ * @param {string} repoPath - Repository root path
22
+ * @returns {string} Full path to telemetry.json
23
+ */
24
+ function getTelemetryPath(repoPath) {
25
+ return path.join(repoPath, '.aether', 'data', DEFAULT_TELEMETRY_FILE);
26
+ }
27
+
28
+ /**
29
+ * Load telemetry data from file
30
+ * @param {string} repoPath - Repository root path
31
+ * @returns {Object} Parsed telemetry data or default structure
32
+ */
33
+ function loadTelemetry(repoPath) {
34
+ const telemetryPath = getTelemetryPath(repoPath);
35
+
36
+ try {
37
+ if (!fs.existsSync(telemetryPath)) {
38
+ return createDefaultTelemetry();
39
+ }
40
+
41
+ const content = fs.readFileSync(telemetryPath, 'utf8');
42
+ const data = JSON.parse(content);
43
+
44
+ // Validate structure
45
+ if (!data.version || !data.models || !Array.isArray(data.routing_decisions)) {
46
+ return createDefaultTelemetry();
47
+ }
48
+
49
+ return data;
50
+ } catch (error) {
51
+ // Return default on any error (corrupted file, permission issues, etc.)
52
+ return createDefaultTelemetry();
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Create default telemetry structure
58
+ * @returns {Object} Default telemetry object
59
+ */
60
+ function createDefaultTelemetry() {
61
+ return {
62
+ version: TELEMETRY_VERSION,
63
+ last_updated: new Date().toISOString(),
64
+ models: {},
65
+ routing_decisions: []
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Save telemetry data atomically
71
+ * @param {string} repoPath - Repository root path
72
+ * @param {Object} data - Telemetry data to save
73
+ * @returns {boolean} True if saved successfully
74
+ */
75
+ function saveTelemetry(repoPath, data) {
76
+ const telemetryPath = getTelemetryPath(repoPath);
77
+
78
+ try {
79
+ // Ensure directory exists
80
+ const dataDir = path.dirname(telemetryPath);
81
+ if (!fs.existsSync(dataDir)) {
82
+ fs.mkdirSync(dataDir, { recursive: true });
83
+ }
84
+
85
+ // Update timestamp
86
+ data.last_updated = new Date().toISOString();
87
+
88
+ // Atomic write: write to temp file, then rename
89
+ const tempPath = `${telemetryPath}.tmp`;
90
+ fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf8');
91
+ fs.renameSync(tempPath, telemetryPath);
92
+
93
+ return true;
94
+ } catch (error) {
95
+ // Silent fail - don't cascade errors from telemetry
96
+ return false;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Record spawn telemetry
102
+ * @param {string} repoPath - Repository root path
103
+ * @param {Object} spawnInfo - Spawn details
104
+ * @param {string} spawnInfo.task - Task description
105
+ * @param {string} spawnInfo.caste - Worker caste (e.g., "builder")
106
+ * @param {string} spawnInfo.model - Model used (e.g., "kimi-k2.5")
107
+ * @param {string} spawnInfo.source - Routing source (e.g., "caste-default", "task-based")
108
+ * @param {string} [spawnInfo.timestamp] - Optional timestamp (defaults to now)
109
+ * @returns {Object} Result with success flag and decision_id
110
+ */
111
+ function recordSpawnTelemetry(repoPath, { task, caste, model, source, timestamp }) {
112
+ try {
113
+ const data = loadTelemetry(repoPath);
114
+ const decisionTimestamp = timestamp || new Date().toISOString();
115
+
116
+ // Initialize model stats if not exists
117
+ if (!data.models[model]) {
118
+ data.models[model] = {
119
+ total_spawns: 0,
120
+ successful_completions: 0,
121
+ failed_completions: 0,
122
+ blocked: 0,
123
+ by_caste: {}
124
+ };
125
+ }
126
+
127
+ const modelStats = data.models[model];
128
+
129
+ // Increment total spawns
130
+ modelStats.total_spawns++;
131
+
132
+ // Initialize caste stats if not exists
133
+ if (!modelStats.by_caste[caste]) {
134
+ modelStats.by_caste[caste] = {
135
+ spawns: 0,
136
+ success: 0,
137
+ failures: 0,
138
+ blocked: 0
139
+ };
140
+ }
141
+
142
+ // Increment caste spawns
143
+ modelStats.by_caste[caste].spawns++;
144
+
145
+ // Create routing decision record with activity tracking fields
146
+ const decision = {
147
+ timestamp: decisionTimestamp,
148
+ task: task || 'unknown',
149
+ caste: caste || 'unknown',
150
+ selected_model: model || 'default',
151
+ source: source || 'unknown',
152
+ tools: { read: 0, grep: 0, edit: 0, bash: 0 },
153
+ tokens: 0,
154
+ started_at: decisionTimestamp
155
+ };
156
+
157
+ // Append to routing decisions
158
+ data.routing_decisions.push(decision);
159
+
160
+ // Rotate if exceeds max
161
+ if (data.routing_decisions.length > MAX_ROUTING_DECISIONS) {
162
+ data.routing_decisions = data.routing_decisions.slice(-MAX_ROUTING_DECISIONS);
163
+ }
164
+
165
+ // Save atomically
166
+ const saved = saveTelemetry(repoPath, data);
167
+
168
+ return {
169
+ success: saved,
170
+ decision_id: decisionTimestamp
171
+ };
172
+ } catch (error) {
173
+ return {
174
+ success: false,
175
+ decision_id: null,
176
+ error: error.message
177
+ };
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Update spawn outcome
183
+ * @param {string} repoPath - Repository root path
184
+ * @param {string} spawnId - Spawn identifier (timestamp from recordSpawnTelemetry)
185
+ * @param {string} outcome - Outcome: 'completed' | 'failed' | 'blocked'
186
+ * @returns {boolean} True if updated successfully
187
+ */
188
+ function updateSpawnOutcome(repoPath, spawnId, outcome) {
189
+ try {
190
+ const data = loadTelemetry(repoPath);
191
+
192
+ // Find the routing decision by timestamp
193
+ const decision = data.routing_decisions.find(d => d.timestamp === spawnId);
194
+
195
+ if (!decision) {
196
+ return false;
197
+ }
198
+
199
+ const { selected_model: model, caste } = decision;
200
+
201
+ // Ensure model exists
202
+ if (!data.models[model]) {
203
+ return false;
204
+ }
205
+
206
+ const modelStats = data.models[model];
207
+
208
+ // Update model-level counters
209
+ switch (outcome) {
210
+ case 'completed':
211
+ modelStats.successful_completions++;
212
+ break;
213
+ case 'failed':
214
+ modelStats.failed_completions++;
215
+ break;
216
+ case 'blocked':
217
+ modelStats.blocked++;
218
+ break;
219
+ default:
220
+ return false;
221
+ }
222
+
223
+ // Update caste-level counters
224
+ if (modelStats.by_caste[caste]) {
225
+ switch (outcome) {
226
+ case 'completed':
227
+ modelStats.by_caste[caste].success++;
228
+ break;
229
+ case 'failed':
230
+ modelStats.by_caste[caste].failures++;
231
+ break;
232
+ case 'blocked':
233
+ modelStats.by_caste[caste].blocked++;
234
+ break;
235
+ }
236
+ }
237
+
238
+ // Save atomically
239
+ return saveTelemetry(repoPath, data);
240
+ } catch (error) {
241
+ return false;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Get telemetry summary
247
+ * @param {string} repoPath - Repository root path
248
+ * @returns {Object} Summary of telemetry data
249
+ */
250
+ function getTelemetrySummary(repoPath) {
251
+ const data = loadTelemetry(repoPath);
252
+
253
+ const totalSpawns = Object.values(data.models).reduce(
254
+ (sum, model) => sum + (model.total_spawns || 0),
255
+ 0
256
+ );
257
+
258
+ const models = {};
259
+ for (const [modelName, stats] of Object.entries(data.models)) {
260
+ const successRate = stats.total_spawns > 0
261
+ ? (stats.successful_completions / stats.total_spawns)
262
+ : 0;
263
+
264
+ models[modelName] = {
265
+ total_spawns: stats.total_spawns,
266
+ success_rate: Math.round(successRate * 100) / 100,
267
+ by_caste: stats.by_caste || {}
268
+ };
269
+ }
270
+
271
+ // Get last 10 routing decisions
272
+ const recentDecisions = data.routing_decisions.slice(-10);
273
+
274
+ return {
275
+ total_spawns: totalSpawns,
276
+ total_models: Object.keys(data.models).length,
277
+ models,
278
+ recent_decisions: recentDecisions
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Get detailed performance for a specific model
284
+ * @param {string} repoPath - Repository root path
285
+ * @param {string} model - Model name
286
+ * @returns {Object|null} Model performance data or null if not found
287
+ */
288
+ function getModelPerformance(repoPath, model) {
289
+ const data = loadTelemetry(repoPath);
290
+
291
+ if (!data.models[model]) {
292
+ return null;
293
+ }
294
+
295
+ const stats = data.models[model];
296
+ const successRate = stats.total_spawns > 0
297
+ ? (stats.successful_completions / stats.total_spawns)
298
+ : 0;
299
+
300
+ return {
301
+ model,
302
+ total_spawns: stats.total_spawns,
303
+ successful_completions: stats.successful_completions || 0,
304
+ failed_completions: stats.failed_completions || 0,
305
+ blocked: stats.blocked || 0,
306
+ success_rate: Math.round(successRate * 100) / 100,
307
+ by_caste: stats.by_caste || {}
308
+ };
309
+ }
310
+
311
+ /**
312
+ * Get routing statistics with optional filtering
313
+ * @param {string} repoPath - Repository root path
314
+ * @param {Object} options - Filter options
315
+ * @param {number} [options.days] - Filter to last N days
316
+ * @param {string} [options.caste] - Filter by caste
317
+ * @returns {Object} Routing statistics
318
+ */
319
+ function getRoutingStats(repoPath, options = {}) {
320
+ const data = loadTelemetry(repoPath);
321
+ const { days, caste } = options;
322
+
323
+ let decisions = [...data.routing_decisions];
324
+
325
+ // Filter by days
326
+ if (days && days > 0) {
327
+ const cutoffDate = new Date();
328
+ cutoffDate.setDate(cutoffDate.getDate() - days);
329
+
330
+ decisions = decisions.filter(d => new Date(d.timestamp) >= cutoffDate);
331
+ }
332
+
333
+ // Filter by caste
334
+ if (caste) {
335
+ decisions = decisions.filter(d => d.caste === caste);
336
+ }
337
+
338
+ // Calculate stats
339
+ const bySource = {};
340
+ const byModel = {};
341
+
342
+ for (const decision of decisions) {
343
+ // Count by source
344
+ bySource[decision.source] = (bySource[decision.source] || 0) + 1;
345
+
346
+ // Count by model
347
+ byModel[decision.selected_model] = (byModel[decision.selected_model] || 0) + 1;
348
+ }
349
+
350
+ return {
351
+ total_decisions: decisions.length,
352
+ by_source: bySource,
353
+ by_model: byModel,
354
+ date_range: decisions.length > 0 ? {
355
+ earliest: decisions[0].timestamp,
356
+ latest: decisions[decisions.length - 1].timestamp
357
+ } : null
358
+ };
359
+ }
360
+
361
+ /**
362
+ * Update tool usage counter for a spawn
363
+ * @param {string} repoPath - Repository root path
364
+ * @param {string} spawnId - Spawn identifier (timestamp from recordSpawnTelemetry)
365
+ * @param {string} toolType - Tool type: 'read', 'grep', 'edit', 'bash'
366
+ * @param {number} [count=1] - Number to increment by
367
+ * @returns {boolean} True if updated successfully
368
+ */
369
+ function updateToolUsage(repoPath, spawnId, toolType, count = 1) {
370
+ try {
371
+ const data = loadTelemetry(repoPath);
372
+
373
+ // Find the routing decision by timestamp
374
+ const decision = data.routing_decisions.find(d => d.timestamp === spawnId);
375
+
376
+ if (!decision) {
377
+ return false;
378
+ }
379
+
380
+ // Initialize tools object if not exists
381
+ if (!decision.tools) {
382
+ decision.tools = { read: 0, grep: 0, edit: 0, bash: 0 };
383
+ }
384
+
385
+ // Validate tool type
386
+ const validTools = ['read', 'grep', 'edit', 'bash'];
387
+ if (!validTools.includes(toolType)) {
388
+ return false;
389
+ }
390
+
391
+ // Increment the specified tool counter
392
+ decision.tools[toolType] += count;
393
+
394
+ // Save atomically
395
+ return saveTelemetry(repoPath, data);
396
+ } catch (error) {
397
+ return false;
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Update token usage (trophallaxis metrics) for a spawn
403
+ * @param {string} repoPath - Repository root path
404
+ * @param {string} spawnId - Spawn identifier (timestamp from recordSpawnTelemetry)
405
+ * @param {number} tokens - Number of tokens to add (cumulative)
406
+ * @returns {boolean} True if updated successfully
407
+ */
408
+ function updateTokenUsage(repoPath, spawnId, tokens) {
409
+ try {
410
+ const data = loadTelemetry(repoPath);
411
+
412
+ // Find the routing decision by timestamp
413
+ const decision = data.routing_decisions.find(d => d.timestamp === spawnId);
414
+
415
+ if (!decision) {
416
+ return false;
417
+ }
418
+
419
+ // Add tokens to existing count (cumulative)
420
+ decision.tokens = (decision.tokens || 0) + tokens;
421
+
422
+ // Save atomically
423
+ return saveTelemetry(repoPath, data);
424
+ } catch (error) {
425
+ return false;
426
+ }
427
+ }
428
+
429
+ module.exports = {
430
+ recordSpawnTelemetry,
431
+ updateSpawnOutcome,
432
+ updateToolUsage,
433
+ updateTokenUsage,
434
+ getTelemetrySummary,
435
+ getModelPerformance,
436
+ getRoutingStats,
437
+ loadTelemetry,
438
+ saveTelemetry,
439
+ TELEMETRY_VERSION,
440
+ MAX_ROUTING_DECISIONS
441
+ };