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.
Files changed (124) hide show
  1. package/.claude/commands/ant/archaeology.md +12 -0
  2. package/.claude/commands/ant/build.md +382 -319
  3. package/.claude/commands/ant/chaos.md +23 -1
  4. package/.claude/commands/ant/colonize.md +147 -87
  5. package/.claude/commands/ant/continue.md +213 -23
  6. package/.claude/commands/ant/council.md +22 -0
  7. package/.claude/commands/ant/dream.md +18 -0
  8. package/.claude/commands/ant/entomb.md +178 -6
  9. package/.claude/commands/ant/init.md +87 -13
  10. package/.claude/commands/ant/lay-eggs.md +45 -5
  11. package/.claude/commands/ant/oracle.md +82 -9
  12. package/.claude/commands/ant/organize.md +2 -2
  13. package/.claude/commands/ant/pause-colony.md +86 -28
  14. package/.claude/commands/ant/phase.md +26 -0
  15. package/.claude/commands/ant/plan.md +204 -111
  16. package/.claude/commands/ant/resume-colony.md +23 -1
  17. package/.claude/commands/ant/resume.md +159 -0
  18. package/.claude/commands/ant/seal.md +177 -3
  19. package/.claude/commands/ant/swarm.md +78 -97
  20. package/.claude/commands/ant/verify-castes.md +7 -7
  21. package/.claude/commands/ant/watch.md +17 -0
  22. package/.opencode/agents/aether-ambassador.md +97 -0
  23. package/.opencode/agents/aether-archaeologist.md +91 -0
  24. package/.opencode/agents/aether-architect.md +66 -0
  25. package/.opencode/agents/aether-auditor.md +111 -0
  26. package/.opencode/agents/aether-builder.md +28 -10
  27. package/.opencode/agents/aether-chaos.md +98 -0
  28. package/.opencode/agents/aether-chronicler.md +80 -0
  29. package/.opencode/agents/aether-gatekeeper.md +107 -0
  30. package/.opencode/agents/aether-guardian.md +107 -0
  31. package/.opencode/agents/aether-includer.md +108 -0
  32. package/.opencode/agents/aether-keeper.md +106 -0
  33. package/.opencode/agents/aether-measurer.md +119 -0
  34. package/.opencode/agents/aether-probe.md +91 -0
  35. package/.opencode/agents/aether-queen.md +72 -19
  36. package/.opencode/agents/aether-route-setter.md +85 -0
  37. package/.opencode/agents/aether-sage.md +98 -0
  38. package/.opencode/agents/aether-scout.md +33 -15
  39. package/.opencode/agents/aether-surveyor-disciplines.md +334 -0
  40. package/.opencode/agents/aether-surveyor-nest.md +272 -0
  41. package/.opencode/agents/aether-surveyor-pathogens.md +209 -0
  42. package/.opencode/agents/aether-surveyor-provisions.md +277 -0
  43. package/.opencode/agents/aether-tracker.md +91 -0
  44. package/.opencode/agents/aether-watcher.md +30 -12
  45. package/.opencode/agents/aether-weaver.md +87 -0
  46. package/.opencode/agents/workers.md +1034 -0
  47. package/.opencode/commands/ant/archaeology.md +44 -26
  48. package/.opencode/commands/ant/build.md +327 -295
  49. package/.opencode/commands/ant/chaos.md +32 -4
  50. package/.opencode/commands/ant/colonize.md +119 -93
  51. package/.opencode/commands/ant/continue.md +98 -10
  52. package/.opencode/commands/ant/council.md +28 -0
  53. package/.opencode/commands/ant/dream.md +24 -0
  54. package/.opencode/commands/ant/entomb.md +73 -1
  55. package/.opencode/commands/ant/feedback.md +8 -2
  56. package/.opencode/commands/ant/flag.md +9 -3
  57. package/.opencode/commands/ant/flags.md +8 -2
  58. package/.opencode/commands/ant/focus.md +8 -2
  59. package/.opencode/commands/ant/help.md +12 -0
  60. package/.opencode/commands/ant/init.md +49 -4
  61. package/.opencode/commands/ant/lay-eggs.md +30 -2
  62. package/.opencode/commands/ant/oracle.md +39 -7
  63. package/.opencode/commands/ant/organize.md +9 -3
  64. package/.opencode/commands/ant/pause-colony.md +54 -1
  65. package/.opencode/commands/ant/phase.md +36 -4
  66. package/.opencode/commands/ant/plan.md +225 -117
  67. package/.opencode/commands/ant/redirect.md +8 -2
  68. package/.opencode/commands/ant/resume-colony.md +51 -26
  69. package/.opencode/commands/ant/seal.md +76 -0
  70. package/.opencode/commands/ant/status.md +50 -20
  71. package/.opencode/commands/ant/swarm.md +108 -104
  72. package/.opencode/commands/ant/tunnels.md +107 -2
  73. package/CHANGELOG.md +21 -0
  74. package/README.md +199 -86
  75. package/bin/cli.js +142 -25
  76. package/bin/generate-commands.sh +100 -16
  77. package/bin/lib/caste-colors.js +5 -5
  78. package/bin/lib/errors.js +16 -0
  79. package/bin/lib/file-lock.js +279 -44
  80. package/bin/lib/state-sync.js +206 -23
  81. package/bin/lib/update-transaction.js +206 -24
  82. package/bin/sync-to-runtime.sh +129 -0
  83. package/package.json +2 -2
  84. package/runtime/CONTEXT.md +160 -0
  85. package/runtime/aether-utils.sh +1421 -55
  86. package/runtime/docs/AETHER-2.0-IMPLEMENTATION-PLAN.md +1343 -0
  87. package/runtime/docs/AETHER-PHEROMONE-SYSTEM-MASTER-SPEC.md +2642 -0
  88. package/runtime/docs/PHEROMONE-INJECTION.md +240 -0
  89. package/runtime/docs/PHEROMONE-INTEGRATION.md +192 -0
  90. package/runtime/docs/PHEROMONE-SYSTEM-DESIGN.md +426 -0
  91. package/runtime/docs/README.md +94 -0
  92. package/runtime/docs/VISUAL-OUTPUT-SPEC.md +219 -0
  93. package/runtime/docs/biological-reference.md +272 -0
  94. package/runtime/docs/codebase-review.md +399 -0
  95. package/runtime/docs/command-sync.md +164 -0
  96. package/runtime/docs/implementation-learnings.md +89 -0
  97. package/runtime/docs/known-issues.md +217 -0
  98. package/runtime/docs/namespace.md +148 -0
  99. package/runtime/docs/planning-discipline.md +159 -0
  100. package/runtime/lib/queen-utils.sh +729 -0
  101. package/runtime/model-profiles.yaml +100 -0
  102. package/runtime/recover.sh +136 -0
  103. package/runtime/templates/QUEEN.md.template +79 -0
  104. package/runtime/utils/atomic-write.sh +5 -5
  105. package/runtime/utils/chamber-utils.sh +6 -3
  106. package/runtime/utils/error-handler.sh +200 -0
  107. package/runtime/utils/queen-to-md.xsl +395 -0
  108. package/runtime/utils/spawn-tree.sh +428 -0
  109. package/runtime/utils/spawn-with-model.sh +56 -0
  110. package/runtime/utils/state-loader.sh +215 -0
  111. package/runtime/utils/swarm-display.sh +5 -5
  112. package/runtime/utils/watch-spawn-tree.sh +90 -22
  113. package/runtime/utils/xml-compose.sh +247 -0
  114. package/runtime/utils/xml-core.sh +186 -0
  115. package/runtime/utils/xml-utils.sh +2161 -0
  116. package/runtime/verification-loop.md +1 -1
  117. package/runtime/workers-new-castes.md +516 -0
  118. package/runtime/workers.md +20 -8
  119. package/.aether/visualizations/anthill-stages/brood-stable.txt +0 -26
  120. package/.aether/visualizations/anthill-stages/crowned-anthill.txt +0 -30
  121. package/.aether/visualizations/anthill-stages/first-mound.txt +0 -18
  122. package/.aether/visualizations/anthill-stages/open-chambers.txt +0 -24
  123. package/.aether/visualizations/anthill-stages/sealed-chambers.txt +0 -28
  124. package/.aether/visualizations/anthill-stages/ventilated-nest.txt +0 -27
