aether-colony 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/.aether/CONTEXT.md +160 -0
  2. package/.aether/QUEEN.md +84 -0
  3. package/.aether/aether-utils.sh +7749 -0
  4. package/.aether/docs/QUEEN-SYSTEM.md +211 -0
  5. package/.aether/docs/README.md +68 -0
  6. package/.aether/docs/caste-system.md +48 -0
  7. package/.aether/docs/disciplines/DISCIPLINES.md +93 -0
  8. package/.aether/docs/disciplines/coding-standards.md +197 -0
  9. package/.aether/docs/disciplines/debugging.md +207 -0
  10. package/.aether/docs/disciplines/learning.md +254 -0
  11. package/.aether/docs/disciplines/tdd.md +257 -0
  12. package/.aether/docs/disciplines/verification-loop.md +167 -0
  13. package/.aether/docs/disciplines/verification.md +116 -0
  14. package/.aether/docs/error-codes.md +268 -0
  15. package/.aether/docs/known-issues.md +233 -0
  16. package/.aether/docs/pheromones.md +205 -0
  17. package/.aether/docs/queen-commands.md +97 -0
  18. package/.aether/exchange/colony-registry.xml +11 -0
  19. package/.aether/exchange/pheromone-xml.sh +575 -0
  20. package/.aether/exchange/pheromones.xml +87 -0
  21. package/.aether/exchange/queen-wisdom.xml +14 -0
  22. package/.aether/exchange/registry-xml.sh +273 -0
  23. package/.aether/exchange/wisdom-xml.sh +319 -0
  24. package/.aether/midden/approach-changes.md +5 -0
  25. package/.aether/midden/build-failures.md +5 -0
  26. package/.aether/midden/test-failures.md +5 -0
  27. package/.aether/model-profiles.yaml +100 -0
  28. package/.aether/rules/aether-colony.md +134 -0
  29. package/.aether/schemas/aether-types.xsd +255 -0
  30. package/.aether/schemas/colony-registry.xsd +309 -0
  31. package/.aether/schemas/example-prompt-builder.xml +234 -0
  32. package/.aether/schemas/pheromone.xsd +163 -0
  33. package/.aether/schemas/prompt.xsd +416 -0
  34. package/.aether/schemas/queen-wisdom.xsd +325 -0
  35. package/.aether/schemas/worker-priming.xsd +276 -0
  36. package/.aether/templates/QUEEN.md.template +79 -0
  37. package/.aether/templates/colony-state-reset.jq.template +22 -0
  38. package/.aether/templates/colony-state.template.json +35 -0
  39. package/.aether/templates/constraints.template.json +9 -0
  40. package/.aether/templates/crowned-anthill.template.md +36 -0
  41. package/.aether/templates/handoff-build-error.template.md +30 -0
  42. package/.aether/templates/handoff-build-success.template.md +39 -0
  43. package/.aether/templates/handoff.template.md +40 -0
  44. package/.aether/templates/learning-observations.template.json +6 -0
  45. package/.aether/templates/midden.template.json +7 -0
  46. package/.aether/templates/pheromones.template.json +6 -0
  47. package/.aether/templates/session.template.json +9 -0
  48. package/.aether/utils/atomic-write.sh +219 -0
  49. package/.aether/utils/chamber-compare.sh +193 -0
  50. package/.aether/utils/chamber-utils.sh +297 -0
  51. package/.aether/utils/colorize-log.sh +132 -0
  52. package/.aether/utils/error-handler.sh +212 -0
  53. package/.aether/utils/file-lock.sh +158 -0
  54. package/.aether/utils/queen-to-md.xsl +395 -0
  55. package/.aether/utils/semantic-cli.sh +413 -0
  56. package/.aether/utils/spawn-tree.sh +428 -0
  57. package/.aether/utils/spawn-with-model.sh +56 -0
  58. package/.aether/utils/state-loader.sh +215 -0
  59. package/.aether/utils/swarm-display.sh +268 -0
  60. package/.aether/utils/watch-spawn-tree.sh +253 -0
  61. package/.aether/utils/xml-compose.sh +253 -0
  62. package/.aether/utils/xml-convert.sh +273 -0
  63. package/.aether/utils/xml-core.sh +186 -0
  64. package/.aether/utils/xml-query.sh +201 -0
  65. package/.aether/utils/xml-utils.sh +110 -0
  66. package/.aether/workers.md +765 -0
  67. package/.claude/agents/ant/aether-ambassador.md +264 -0
  68. package/.claude/agents/ant/aether-archaeologist.md +322 -0
  69. package/.claude/agents/ant/aether-auditor.md +266 -0
  70. package/.claude/agents/ant/aether-builder.md +187 -0
  71. package/.claude/agents/ant/aether-chaos.md +268 -0
  72. package/.claude/agents/ant/aether-chronicler.md +304 -0
  73. package/.claude/agents/ant/aether-gatekeeper.md +325 -0
  74. package/.claude/agents/ant/aether-includer.md +373 -0
  75. package/.claude/agents/ant/aether-keeper.md +271 -0
  76. package/.claude/agents/ant/aether-measurer.md +317 -0
  77. package/.claude/agents/ant/aether-probe.md +210 -0
  78. package/.claude/agents/ant/aether-queen.md +325 -0
  79. package/.claude/agents/ant/aether-route-setter.md +173 -0
  80. package/.claude/agents/ant/aether-sage.md +353 -0
  81. package/.claude/agents/ant/aether-scout.md +142 -0
  82. package/.claude/agents/ant/aether-surveyor-disciplines.md +416 -0
  83. package/.claude/agents/ant/aether-surveyor-nest.md +354 -0
  84. package/.claude/agents/ant/aether-surveyor-pathogens.md +288 -0
  85. package/.claude/agents/ant/aether-surveyor-provisions.md +359 -0
  86. package/.claude/agents/ant/aether-tracker.md +265 -0
  87. package/.claude/agents/ant/aether-watcher.md +244 -0
  88. package/.claude/agents/ant/aether-weaver.md +247 -0
  89. package/.claude/commands/ant/archaeology.md +341 -0
  90. package/.claude/commands/ant/build.md +1160 -0
  91. package/.claude/commands/ant/chaos.md +349 -0
  92. package/.claude/commands/ant/colonize.md +270 -0
  93. package/.claude/commands/ant/continue.md +1070 -0
  94. package/.claude/commands/ant/council.md +309 -0
  95. package/.claude/commands/ant/dream.md +265 -0
  96. package/.claude/commands/ant/entomb.md +487 -0
  97. package/.claude/commands/ant/feedback.md +78 -0
  98. package/.claude/commands/ant/flag.md +139 -0
  99. package/.claude/commands/ant/flags.md +155 -0
  100. package/.claude/commands/ant/focus.md +58 -0
  101. package/.claude/commands/ant/help.md +122 -0
  102. package/.claude/commands/ant/history.md +137 -0
  103. package/.claude/commands/ant/init.md +409 -0
  104. package/.claude/commands/ant/interpret.md +267 -0
  105. package/.claude/commands/ant/lay-eggs.md +201 -0
  106. package/.claude/commands/ant/maturity.md +102 -0
  107. package/.claude/commands/ant/memory-details.md +77 -0
  108. package/.claude/commands/ant/migrate-state.md +165 -0
  109. package/.claude/commands/ant/oracle.md +387 -0
  110. package/.claude/commands/ant/organize.md +227 -0
  111. package/.claude/commands/ant/pause-colony.md +247 -0
  112. package/.claude/commands/ant/phase.md +126 -0
  113. package/.claude/commands/ant/plan.md +544 -0
  114. package/.claude/commands/ant/redirect.md +58 -0
  115. package/.claude/commands/ant/resume-colony.md +182 -0
  116. package/.claude/commands/ant/resume.md +363 -0
  117. package/.claude/commands/ant/seal.md +306 -0
  118. package/.claude/commands/ant/status.md +272 -0
  119. package/.claude/commands/ant/swarm.md +361 -0
  120. package/.claude/commands/ant/tunnels.md +425 -0
  121. package/.claude/commands/ant/update.md +209 -0
  122. package/.claude/commands/ant/verify-castes.md +95 -0
  123. package/.claude/commands/ant/watch.md +238 -0
  124. package/.opencode/agents/aether-ambassador.md +140 -0
  125. package/.opencode/agents/aether-archaeologist.md +108 -0
  126. package/.opencode/agents/aether-auditor.md +144 -0
  127. package/.opencode/agents/aether-builder.md +184 -0
  128. package/.opencode/agents/aether-chaos.md +115 -0
  129. package/.opencode/agents/aether-chronicler.md +122 -0
  130. package/.opencode/agents/aether-gatekeeper.md +116 -0
  131. package/.opencode/agents/aether-includer.md +117 -0
  132. package/.opencode/agents/aether-keeper.md +177 -0
  133. package/.opencode/agents/aether-measurer.md +128 -0
  134. package/.opencode/agents/aether-probe.md +133 -0
  135. package/.opencode/agents/aether-queen.md +286 -0
  136. package/.opencode/agents/aether-route-setter.md +130 -0
  137. package/.opencode/agents/aether-sage.md +106 -0
  138. package/.opencode/agents/aether-scout.md +101 -0
  139. package/.opencode/agents/aether-surveyor-disciplines.md +386 -0
  140. package/.opencode/agents/aether-surveyor-nest.md +324 -0
  141. package/.opencode/agents/aether-surveyor-pathogens.md +259 -0
  142. package/.opencode/agents/aether-surveyor-provisions.md +329 -0
  143. package/.opencode/agents/aether-tracker.md +137 -0
  144. package/.opencode/agents/aether-watcher.md +174 -0
  145. package/.opencode/agents/aether-weaver.md +130 -0
  146. package/.opencode/commands/ant/archaeology.md +338 -0
  147. package/.opencode/commands/ant/build.md +1200 -0
  148. package/.opencode/commands/ant/chaos.md +346 -0
  149. package/.opencode/commands/ant/colonize.md +202 -0
  150. package/.opencode/commands/ant/continue.md +938 -0
  151. package/.opencode/commands/ant/council.md +305 -0
  152. package/.opencode/commands/ant/dream.md +262 -0
  153. package/.opencode/commands/ant/entomb.md +367 -0
  154. package/.opencode/commands/ant/feedback.md +80 -0
  155. package/.opencode/commands/ant/flag.md +137 -0
  156. package/.opencode/commands/ant/flags.md +153 -0
  157. package/.opencode/commands/ant/focus.md +56 -0
  158. package/.opencode/commands/ant/help.md +124 -0
  159. package/.opencode/commands/ant/history.md +127 -0
  160. package/.opencode/commands/ant/init.md +337 -0
  161. package/.opencode/commands/ant/interpret.md +256 -0
  162. package/.opencode/commands/ant/lay-eggs.md +141 -0
  163. package/.opencode/commands/ant/maturity.md +92 -0
  164. package/.opencode/commands/ant/memory-details.md +77 -0
  165. package/.opencode/commands/ant/migrate-state.md +153 -0
  166. package/.opencode/commands/ant/oracle.md +338 -0
  167. package/.opencode/commands/ant/organize.md +224 -0
  168. package/.opencode/commands/ant/pause-colony.md +220 -0
  169. package/.opencode/commands/ant/phase.md +123 -0
  170. package/.opencode/commands/ant/plan.md +531 -0
  171. package/.opencode/commands/ant/redirect.md +67 -0
  172. package/.opencode/commands/ant/resume-colony.md +178 -0
  173. package/.opencode/commands/ant/resume.md +363 -0
  174. package/.opencode/commands/ant/seal.md +247 -0
  175. package/.opencode/commands/ant/status.md +272 -0
  176. package/.opencode/commands/ant/swarm.md +357 -0
  177. package/.opencode/commands/ant/tunnels.md +406 -0
  178. package/.opencode/commands/ant/update.md +191 -0
  179. package/.opencode/commands/ant/verify-castes.md +85 -0
  180. package/.opencode/commands/ant/watch.md +220 -0
  181. package/.opencode/opencode.json +3 -0
  182. package/CHANGELOG.md +325 -0
  183. package/DISCLAIMER.md +74 -0
  184. package/LICENSE +21 -0
  185. package/README.md +258 -0
  186. package/bin/cli.js +2436 -0
  187. package/bin/generate-commands.sh +291 -0
  188. package/bin/lib/caste-colors.js +57 -0
  189. package/bin/lib/colors.js +76 -0
  190. package/bin/lib/errors.js +255 -0
  191. package/bin/lib/event-types.js +190 -0
  192. package/bin/lib/file-lock.js +695 -0
  193. package/bin/lib/init.js +454 -0
  194. package/bin/lib/logger.js +242 -0
  195. package/bin/lib/model-profiles.js +445 -0
  196. package/bin/lib/model-verify.js +288 -0
  197. package/bin/lib/nestmate-loader.js +130 -0
  198. package/bin/lib/proxy-health.js +253 -0
  199. package/bin/lib/spawn-logger.js +266 -0
  200. package/bin/lib/state-guard.js +602 -0
  201. package/bin/lib/state-sync.js +516 -0
  202. package/bin/lib/telemetry.js +441 -0
  203. package/bin/lib/update-transaction.js +1454 -0
  204. package/bin/npx-install.js +178 -0
  205. package/bin/sync-to-runtime.sh +6 -0
  206. package/bin/validate-package.sh +88 -0
  207. package/package.json +70 -0
