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,454 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Initialization Module
4
+ *
5
+ * Handles new repo initialization with local state files.
6
+ * Creates COLONY_STATE.json and required directory structure.
7
+ *
8
+ * @module bin/lib/init
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ // Hub paths (for copying system files during init)
15
+ const HOME = process.env.HOME || process.env.USERPROFILE;
16
+ const HUB_DIR = HOME ? path.join(HOME, '.aether') : null;
17
+ const HUB_SYSTEM = HUB_DIR ? path.join(HUB_DIR, 'system') : null;
18
+ const HUB_COMMANDS_CLAUDE = HUB_SYSTEM ? path.join(HUB_SYSTEM, 'commands', 'claude') : null;
19
+ const HUB_COMMANDS_OPENCODE = HUB_SYSTEM ? path.join(HUB_SYSTEM, 'commands', 'opencode') : null;
20
+ const HUB_AGENTS = HUB_SYSTEM ? path.join(HUB_SYSTEM, 'agents') : null;
21
+ const HUB_AGENTS_CLAUDE = HUB_SYSTEM ? path.join(HUB_SYSTEM, 'agents-claude') : null;
22
+ const HUB_REGISTRY = HUB_DIR ? path.join(HUB_DIR, 'registry.json') : null;
23
+ const HUB_VERSION = HUB_DIR ? path.join(HUB_DIR, 'version.json') : null;
24
+
25
+ /**
26
+ * Generate a unique session ID
27
+ * @returns {string} Session ID in format session_{timestamp}_{random}
28
+ */
29
+ function generateSessionId() {
30
+ const timestamp = Date.now();
31
+ const random = Math.random().toString(36).substring(2, 8);
32
+ return `session_${timestamp}_${random}`;
33
+ }
34
+
35
+ /**
36
+ * Read JSON file safely
37
+ * @param {string} filePath - Path to JSON file
38
+ * @returns {object|null} Parsed JSON or null if file doesn't exist or is invalid
39
+ */
40
+ function readJsonSafe(filePath) {
41
+ try {
42
+ if (!fs.existsSync(filePath)) return null;
43
+ const content = fs.readFileSync(filePath, 'utf8');
44
+ return JSON.parse(content);
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Write JSON file atomically
52
+ * @param {string} filePath - Path to write
53
+ * @param {object} data - Data to write
54
+ */
55
+ function writeJsonSync(filePath, data) {
56
+ const dir = path.dirname(filePath);
57
+ if (!fs.existsSync(dir)) {
58
+ fs.mkdirSync(dir, { recursive: true });
59
+ }
60
+ const tempFile = `${filePath}.tmp.${Date.now()}`;
61
+ fs.writeFileSync(tempFile, JSON.stringify(data, null, 2) + '\n');
62
+ fs.renameSync(tempFile, filePath);
63
+ }
64
+
65
+ /**
66
+ * List files recursively in a directory
67
+ * @param {string} dir - Directory to list
68
+ * @param {string} prefix - Prefix for relative paths
69
+ * @returns {string[]} Array of relative file paths
70
+ */
71
+ function listFilesRecursive(dir, prefix = '') {
72
+ const files = [];
73
+ if (!fs.existsSync(dir)) return files;
74
+
75
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
76
+ for (const entry of entries) {
77
+ const fullPath = path.join(dir, entry.name);
78
+ const relPath = prefix ? path.join(prefix, entry.name) : entry.name;
79
+
80
+ if (entry.isDirectory()) {
81
+ files.push(...listFilesRecursive(fullPath, relPath));
82
+ } else {
83
+ files.push(relPath);
84
+ }
85
+ }
86
+ return files;
87
+ }
88
+
89
+ /**
90
+ * Sync files from source to destination directory
91
+ * @param {string} src - Source directory
92
+ * @param {string} dest - Destination directory
93
+ * @returns {object} Result: { copied: number, skipped: number }
94
+ */
95
+ function syncFiles(src, dest) {
96
+ let copied = 0;
97
+ let skipped = 0;
98
+
99
+ if (!fs.existsSync(src)) {
100
+ return { copied, skipped, error: `Source directory not found: ${src}` };
101
+ }
102
+
103
+ fs.mkdirSync(dest, { recursive: true });
104
+
105
+ const files = listFilesRecursive(src);
106
+ for (const relPath of files) {
107
+ const srcPath = path.join(src, relPath);
108
+ const destPath = path.join(dest, relPath);
109
+
110
+ try {
111
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
112
+
113
+ // Check if file exists and compare content
114
+ let shouldCopy = true;
115
+ if (fs.existsSync(destPath)) {
116
+ const srcContent = fs.readFileSync(srcPath);
117
+ const destContent = fs.readFileSync(destPath);
118
+ if (srcContent.equals(destContent)) {
119
+ shouldCopy = false;
120
+ skipped++;
121
+ }
122
+ }
123
+
124
+ if (shouldCopy) {
125
+ fs.copyFileSync(srcPath, destPath);
126
+ if (relPath.endsWith('.sh')) {
127
+ fs.chmodSync(destPath, 0o755);
128
+ }
129
+ copied++;
130
+ }
131
+ } catch (err) {
132
+ console.error(`Warning: could not copy ${relPath}: ${err.message}`);
133
+ }
134
+ }
135
+
136
+ return { copied, skipped };
137
+ }
138
+
139
+ /**
140
+ * Register a repository in the global registry
141
+ * @param {string} repoPath - Path to repository
142
+ * @param {string} version - Aether version
143
+ * @returns {object} Result: { success: boolean, message: string }
144
+ */
145
+ function registerRepo(repoPath, version) {
146
+ if (!HUB_REGISTRY) {
147
+ return { success: false, message: 'HOME environment variable not set' };
148
+ }
149
+
150
+ // Initialize registry if it doesn't exist
151
+ let registry = readJsonSafe(HUB_REGISTRY);
152
+ if (!registry) {
153
+ registry = { schema_version: 1, repos: [] };
154
+ }
155
+
156
+ // Check if repo already exists
157
+ const existingIndex = registry.repos.findIndex(r => r.path === repoPath);
158
+ const now = new Date().toISOString();
159
+
160
+ if (existingIndex >= 0) {
161
+ // Update existing entry
162
+ registry.repos[existingIndex].version = version;
163
+ registry.repos[existingIndex].updated_at = now;
164
+ } else {
165
+ // Add new entry
166
+ registry.repos.push({
167
+ path: repoPath,
168
+ version: version,
169
+ registered_at: now,
170
+ updated_at: now
171
+ });
172
+ }
173
+
174
+ writeJsonSync(HUB_REGISTRY, registry);
175
+ return { success: true, message: existingIndex >= 0 ? 'Updated registry entry' : 'Added to registry' };
176
+ }
177
+
178
+ /**
179
+ * Create initial state object for new colony
180
+ * @param {string} goal - Colony goal
181
+ * @returns {object} Initial state object
182
+ */
183
+ function createInitialState(goal) {
184
+ const now = new Date().toISOString();
185
+ const sessionId = generateSessionId();
186
+
187
+ return {
188
+ version: '3.0',
189
+ goal: goal || 'Aether colony initialization',
190
+ state: 'INITIALIZING',
191
+ current_phase: 0,
192
+ session_id: sessionId,
193
+ initialized_at: now,
194
+ build_started_at: null,
195
+ plan: {
196
+ generated_at: null,
197
+ confidence: null,
198
+ phases: []
199
+ },
200
+ memory: {
201
+ phase_learnings: [],
202
+ decisions: [],
203
+ instincts: []
204
+ },
205
+ errors: {
206
+ records: [],
207
+ flagged_patterns: []
208
+ },
209
+ signals: [],
210
+ graveyards: [],
211
+ events: [
212
+ {
213
+ timestamp: now,
214
+ type: 'colony_initialized',
215
+ worker: 'init',
216
+ details: `Colony initialized with goal: ${goal || 'Aether colony initialization'}`
217
+ }
218
+ ],
219
+ created_at: now,
220
+ last_updated: now
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Check if a repository is already initialized
226
+ * @param {string} repoPath - Path to repository root
227
+ * @returns {boolean} True if initialized
228
+ */
229
+ function isInitialized(repoPath) {
230
+ const stateFile = path.join(repoPath, '.aether', 'data', 'COLONY_STATE.json');
231
+
232
+ // Check if state file exists
233
+ if (!fs.existsSync(stateFile)) {
234
+ return false;
235
+ }
236
+
237
+ // Check if required directories exist
238
+ const requiredDirs = [
239
+ path.join(repoPath, '.aether'),
240
+ path.join(repoPath, '.aether', 'data'),
241
+ path.join(repoPath, '.aether', 'checkpoints'),
242
+ path.join(repoPath, '.aether', 'locks')
243
+ ];
244
+
245
+ for (const dir of requiredDirs) {
246
+ if (!fs.existsSync(dir)) {
247
+ return false;
248
+ }
249
+ }
250
+
251
+ return true;
252
+ }
253
+
254
+ /**
255
+ * Validate initialization of a repository
256
+ * @param {string} repoPath - Path to repository root
257
+ * @returns {object} Validation result: { valid: boolean, errors: string[] }
258
+ */
259
+ function validateInitialization(repoPath) {
260
+ const errors = [];
261
+
262
+ // Check required directories
263
+ const requiredDirs = [
264
+ { path: path.join(repoPath, '.aether'), name: '.aether/' },
265
+ { path: path.join(repoPath, '.aether', 'data'), name: '.aether/data/' },
266
+ { path: path.join(repoPath, '.aether', 'checkpoints'), name: '.aether/checkpoints/' },
267
+ { path: path.join(repoPath, '.aether', 'locks'), name: '.aether/locks/' }
268
+ ];
269
+
270
+ for (const dir of requiredDirs) {
271
+ if (!fs.existsSync(dir.path)) {
272
+ errors.push(`Missing directory: ${dir.name}`);
273
+ }
274
+ }
275
+
276
+ // Check state file
277
+ const stateFile = path.join(repoPath, '.aether', 'data', 'COLONY_STATE.json');
278
+ if (!fs.existsSync(stateFile)) {
279
+ errors.push('Missing state file: .aether/data/COLONY_STATE.json');
280
+ } else {
281
+ // Validate JSON structure
282
+ try {
283
+ const content = fs.readFileSync(stateFile, 'utf8');
284
+ const state = JSON.parse(content);
285
+
286
+ // Check required fields
287
+ const requiredFields = ['version', 'goal', 'state', 'current_phase', 'session_id', 'initialized_at'];
288
+ for (const field of requiredFields) {
289
+ if (!(field in state)) {
290
+ errors.push(`State file missing required field: ${field}`);
291
+ }
292
+ }
293
+
294
+ // Validate events array
295
+ if (!Array.isArray(state.events)) {
296
+ errors.push('State file events field must be an array');
297
+ }
298
+
299
+ // Validate current_phase is a number
300
+ if (typeof state.current_phase !== 'number') {
301
+ errors.push('State file current_phase must be a number');
302
+ }
303
+
304
+ } catch (err) {
305
+ errors.push(`Invalid JSON in state file: ${err.message}`);
306
+ }
307
+ }
308
+
309
+ return {
310
+ valid: errors.length === 0,
311
+ errors
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Initialize a new repository with Aether colony
317
+ * @param {string} repoPath - Path to repository root
318
+ * @param {object} options - Initialization options
319
+ * @param {string} options.goal - Colony goal
320
+ * @param {boolean} options.skipIfExists - Skip if already initialized
321
+ * @returns {object} Result: { success: boolean, stateFile: string|null, message: string }
322
+ */
323
+ async function initializeRepo(repoPath, options = {}) {
324
+ const { goal, skipIfExists = false, quiet = false } = options;
325
+
326
+ // Check if already initialized
327
+ if (isInitialized(repoPath) && skipIfExists) {
328
+ return {
329
+ success: true,
330
+ stateFile: path.join(repoPath, '.aether', 'data', 'COLONY_STATE.json'),
331
+ message: 'Repository already initialized, skipping'
332
+ };
333
+ }
334
+
335
+ // Check if hub exists
336
+ if (!HUB_DIR || !fs.existsSync(HUB_DIR)) {
337
+ return {
338
+ success: false,
339
+ stateFile: null,
340
+ message: 'Aether hub not found. Run "aether install" first to set up the distribution hub.'
341
+ };
342
+ }
343
+
344
+ const results = {
345
+ system: { copied: 0, skipped: 0 },
346
+ commands: { copied: 0, skipped: 0 },
347
+ agents: { copied: 0, skipped: 0 }
348
+ };
349
+
350
+ // Sync system files from hub
351
+ if (HUB_SYSTEM && fs.existsSync(HUB_SYSTEM)) {
352
+ const destSystem = path.join(repoPath, '.aether');
353
+ results.system = syncFiles(HUB_SYSTEM, destSystem);
354
+ if (!quiet && results.system.copied > 0) {
355
+ console.log(` System files: ${results.system.copied} copied, ${results.system.skipped} skipped`);
356
+ }
357
+ }
358
+
359
+ // Sync Claude commands
360
+ if (HUB_COMMANDS_CLAUDE && fs.existsSync(HUB_COMMANDS_CLAUDE)) {
361
+ const destClaude = path.join(repoPath, '.claude', 'commands', 'ant');
362
+ results.commands = syncFiles(HUB_COMMANDS_CLAUDE, destClaude);
363
+ if (!quiet && results.commands.copied > 0) {
364
+ console.log(` Claude commands: ${results.commands.copied} copied, ${results.commands.skipped} skipped`);
365
+ }
366
+ }
367
+
368
+ // Sync OpenCode commands
369
+ if (HUB_COMMANDS_OPENCODE && fs.existsSync(HUB_COMMANDS_OPENCODE)) {
370
+ const destOpencode = path.join(repoPath, '.opencode', 'commands', 'ant');
371
+ const opencodeResult = syncFiles(HUB_COMMANDS_OPENCODE, destOpencode);
372
+ results.commands.copied += opencodeResult.copied;
373
+ results.commands.skipped += opencodeResult.skipped;
374
+ }
375
+
376
+ // Sync agents
377
+ if (HUB_AGENTS && fs.existsSync(HUB_AGENTS)) {
378
+ const destAgents = path.join(repoPath, '.opencode', 'agents');
379
+ results.agents = syncFiles(HUB_AGENTS, destAgents);
380
+ if (!quiet && results.agents.copied > 0) {
381
+ console.log(` Agents: ${results.agents.copied} copied, ${results.agents.skipped} skipped`);
382
+ }
383
+ }
384
+
385
+ // Sync claude agents
386
+ if (HUB_AGENTS_CLAUDE && fs.existsSync(HUB_AGENTS_CLAUDE)) {
387
+ const destClaudeAgents = path.join(repoPath, '.claude', 'agents', 'ant');
388
+ const claudeAgentsResult = syncFiles(HUB_AGENTS_CLAUDE, destClaudeAgents);
389
+ if (!quiet && claudeAgentsResult.copied > 0) {
390
+ console.log(` Agents (claude): ${claudeAgentsResult.copied} copied, ${claudeAgentsResult.skipped} skipped`);
391
+ }
392
+ }
393
+
394
+ // Create directory structure (in case some weren't created by sync)
395
+ const dirs = [
396
+ path.join(repoPath, '.aether', 'data'),
397
+ path.join(repoPath, '.aether', 'checkpoints'),
398
+ path.join(repoPath, '.aether', 'locks')
399
+ ];
400
+
401
+ for (const dir of dirs) {
402
+ fs.mkdirSync(dir, { recursive: true });
403
+ }
404
+
405
+ // Create .gitignore for .aether directory
406
+ const gitignorePath = path.join(repoPath, '.aether', '.gitignore');
407
+ const gitignoreContent = `# Aether local state - not versioned
408
+ data/
409
+ checkpoints/
410
+ locks/
411
+ `;
412
+ fs.writeFileSync(gitignorePath, gitignoreContent);
413
+
414
+ // Create initial state
415
+ const state = createInitialState(goal);
416
+
417
+ // Write state file
418
+ const stateFile = path.join(repoPath, '.aether', 'data', 'COLONY_STATE.json');
419
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2) + '\n');
420
+
421
+ // Get hub version
422
+ const hubVersion = readJsonSafe(HUB_VERSION);
423
+ const version = hubVersion ? hubVersion.version : '1.0.0';
424
+
425
+ // Register in global registry
426
+ const registerResult = registerRepo(repoPath, version);
427
+ if (!quiet && registerResult.success) {
428
+ console.log(` ${registerResult.message}`);
429
+ }
430
+
431
+ // Write version file
432
+ const versionFile = path.join(repoPath, '.aether', 'version.json');
433
+ writeJsonSync(versionFile, {
434
+ version: version,
435
+ initialized_at: new Date().toISOString()
436
+ });
437
+
438
+ return {
439
+ success: true,
440
+ stateFile,
441
+ message: 'Repository initialized successfully',
442
+ version: version,
443
+ filesCopied: results.system.copied + results.commands.copied + results.agents.copied,
444
+ registered: registerResult.success
445
+ };
446
+ }
447
+
448
+ module.exports = {
449
+ initializeRepo,
450
+ isInitialized,
451
+ validateInitialization,
452
+ createInitialState,
453
+ generateSessionId
454
+ };
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Structured Logging Module
4
+ *
5
+ * Provides consistent logging to activity.log with proper formatting
6
+ * and error handling. All logging operations fail silently to avoid
7
+ * cascading errors.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ // Log level emojis
14
+ const EMOJI = {
15
+ ERROR: '❌',
16
+ WARN: '⚠️',
17
+ INFO: 'ℹ️',
18
+ SUCCESS: '✓',
19
+ };
20
+
21
+ // Caste emojis (matching cli.js pattern)
22
+ const CASTE_EMOJI = {
23
+ queen: '👑',
24
+ scout: '🔍',
25
+ builder: '🔨',
26
+ watcher: '👁️',
27
+ chaos: '🎲',
28
+ ant: '🐜',
29
+ };
30
+
31
+ /**
32
+ * Get the path to activity.log
33
+ * @returns {string} Path to activity.log
34
+ */
35
+ function getActivityLogPath() {
36
+ const home = process.env.HOME || process.env.USERPROFILE;
37
+ if (!home) {
38
+ return null;
39
+ }
40
+ return path.join(home, '.aether', 'data', 'activity.log');
41
+ }
42
+
43
+ /**
44
+ * Format a timestamp as HH:MM:SS
45
+ * @param {string|Date} [timestamp] - ISO string or Date object
46
+ * @returns {string} Formatted time string
47
+ */
48
+ function formatTimestamp(timestamp) {
49
+ const date = timestamp ? new Date(timestamp) : new Date();
50
+ return date.toISOString().split('T')[1].slice(0, 8); // HH:MM:SS
51
+ }
52
+
53
+ /**
54
+ * Sanitize a string for logging
55
+ * - Removes newlines
56
+ * - Trims whitespace
57
+ * - Limits to 200 characters
58
+ * - Escapes control characters
59
+ * @param {string} str - String to sanitize
60
+ * @returns {string} Sanitized string
61
+ */
62
+ function sanitizeForLog(str) {
63
+ if (typeof str !== 'string') {
64
+ str = String(str);
65
+ }
66
+ return str
67
+ .replace(/[\n\r]/g, ' ')
68
+ .replace(/[\x00-\x1F\x7F]/g, '')
69
+ .trim()
70
+ .slice(0, 200);
71
+ }
72
+
73
+ /**
74
+ * Append a line to activity.log
75
+ * Fails silently if logging fails
76
+ * @param {string} line - Line to append
77
+ * @returns {boolean} True if logged successfully, false otherwise
78
+ */
79
+ function appendToLog(line) {
80
+ try {
81
+ const logPath = getActivityLogPath();
82
+ if (!logPath) {
83
+ return false;
84
+ }
85
+
86
+ // Ensure directory exists
87
+ const logDir = path.dirname(logPath);
88
+ if (!fs.existsSync(logDir)) {
89
+ fs.mkdirSync(logDir, { recursive: true });
90
+ }
91
+
92
+ fs.appendFileSync(logPath, line + '\n');
93
+ return true;
94
+ } catch {
95
+ // Silent fail - don't cascade errors from logging
96
+ return false;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Log an error to activity.log
102
+ * @param {Error|object} error - Error to log (AetherError or plain Error)
103
+ * @returns {boolean} True if logged successfully
104
+ */
105
+ function logError(error) {
106
+ try {
107
+ const timestamp = formatTimestamp(error.timestamp);
108
+ const emoji = EMOJI.ERROR;
109
+
110
+ // Extract code and message
111
+ let code = 'E_UNKNOWN';
112
+ let message = 'Unknown error';
113
+
114
+ if (error.code) {
115
+ code = error.code;
116
+ message = error.message;
117
+ } else if (error.message) {
118
+ message = error.message;
119
+ }
120
+
121
+ const sanitizedMessage = sanitizeForLog(message);
122
+ const logLine = `[${timestamp}] ${emoji} ERROR ${code}: ${sanitizedMessage}`;
123
+
124
+ return appendToLog(logLine);
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Log an activity/event to activity.log
132
+ * @param {string} action - Action name (e.g., 'SPAWN', 'COMPLETED')
133
+ * @param {string} caste - Caste name (e.g., 'queen', 'scout', 'builder')
134
+ * @param {string} description - Activity description
135
+ * @returns {boolean} True if logged successfully
136
+ */
137
+ function logActivity(action, caste, description) {
138
+ try {
139
+ const timestamp = formatTimestamp();
140
+ const emoji = CASTE_EMOJI[caste] || CASTE_EMOJI.ant;
141
+ const sanitizedDescription = sanitizeForLog(description);
142
+
143
+ const logLine = `[${timestamp}] ${emoji} ${action} ${caste}: ${sanitizedDescription}`;
144
+
145
+ return appendToLog(logLine);
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Log a warning to activity.log
153
+ * @param {string} code - Warning code (e.g., 'W_CONFIG_MISSING')
154
+ * @param {string} message - Warning message
155
+ * @returns {boolean} True if logged successfully
156
+ */
157
+ function logWarning(code, message) {
158
+ try {
159
+ const timestamp = formatTimestamp();
160
+ const emoji = EMOJI.WARN;
161
+ const sanitizedMessage = sanitizeForLog(message);
162
+
163
+ const logLine = `[${timestamp}] ${emoji} WARN ${code}: ${sanitizedMessage}`;
164
+
165
+ return appendToLog(logLine);
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Log an info message to activity.log
173
+ * @param {string} message - Info message
174
+ * @returns {boolean} True if logged successfully
175
+ */
176
+ function logInfo(message) {
177
+ try {
178
+ const timestamp = formatTimestamp();
179
+ const emoji = EMOJI.INFO;
180
+ const sanitizedMessage = sanitizeForLog(message);
181
+
182
+ const logLine = `[${timestamp}] ${emoji} ${sanitizedMessage}`;
183
+
184
+ return appendToLog(logLine);
185
+ } catch {
186
+ return false;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Log a success message to activity.log
192
+ * @param {string} caste - Caste name
193
+ * @param {string} description - Success description
194
+ * @returns {boolean} True if logged successfully
195
+ */
196
+ function logSuccess(caste, description) {
197
+ try {
198
+ const timestamp = formatTimestamp();
199
+ const emoji = EMOJI.SUCCESS;
200
+ const sanitizedDescription = sanitizeForLog(description);
201
+
202
+ const logLine = `[${timestamp}] ${emoji} ${caste}: ${sanitizedDescription}`;
203
+
204
+ return appendToLog(logLine);
205
+ } catch {
206
+ return false;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Get recent log entries
212
+ * @param {number} [lines=10] - Number of lines to retrieve
213
+ * @returns {string[]} Array of log lines
214
+ */
215
+ function getRecentLogs(lines = 10) {
216
+ try {
217
+ const logPath = getActivityLogPath();
218
+ if (!logPath || !fs.existsSync(logPath)) {
219
+ return [];
220
+ }
221
+
222
+ const content = fs.readFileSync(logPath, 'utf8');
223
+ const allLines = content.split('\n').filter(Boolean);
224
+ return allLines.slice(-lines);
225
+ } catch {
226
+ return [];
227
+ }
228
+ }
229
+
230
+ module.exports = {
231
+ EMOJI,
232
+ CASTE_EMOJI,
233
+ getActivityLogPath,
234
+ formatTimestamp,
235
+ sanitizeForLog,
236
+ logError,
237
+ logActivity,
238
+ logWarning,
239
+ logInfo,
240
+ logSuccess,
241
+ getRecentLogs,
242
+ };