@@ -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
- return 'INITIALIZING';
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
- if (phase === 0) {
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
- if (!fs.existsSync(stateMdPath)) {
151
- return { synced: false, updates: [], error: 'STATE.md not found' };
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
- const stateMdContent = fs.readFileSync(stateMdPath, 'utf8');
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
- const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
162
- phases = parseRoadmapMd(roadmapContent);
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
- const colonyStatePath = path.join(repoPath, '.aether', 'data', 'COLONY_STATE.json');
167
- if (!fs.existsSync(colonyStatePath)) {
168
- return { synced: false, updates: [], error: 'COLONY_STATE.json not found' };
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
- const colonyState = JSON.parse(fs.readFileSync(colonyStatePath, 'utf8'));
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
- // Write updated state
231
- fs.writeFileSync(colonyStatePath, JSON.stringify(colonyState, null, 2) + '\n');
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
- const colonyState = JSON.parse(fs.readFileSync(colonyStatePath, 'utf8'));
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
- const fileArgs = files.map(f => `"${f}"`).join(' ');
409
- execSync(`git stash push -m "aether-update-backup" -- ${fileArgs}`, {
410
- cwd: this.repoPath,
411
- stdio: 'pipe',
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
- // Get the stash reference
415
- const stashList = execSync('git stash list', { cwd: this.repoPath, encoding: 'utf8' });
416
- const match = stashList.match(/^(stash@\{[^}]+\})/m);
417
- return match ? match[1] : null;
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 system files from hub
707
- if (fs.existsSync(this.HUB_SYSTEM)) {
708
- results.system = this.syncSystemFilesWithCleanup(this.HUB_SYSTEM, repoAether, { dryRun });
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.HUB_SYSTEM, repoAether);
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.HUB_SYSTEM, 'system');
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.HUB_SYSTEM, repoAether);
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'));