@@ -0,0 +1,516 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * State Synchronization Module
4
+ *
5
+ * Fixes the "split brain" between .planning/STATE.md and COLONY_STATE.json.
6
+ * Synchronizes planning state with runtime state to ensure consistency.
7
+ *
8
+ * @module bin/lib/state-sync
9
+ */
10
+
11
+ const fs = require('fs');
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
+ }
103
+
104
+ /**
105
+ * Parse STATE.md markdown content to extract current state
106
+ * @param {string} content - STATE.md file content
107
+ * @returns {object} Parsed state: { phase, milestone, status, lastAction }
108
+ */
109
+ function parseStateMd(content) {
110
+ const result = {
111
+ phase: null,
112
+ milestone: null,
113
+ status: null,
114
+ lastAction: null
115
+ };
116
+
117
+ if (!content || typeof content !== 'string') {
118
+ return result;
119
+ }
120
+
121
+ // Extract phase from "Phase X" or "Phase: X"
122
+ const phaseMatch = content.match(/Phase\s*(\d+)/i);
123
+ if (phaseMatch) {
124
+ result.phase = parseInt(phaseMatch[1], 10);
125
+ }
126
+
127
+ // Extract milestone from "Current Milestone:** ..." or "Milestone:** ..."
128
+ const milestoneMatch = content.match(/(?:Current\s*)?Milestone:\*\*?\s*([^\n]+)/i);
129
+ if (milestoneMatch) {
130
+ result.milestone = milestoneMatch[1].trim();
131
+ }
132
+
133
+ // Extract status from "Status:** ..." or similar
134
+ const statusMatch = content.match(/Status:\*\*?\s*([^\n]+)/i);
135
+ if (statusMatch) {
136
+ result.status = statusMatch[1].trim();
137
+ }
138
+
139
+ // Extract last action from "Last Action:** ..." or similar
140
+ const lastActionMatch = content.match(/Last\s*Action:\*\*?\s*([^\n]+)/i);
141
+ if (lastActionMatch) {
142
+ result.lastAction = lastActionMatch[1].trim();
143
+ }
144
+
145
+ return result;
146
+ }
147
+
148
+ /**
149
+ * Parse ROADMAP.md to extract phases
150
+ * @param {string} content - ROADMAP.md file content
151
+ * @returns {Array} Array of phase objects: { number, name, status }
152
+ */
153
+ function parseRoadmapMd(content) {
154
+ const phases = [];
155
+
156
+ if (!content || typeof content !== 'string') {
157
+ return phases;
158
+ }
159
+
160
+ // Match phase headers like "## Phase 1: Name" or "### Phase 1: Name"
161
+ const phaseRegex = /#{2,3}\s*Phase\s*(\d+)[:\s]+([^\n]+)/gi;
162
+ let match;
163
+
164
+ while ((match = phaseRegex.exec(content)) !== null) {
165
+ const phaseNum = parseInt(match[1], 10);
166
+ const phaseName = match[2].trim();
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
+
173
+ // Look for status indicators near this phase
174
+ const sectionStart = match.index;
175
+ const nextPhaseMatch = phaseRegex.exec(content);
176
+ phaseRegex.lastIndex = sectionStart + 1; // Reset for next iteration
177
+
178
+ const sectionEnd = nextPhaseMatch ? nextPhaseMatch.index : content.length;
179
+ const section = content.substring(sectionStart, sectionEnd);
180
+
181
+ // Determine status from section content
182
+ let status = 'planned';
183
+ if (section.includes('Status: complete') || section.includes('COMPLETE')) {
184
+ status = 'completed';
185
+ } else if (section.includes('Status: in progress') || section.includes('IN PROGRESS')) {
186
+ status = 'in_progress';
187
+ } else if (section.includes('Status: ready') || section.includes('READY')) {
188
+ status = 'ready';
189
+ }
190
+
191
+ phases.push({
192
+ number: phaseNum,
193
+ name: phaseName,
194
+ status
195
+ });
196
+ }
197
+
198
+ return phases;
199
+ }
200
+
201
+ /**
202
+ * Determine colony state based on planning status
203
+ * @param {string} status - Status from STATE.md
204
+ * @param {number} phase - Current phase number
205
+ * @returns {string} Colony state: INITIALIZING|PLANNING|BUILDING|COMPLETED
206
+ */
207
+ function determineColonyState(status, phase) {
208
+ // No status - determine by phase (PLAN-006 fix #7)
209
+ if (!status) {
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';
215
+ }
216
+
217
+ const statusLower = status.toLowerCase();
218
+
219
+ if (statusLower.includes('complete') && !statusLower.includes('ready')) {
220
+ return 'COMPLETED';
221
+ }
222
+
223
+ if (statusLower.includes('plan') || statusLower.includes('ready')) {
224
+ return 'PLANNING';
225
+ }
226
+
227
+ if (statusLower.includes('build') || statusLower.includes('progress') || statusLower.includes('in progress')) {
228
+ return 'BUILDING';
229
+ }
230
+
231
+ // Default based on phase - Phase 0 is PLANNING, not INITIALIZING
232
+ return phase === 0 ? 'PLANNING' : 'BUILDING';
233
+ }
234
+
235
+ /**
236
+ * Synchronize COLONY_STATE.json with .planning/STATE.md
237
+ * @param {string} repoPath - Path to repository root
238
+ * @returns {object} Sync result: { synced: boolean, updates: string[], error?: string, recovery?: string }
239
+ */
240
+ function syncStateFromPlanning(repoPath) {
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
+ }
260
+
261
+ try {
262
+ // Read STATE.md with improved error handling (PLAN-006 fix #9)
263
+ const stateMdPath = path.join(repoPath, '.planning', 'STATE.md');
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}` };
273
+ }
274
+
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
+ }
284
+ const planningState = parseStateMd(stateMdContent);
285
+
286
+ // Read ROADMAP.md for phases
287
+ const roadmapPath = path.join(repoPath, '.planning', 'ROADMAP.md');
288
+ let phases = [];
289
+ if (fs.existsSync(roadmapPath)) {
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
+ }
297
+ }
298
+
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
+ };
322
+ }
323
+
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
+ }
334
+
335
+ // Track if any changes were made
336
+ let changed = false;
337
+
338
+ // Update goal from milestone
339
+ if (planningState.milestone && planningState.milestone !== colonyState.goal) {
340
+ colonyState.goal = planningState.milestone;
341
+ updates.push(`goal: ${colonyState.goal}`);
342
+ changed = true;
343
+ }
344
+
345
+ // Update current_phase
346
+ if (planningState.phase !== null && planningState.phase !== colonyState.current_phase) {
347
+ colonyState.current_phase = planningState.phase;
348
+ updates.push(`current_phase: ${colonyState.current_phase}`);
349
+ changed = true;
350
+ }
351
+
352
+ // Update state based on status
353
+ const newState = determineColonyState(planningState.status, planningState.phase);
354
+ if (newState !== colonyState.state) {
355
+ colonyState.state = newState;
356
+ updates.push(`state: ${colonyState.state}`);
357
+ changed = true;
358
+ }
359
+
360
+ // Update plan.phases from ROADMAP
361
+ if (phases.length > 0) {
362
+ const existingPhases = colonyState.plan?.phases || [];
363
+ if (JSON.stringify(phases) !== JSON.stringify(existingPhases)) {
364
+ if (!colonyState.plan) {
365
+ colonyState.plan = {};
366
+ }
367
+ colonyState.plan.phases = phases;
368
+ updates.push(`plan.phases: ${phases.length} phases`);
369
+ changed = true;
370
+ }
371
+ }
372
+
373
+ // Add sync event if changed
374
+ if (changed) {
375
+ if (!colonyState.events) {
376
+ colonyState.events = [];
377
+ }
378
+
379
+ colonyState.events.push({
380
+ timestamp: new Date().toISOString(),
381
+ type: 'state_synced_from_planning',
382
+ worker: 'state-sync',
383
+ details: {
384
+ updates: updates,
385
+ source: '.planning/STATE.md'
386
+ }
387
+ });
388
+
389
+ // Prune events to prevent unbounded growth (PLAN-007 Fix 2)
390
+ colonyState.events = pruneEvents(colonyState.events);
391
+
392
+ // Update last_updated
393
+ colonyState.last_updated = new Date().toISOString();
394
+
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);
399
+ }
400
+
401
+ return {
402
+ synced: true,
403
+ updates,
404
+ changed
405
+ };
406
+
407
+ } catch (error) {
408
+ return {
409
+ synced: false,
410
+ updates: [],
411
+ error: error.message
412
+ };
413
+ } finally {
414
+ // Always release lock
415
+ lock.release();
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Reconcile STATE.md and COLONY_STATE.json to detect mismatches
421
+ * @param {string} repoPath - Path to repository root
422
+ * @returns {object} Reconciliation result: { consistent: boolean, mismatches: string[], resolution: string }
423
+ */
424
+ function reconcileStates(repoPath) {
425
+ const mismatches = [];
426
+
427
+ try {
428
+ // Read STATE.md
429
+ const stateMdPath = path.join(repoPath, '.planning', 'STATE.md');
430
+ if (!fs.existsSync(stateMdPath)) {
431
+ return {
432
+ consistent: false,
433
+ mismatches: ['STATE.md not found'],
434
+ resolution: 'Create .planning/STATE.md'
435
+ };
436
+ }
437
+
438
+ const stateMdContent = fs.readFileSync(stateMdPath, 'utf8');
439
+ const planningState = parseStateMd(stateMdContent);
440
+
441
+ // Read COLONY_STATE.json
442
+ const colonyStatePath = path.join(repoPath, '.aether', 'data', 'COLONY_STATE.json');
443
+ if (!fs.existsSync(colonyStatePath)) {
444
+ return {
445
+ consistent: false,
446
+ mismatches: ['COLONY_STATE.json not found'],
447
+ resolution: 'Run: aether init'
448
+ };
449
+ }
450
+
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
+ }
462
+
463
+ // Check phase mismatch
464
+ if (planningState.phase !== null && planningState.phase !== colonyState.current_phase) {
465
+ mismatches.push(`Phase mismatch: STATE.md says ${planningState.phase}, COLONY_STATE.json says ${colonyState.current_phase}`);
466
+ }
467
+
468
+ // Check goal/milestone mismatch
469
+ if (planningState.milestone && planningState.milestone !== colonyState.goal) {
470
+ mismatches.push(`Goal mismatch: STATE.md milestone differs from COLONY_STATE.json goal`);
471
+ }
472
+
473
+ // Check status contradiction
474
+ const expectedState = determineColonyState(planningState.status, planningState.phase);
475
+ if (expectedState !== colonyState.state) {
476
+ mismatches.push(`Status contradiction: STATE.md implies ${expectedState}, COLONY_STATE.json is ${colonyState.state}`);
477
+ }
478
+
479
+ return {
480
+ consistent: mismatches.length === 0,
481
+ mismatches,
482
+ resolution: mismatches.length > 0
483
+ ? 'Run: aether sync-state'
484
+ : 'No action needed'
485
+ };
486
+
487
+ } catch (error) {
488
+ return {
489
+ consistent: false,
490
+ mismatches: [`Error during reconciliation: ${error.message}`],
491
+ resolution: 'Check file permissions and try again'
492
+ };
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Full sync: read STATE.md and update COLONY_STATE.json
498
+ * This is a convenience wrapper around syncStateFromPlanning
499
+ * @param {string} repoPath - Path to repository root
500
+ * @returns {object} Sync result
501
+ */
502
+ function updateColonyStateFromPlanning(repoPath) {
503
+ return syncStateFromPlanning(repoPath);
504
+ }
505
+
506
+ module.exports = {
507
+ parseStateMd,
508
+ parseRoadmapMd,
509
+ determineColonyState,
510
+ syncStateFromPlanning,
511
+ reconcileStates,
512
+ updateColonyStateFromPlanning,
513
+ validateStateSchema,
514
+ pruneEvents,
515
+ DEFAULT_MAX_EVENTS,
516
+ };