aether-colony 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.aether/CONTEXT.md +160 -0
- package/.aether/QUEEN.md +84 -0
- package/.aether/aether-utils.sh +7749 -0
- package/.aether/docs/QUEEN-SYSTEM.md +211 -0
- package/.aether/docs/README.md +68 -0
- package/.aether/docs/caste-system.md +48 -0
- package/.aether/docs/disciplines/DISCIPLINES.md +93 -0
- package/.aether/docs/disciplines/coding-standards.md +197 -0
- package/.aether/docs/disciplines/debugging.md +207 -0
- package/.aether/docs/disciplines/learning.md +254 -0
- package/.aether/docs/disciplines/tdd.md +257 -0
- package/.aether/docs/disciplines/verification-loop.md +167 -0
- package/.aether/docs/disciplines/verification.md +116 -0
- package/.aether/docs/error-codes.md +268 -0
- package/.aether/docs/known-issues.md +233 -0
- package/.aether/docs/pheromones.md +205 -0
- package/.aether/docs/queen-commands.md +97 -0
- package/.aether/exchange/colony-registry.xml +11 -0
- package/.aether/exchange/pheromone-xml.sh +575 -0
- package/.aether/exchange/pheromones.xml +87 -0
- package/.aether/exchange/queen-wisdom.xml +14 -0
- package/.aether/exchange/registry-xml.sh +273 -0
- package/.aether/exchange/wisdom-xml.sh +319 -0
- package/.aether/midden/approach-changes.md +5 -0
- package/.aether/midden/build-failures.md +5 -0
- package/.aether/midden/test-failures.md +5 -0
- package/.aether/model-profiles.yaml +100 -0
- package/.aether/rules/aether-colony.md +134 -0
- package/.aether/schemas/aether-types.xsd +255 -0
- package/.aether/schemas/colony-registry.xsd +309 -0
- package/.aether/schemas/example-prompt-builder.xml +234 -0
- package/.aether/schemas/pheromone.xsd +163 -0
- package/.aether/schemas/prompt.xsd +416 -0
- package/.aether/schemas/queen-wisdom.xsd +325 -0
- package/.aether/schemas/worker-priming.xsd +276 -0
- package/.aether/templates/QUEEN.md.template +79 -0
- package/.aether/templates/colony-state-reset.jq.template +22 -0
- package/.aether/templates/colony-state.template.json +35 -0
- package/.aether/templates/constraints.template.json +9 -0
- package/.aether/templates/crowned-anthill.template.md +36 -0
- package/.aether/templates/handoff-build-error.template.md +30 -0
- package/.aether/templates/handoff-build-success.template.md +39 -0
- package/.aether/templates/handoff.template.md +40 -0
- package/.aether/templates/learning-observations.template.json +6 -0
- package/.aether/templates/midden.template.json +7 -0
- package/.aether/templates/pheromones.template.json +6 -0
- package/.aether/templates/session.template.json +9 -0
- package/.aether/utils/atomic-write.sh +219 -0
- package/.aether/utils/chamber-compare.sh +193 -0
- package/.aether/utils/chamber-utils.sh +297 -0
- package/.aether/utils/colorize-log.sh +132 -0
- package/.aether/utils/error-handler.sh +212 -0
- package/.aether/utils/file-lock.sh +158 -0
- package/.aether/utils/queen-to-md.xsl +395 -0
- package/.aether/utils/semantic-cli.sh +413 -0
- package/.aether/utils/spawn-tree.sh +428 -0
- package/.aether/utils/spawn-with-model.sh +56 -0
- package/.aether/utils/state-loader.sh +215 -0
- package/.aether/utils/swarm-display.sh +268 -0
- package/.aether/utils/watch-spawn-tree.sh +253 -0
- package/.aether/utils/xml-compose.sh +253 -0
- package/.aether/utils/xml-convert.sh +273 -0
- package/.aether/utils/xml-core.sh +186 -0
- package/.aether/utils/xml-query.sh +201 -0
- package/.aether/utils/xml-utils.sh +110 -0
- package/.aether/workers.md +765 -0
- package/.claude/agents/ant/aether-ambassador.md +264 -0
- package/.claude/agents/ant/aether-archaeologist.md +322 -0
- package/.claude/agents/ant/aether-auditor.md +266 -0
- package/.claude/agents/ant/aether-builder.md +187 -0
- package/.claude/agents/ant/aether-chaos.md +268 -0
- package/.claude/agents/ant/aether-chronicler.md +304 -0
- package/.claude/agents/ant/aether-gatekeeper.md +325 -0
- package/.claude/agents/ant/aether-includer.md +373 -0
- package/.claude/agents/ant/aether-keeper.md +271 -0
- package/.claude/agents/ant/aether-measurer.md +317 -0
- package/.claude/agents/ant/aether-probe.md +210 -0
- package/.claude/agents/ant/aether-queen.md +325 -0
- package/.claude/agents/ant/aether-route-setter.md +173 -0
- package/.claude/agents/ant/aether-sage.md +353 -0
- package/.claude/agents/ant/aether-scout.md +142 -0
- package/.claude/agents/ant/aether-surveyor-disciplines.md +416 -0
- package/.claude/agents/ant/aether-surveyor-nest.md +354 -0
- package/.claude/agents/ant/aether-surveyor-pathogens.md +288 -0
- package/.claude/agents/ant/aether-surveyor-provisions.md +359 -0
- package/.claude/agents/ant/aether-tracker.md +265 -0
- package/.claude/agents/ant/aether-watcher.md +244 -0
- package/.claude/agents/ant/aether-weaver.md +247 -0
- package/.claude/commands/ant/archaeology.md +341 -0
- package/.claude/commands/ant/build.md +1160 -0
- package/.claude/commands/ant/chaos.md +349 -0
- package/.claude/commands/ant/colonize.md +270 -0
- package/.claude/commands/ant/continue.md +1070 -0
- package/.claude/commands/ant/council.md +309 -0
- package/.claude/commands/ant/dream.md +265 -0
- package/.claude/commands/ant/entomb.md +487 -0
- package/.claude/commands/ant/feedback.md +78 -0
- package/.claude/commands/ant/flag.md +139 -0
- package/.claude/commands/ant/flags.md +155 -0
- package/.claude/commands/ant/focus.md +58 -0
- package/.claude/commands/ant/help.md +122 -0
- package/.claude/commands/ant/history.md +137 -0
- package/.claude/commands/ant/init.md +409 -0
- package/.claude/commands/ant/interpret.md +267 -0
- package/.claude/commands/ant/lay-eggs.md +201 -0
- package/.claude/commands/ant/maturity.md +102 -0
- package/.claude/commands/ant/memory-details.md +77 -0
- package/.claude/commands/ant/migrate-state.md +165 -0
- package/.claude/commands/ant/oracle.md +387 -0
- package/.claude/commands/ant/organize.md +227 -0
- package/.claude/commands/ant/pause-colony.md +247 -0
- package/.claude/commands/ant/phase.md +126 -0
- package/.claude/commands/ant/plan.md +544 -0
- package/.claude/commands/ant/redirect.md +58 -0
- package/.claude/commands/ant/resume-colony.md +182 -0
- package/.claude/commands/ant/resume.md +363 -0
- package/.claude/commands/ant/seal.md +306 -0
- package/.claude/commands/ant/status.md +272 -0
- package/.claude/commands/ant/swarm.md +361 -0
- package/.claude/commands/ant/tunnels.md +425 -0
- package/.claude/commands/ant/update.md +209 -0
- package/.claude/commands/ant/verify-castes.md +95 -0
- package/.claude/commands/ant/watch.md +238 -0
- package/.opencode/agents/aether-ambassador.md +140 -0
- package/.opencode/agents/aether-archaeologist.md +108 -0
- package/.opencode/agents/aether-auditor.md +144 -0
- package/.opencode/agents/aether-builder.md +184 -0
- package/.opencode/agents/aether-chaos.md +115 -0
- package/.opencode/agents/aether-chronicler.md +122 -0
- package/.opencode/agents/aether-gatekeeper.md +116 -0
- package/.opencode/agents/aether-includer.md +117 -0
- package/.opencode/agents/aether-keeper.md +177 -0
- package/.opencode/agents/aether-measurer.md +128 -0
- package/.opencode/agents/aether-probe.md +133 -0
- package/.opencode/agents/aether-queen.md +286 -0
- package/.opencode/agents/aether-route-setter.md +130 -0
- package/.opencode/agents/aether-sage.md +106 -0
- package/.opencode/agents/aether-scout.md +101 -0
- package/.opencode/agents/aether-surveyor-disciplines.md +386 -0
- package/.opencode/agents/aether-surveyor-nest.md +324 -0
- package/.opencode/agents/aether-surveyor-pathogens.md +259 -0
- package/.opencode/agents/aether-surveyor-provisions.md +329 -0
- package/.opencode/agents/aether-tracker.md +137 -0
- package/.opencode/agents/aether-watcher.md +174 -0
- package/.opencode/agents/aether-weaver.md +130 -0
- package/.opencode/commands/ant/archaeology.md +338 -0
- package/.opencode/commands/ant/build.md +1200 -0
- package/.opencode/commands/ant/chaos.md +346 -0
- package/.opencode/commands/ant/colonize.md +202 -0
- package/.opencode/commands/ant/continue.md +938 -0
- package/.opencode/commands/ant/council.md +305 -0
- package/.opencode/commands/ant/dream.md +262 -0
- package/.opencode/commands/ant/entomb.md +367 -0
- package/.opencode/commands/ant/feedback.md +80 -0
- package/.opencode/commands/ant/flag.md +137 -0
- package/.opencode/commands/ant/flags.md +153 -0
- package/.opencode/commands/ant/focus.md +56 -0
- package/.opencode/commands/ant/help.md +124 -0
- package/.opencode/commands/ant/history.md +127 -0
- package/.opencode/commands/ant/init.md +337 -0
- package/.opencode/commands/ant/interpret.md +256 -0
- package/.opencode/commands/ant/lay-eggs.md +141 -0
- package/.opencode/commands/ant/maturity.md +92 -0
- package/.opencode/commands/ant/memory-details.md +77 -0
- package/.opencode/commands/ant/migrate-state.md +153 -0
- package/.opencode/commands/ant/oracle.md +338 -0
- package/.opencode/commands/ant/organize.md +224 -0
- package/.opencode/commands/ant/pause-colony.md +220 -0
- package/.opencode/commands/ant/phase.md +123 -0
- package/.opencode/commands/ant/plan.md +531 -0
- package/.opencode/commands/ant/redirect.md +67 -0
- package/.opencode/commands/ant/resume-colony.md +178 -0
- package/.opencode/commands/ant/resume.md +363 -0
- package/.opencode/commands/ant/seal.md +247 -0
- package/.opencode/commands/ant/status.md +272 -0
- package/.opencode/commands/ant/swarm.md +357 -0
- package/.opencode/commands/ant/tunnels.md +406 -0
- package/.opencode/commands/ant/update.md +191 -0
- package/.opencode/commands/ant/verify-castes.md +85 -0
- package/.opencode/commands/ant/watch.md +220 -0
- package/.opencode/opencode.json +3 -0
- package/CHANGELOG.md +325 -0
- package/DISCLAIMER.md +74 -0
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/bin/cli.js +2436 -0
- package/bin/generate-commands.sh +291 -0
- package/bin/lib/caste-colors.js +57 -0
- package/bin/lib/colors.js +76 -0
- package/bin/lib/errors.js +255 -0
- package/bin/lib/event-types.js +190 -0
- package/bin/lib/file-lock.js +695 -0
- package/bin/lib/init.js +454 -0
- package/bin/lib/logger.js +242 -0
- package/bin/lib/model-profiles.js +445 -0
- package/bin/lib/model-verify.js +288 -0
- package/bin/lib/nestmate-loader.js +130 -0
- package/bin/lib/proxy-health.js +253 -0
- package/bin/lib/spawn-logger.js +266 -0
- package/bin/lib/state-guard.js +602 -0
- package/bin/lib/state-sync.js +516 -0
- package/bin/lib/telemetry.js +441 -0
- package/bin/lib/update-transaction.js +1454 -0
- package/bin/npx-install.js +178 -0
- package/bin/sync-to-runtime.sh +6 -0
- package/bin/validate-package.sh +88 -0
- package/package.json +70 -0
|
@@ -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
|
+
};
|