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,602 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* State Guard Module
|
|
4
|
+
*
|
|
5
|
+
* Enforces the Iron Law: phase advancement requires fresh verification evidence.
|
|
6
|
+
* Provides StateGuard class with idempotency checks, file locking, and structured errors.
|
|
7
|
+
*
|
|
8
|
+
* @module bin/lib/state-guard
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { EventTypes, createEvent } = require('./event-types');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Error codes for StateGuard errors
|
|
17
|
+
*/
|
|
18
|
+
const StateGuardErrorCodes = {
|
|
19
|
+
E_IRON_LAW_VIOLATION: 'E_IRON_LAW_VIOLATION',
|
|
20
|
+
E_IDEMPOTENCY_CHECK: 'E_IDEMPOTENCY_CHECK',
|
|
21
|
+
E_LOCK_TIMEOUT: 'E_LOCK_TIMEOUT',
|
|
22
|
+
E_INVALID_TRANSITION: 'E_INVALID_TRANSITION',
|
|
23
|
+
E_STATE_NOT_FOUND: 'E_STATE_NOT_FOUND',
|
|
24
|
+
E_STATE_INVALID: 'E_STATE_INVALID',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* StateGuardError - Structured error for state guard violations
|
|
29
|
+
*/
|
|
30
|
+
class StateGuardError extends Error {
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} code - Error code from StateGuardErrorCodes
|
|
33
|
+
* @param {string} message - Human-readable error message
|
|
34
|
+
* @param {object} details - Additional error context
|
|
35
|
+
* @param {string|null} recovery - Recovery suggestion for user
|
|
36
|
+
*/
|
|
37
|
+
constructor(code, message, details = {}, recovery = null) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'StateGuardError';
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.details = details;
|
|
42
|
+
this.recovery = recovery;
|
|
43
|
+
this.timestamp = new Date().toISOString();
|
|
44
|
+
|
|
45
|
+
// Maintain proper stack trace in V8 environments
|
|
46
|
+
if (Error.captureStackTrace) {
|
|
47
|
+
Error.captureStackTrace(this, StateGuardError);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Convert error to structured JSON object
|
|
53
|
+
* @returns {object} Structured error representation
|
|
54
|
+
*/
|
|
55
|
+
toJSON() {
|
|
56
|
+
return {
|
|
57
|
+
error: {
|
|
58
|
+
name: this.name,
|
|
59
|
+
code: this.code,
|
|
60
|
+
message: this.message,
|
|
61
|
+
details: this.details,
|
|
62
|
+
recovery: this.recovery,
|
|
63
|
+
timestamp: this.timestamp,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Convert error to string for console output
|
|
70
|
+
* @returns {string} Formatted error string
|
|
71
|
+
*/
|
|
72
|
+
toString() {
|
|
73
|
+
let str = `${this.code}: ${this.message}`;
|
|
74
|
+
if (this.recovery) {
|
|
75
|
+
str += `\n Recovery: ${this.recovery}`;
|
|
76
|
+
}
|
|
77
|
+
return str;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* FileLock - PID-based file locking with stale lock detection
|
|
83
|
+
*/
|
|
84
|
+
class FileLock {
|
|
85
|
+
/**
|
|
86
|
+
* @param {string} lockDir - Directory for lock files (default: .aether/locks)
|
|
87
|
+
* @param {object} options - Lock configuration options
|
|
88
|
+
*/
|
|
89
|
+
constructor(lockDir = '.aether/locks', options = {}) {
|
|
90
|
+
this.lockDir = lockDir;
|
|
91
|
+
this.lockTimeout = options.lockTimeout || 300000; // 5 minutes
|
|
92
|
+
this.retryInterval = options.retryInterval || 500; // 500ms
|
|
93
|
+
this.maxRetries = options.maxRetries || 100; // 50 seconds max wait
|
|
94
|
+
this.currentLock = null;
|
|
95
|
+
this.currentPidFile = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Acquire lock on a file
|
|
100
|
+
* @param {string} filePath - Path to file to lock
|
|
101
|
+
* @returns {Promise<boolean>} True if lock acquired, false otherwise
|
|
102
|
+
*/
|
|
103
|
+
async acquire(filePath) {
|
|
104
|
+
const lockFile = path.join(this.lockDir, `${path.basename(filePath)}.lock`);
|
|
105
|
+
const pidFile = `${lockFile}.pid`;
|
|
106
|
+
|
|
107
|
+
// Ensure lock directory exists
|
|
108
|
+
if (!fs.existsSync(this.lockDir)) {
|
|
109
|
+
fs.mkdirSync(this.lockDir, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check for stale lock
|
|
113
|
+
if (fs.existsSync(lockFile)) {
|
|
114
|
+
const lockPid = this.readPidFile(pidFile);
|
|
115
|
+
if (lockPid && !this.isProcessRunning(lockPid)) {
|
|
116
|
+
console.log(`Lock stale (PID ${lockPid} not running), cleaning up...`);
|
|
117
|
+
this.cleanupLock(lockFile, pidFile);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Try to acquire with retry
|
|
122
|
+
for (let retry = 0; retry < this.maxRetries; retry++) {
|
|
123
|
+
try {
|
|
124
|
+
// Atomic lock creation using exclusive flag
|
|
125
|
+
const fd = fs.openSync(lockFile, 'wx');
|
|
126
|
+
fs.writeSync(fd, process.pid.toString());
|
|
127
|
+
fs.closeSync(fd);
|
|
128
|
+
|
|
129
|
+
// Write PID file
|
|
130
|
+
fs.writeFileSync(pidFile, process.pid.toString());
|
|
131
|
+
|
|
132
|
+
this.currentLock = lockFile;
|
|
133
|
+
this.currentPidFile = pidFile;
|
|
134
|
+
return true;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (err.code !== 'EEXIST') throw err;
|
|
137
|
+
|
|
138
|
+
// Wait before retry
|
|
139
|
+
if (retry < this.maxRetries - 1) {
|
|
140
|
+
await this.sleep(this.retryInterval);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Release the current lock
|
|
150
|
+
*/
|
|
151
|
+
release() {
|
|
152
|
+
if (this.currentLock) {
|
|
153
|
+
this.cleanupLock(this.currentLock, this.currentPidFile);
|
|
154
|
+
this.currentLock = null;
|
|
155
|
+
this.currentPidFile = null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if a process is running
|
|
161
|
+
* @param {string} pid - Process ID to check
|
|
162
|
+
* @returns {boolean} True if process is running
|
|
163
|
+
*/
|
|
164
|
+
isProcessRunning(pid) {
|
|
165
|
+
try {
|
|
166
|
+
process.kill(parseInt(pid), 0);
|
|
167
|
+
return true;
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Read PID from PID file
|
|
175
|
+
* @param {string} pidFile - Path to PID file
|
|
176
|
+
* @returns {string|null} PID or null if file doesn't exist
|
|
177
|
+
*/
|
|
178
|
+
readPidFile(pidFile) {
|
|
179
|
+
try {
|
|
180
|
+
return fs.readFileSync(pidFile, 'utf8').trim();
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Clean up lock files
|
|
188
|
+
* @param {string} lockFile - Path to lock file
|
|
189
|
+
* @param {string} pidFile - Path to PID file
|
|
190
|
+
*/
|
|
191
|
+
cleanupLock(lockFile, pidFile) {
|
|
192
|
+
try { fs.unlinkSync(lockFile); } catch {}
|
|
193
|
+
try { fs.unlinkSync(pidFile); } catch {}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Sleep for specified milliseconds
|
|
198
|
+
* @param {number} ms - Milliseconds to sleep
|
|
199
|
+
* @returns {Promise<void>}
|
|
200
|
+
*/
|
|
201
|
+
sleep(ms) {
|
|
202
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* StateGuard - Enforces Iron Law and manages phase transitions
|
|
208
|
+
*/
|
|
209
|
+
class StateGuard {
|
|
210
|
+
/**
|
|
211
|
+
* @param {string} stateFilePath - Path to COLONY_STATE.json
|
|
212
|
+
* @param {object} options - Configuration options
|
|
213
|
+
*/
|
|
214
|
+
constructor(stateFilePath, options = {}) {
|
|
215
|
+
this.stateFile = stateFilePath;
|
|
216
|
+
this.lock = options.lock || new FileLock(options.lockDir);
|
|
217
|
+
this.locked = false;
|
|
218
|
+
this.worker = options.worker || process.env.WORKER_NAME || 'state-guard';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Acquire lock on state file
|
|
223
|
+
* @returns {Promise<void>}
|
|
224
|
+
* @throws {StateGuardError} If lock cannot be acquired
|
|
225
|
+
*/
|
|
226
|
+
async acquireLock() {
|
|
227
|
+
const acquired = await this.lock.acquire(this.stateFile);
|
|
228
|
+
if (!acquired) {
|
|
229
|
+
throw new StateGuardError(
|
|
230
|
+
StateGuardErrorCodes.E_LOCK_TIMEOUT,
|
|
231
|
+
`Could not acquire lock on ${this.stateFile} after ${this.lock.maxRetries} retries`,
|
|
232
|
+
{ lockFile: `${this.stateFile}.lock`, maxRetries: this.lock.maxRetries },
|
|
233
|
+
'Check for stuck processes or manually remove stale lock file'
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
this.locked = true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Release lock on state file
|
|
241
|
+
* Safe to call even if not locked
|
|
242
|
+
*/
|
|
243
|
+
releaseLock() {
|
|
244
|
+
this.lock.release();
|
|
245
|
+
this.locked = false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Load and parse state file
|
|
250
|
+
* @returns {object} Parsed state object
|
|
251
|
+
* @throws {StateGuardError} If file missing or invalid
|
|
252
|
+
*/
|
|
253
|
+
loadState() {
|
|
254
|
+
// Check if file exists
|
|
255
|
+
if (!fs.existsSync(this.stateFile)) {
|
|
256
|
+
throw new StateGuardError(
|
|
257
|
+
StateGuardErrorCodes.E_STATE_NOT_FOUND,
|
|
258
|
+
`State file not found: ${this.stateFile}`,
|
|
259
|
+
{ path: this.stateFile },
|
|
260
|
+
'Run: aether init'
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Read and parse
|
|
265
|
+
let content;
|
|
266
|
+
try {
|
|
267
|
+
content = fs.readFileSync(this.stateFile, 'utf8');
|
|
268
|
+
} catch (err) {
|
|
269
|
+
throw new StateGuardError(
|
|
270
|
+
StateGuardErrorCodes.E_STATE_INVALID,
|
|
271
|
+
`Failed to read state file: ${err.message}`,
|
|
272
|
+
{ path: this.stateFile, error: err.message },
|
|
273
|
+
'Check file permissions and disk space'
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let state;
|
|
278
|
+
try {
|
|
279
|
+
state = JSON.parse(content);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
throw new StateGuardError(
|
|
282
|
+
StateGuardErrorCodes.E_STATE_INVALID,
|
|
283
|
+
`Invalid JSON in state file: ${err.message}`,
|
|
284
|
+
{ path: this.stateFile, error: err.message },
|
|
285
|
+
'Restore from backup or reinitialize'
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Validate basic structure
|
|
290
|
+
if (!state.version || typeof state.current_phase !== 'number' || !Array.isArray(state.events)) {
|
|
291
|
+
throw new StateGuardError(
|
|
292
|
+
StateGuardErrorCodes.E_STATE_INVALID,
|
|
293
|
+
'State file missing required fields: version, current_phase, events',
|
|
294
|
+
{ path: this.stateFile, hasVersion: !!state.version, hasPhase: typeof state.current_phase === 'number', hasEvents: Array.isArray(state.events) },
|
|
295
|
+
'Restore from backup or reinitialize'
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return state;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Save state atomically (write to temp, then rename)
|
|
304
|
+
* @param {object} state - State object to save
|
|
305
|
+
*/
|
|
306
|
+
saveState(state) {
|
|
307
|
+
// Update timestamp
|
|
308
|
+
state.last_updated = new Date().toISOString();
|
|
309
|
+
|
|
310
|
+
// Write to temp file first (atomic write)
|
|
311
|
+
const tempFile = `${this.stateFile}.tmp`;
|
|
312
|
+
fs.writeFileSync(tempFile, JSON.stringify(state, null, 2), 'utf8');
|
|
313
|
+
|
|
314
|
+
// Atomic rename
|
|
315
|
+
fs.renameSync(tempFile, this.stateFile);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Check if evidence has all required fields and is fresh
|
|
320
|
+
* @param {object} state - Current state object
|
|
321
|
+
* @param {number} phase - Phase number
|
|
322
|
+
* @param {object} evidence - Evidence object to validate
|
|
323
|
+
* @returns {boolean} True if evidence is fresh and valid
|
|
324
|
+
*/
|
|
325
|
+
hasFreshEvidence(state, phase, evidence) {
|
|
326
|
+
// Evidence must be an object
|
|
327
|
+
if (!evidence || typeof evidence !== 'object') {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Required fields
|
|
332
|
+
const requiredFields = ['checkpoint_hash', 'test_results', 'timestamp'];
|
|
333
|
+
for (const field of requiredFields) {
|
|
334
|
+
if (!(field in evidence)) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Validate timestamp is ISO 8601 format
|
|
340
|
+
const timestamp = new Date(evidence.timestamp);
|
|
341
|
+
if (isNaN(timestamp.getTime())) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Check evidence is fresh (after state initialization)
|
|
346
|
+
const stateInitTime = new Date(state.initialized_at || '1970-01-01T00:00:00Z');
|
|
347
|
+
if (timestamp <= stateInitTime) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Check evidence is from current session (not inherited)
|
|
352
|
+
// Evidence timestamp should be after state initialization
|
|
353
|
+
const evidenceAge = Date.now() - timestamp.getTime();
|
|
354
|
+
const sessionAge = Date.now() - stateInitTime.getTime();
|
|
355
|
+
if (evidenceAge > sessionAge) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Enforce Iron Law: phase advancement requires fresh verification evidence
|
|
364
|
+
* @param {object} state - Current state object
|
|
365
|
+
* @param {number} phase - Phase number
|
|
366
|
+
* @param {object} evidence - Evidence object
|
|
367
|
+
* @throws {StateGuardError} If Iron Law is violated
|
|
368
|
+
*/
|
|
369
|
+
enforceIronLaw(state, phase, evidence) {
|
|
370
|
+
if (!this.hasFreshEvidence(state, phase, evidence)) {
|
|
371
|
+
const requiredFields = ['checkpoint_hash', 'test_results', 'timestamp'];
|
|
372
|
+
const providedFields = evidence && typeof evidence === 'object' ? Object.keys(evidence) : [];
|
|
373
|
+
const missing = requiredFields.filter(f => !providedFields.includes(f));
|
|
374
|
+
|
|
375
|
+
throw new StateGuardError(
|
|
376
|
+
StateGuardErrorCodes.E_IRON_LAW_VIOLATION,
|
|
377
|
+
`Phase ${phase} advancement requires fresh verification evidence`,
|
|
378
|
+
{
|
|
379
|
+
phase,
|
|
380
|
+
missing,
|
|
381
|
+
provided: providedFields,
|
|
382
|
+
evidence_timestamp: evidence?.timestamp,
|
|
383
|
+
state_initialized_at: state.initialized_at
|
|
384
|
+
},
|
|
385
|
+
`Provide verification evidence: ${missing.length > 0 ? missing.join(', ') : 'fresh timestamp after state initialization'}`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Check idempotency - prevent rebuilding completed phases or skipping ahead
|
|
392
|
+
* @param {object} state - Current state object
|
|
393
|
+
* @param {number} fromPhase - Phase we're trying to advance from
|
|
394
|
+
* @returns {object} Result with canProceed flag and reason
|
|
395
|
+
*/
|
|
396
|
+
checkIdempotency(state, fromPhase) {
|
|
397
|
+
const currentPhase = state.current_phase;
|
|
398
|
+
|
|
399
|
+
if (currentPhase > fromPhase) {
|
|
400
|
+
return {
|
|
401
|
+
canProceed: false,
|
|
402
|
+
reason: 'already_complete',
|
|
403
|
+
currentPhase: currentPhase,
|
|
404
|
+
message: `Phase ${fromPhase} already completed (currently at phase ${currentPhase})`
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (currentPhase < fromPhase) {
|
|
409
|
+
return {
|
|
410
|
+
canProceed: false,
|
|
411
|
+
reason: 'previous_incomplete',
|
|
412
|
+
currentPhase: currentPhase,
|
|
413
|
+
message: `Cannot advance from phase ${fromPhase} - currently at phase ${currentPhase} (previous phases incomplete)`
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return { canProceed: true };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Validate phase transition is sequential
|
|
422
|
+
* @param {number} fromPhase - Current phase
|
|
423
|
+
* @param {number} toPhase - Target phase
|
|
424
|
+
* @throws {StateGuardError} If transition is invalid
|
|
425
|
+
*/
|
|
426
|
+
validateTransition(fromPhase, toPhase) {
|
|
427
|
+
if (toPhase !== fromPhase + 1) {
|
|
428
|
+
throw new StateGuardError(
|
|
429
|
+
StateGuardErrorCodes.E_INVALID_TRANSITION,
|
|
430
|
+
`Invalid phase transition: ${fromPhase} -> ${toPhase}`,
|
|
431
|
+
{ from: fromPhase, to: toPhase, expected: fromPhase + 1 },
|
|
432
|
+
'Phase transitions must be sequential (n -> n+1)'
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Add an event to the state events array
|
|
439
|
+
* @param {object} state - Current state object
|
|
440
|
+
* @param {string} type - Event type from EventTypes
|
|
441
|
+
* @param {object} details - Event details
|
|
442
|
+
* @returns {object} The created event
|
|
443
|
+
*/
|
|
444
|
+
addEvent(state, type, details = {}) {
|
|
445
|
+
// Ensure events array exists
|
|
446
|
+
if (!state.events) {
|
|
447
|
+
state.events = [];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Create event using event-types helper
|
|
451
|
+
const event = createEvent(type, this.worker, details);
|
|
452
|
+
|
|
453
|
+
// Add to state events
|
|
454
|
+
state.events.push(event);
|
|
455
|
+
|
|
456
|
+
return event;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Transition state from one phase to another
|
|
461
|
+
* @param {object} state - Current state object
|
|
462
|
+
* @param {number} fromPhase - Current phase
|
|
463
|
+
* @param {number} toPhase - Target phase
|
|
464
|
+
* @param {object} evidence - Verification evidence
|
|
465
|
+
* @returns {object} Updated state object
|
|
466
|
+
*/
|
|
467
|
+
transitionState(state, fromPhase, toPhase, evidence) {
|
|
468
|
+
// Update phase
|
|
469
|
+
state.current_phase = toPhase;
|
|
470
|
+
|
|
471
|
+
// Update last_updated timestamp
|
|
472
|
+
state.last_updated = new Date().toISOString();
|
|
473
|
+
|
|
474
|
+
// Add phase_transition event using addEvent method
|
|
475
|
+
this.addEvent(state, EventTypes.PHASE_TRANSITION, {
|
|
476
|
+
from: fromPhase,
|
|
477
|
+
to: toPhase,
|
|
478
|
+
evidence_id: evidence?.checkpoint_hash || null,
|
|
479
|
+
checkpoint_hash: evidence?.checkpoint_hash || null
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
return state;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Get events from state with optional filtering
|
|
487
|
+
* @param {object} state - State object containing events array
|
|
488
|
+
* @param {object} options - Filter options
|
|
489
|
+
* @param {string} [options.type] - Filter by event type
|
|
490
|
+
* @param {string} [options.since] - Filter events after this ISO 8601 timestamp
|
|
491
|
+
* @param {number} [options.limit] - Limit number of events returned (most recent first)
|
|
492
|
+
* @returns {object[]} Array of matching events
|
|
493
|
+
*/
|
|
494
|
+
static getEvents(state, options = {}) {
|
|
495
|
+
if (!state || !Array.isArray(state.events)) {
|
|
496
|
+
return [];
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
let events = [...state.events];
|
|
500
|
+
|
|
501
|
+
// Filter by type
|
|
502
|
+
if (options.type) {
|
|
503
|
+
events = events.filter(e => e.type === options.type);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Filter by timestamp (events after 'since')
|
|
507
|
+
if (options.since) {
|
|
508
|
+
const sinceDate = new Date(options.since);
|
|
509
|
+
if (!isNaN(sinceDate.getTime())) {
|
|
510
|
+
events = events.filter(e => new Date(e.timestamp) > sinceDate);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Sort by timestamp descending (most recent first)
|
|
515
|
+
events.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
516
|
+
|
|
517
|
+
// Limit results
|
|
518
|
+
if (options.limit && options.limit > 0) {
|
|
519
|
+
events = events.slice(0, options.limit);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return events;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Get the most recent event of a specific type
|
|
527
|
+
* @param {object} state - State object containing events array
|
|
528
|
+
* @param {string} [type] - Optional event type to filter by
|
|
529
|
+
* @returns {object|null} Most recent event or null if none found
|
|
530
|
+
*/
|
|
531
|
+
static getLatestEvent(state, type = null) {
|
|
532
|
+
const events = StateGuard.getEvents(state, { type, limit: 1 });
|
|
533
|
+
return events.length > 0 ? events[0] : null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Advance phase with full guard enforcement
|
|
538
|
+
* @param {number} fromPhase - Current phase number
|
|
539
|
+
* @param {number} toPhase - Target phase number
|
|
540
|
+
* @param {object} evidence - Verification evidence
|
|
541
|
+
* @returns {Promise<object>} Result object with status
|
|
542
|
+
*/
|
|
543
|
+
async advancePhase(fromPhase, toPhase, evidence) {
|
|
544
|
+
// Acquire lock
|
|
545
|
+
await this.acquireLock();
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
// Load state
|
|
549
|
+
const state = this.loadState();
|
|
550
|
+
|
|
551
|
+
// Check idempotency (STATE-02)
|
|
552
|
+
const idempotency = this.checkIdempotency(state, fromPhase);
|
|
553
|
+
if (!idempotency.canProceed) {
|
|
554
|
+
if (idempotency.reason === 'already_complete') {
|
|
555
|
+
// Return success but indicate already complete
|
|
556
|
+
return {
|
|
557
|
+
status: 'already_complete',
|
|
558
|
+
from: fromPhase,
|
|
559
|
+
to: toPhase,
|
|
560
|
+
currentPhase: idempotency.currentPhase
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
// Previous incomplete - throw error
|
|
564
|
+
throw new StateGuardError(
|
|
565
|
+
StateGuardErrorCodes.E_IDEMPOTENCY_CHECK,
|
|
566
|
+
idempotency.message,
|
|
567
|
+
{ reason: idempotency.reason, currentPhase: idempotency.currentPhase },
|
|
568
|
+
'Complete previous phases before advancing'
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Validate transition
|
|
573
|
+
this.validateTransition(fromPhase, toPhase);
|
|
574
|
+
|
|
575
|
+
// Enforce Iron Law (STATE-01)
|
|
576
|
+
this.enforceIronLaw(state, fromPhase, evidence);
|
|
577
|
+
|
|
578
|
+
// Transition state
|
|
579
|
+
const updatedState = this.transitionState(state, fromPhase, toPhase, evidence);
|
|
580
|
+
|
|
581
|
+
// Save state
|
|
582
|
+
this.saveState(updatedState);
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
status: 'transitioned',
|
|
586
|
+
from: fromPhase,
|
|
587
|
+
to: toPhase
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
} finally {
|
|
591
|
+
// Always release lock
|
|
592
|
+
this.releaseLock();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
module.exports = {
|
|
598
|
+
StateGuard,
|
|
599
|
+
StateGuardError,
|
|
600
|
+
StateGuardErrorCodes,
|
|
601
|
+
FileLock,
|
|
602
|
+
};
|