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
package/bin/lib/init.js
ADDED
|
@@ -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
|
+
};
|