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,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
+ };