aether-colony 3.1.4 → 3.1.15
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/.claude/commands/ant/archaeology.md +12 -0
- package/.claude/commands/ant/build.md +382 -319
- package/.claude/commands/ant/chaos.md +23 -1
- package/.claude/commands/ant/colonize.md +147 -87
- package/.claude/commands/ant/continue.md +213 -23
- package/.claude/commands/ant/council.md +22 -0
- package/.claude/commands/ant/dream.md +18 -0
- package/.claude/commands/ant/entomb.md +178 -6
- package/.claude/commands/ant/init.md +87 -13
- package/.claude/commands/ant/lay-eggs.md +45 -5
- package/.claude/commands/ant/oracle.md +82 -9
- package/.claude/commands/ant/organize.md +2 -2
- package/.claude/commands/ant/pause-colony.md +86 -28
- package/.claude/commands/ant/phase.md +26 -0
- package/.claude/commands/ant/plan.md +204 -111
- package/.claude/commands/ant/resume-colony.md +23 -1
- package/.claude/commands/ant/resume.md +159 -0
- package/.claude/commands/ant/seal.md +177 -3
- package/.claude/commands/ant/swarm.md +78 -97
- package/.claude/commands/ant/verify-castes.md +7 -7
- package/.claude/commands/ant/watch.md +17 -0
- package/.opencode/agents/aether-ambassador.md +97 -0
- package/.opencode/agents/aether-archaeologist.md +91 -0
- package/.opencode/agents/aether-architect.md +66 -0
- package/.opencode/agents/aether-auditor.md +111 -0
- package/.opencode/agents/aether-builder.md +28 -10
- package/.opencode/agents/aether-chaos.md +98 -0
- package/.opencode/agents/aether-chronicler.md +80 -0
- package/.opencode/agents/aether-gatekeeper.md +107 -0
- package/.opencode/agents/aether-guardian.md +107 -0
- package/.opencode/agents/aether-includer.md +108 -0
- package/.opencode/agents/aether-keeper.md +106 -0
- package/.opencode/agents/aether-measurer.md +119 -0
- package/.opencode/agents/aether-probe.md +91 -0
- package/.opencode/agents/aether-queen.md +72 -19
- package/.opencode/agents/aether-route-setter.md +85 -0
- package/.opencode/agents/aether-sage.md +98 -0
- package/.opencode/agents/aether-scout.md +33 -15
- package/.opencode/agents/aether-surveyor-disciplines.md +334 -0
- package/.opencode/agents/aether-surveyor-nest.md +272 -0
- package/.opencode/agents/aether-surveyor-pathogens.md +209 -0
- package/.opencode/agents/aether-surveyor-provisions.md +277 -0
- package/.opencode/agents/aether-tracker.md +91 -0
- package/.opencode/agents/aether-watcher.md +30 -12
- package/.opencode/agents/aether-weaver.md +87 -0
- package/.opencode/agents/workers.md +1034 -0
- package/.opencode/commands/ant/archaeology.md +44 -26
- package/.opencode/commands/ant/build.md +327 -295
- package/.opencode/commands/ant/chaos.md +32 -4
- package/.opencode/commands/ant/colonize.md +119 -93
- package/.opencode/commands/ant/continue.md +98 -10
- package/.opencode/commands/ant/council.md +28 -0
- package/.opencode/commands/ant/dream.md +24 -0
- package/.opencode/commands/ant/entomb.md +73 -1
- package/.opencode/commands/ant/feedback.md +8 -2
- package/.opencode/commands/ant/flag.md +9 -3
- package/.opencode/commands/ant/flags.md +8 -2
- package/.opencode/commands/ant/focus.md +8 -2
- package/.opencode/commands/ant/help.md +12 -0
- package/.opencode/commands/ant/init.md +49 -4
- package/.opencode/commands/ant/lay-eggs.md +30 -2
- package/.opencode/commands/ant/oracle.md +39 -7
- package/.opencode/commands/ant/organize.md +9 -3
- package/.opencode/commands/ant/pause-colony.md +54 -1
- package/.opencode/commands/ant/phase.md +36 -4
- package/.opencode/commands/ant/plan.md +225 -117
- package/.opencode/commands/ant/redirect.md +8 -2
- package/.opencode/commands/ant/resume-colony.md +51 -26
- package/.opencode/commands/ant/seal.md +76 -0
- package/.opencode/commands/ant/status.md +50 -20
- package/.opencode/commands/ant/swarm.md +108 -104
- package/.opencode/commands/ant/tunnels.md +107 -2
- package/CHANGELOG.md +21 -0
- package/README.md +199 -86
- package/bin/cli.js +142 -25
- package/bin/generate-commands.sh +100 -16
- package/bin/lib/caste-colors.js +5 -5
- package/bin/lib/errors.js +16 -0
- package/bin/lib/file-lock.js +279 -44
- package/bin/lib/state-sync.js +206 -23
- package/bin/lib/update-transaction.js +206 -24
- package/bin/sync-to-runtime.sh +129 -0
- package/package.json +2 -2
- package/runtime/CONTEXT.md +160 -0
- package/runtime/aether-utils.sh +1421 -55
- package/runtime/docs/AETHER-2.0-IMPLEMENTATION-PLAN.md +1343 -0
- package/runtime/docs/AETHER-PHEROMONE-SYSTEM-MASTER-SPEC.md +2642 -0
- package/runtime/docs/PHEROMONE-INJECTION.md +240 -0
- package/runtime/docs/PHEROMONE-INTEGRATION.md +192 -0
- package/runtime/docs/PHEROMONE-SYSTEM-DESIGN.md +426 -0
- package/runtime/docs/README.md +94 -0
- package/runtime/docs/VISUAL-OUTPUT-SPEC.md +219 -0
- package/runtime/docs/biological-reference.md +272 -0
- package/runtime/docs/codebase-review.md +399 -0
- package/runtime/docs/command-sync.md +164 -0
- package/runtime/docs/implementation-learnings.md +89 -0
- package/runtime/docs/known-issues.md +217 -0
- package/runtime/docs/namespace.md +148 -0
- package/runtime/docs/planning-discipline.md +159 -0
- package/runtime/lib/queen-utils.sh +729 -0
- package/runtime/model-profiles.yaml +100 -0
- package/runtime/recover.sh +136 -0
- package/runtime/templates/QUEEN.md.template +79 -0
- package/runtime/utils/atomic-write.sh +5 -5
- package/runtime/utils/chamber-utils.sh +6 -3
- package/runtime/utils/error-handler.sh +200 -0
- package/runtime/utils/queen-to-md.xsl +395 -0
- package/runtime/utils/spawn-tree.sh +428 -0
- package/runtime/utils/spawn-with-model.sh +56 -0
- package/runtime/utils/state-loader.sh +215 -0
- package/runtime/utils/swarm-display.sh +5 -5
- package/runtime/utils/watch-spawn-tree.sh +90 -22
- package/runtime/utils/xml-compose.sh +247 -0
- package/runtime/utils/xml-core.sh +186 -0
- package/runtime/utils/xml-utils.sh +2161 -0
- package/runtime/verification-loop.md +1 -1
- package/runtime/workers-new-castes.md +516 -0
- package/runtime/workers.md +20 -8
- package/.aether/visualizations/anthill-stages/brood-stable.txt +0 -26
- package/.aether/visualizations/anthill-stages/crowned-anthill.txt +0 -30
- package/.aether/visualizations/anthill-stages/first-mound.txt +0 -18
- package/.aether/visualizations/anthill-stages/open-chambers.txt +0 -24
- package/.aether/visualizations/anthill-stages/sealed-chambers.txt +0 -28
- package/.aether/visualizations/anthill-stages/ventilated-nest.txt +0 -27
package/bin/lib/state-sync.js
CHANGED
|
@@ -10,6 +10,96 @@
|
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
+
const { FileLock } = require('./file-lock');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default maximum number of events to keep (PLAN-007 Fix 2)
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_MAX_EVENTS = 100;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Prune events array to prevent unbounded growth (PLAN-007 Fix 2)
|
|
22
|
+
* @param {Array} events - Events array to prune
|
|
23
|
+
* @param {number} maxEvents - Maximum number of events to keep
|
|
24
|
+
* @returns {Array} Pruned events array (most recent by timestamp)
|
|
25
|
+
*/
|
|
26
|
+
function pruneEvents(events, maxEvents = DEFAULT_MAX_EVENTS) {
|
|
27
|
+
if (!Array.isArray(events)) return events;
|
|
28
|
+
if (events.length <= maxEvents) return events;
|
|
29
|
+
|
|
30
|
+
// Keep most recent events by timestamp
|
|
31
|
+
const sorted = [...events].sort((a, b) => {
|
|
32
|
+
const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
|
33
|
+
const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
|
34
|
+
return timeB - timeA; // Descending (most recent first)
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return sorted.slice(0, maxEvents);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* State schema definition for validation (PLAN-007 Fix 3)
|
|
42
|
+
*/
|
|
43
|
+
const STATE_SCHEMA = {
|
|
44
|
+
version: { required: true, type: 'string' },
|
|
45
|
+
current_phase: { required: true, type: 'number' },
|
|
46
|
+
events: { required: true, type: 'array' },
|
|
47
|
+
goal: { required: false, type: 'string' },
|
|
48
|
+
state: { required: false, type: 'string' },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validate state object against schema (PLAN-007 Fix 3)
|
|
53
|
+
* @param {object} state - State object to validate
|
|
54
|
+
* @returns {object} Validation result: { valid: boolean, errors: string[] }
|
|
55
|
+
*/
|
|
56
|
+
function validateStateSchema(state) {
|
|
57
|
+
const errors = [];
|
|
58
|
+
|
|
59
|
+
// Check state is a non-null object
|
|
60
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) {
|
|
61
|
+
return { valid: false, errors: ['State must be a non-null object'] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate each field against schema
|
|
65
|
+
for (const [field, config] of Object.entries(STATE_SCHEMA)) {
|
|
66
|
+
// Check required fields
|
|
67
|
+
if (config.required && !(field in state)) {
|
|
68
|
+
errors.push(`Missing required field: ${field}`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check types for present non-null fields
|
|
73
|
+
if ((field in state) && state[field] !== null && state[field] !== undefined) {
|
|
74
|
+
const actualType = Array.isArray(state[field]) ? 'array' : typeof state[field];
|
|
75
|
+
if (actualType !== config.type) {
|
|
76
|
+
errors.push(`Field "${field}" must be ${config.type}, got ${actualType}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Validate events array elements
|
|
82
|
+
if (Array.isArray(state.events)) {
|
|
83
|
+
for (let i = 0; i < state.events.length; i++) {
|
|
84
|
+
const event = state.events[i];
|
|
85
|
+
if (!event || typeof event !== 'object') {
|
|
86
|
+
errors.push(`events[${i}] must be an object`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!event.timestamp) {
|
|
90
|
+
errors.push(`events[${i}] missing required field: timestamp`);
|
|
91
|
+
}
|
|
92
|
+
if (!event.type) {
|
|
93
|
+
errors.push(`events[${i}] missing required field: type`);
|
|
94
|
+
}
|
|
95
|
+
if (!event.worker) {
|
|
96
|
+
errors.push(`events[${i}] missing required field: worker`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { valid: errors.length === 0, errors };
|
|
102
|
+
}
|
|
13
103
|
|
|
14
104
|
/**
|
|
15
105
|
* Parse STATE.md markdown content to extract current state
|
|
@@ -75,6 +165,11 @@ function parseRoadmapMd(content) {
|
|
|
75
165
|
const phaseNum = parseInt(match[1], 10);
|
|
76
166
|
const phaseName = match[2].trim();
|
|
77
167
|
|
|
168
|
+
// Warn on unreasonable phase numbers (PLAN-006 fix #8)
|
|
169
|
+
if (phaseNum > 100 || phaseNum < 0) {
|
|
170
|
+
console.warn(`Warning: Unusual phase number ${phaseNum} in ROADMAP.md`);
|
|
171
|
+
}
|
|
172
|
+
|
|
78
173
|
// Look for status indicators near this phase
|
|
79
174
|
const sectionStart = match.index;
|
|
80
175
|
const nextPhaseMatch = phaseRegex.exec(content);
|
|
@@ -110,8 +205,13 @@ function parseRoadmapMd(content) {
|
|
|
110
205
|
* @returns {string} Colony state: INITIALIZING|PLANNING|BUILDING|COMPLETED
|
|
111
206
|
*/
|
|
112
207
|
function determineColonyState(status, phase) {
|
|
208
|
+
// No status - determine by phase (PLAN-006 fix #7)
|
|
113
209
|
if (!status) {
|
|
114
|
-
|
|
210
|
+
if (phase === null || phase === undefined) {
|
|
211
|
+
return 'INITIALIZING';
|
|
212
|
+
}
|
|
213
|
+
// Phase 0 can be valid - don't force INITIALIZING
|
|
214
|
+
return phase === 0 ? 'PLANNING' : 'BUILDING';
|
|
115
215
|
}
|
|
116
216
|
|
|
117
217
|
const statusLower = status.toLowerCase();
|
|
@@ -128,47 +228,109 @@ function determineColonyState(status, phase) {
|
|
|
128
228
|
return 'BUILDING';
|
|
129
229
|
}
|
|
130
230
|
|
|
131
|
-
// Default based on phase
|
|
132
|
-
|
|
133
|
-
return 'INITIALIZING';
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return 'BUILDING';
|
|
231
|
+
// Default based on phase - Phase 0 is PLANNING, not INITIALIZING
|
|
232
|
+
return phase === 0 ? 'PLANNING' : 'BUILDING';
|
|
137
233
|
}
|
|
138
234
|
|
|
139
235
|
/**
|
|
140
236
|
* Synchronize COLONY_STATE.json with .planning/STATE.md
|
|
141
237
|
* @param {string} repoPath - Path to repository root
|
|
142
|
-
* @returns {object} Sync result: { synced: boolean, updates: string[], error?: string }
|
|
238
|
+
* @returns {object} Sync result: { synced: boolean, updates: string[], error?: string, recovery?: string }
|
|
143
239
|
*/
|
|
144
240
|
function syncStateFromPlanning(repoPath) {
|
|
145
241
|
const updates = [];
|
|
242
|
+
const lockDir = path.join(repoPath, '.aether', 'locks');
|
|
243
|
+
const colonyStatePath = path.join(repoPath, '.aether', 'data', 'COLONY_STATE.json');
|
|
244
|
+
|
|
245
|
+
// Create lock instance
|
|
246
|
+
const lock = new FileLock({
|
|
247
|
+
lockDir,
|
|
248
|
+
timeout: 5000,
|
|
249
|
+
maxRetries: 50
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Attempt to acquire lock
|
|
253
|
+
if (!lock.acquire(colonyStatePath)) {
|
|
254
|
+
return {
|
|
255
|
+
synced: false,
|
|
256
|
+
updates: [],
|
|
257
|
+
error: 'Could not acquire state lock - another sync in progress'
|
|
258
|
+
};
|
|
259
|
+
}
|
|
146
260
|
|
|
147
261
|
try {
|
|
148
|
-
// Read STATE.md
|
|
262
|
+
// Read STATE.md with improved error handling (PLAN-006 fix #9)
|
|
149
263
|
const stateMdPath = path.join(repoPath, '.planning', 'STATE.md');
|
|
150
|
-
|
|
151
|
-
|
|
264
|
+
try {
|
|
265
|
+
if (!fs.existsSync(stateMdPath)) {
|
|
266
|
+
return { synced: false, updates: [], error: '.planning/STATE.md not found' };
|
|
267
|
+
}
|
|
268
|
+
} catch (accessError) {
|
|
269
|
+
if (accessError.code === 'EACCES') {
|
|
270
|
+
return { synced: false, updates: [], error: '.planning/STATE.md not accessible (permission denied)' };
|
|
271
|
+
}
|
|
272
|
+
return { synced: false, updates: [], error: `Failed to check STATE.md: ${accessError.message}` };
|
|
152
273
|
}
|
|
153
274
|
|
|
154
|
-
|
|
275
|
+
let stateMdContent;
|
|
276
|
+
try {
|
|
277
|
+
stateMdContent = fs.readFileSync(stateMdPath, 'utf8');
|
|
278
|
+
} catch (readError) {
|
|
279
|
+
if (readError.code === 'EACCES') {
|
|
280
|
+
return { synced: false, updates: [], error: '.planning/STATE.md not readable (permission denied)' };
|
|
281
|
+
}
|
|
282
|
+
return { synced: false, updates: [], error: `Failed to read STATE.md: ${readError.message}` };
|
|
283
|
+
}
|
|
155
284
|
const planningState = parseStateMd(stateMdContent);
|
|
156
285
|
|
|
157
286
|
// Read ROADMAP.md for phases
|
|
158
287
|
const roadmapPath = path.join(repoPath, '.planning', 'ROADMAP.md');
|
|
159
288
|
let phases = [];
|
|
160
289
|
if (fs.existsSync(roadmapPath)) {
|
|
161
|
-
|
|
162
|
-
|
|
290
|
+
try {
|
|
291
|
+
const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
|
|
292
|
+
phases = parseRoadmapMd(roadmapContent);
|
|
293
|
+
} catch (roadmapError) {
|
|
294
|
+
// Non-fatal - phases are optional
|
|
295
|
+
console.warn(`Warning: Could not read ROADMAP.md: ${roadmapError.message}`);
|
|
296
|
+
}
|
|
163
297
|
}
|
|
164
298
|
|
|
165
|
-
// Read current COLONY_STATE.json
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
299
|
+
// Read current COLONY_STATE.json with improved error handling
|
|
300
|
+
try {
|
|
301
|
+
if (!fs.existsSync(colonyStatePath)) {
|
|
302
|
+
return { synced: false, updates: [], error: '.aether/data/COLONY_STATE.json not found' };
|
|
303
|
+
}
|
|
304
|
+
} catch (accessError) {
|
|
305
|
+
if (accessError.code === 'EACCES') {
|
|
306
|
+
return { synced: false, updates: [], error: 'COLONY_STATE.json not accessible (permission denied)' };
|
|
307
|
+
}
|
|
308
|
+
return { synced: false, updates: [], error: `Failed to check COLONY_STATE.json: ${accessError.message}` };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Parse JSON with error handling (PLAN-002)
|
|
312
|
+
let colonyState;
|
|
313
|
+
try {
|
|
314
|
+
colonyState = JSON.parse(fs.readFileSync(colonyStatePath, 'utf8'));
|
|
315
|
+
} catch (parseError) {
|
|
316
|
+
return {
|
|
317
|
+
synced: false,
|
|
318
|
+
updates: [],
|
|
319
|
+
error: `COLONY_STATE.json contains invalid JSON: ${parseError.message}`,
|
|
320
|
+
recovery: 'Manually fix or delete .aether/data/COLONY_STATE.json and reinitialize'
|
|
321
|
+
};
|
|
169
322
|
}
|
|
170
323
|
|
|
171
|
-
|
|
324
|
+
// Validate state schema before any modifications (PLAN-007 Fix 3)
|
|
325
|
+
const validation = validateStateSchema(colonyState);
|
|
326
|
+
if (!validation.valid) {
|
|
327
|
+
return {
|
|
328
|
+
synced: false,
|
|
329
|
+
updates: [],
|
|
330
|
+
error: `State schema validation failed: ${validation.errors.join('; ')}`,
|
|
331
|
+
recovery: 'Fix state schema errors or restore from backup'
|
|
332
|
+
};
|
|
333
|
+
}
|
|
172
334
|
|
|
173
335
|
// Track if any changes were made
|
|
174
336
|
let changed = false;
|
|
@@ -224,11 +386,16 @@ function syncStateFromPlanning(repoPath) {
|
|
|
224
386
|
}
|
|
225
387
|
});
|
|
226
388
|
|
|
389
|
+
// Prune events to prevent unbounded growth (PLAN-007 Fix 2)
|
|
390
|
+
colonyState.events = pruneEvents(colonyState.events);
|
|
391
|
+
|
|
227
392
|
// Update last_updated
|
|
228
393
|
colonyState.last_updated = new Date().toISOString();
|
|
229
394
|
|
|
230
|
-
//
|
|
231
|
-
|
|
395
|
+
// Atomic write: write to temp file, then rename (PLAN-002)
|
|
396
|
+
const tempPath = `${colonyStatePath}.tmp`;
|
|
397
|
+
fs.writeFileSync(tempPath, JSON.stringify(colonyState, null, 2) + '\n');
|
|
398
|
+
fs.renameSync(tempPath, colonyStatePath);
|
|
232
399
|
}
|
|
233
400
|
|
|
234
401
|
return {
|
|
@@ -243,6 +410,9 @@ function syncStateFromPlanning(repoPath) {
|
|
|
243
410
|
updates: [],
|
|
244
411
|
error: error.message
|
|
245
412
|
};
|
|
413
|
+
} finally {
|
|
414
|
+
// Always release lock
|
|
415
|
+
lock.release();
|
|
246
416
|
}
|
|
247
417
|
}
|
|
248
418
|
|
|
@@ -278,7 +448,17 @@ function reconcileStates(repoPath) {
|
|
|
278
448
|
};
|
|
279
449
|
}
|
|
280
450
|
|
|
281
|
-
|
|
451
|
+
// Parse JSON with error handling
|
|
452
|
+
let colonyState;
|
|
453
|
+
try {
|
|
454
|
+
colonyState = JSON.parse(fs.readFileSync(colonyStatePath, 'utf8'));
|
|
455
|
+
} catch (parseError) {
|
|
456
|
+
return {
|
|
457
|
+
consistent: false,
|
|
458
|
+
mismatches: [`COLONY_STATE.json contains invalid JSON: ${parseError.message}`],
|
|
459
|
+
resolution: 'Manually fix or delete .aether/data/COLONY_STATE.json and reinitialize'
|
|
460
|
+
};
|
|
461
|
+
}
|
|
282
462
|
|
|
283
463
|
// Check phase mismatch
|
|
284
464
|
if (planningState.phase !== null && planningState.phase !== colonyState.current_phase) {
|
|
@@ -329,5 +509,8 @@ module.exports = {
|
|
|
329
509
|
determineColonyState,
|
|
330
510
|
syncStateFromPlanning,
|
|
331
511
|
reconcileStates,
|
|
332
|
-
updateColonyStateFromPlanning
|
|
512
|
+
updateColonyStateFromPlanning,
|
|
513
|
+
validateStateSchema,
|
|
514
|
+
pruneEvents,
|
|
515
|
+
DEFAULT_MAX_EVENTS,
|
|
333
516
|
};
|
|
@@ -144,11 +144,13 @@ class UpdateTransaction {
|
|
|
144
144
|
* @param {object} options - Transaction options
|
|
145
145
|
* @param {string} options.sourceVersion - Version to update to
|
|
146
146
|
* @param {boolean} options.quiet - Suppress output
|
|
147
|
+
* @param {boolean} options.force - Force update even with dirty files
|
|
147
148
|
*/
|
|
148
149
|
constructor(repoPath, options = {}) {
|
|
149
150
|
this.repoPath = repoPath;
|
|
150
151
|
this.sourceVersion = options.sourceVersion || null;
|
|
151
152
|
this.quiet = options.quiet || false;
|
|
153
|
+
this.force = options.force || false;
|
|
152
154
|
|
|
153
155
|
// Transaction state
|
|
154
156
|
this.state = TransactionStates.PENDING;
|
|
@@ -159,14 +161,15 @@ class UpdateTransaction {
|
|
|
159
161
|
// Hub paths (from cli.js)
|
|
160
162
|
this.HOME = process.env.HOME || process.env.USERPROFILE;
|
|
161
163
|
this.HUB_DIR = path.join(this.HOME, '.aether');
|
|
162
|
-
this.HUB_SYSTEM = path.join(this.HUB_DIR, 'system');
|
|
163
164
|
this.HUB_COMMANDS_CLAUDE = path.join(this.HUB_DIR, 'commands', 'claude');
|
|
164
165
|
this.HUB_COMMANDS_OPENCODE = path.join(this.HUB_DIR, 'commands', 'opencode');
|
|
165
166
|
this.HUB_AGENTS = path.join(this.HUB_DIR, 'agents');
|
|
166
|
-
this.HUB_VISUALIZATIONS = path.join(this.HUB_DIR, 'visualizations');
|
|
167
167
|
this.HUB_VERSION = path.join(this.HUB_DIR, 'version.json');
|
|
168
168
|
this.HUB_REGISTRY = path.join(this.HUB_DIR, 'registry.json');
|
|
169
169
|
|
|
170
|
+
// Directories to exclude from sync (user data, local state)
|
|
171
|
+
this.EXCLUDE_DIRS = ['data', 'dreams', 'checkpoints', 'locks', 'temp'];
|
|
172
|
+
|
|
170
173
|
// Target directories for git safety checks
|
|
171
174
|
this.targetDirs = ['.aether', '.claude/commands/ant', '.opencode/commands/ant', '.opencode/agents'];
|
|
172
175
|
|
|
@@ -183,15 +186,36 @@ class UpdateTransaction {
|
|
|
183
186
|
'verification-loop.md',
|
|
184
187
|
'verification.md',
|
|
185
188
|
'workers.md',
|
|
189
|
+
'workers-new-castes.md',
|
|
190
|
+
'docs/biological-reference.md',
|
|
191
|
+
'docs/command-sync.md',
|
|
186
192
|
'docs/constraints.md',
|
|
193
|
+
'docs/namespace.md',
|
|
187
194
|
'docs/pathogen-schema-example.json',
|
|
188
195
|
'docs/pathogen-schema.md',
|
|
196
|
+
'docs/PHEROMONE-INJECTION.md',
|
|
197
|
+
'docs/PHEROMONE-INTEGRATION.md',
|
|
198
|
+
'docs/PHEROMONE-SYSTEM-DESIGN.md',
|
|
189
199
|
'docs/pheromones.md',
|
|
190
200
|
'docs/progressive-disclosure.md',
|
|
201
|
+
'docs/README.md',
|
|
202
|
+
'docs/VISUAL-OUTPUT-SPEC.md',
|
|
203
|
+
'docs/known-issues.md',
|
|
204
|
+
'docs/implementation-learnings.md',
|
|
205
|
+
'docs/codebase-review.md',
|
|
206
|
+
'docs/planning-discipline.md',
|
|
191
207
|
'utils/atomic-write.sh',
|
|
208
|
+
'utils/chamber-compare.sh',
|
|
209
|
+
'utils/chamber-utils.sh',
|
|
192
210
|
'utils/colorize-log.sh',
|
|
211
|
+
'utils/error-handler.sh',
|
|
193
212
|
'utils/file-lock.sh',
|
|
213
|
+
'utils/spawn-tree.sh',
|
|
214
|
+
'utils/spawn-with-model.sh',
|
|
215
|
+
'utils/state-loader.sh',
|
|
216
|
+
'utils/swarm-display.sh',
|
|
194
217
|
'utils/watch-spawn-tree.sh',
|
|
218
|
+
'templates/QUEEN.md.template',
|
|
195
219
|
];
|
|
196
220
|
}
|
|
197
221
|
|
|
@@ -330,6 +354,12 @@ class UpdateTransaction {
|
|
|
330
354
|
return { clean: true };
|
|
331
355
|
}
|
|
332
356
|
|
|
357
|
+
// If force flag is set, allow dirty repo (will be stashed in checkpoint)
|
|
358
|
+
if (this.force) {
|
|
359
|
+
this.log(' Force flag set: proceeding with dirty repository (will stash changes)');
|
|
360
|
+
return { clean: true, dirty: dirtyState, force: true };
|
|
361
|
+
}
|
|
362
|
+
|
|
333
363
|
// Build detailed error message
|
|
334
364
|
const lines = [
|
|
335
365
|
'Cannot update: repository has uncommitted changes',
|
|
@@ -405,16 +435,48 @@ class UpdateTransaction {
|
|
|
405
435
|
*/
|
|
406
436
|
gitStashFiles(files) {
|
|
407
437
|
try {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
438
|
+
// Separate tracked and untracked files
|
|
439
|
+
const trackedFiles = [];
|
|
440
|
+
const untrackedFiles = [];
|
|
441
|
+
|
|
442
|
+
for (const file of files) {
|
|
443
|
+
const fullPath = path.join(this.repoPath, file);
|
|
444
|
+
try {
|
|
445
|
+
// Check if file is tracked by git
|
|
446
|
+
execSync(`git ls-files --error-unmatch "${file}"`, {
|
|
447
|
+
cwd: this.repoPath,
|
|
448
|
+
stdio: 'pipe'
|
|
449
|
+
});
|
|
450
|
+
trackedFiles.push(file);
|
|
451
|
+
} catch {
|
|
452
|
+
// File is not tracked (untracked or in .gitignore)
|
|
453
|
+
untrackedFiles.push(file);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
413
456
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
457
|
+
let stashRef = null;
|
|
458
|
+
|
|
459
|
+
// Stash tracked files
|
|
460
|
+
if (trackedFiles.length > 0) {
|
|
461
|
+
const fileArgs = trackedFiles.map(f => `"${f}"`).join(' ');
|
|
462
|
+
execSync(`git stash push -m "aether-update-backup" -- ${fileArgs}`, {
|
|
463
|
+
cwd: this.repoPath,
|
|
464
|
+
stdio: 'pipe',
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Get the stash reference
|
|
468
|
+
const stashList = execSync('git stash list', { cwd: this.repoPath, encoding: 'utf8' });
|
|
469
|
+
const match = stashList.match(/^(stash@\{[^}]+\})/m);
|
|
470
|
+
stashRef = match ? match[1] : null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// For untracked files, we can't stash them easily
|
|
474
|
+
// Just log a warning - they'll be left as-is during the update
|
|
475
|
+
if (untrackedFiles.length > 0) {
|
|
476
|
+
this.log(` Note: ${untrackedFiles.length} untracked files won't be stashed (left in place)`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return stashRef;
|
|
418
480
|
} catch (err) {
|
|
419
481
|
this.log(` Warning: git stash failed (${err.message}). Proceeding without stash.`);
|
|
420
482
|
return null;
|
|
@@ -684,6 +746,127 @@ class UpdateTransaction {
|
|
|
684
746
|
return { copied, removed, skipped };
|
|
685
747
|
}
|
|
686
748
|
|
|
749
|
+
/**
|
|
750
|
+
* Check if a path should be excluded from sync
|
|
751
|
+
* @param {string} relPath - Relative path
|
|
752
|
+
* @returns {boolean} True if should be excluded
|
|
753
|
+
* @private
|
|
754
|
+
*/
|
|
755
|
+
shouldExclude(relPath) {
|
|
756
|
+
const parts = relPath.split(path.sep);
|
|
757
|
+
return parts.some(part => this.EXCLUDE_DIRS.includes(part));
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Sync .aether/ directory from hub to repo, excluding user data directories
|
|
762
|
+
* @param {string} srcDir - Source hub directory
|
|
763
|
+
* @param {string} destDir - Destination repo .aether/ directory
|
|
764
|
+
* @param {object} opts - Options
|
|
765
|
+
* @returns {object} Sync result: { copied, removed, skipped }
|
|
766
|
+
* @private
|
|
767
|
+
*/
|
|
768
|
+
syncAetherToRepo(srcDir, destDir, opts) {
|
|
769
|
+
opts = opts || {};
|
|
770
|
+
const dryRun = opts.dryRun || false;
|
|
771
|
+
|
|
772
|
+
if (!fs.existsSync(srcDir)) {
|
|
773
|
+
return { copied: 0, removed: [], skipped: 0 };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Collect all files in source, filtering out excluded directories
|
|
777
|
+
const srcFiles = [];
|
|
778
|
+
const collectFiles = (dir, base) => {
|
|
779
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
780
|
+
for (const entry of entries) {
|
|
781
|
+
if (entry.name.startsWith('.')) continue;
|
|
782
|
+
const fullPath = path.join(dir, entry.name);
|
|
783
|
+
const relPath = path.relative(base, fullPath);
|
|
784
|
+
|
|
785
|
+
if (this.shouldExclude(relPath)) continue;
|
|
786
|
+
|
|
787
|
+
if (entry.isDirectory()) {
|
|
788
|
+
collectFiles(fullPath, base);
|
|
789
|
+
} else {
|
|
790
|
+
srcFiles.push(relPath);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
collectFiles(srcDir, srcDir);
|
|
795
|
+
|
|
796
|
+
// Copy files with hash comparison
|
|
797
|
+
let copied = 0;
|
|
798
|
+
let skipped = 0;
|
|
799
|
+
for (const relPath of srcFiles) {
|
|
800
|
+
const srcPath = path.join(srcDir, relPath);
|
|
801
|
+
const destPath = path.join(destDir, relPath);
|
|
802
|
+
|
|
803
|
+
if (!dryRun) {
|
|
804
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
805
|
+
|
|
806
|
+
// Hash comparison
|
|
807
|
+
let shouldCopy = true;
|
|
808
|
+
if (fs.existsSync(destPath)) {
|
|
809
|
+
const srcHash = this.hashFileSync(srcPath);
|
|
810
|
+
const destHash = this.hashFileSync(destPath);
|
|
811
|
+
if (srcHash === destHash) {
|
|
812
|
+
shouldCopy = false;
|
|
813
|
+
skipped++;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (shouldCopy) {
|
|
818
|
+
fs.copyFileSync(srcPath, destPath);
|
|
819
|
+
if (relPath.endsWith('.sh')) {
|
|
820
|
+
fs.chmodSync(destPath, 0o755);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
copied++;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Cleanup: remove files in dest that aren't in source
|
|
828
|
+
const destFiles = [];
|
|
829
|
+
const collectDestFiles = (dir, base) => {
|
|
830
|
+
if (!fs.existsSync(dir)) return;
|
|
831
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
832
|
+
for (const entry of entries) {
|
|
833
|
+
if (entry.name.startsWith('.')) continue;
|
|
834
|
+
const fullPath = path.join(dir, entry.name);
|
|
835
|
+
const relPath = path.relative(base, fullPath);
|
|
836
|
+
|
|
837
|
+
if (this.shouldExclude(relPath)) continue;
|
|
838
|
+
|
|
839
|
+
if (entry.isDirectory()) {
|
|
840
|
+
collectDestFiles(fullPath, base);
|
|
841
|
+
} else {
|
|
842
|
+
destFiles.push(relPath);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
collectDestFiles(destDir, destDir);
|
|
847
|
+
|
|
848
|
+
const srcSet = new Set(srcFiles);
|
|
849
|
+
const removed = [];
|
|
850
|
+
for (const relPath of destFiles) {
|
|
851
|
+
if (!srcSet.has(relPath)) {
|
|
852
|
+
removed.push(relPath);
|
|
853
|
+
if (!dryRun) {
|
|
854
|
+
try {
|
|
855
|
+
fs.unlinkSync(path.join(destDir, relPath));
|
|
856
|
+
} catch (err) {
|
|
857
|
+
// Ignore cleanup errors
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (!dryRun && removed.length > 0) {
|
|
864
|
+
this.cleanEmptyDirs(destDir);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return { copied, removed, skipped };
|
|
868
|
+
}
|
|
869
|
+
|
|
687
870
|
/**
|
|
688
871
|
* Sync files from hub to repo
|
|
689
872
|
* @param {string} sourceVersion - Version to sync from
|
|
@@ -697,15 +880,14 @@ class UpdateTransaction {
|
|
|
697
880
|
system: { copied: 0, removed: 0, skipped: 0 },
|
|
698
881
|
commands: { copied: 0, removed: 0, skipped: 0 },
|
|
699
882
|
agents: { copied: 0, removed: 0, skipped: 0 },
|
|
700
|
-
visualizations: { copied: 0, removed: 0, skipped: 0 },
|
|
701
883
|
errors: [],
|
|
702
884
|
};
|
|
703
885
|
|
|
704
886
|
const repoAether = path.join(this.repoPath, '.aether');
|
|
705
887
|
|
|
706
|
-
// Sync
|
|
707
|
-
if (fs.existsSync(this.
|
|
708
|
-
results.system = this.
|
|
888
|
+
// Sync .aether/ from hub to repo (excluding user data directories)
|
|
889
|
+
if (fs.existsSync(this.HUB_DIR)) {
|
|
890
|
+
results.system = this.syncAetherToRepo(this.HUB_DIR, repoAether, { dryRun });
|
|
709
891
|
}
|
|
710
892
|
|
|
711
893
|
// Sync commands from hub
|
|
@@ -729,12 +911,6 @@ class UpdateTransaction {
|
|
|
729
911
|
results.agents = this.syncDirWithCleanup(this.HUB_AGENTS, repoAgents, { dryRun });
|
|
730
912
|
}
|
|
731
913
|
|
|
732
|
-
// Sync visualizations from hub
|
|
733
|
-
const repoVisualizations = path.join(this.repoPath, '.aether', 'visualizations');
|
|
734
|
-
if (fs.existsSync(this.HUB_VISUALIZATIONS)) {
|
|
735
|
-
results.visualizations = this.syncDirWithCleanup(this.HUB_VISUALIZATIONS, repoVisualizations, { dryRun });
|
|
736
|
-
}
|
|
737
|
-
|
|
738
914
|
this.syncResult = results;
|
|
739
915
|
return results;
|
|
740
916
|
}
|
|
@@ -754,6 +930,9 @@ class UpdateTransaction {
|
|
|
754
930
|
|
|
755
931
|
const files = this.listFilesRecursive(hubDir);
|
|
756
932
|
for (const relPath of files) {
|
|
933
|
+
// Skip excluded directories
|
|
934
|
+
if (this.shouldExclude(relPath)) continue;
|
|
935
|
+
|
|
757
936
|
const hubPath = path.join(hubDir, relPath);
|
|
758
937
|
const repoPath = path.join(repoDir, relPath);
|
|
759
938
|
|
|
@@ -774,7 +953,7 @@ class UpdateTransaction {
|
|
|
774
953
|
};
|
|
775
954
|
|
|
776
955
|
const repoAether = path.join(this.repoPath, '.aether');
|
|
777
|
-
verifyDir(this.
|
|
956
|
+
verifyDir(this.HUB_DIR, repoAether);
|
|
778
957
|
verifyDir(this.HUB_COMMANDS_CLAUDE, path.join(this.repoPath, '.claude', 'commands', 'ant'));
|
|
779
958
|
verifyDir(this.HUB_COMMANDS_OPENCODE, path.join(this.repoPath, '.opencode', 'commands', 'ant'));
|
|
780
959
|
verifyDir(this.HUB_AGENTS, path.join(this.repoPath, '.opencode', 'agents'));
|
|
@@ -819,7 +998,7 @@ class UpdateTransaction {
|
|
|
819
998
|
}
|
|
820
999
|
};
|
|
821
1000
|
|
|
822
|
-
checkDir(this.
|
|
1001
|
+
checkDir(this.HUB_DIR, '.aether');
|
|
823
1002
|
checkDir(this.HUB_COMMANDS_CLAUDE, 'commands/claude');
|
|
824
1003
|
checkDir(this.HUB_COMMANDS_OPENCODE, 'commands/opencode');
|
|
825
1004
|
checkDir(this.HUB_AGENTS, 'agents');
|
|
@@ -865,6 +1044,9 @@ class UpdateTransaction {
|
|
|
865
1044
|
|
|
866
1045
|
const files = this.listFilesRecursive(hubDir);
|
|
867
1046
|
for (const relPath of files) {
|
|
1047
|
+
// Skip excluded directories
|
|
1048
|
+
if (this.shouldExclude(relPath)) continue;
|
|
1049
|
+
|
|
868
1050
|
const hubPath = path.join(hubDir, relPath);
|
|
869
1051
|
const repoPath = path.join(repoDir, relPath);
|
|
870
1052
|
|
|
@@ -916,7 +1098,7 @@ class UpdateTransaction {
|
|
|
916
1098
|
};
|
|
917
1099
|
|
|
918
1100
|
const repoAether = path.join(this.repoPath, '.aether');
|
|
919
|
-
checkDir(this.
|
|
1101
|
+
checkDir(this.HUB_DIR, repoAether);
|
|
920
1102
|
checkDir(this.HUB_COMMANDS_CLAUDE, path.join(this.repoPath, '.claude', 'commands', 'ant'));
|
|
921
1103
|
checkDir(this.HUB_COMMANDS_OPENCODE, path.join(this.repoPath, '.opencode', 'commands', 'ant'));
|
|
922
1104
|
checkDir(this.HUB_AGENTS, path.join(this.repoPath, '.opencode', 'agents'));
|