@lumenflow/core 1.0.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 (263) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +119 -0
  3. package/dist/active-wu-detector.d.ts +33 -0
  4. package/dist/active-wu-detector.js +106 -0
  5. package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
  6. package/dist/adapters/filesystem-metrics.adapter.js +519 -0
  7. package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
  8. package/dist/adapters/terminal-renderer.adapter.js +337 -0
  9. package/dist/arg-parser.d.ts +63 -0
  10. package/dist/arg-parser.js +560 -0
  11. package/dist/backlog-editor.d.ts +98 -0
  12. package/dist/backlog-editor.js +179 -0
  13. package/dist/backlog-generator.d.ts +111 -0
  14. package/dist/backlog-generator.js +381 -0
  15. package/dist/backlog-parser.d.ts +45 -0
  16. package/dist/backlog-parser.js +102 -0
  17. package/dist/backlog-sync-validator.d.ts +78 -0
  18. package/dist/backlog-sync-validator.js +294 -0
  19. package/dist/branch-drift.d.ts +34 -0
  20. package/dist/branch-drift.js +51 -0
  21. package/dist/cleanup-install-config.d.ts +33 -0
  22. package/dist/cleanup-install-config.js +37 -0
  23. package/dist/cleanup-lock.d.ts +139 -0
  24. package/dist/cleanup-lock.js +313 -0
  25. package/dist/code-path-validator.d.ts +146 -0
  26. package/dist/code-path-validator.js +537 -0
  27. package/dist/code-paths-overlap.d.ts +55 -0
  28. package/dist/code-paths-overlap.js +245 -0
  29. package/dist/commands-logger.d.ts +77 -0
  30. package/dist/commands-logger.js +254 -0
  31. package/dist/commit-message-utils.d.ts +25 -0
  32. package/dist/commit-message-utils.js +41 -0
  33. package/dist/compliance-parser.d.ts +150 -0
  34. package/dist/compliance-parser.js +507 -0
  35. package/dist/constants/backlog-patterns.d.ts +20 -0
  36. package/dist/constants/backlog-patterns.js +23 -0
  37. package/dist/constants/dora-constants.d.ts +49 -0
  38. package/dist/constants/dora-constants.js +53 -0
  39. package/dist/constants/gate-constants.d.ts +15 -0
  40. package/dist/constants/gate-constants.js +15 -0
  41. package/dist/constants/linter-constants.d.ts +16 -0
  42. package/dist/constants/linter-constants.js +16 -0
  43. package/dist/constants/tokenizer-constants.d.ts +15 -0
  44. package/dist/constants/tokenizer-constants.js +15 -0
  45. package/dist/core/scope-checker.d.ts +97 -0
  46. package/dist/core/scope-checker.js +163 -0
  47. package/dist/core/tool-runner.d.ts +161 -0
  48. package/dist/core/tool-runner.js +393 -0
  49. package/dist/core/tool.constants.d.ts +105 -0
  50. package/dist/core/tool.constants.js +101 -0
  51. package/dist/core/tool.schemas.d.ts +226 -0
  52. package/dist/core/tool.schemas.js +226 -0
  53. package/dist/core/worktree-guard.d.ts +130 -0
  54. package/dist/core/worktree-guard.js +242 -0
  55. package/dist/coverage-gate.d.ts +108 -0
  56. package/dist/coverage-gate.js +196 -0
  57. package/dist/date-utils.d.ts +75 -0
  58. package/dist/date-utils.js +140 -0
  59. package/dist/dependency-graph.d.ts +142 -0
  60. package/dist/dependency-graph.js +550 -0
  61. package/dist/dependency-guard.d.ts +54 -0
  62. package/dist/dependency-guard.js +142 -0
  63. package/dist/dependency-validator.d.ts +105 -0
  64. package/dist/dependency-validator.js +154 -0
  65. package/dist/docs-path-validator.d.ts +36 -0
  66. package/dist/docs-path-validator.js +95 -0
  67. package/dist/domain/orchestration.constants.d.ts +99 -0
  68. package/dist/domain/orchestration.constants.js +97 -0
  69. package/dist/domain/orchestration.schemas.d.ts +280 -0
  70. package/dist/domain/orchestration.schemas.js +211 -0
  71. package/dist/domain/orchestration.types.d.ts +133 -0
  72. package/dist/domain/orchestration.types.js +12 -0
  73. package/dist/error-handler.d.ts +116 -0
  74. package/dist/error-handler.js +136 -0
  75. package/dist/file-classifiers.d.ts +62 -0
  76. package/dist/file-classifiers.js +108 -0
  77. package/dist/gates-agent-mode.d.ts +81 -0
  78. package/dist/gates-agent-mode.js +94 -0
  79. package/dist/generate-traceability.d.ts +107 -0
  80. package/dist/generate-traceability.js +411 -0
  81. package/dist/git-adapter.d.ts +395 -0
  82. package/dist/git-adapter.js +649 -0
  83. package/dist/git-staged-validator.d.ts +32 -0
  84. package/dist/git-staged-validator.js +48 -0
  85. package/dist/hardcoded-strings.d.ts +61 -0
  86. package/dist/hardcoded-strings.js +270 -0
  87. package/dist/incremental-lint.d.ts +78 -0
  88. package/dist/incremental-lint.js +129 -0
  89. package/dist/incremental-test.d.ts +39 -0
  90. package/dist/incremental-test.js +61 -0
  91. package/dist/index.d.ts +42 -0
  92. package/dist/index.js +61 -0
  93. package/dist/invariants/check-automated-tests.d.ts +50 -0
  94. package/dist/invariants/check-automated-tests.js +166 -0
  95. package/dist/invariants-runner.d.ts +103 -0
  96. package/dist/invariants-runner.js +527 -0
  97. package/dist/lane-checker.d.ts +50 -0
  98. package/dist/lane-checker.js +319 -0
  99. package/dist/lane-inference.d.ts +39 -0
  100. package/dist/lane-inference.js +195 -0
  101. package/dist/lane-lock.d.ts +211 -0
  102. package/dist/lane-lock.js +474 -0
  103. package/dist/lane-validator.d.ts +48 -0
  104. package/dist/lane-validator.js +114 -0
  105. package/dist/logs-lib.d.ts +104 -0
  106. package/dist/logs-lib.js +207 -0
  107. package/dist/lumenflow-config-schema.d.ts +272 -0
  108. package/dist/lumenflow-config-schema.js +207 -0
  109. package/dist/lumenflow-config.d.ts +95 -0
  110. package/dist/lumenflow-config.js +236 -0
  111. package/dist/manual-test-validator.d.ts +80 -0
  112. package/dist/manual-test-validator.js +200 -0
  113. package/dist/merge-lock.d.ts +115 -0
  114. package/dist/merge-lock.js +251 -0
  115. package/dist/micro-worktree.d.ts +159 -0
  116. package/dist/micro-worktree.js +427 -0
  117. package/dist/migration-deployer.d.ts +69 -0
  118. package/dist/migration-deployer.js +151 -0
  119. package/dist/orchestration-advisory-loader.d.ts +28 -0
  120. package/dist/orchestration-advisory-loader.js +87 -0
  121. package/dist/orchestration-advisory.d.ts +58 -0
  122. package/dist/orchestration-advisory.js +94 -0
  123. package/dist/orchestration-di.d.ts +48 -0
  124. package/dist/orchestration-di.js +57 -0
  125. package/dist/orchestration-rules.d.ts +57 -0
  126. package/dist/orchestration-rules.js +201 -0
  127. package/dist/orphan-detector.d.ts +131 -0
  128. package/dist/orphan-detector.js +226 -0
  129. package/dist/path-classifiers.d.ts +57 -0
  130. package/dist/path-classifiers.js +93 -0
  131. package/dist/piped-command-detector.d.ts +34 -0
  132. package/dist/piped-command-detector.js +64 -0
  133. package/dist/ports/dashboard-renderer.port.d.ts +112 -0
  134. package/dist/ports/dashboard-renderer.port.js +25 -0
  135. package/dist/ports/metrics-collector.port.d.ts +132 -0
  136. package/dist/ports/metrics-collector.port.js +26 -0
  137. package/dist/process-detector.d.ts +84 -0
  138. package/dist/process-detector.js +172 -0
  139. package/dist/prompt-linter.d.ts +72 -0
  140. package/dist/prompt-linter.js +312 -0
  141. package/dist/prompt-monitor.d.ts +15 -0
  142. package/dist/prompt-monitor.js +205 -0
  143. package/dist/rebase-artifact-cleanup.d.ts +145 -0
  144. package/dist/rebase-artifact-cleanup.js +433 -0
  145. package/dist/retry-strategy.d.ts +189 -0
  146. package/dist/retry-strategy.js +283 -0
  147. package/dist/risk-detector.d.ts +108 -0
  148. package/dist/risk-detector.js +252 -0
  149. package/dist/rollback-utils.d.ts +76 -0
  150. package/dist/rollback-utils.js +104 -0
  151. package/dist/section-headings.d.ts +43 -0
  152. package/dist/section-headings.js +49 -0
  153. package/dist/spawn-escalation.d.ts +90 -0
  154. package/dist/spawn-escalation.js +253 -0
  155. package/dist/spawn-monitor.d.ts +229 -0
  156. package/dist/spawn-monitor.js +672 -0
  157. package/dist/spawn-recovery.d.ts +82 -0
  158. package/dist/spawn-recovery.js +298 -0
  159. package/dist/spawn-registry-schema.d.ts +98 -0
  160. package/dist/spawn-registry-schema.js +108 -0
  161. package/dist/spawn-registry-store.d.ts +146 -0
  162. package/dist/spawn-registry-store.js +273 -0
  163. package/dist/spawn-tree.d.ts +121 -0
  164. package/dist/spawn-tree.js +285 -0
  165. package/dist/stamp-status-validator.d.ts +84 -0
  166. package/dist/stamp-status-validator.js +134 -0
  167. package/dist/stamp-utils.d.ts +100 -0
  168. package/dist/stamp-utils.js +229 -0
  169. package/dist/state-machine.d.ts +26 -0
  170. package/dist/state-machine.js +83 -0
  171. package/dist/system-map-validator.d.ts +80 -0
  172. package/dist/system-map-validator.js +272 -0
  173. package/dist/telemetry.d.ts +80 -0
  174. package/dist/telemetry.js +213 -0
  175. package/dist/token-counter.d.ts +51 -0
  176. package/dist/token-counter.js +145 -0
  177. package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
  178. package/dist/usecases/get-dashboard-data.usecase.js +61 -0
  179. package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
  180. package/dist/usecases/get-suggestions.usecase.js +153 -0
  181. package/dist/user-normalizer.d.ts +41 -0
  182. package/dist/user-normalizer.js +141 -0
  183. package/dist/validators/phi-constants.d.ts +97 -0
  184. package/dist/validators/phi-constants.js +152 -0
  185. package/dist/validators/phi-scanner.d.ts +58 -0
  186. package/dist/validators/phi-scanner.js +215 -0
  187. package/dist/worktree-ownership.d.ts +50 -0
  188. package/dist/worktree-ownership.js +74 -0
  189. package/dist/worktree-scanner.d.ts +103 -0
  190. package/dist/worktree-scanner.js +168 -0
  191. package/dist/worktree-symlink.d.ts +99 -0
  192. package/dist/worktree-symlink.js +359 -0
  193. package/dist/wu-backlog-updater.d.ts +17 -0
  194. package/dist/wu-backlog-updater.js +37 -0
  195. package/dist/wu-checkpoint.d.ts +124 -0
  196. package/dist/wu-checkpoint.js +233 -0
  197. package/dist/wu-claim-helpers.d.ts +26 -0
  198. package/dist/wu-claim-helpers.js +63 -0
  199. package/dist/wu-claim-resume.d.ts +106 -0
  200. package/dist/wu-claim-resume.js +276 -0
  201. package/dist/wu-consistency-checker.d.ts +95 -0
  202. package/dist/wu-consistency-checker.js +567 -0
  203. package/dist/wu-constants.d.ts +1275 -0
  204. package/dist/wu-constants.js +1382 -0
  205. package/dist/wu-create-validators.d.ts +42 -0
  206. package/dist/wu-create-validators.js +93 -0
  207. package/dist/wu-done-branch-only.d.ts +63 -0
  208. package/dist/wu-done-branch-only.js +191 -0
  209. package/dist/wu-done-messages.d.ts +119 -0
  210. package/dist/wu-done-messages.js +185 -0
  211. package/dist/wu-done-pr.d.ts +72 -0
  212. package/dist/wu-done-pr.js +174 -0
  213. package/dist/wu-done-retry-helpers.d.ts +85 -0
  214. package/dist/wu-done-retry-helpers.js +172 -0
  215. package/dist/wu-done-ui.d.ts +37 -0
  216. package/dist/wu-done-ui.js +69 -0
  217. package/dist/wu-done-validators.d.ts +411 -0
  218. package/dist/wu-done-validators.js +1229 -0
  219. package/dist/wu-done-worktree.d.ts +182 -0
  220. package/dist/wu-done-worktree.js +1097 -0
  221. package/dist/wu-helpers.d.ts +128 -0
  222. package/dist/wu-helpers.js +248 -0
  223. package/dist/wu-lint.d.ts +70 -0
  224. package/dist/wu-lint.js +234 -0
  225. package/dist/wu-paths.d.ts +171 -0
  226. package/dist/wu-paths.js +178 -0
  227. package/dist/wu-preflight-validators.d.ts +86 -0
  228. package/dist/wu-preflight-validators.js +251 -0
  229. package/dist/wu-recovery.d.ts +138 -0
  230. package/dist/wu-recovery.js +341 -0
  231. package/dist/wu-repair-core.d.ts +131 -0
  232. package/dist/wu-repair-core.js +669 -0
  233. package/dist/wu-schema-normalization.d.ts +17 -0
  234. package/dist/wu-schema-normalization.js +82 -0
  235. package/dist/wu-schema.d.ts +793 -0
  236. package/dist/wu-schema.js +881 -0
  237. package/dist/wu-spawn-helpers.d.ts +121 -0
  238. package/dist/wu-spawn-helpers.js +271 -0
  239. package/dist/wu-spawn.d.ts +158 -0
  240. package/dist/wu-spawn.js +1306 -0
  241. package/dist/wu-state-schema.d.ts +213 -0
  242. package/dist/wu-state-schema.js +156 -0
  243. package/dist/wu-state-store.d.ts +264 -0
  244. package/dist/wu-state-store.js +691 -0
  245. package/dist/wu-status-transition.d.ts +63 -0
  246. package/dist/wu-status-transition.js +382 -0
  247. package/dist/wu-status-updater.d.ts +25 -0
  248. package/dist/wu-status-updater.js +116 -0
  249. package/dist/wu-transaction-collectors.d.ts +116 -0
  250. package/dist/wu-transaction-collectors.js +272 -0
  251. package/dist/wu-transaction.d.ts +170 -0
  252. package/dist/wu-transaction.js +273 -0
  253. package/dist/wu-validation-constants.d.ts +60 -0
  254. package/dist/wu-validation-constants.js +66 -0
  255. package/dist/wu-validation.d.ts +118 -0
  256. package/dist/wu-validation.js +243 -0
  257. package/dist/wu-validator.d.ts +62 -0
  258. package/dist/wu-validator.js +325 -0
  259. package/dist/wu-yaml-fixer.d.ts +97 -0
  260. package/dist/wu-yaml-fixer.js +264 -0
  261. package/dist/wu-yaml.d.ts +86 -0
  262. package/dist/wu-yaml.js +222 -0
  263. package/package.json +114 -0
@@ -0,0 +1,691 @@
1
+ /**
2
+ * WU State Store (WU-1570, WU-2240)
3
+ *
4
+ * Event-sourced state store for WU lifecycle following INIT-007 pattern.
5
+ * Stores events in .beacon/state/wu-events.jsonl (append-only, git-friendly).
6
+ *
7
+ * Features:
8
+ * - Event sourcing with replay for current state
9
+ * - Atomic append operations (WU-2240: temp file + fsync + rename)
10
+ * - O(1) queries by status and lane via in-memory indexes
11
+ * - State machine validation for legal transitions
12
+ * - File locking with stale detection (WU-2240)
13
+ * - Corruption recovery via repairStateFile (WU-2240)
14
+ *
15
+ * @see {@link tools/__tests__/state-store-concurrent.test.mjs} - Concurrent access tests
16
+ * @see {@link tools/lib/wu-state-schema.mjs} - Schema definitions
17
+ */
18
+ import fs from 'node:fs/promises';
19
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync, openSync, closeSync, fsyncSync, } from 'node:fs';
20
+ import path from 'node:path';
21
+ import os from 'node:os';
22
+ import { validateWUEvent } from './wu-state-schema.js';
23
+ /**
24
+ * Lock timeout in milliseconds (5 minutes)
25
+ */
26
+ const LOCK_TIMEOUT_MS = 5 * 60 * 1000;
27
+ /**
28
+ * Lock retry configuration
29
+ */
30
+ const LOCK_RETRY_DELAY_MS = 50;
31
+ const LOCK_MAX_RETRIES = 100; // 5 seconds total
32
+ /**
33
+ * WU events file name constant
34
+ */
35
+ export const WU_EVENTS_FILE_NAME = 'wu-events.jsonl';
36
+ /**
37
+ * WU State Store class
38
+ *
39
+ * Manages WU lifecycle state via event sourcing pattern.
40
+ * Events are appended to JSONL file, state is rebuilt by replaying events.
41
+ */
42
+ export class WUStateStore {
43
+ baseDir;
44
+ eventsFilePath;
45
+ wuState;
46
+ byStatus;
47
+ byLane;
48
+ byParent;
49
+ constructor(baseDir) {
50
+ this.baseDir = baseDir;
51
+ this.eventsFilePath = path.join(baseDir, WU_EVENTS_FILE_NAME);
52
+ // In-memory state (rebuilt from events)
53
+ this.wuState = new Map();
54
+ this.byStatus = new Map();
55
+ this.byLane = new Map();
56
+ this.byParent = new Map();
57
+ }
58
+ /**
59
+ * Loads and replays events from JSONL file into current state.
60
+ *
61
+ * Handles:
62
+ * - Missing file: returns empty state
63
+ * - Empty file: returns empty state
64
+ * - Empty lines: skipped gracefully
65
+ * - Malformed JSON: throws error with line info
66
+ * - Invalid events: throws validation error
67
+ *
68
+ * @throws Error If file contains malformed JSON or invalid events
69
+ *
70
+ * @example
71
+ * const store = new WUStateStore('/path/to/project');
72
+ * await store.load();
73
+ * const inProgress = store.getByStatus('in_progress');
74
+ */
75
+ async load() {
76
+ // Reset state
77
+ this.wuState.clear();
78
+ this.byStatus.clear();
79
+ this.byLane.clear();
80
+ this.byParent.clear();
81
+ // Check if file exists
82
+ let content;
83
+ try {
84
+ content = await fs.readFile(this.eventsFilePath, 'utf-8');
85
+ }
86
+ catch (error) {
87
+ if (error.code === 'ENOENT') {
88
+ // File doesn't exist - return empty state
89
+ return;
90
+ }
91
+ throw error;
92
+ }
93
+ // Parse JSONL content
94
+ const lines = content.split('\n');
95
+ for (let i = 0; i < lines.length; i++) {
96
+ const line = lines[i].trim();
97
+ // Skip empty lines
98
+ if (!line) {
99
+ continue;
100
+ }
101
+ // Parse JSON line
102
+ let parsed;
103
+ try {
104
+ parsed = JSON.parse(line);
105
+ }
106
+ catch (error) {
107
+ throw new Error(`Malformed JSON on line ${i + 1}: ${error.message}`);
108
+ }
109
+ // Validate against schema
110
+ const validation = validateWUEvent(parsed);
111
+ if (!validation.success) {
112
+ const issues = validation.error.issues
113
+ .map((issue) => `${issue.path.join('.')}: ${issue.message}`)
114
+ .join(', ');
115
+ throw new Error(`Validation error on line ${i + 1}: ${issues}`);
116
+ }
117
+ const event = validation.data;
118
+ // Apply event to state
119
+ this._applyEvent(event);
120
+ }
121
+ }
122
+ /**
123
+ * Transition WU to a new status if it exists.
124
+ */
125
+ _transitionToStatus(wuId, newStatus) {
126
+ const current = this.wuState.get(wuId);
127
+ if (current) {
128
+ this._setState(wuId, newStatus, current.lane, current.title);
129
+ }
130
+ }
131
+ /**
132
+ * Applies an event to the in-memory state.
133
+ */
134
+ _applyEvent(event) {
135
+ const { wuId, type } = event;
136
+ if (type === 'create' || type === 'claim') {
137
+ const claimEvent = event;
138
+ this._setState(wuId, 'in_progress', claimEvent.lane, claimEvent.title);
139
+ return;
140
+ }
141
+ if (type === 'block') {
142
+ this._transitionToStatus(wuId, 'blocked');
143
+ return;
144
+ }
145
+ if (type === 'unblock') {
146
+ this._transitionToStatus(wuId, 'in_progress');
147
+ return;
148
+ }
149
+ if (type === 'complete') {
150
+ this._transitionToStatus(wuId, 'done');
151
+ // WU-2244: Store completion timestamp for accurate date reporting
152
+ const current = this.wuState.get(wuId);
153
+ if (current) {
154
+ current.completedAt = event.timestamp;
155
+ }
156
+ return;
157
+ }
158
+ if (type === 'checkpoint') {
159
+ const checkpointEvent = event;
160
+ const currentCheckpoint = this.wuState.get(wuId);
161
+ if (currentCheckpoint) {
162
+ currentCheckpoint.lastCheckpoint = event.timestamp;
163
+ currentCheckpoint.lastCheckpointNote = checkpointEvent.note;
164
+ }
165
+ return;
166
+ }
167
+ if (type === 'spawn') {
168
+ const spawnEvent = event;
169
+ const { parentWuId } = spawnEvent;
170
+ if (!this.byParent.has(parentWuId)) {
171
+ this.byParent.set(parentWuId, new Set());
172
+ }
173
+ this.byParent.get(parentWuId).add(wuId);
174
+ }
175
+ }
176
+ /**
177
+ * Sets WU state and updates indexes.
178
+ */
179
+ _setState(wuId, status, lane, title) {
180
+ // Remove from old status index
181
+ const oldState = this.wuState.get(wuId);
182
+ if (oldState) {
183
+ const oldStatusSet = this.byStatus.get(oldState.status);
184
+ if (oldStatusSet) {
185
+ oldStatusSet.delete(wuId);
186
+ }
187
+ // Remove from old lane index
188
+ const oldLaneSet = this.byLane.get(oldState.lane);
189
+ if (oldLaneSet) {
190
+ oldLaneSet.delete(wuId);
191
+ }
192
+ }
193
+ // Update state
194
+ this.wuState.set(wuId, { status, lane, title });
195
+ // Add to new status index
196
+ if (!this.byStatus.has(status)) {
197
+ this.byStatus.set(status, new Set());
198
+ }
199
+ this.byStatus.get(status).add(wuId);
200
+ // Add to new lane index
201
+ if (!this.byLane.has(lane)) {
202
+ this.byLane.set(lane, new Set());
203
+ }
204
+ this.byLane.get(lane).add(wuId);
205
+ }
206
+ /**
207
+ * Appends an event to the events file.
208
+ *
209
+ * Uses append mode to avoid full file rewrite.
210
+ * Creates file and parent directories if they don't exist.
211
+ * Validates event before appending.
212
+ *
213
+ * @throws Error If event fails validation
214
+ */
215
+ async _appendEvent(event) {
216
+ // Validate event before appending
217
+ const validation = validateWUEvent(event);
218
+ if (!validation.success) {
219
+ const issues = validation.error.issues
220
+ .map((issue) => `${issue.path.join('.')}: ${issue.message}`)
221
+ .join(', ');
222
+ throw new Error(`Validation error: ${issues}`);
223
+ }
224
+ const line = `${JSON.stringify(event)}\n`;
225
+ // WU-1740: Ensure parent directory exists before appending
226
+ // fs.appendFile creates the file but not parent directories
227
+ await fs.mkdir(this.baseDir, { recursive: true });
228
+ // Use append flag to avoid rewriting the file
229
+ await fs.appendFile(this.eventsFilePath, line, 'utf-8');
230
+ }
231
+ /**
232
+ * Claims a WU (transitions to in_progress).
233
+ *
234
+ * @throws Error If WU is already in_progress
235
+ *
236
+ * @example
237
+ * await store.claim('WU-1570', 'Operations: Tooling', 'Test WU');
238
+ */
239
+ async claim(wuId, lane, title) {
240
+ // Check state machine: can't claim if already in_progress
241
+ const currentState = this.wuState.get(wuId);
242
+ if (currentState && currentState.status === 'in_progress') {
243
+ throw new Error(`WU ${wuId} is already in_progress`);
244
+ }
245
+ const event = {
246
+ type: 'claim',
247
+ wuId,
248
+ lane,
249
+ title,
250
+ timestamp: new Date().toISOString(),
251
+ };
252
+ await this._appendEvent(event);
253
+ this._applyEvent(event);
254
+ }
255
+ /**
256
+ * Completes a WU (transitions to done).
257
+ *
258
+ * @throws Error If WU is not in_progress
259
+ *
260
+ * @example
261
+ * await store.complete('WU-1570');
262
+ */
263
+ async complete(wuId) {
264
+ // Check state machine: can only complete if in_progress
265
+ const currentState = this.wuState.get(wuId);
266
+ if (!currentState || currentState.status !== 'in_progress') {
267
+ throw new Error(`WU ${wuId} is not in_progress`);
268
+ }
269
+ const event = {
270
+ type: 'complete',
271
+ wuId,
272
+ timestamp: new Date().toISOString(),
273
+ };
274
+ await this._appendEvent(event);
275
+ this._applyEvent(event);
276
+ }
277
+ /**
278
+ * Get current in-memory state for a WU.
279
+ */
280
+ getWUState(wuId) {
281
+ return this.wuState.get(wuId);
282
+ }
283
+ /**
284
+ * Create a complete event without writing to disk.
285
+ *
286
+ * Used by transactional flows where event log writes are staged and committed atomically.
287
+ *
288
+ * @throws Error If WU is not in_progress or event fails validation
289
+ */
290
+ createCompleteEvent(wuId, timestamp = new Date().toISOString()) {
291
+ const currentState = this.wuState.get(wuId);
292
+ if (!currentState || currentState.status !== 'in_progress') {
293
+ throw new Error(`WU ${wuId} is not in_progress`);
294
+ }
295
+ const event = { type: 'complete', wuId, timestamp };
296
+ const validation = validateWUEvent(event);
297
+ if (!validation.success) {
298
+ const issues = validation.error.issues
299
+ .map((issue) => `${issue.path.join('.')}: ${issue.message}`)
300
+ .join(', ');
301
+ throw new Error(`Validation error: ${issues}`);
302
+ }
303
+ return validation.data;
304
+ }
305
+ /**
306
+ * Apply a validated event to in-memory state without writing to disk.
307
+ *
308
+ * @throws Error If event fails validation
309
+ */
310
+ applyEvent(event) {
311
+ const validation = validateWUEvent(event);
312
+ if (!validation.success) {
313
+ const issues = validation.error.issues
314
+ .map((issue) => `${issue.path.join('.')}: ${issue.message}`)
315
+ .join(', ');
316
+ throw new Error(`Validation error: ${issues}`);
317
+ }
318
+ this._applyEvent(validation.data);
319
+ }
320
+ /**
321
+ * Blocks a WU (transitions to blocked).
322
+ *
323
+ * @throws Error If WU is not in_progress
324
+ *
325
+ * @example
326
+ * await store.block('WU-1570', 'Blocked by dependency');
327
+ */
328
+ async block(wuId, reason) {
329
+ // Check state machine: can only block if in_progress
330
+ const currentState = this.wuState.get(wuId);
331
+ if (!currentState || currentState.status !== 'in_progress') {
332
+ throw new Error(`WU ${wuId} is not in_progress`);
333
+ }
334
+ const event = {
335
+ type: 'block',
336
+ wuId,
337
+ reason,
338
+ timestamp: new Date().toISOString(),
339
+ };
340
+ await this._appendEvent(event);
341
+ this._applyEvent(event);
342
+ }
343
+ /**
344
+ * Unblocks a WU (transitions back to in_progress).
345
+ *
346
+ * @throws Error If WU is not blocked
347
+ *
348
+ * @example
349
+ * await store.unblock('WU-1570');
350
+ */
351
+ async unblock(wuId) {
352
+ // Check state machine: can only unblock if blocked
353
+ const currentState = this.wuState.get(wuId);
354
+ if (!currentState || currentState.status !== 'blocked') {
355
+ throw new Error(`WU ${wuId} is not blocked`);
356
+ }
357
+ const event = {
358
+ type: 'unblock',
359
+ wuId,
360
+ timestamp: new Date().toISOString(),
361
+ };
362
+ await this._appendEvent(event);
363
+ this._applyEvent(event);
364
+ }
365
+ /**
366
+ * Records a checkpoint for a WU (WU-1748: cross-agent visibility).
367
+ *
368
+ * Checkpoints are recorded for visibility but don't change WU state.
369
+ * Used to track progress and detect abandoned WUs.
370
+ *
371
+ * @example
372
+ * await store.checkpoint('WU-1748', 'Completed worktree scanner', {
373
+ * progress: 'Scanner implemented and tests passing',
374
+ * nextSteps: 'Integrate into orchestrate:status'
375
+ * });
376
+ */
377
+ async checkpoint(wuId, note, options = {}) {
378
+ const { sessionId, progress, nextSteps } = options;
379
+ const event = {
380
+ type: 'checkpoint',
381
+ wuId,
382
+ note,
383
+ timestamp: new Date().toISOString(),
384
+ };
385
+ if (sessionId)
386
+ event.sessionId = sessionId;
387
+ if (progress)
388
+ event.progress = progress;
389
+ if (nextSteps)
390
+ event.nextSteps = nextSteps;
391
+ await this._appendEvent(event);
392
+ this._applyEvent(event);
393
+ }
394
+ /**
395
+ * Gets WU IDs by status (O(1) lookup).
396
+ *
397
+ * @example
398
+ * const inProgress = store.getByStatus('in_progress');
399
+ * for (const wuId of inProgress) {
400
+ * console.log(wuId);
401
+ * }
402
+ */
403
+ getByStatus(status) {
404
+ return this.byStatus.get(status) ?? new Set();
405
+ }
406
+ /**
407
+ * Gets WU IDs by lane (O(1) lookup).
408
+ *
409
+ * @example
410
+ * const tooling = store.getByLane('Operations: Tooling');
411
+ * for (const wuId of tooling) {
412
+ * console.log(wuId);
413
+ * }
414
+ */
415
+ getByLane(lane) {
416
+ return this.byLane.get(lane) ?? new Set();
417
+ }
418
+ /**
419
+ * Gets child WU IDs spawned from a parent WU (O(1) lookup).
420
+ * WU-1947: Parent-child relationship tracking.
421
+ *
422
+ * @example
423
+ * const children = store.getChildWUs('WU-100');
424
+ * for (const childId of children) {
425
+ * console.log(`Child WU: ${childId}`);
426
+ * }
427
+ */
428
+ getChildWUs(parentWuId) {
429
+ return this.byParent.get(parentWuId) ?? new Set();
430
+ }
431
+ /**
432
+ * Records a spawn relationship between parent and child WUs.
433
+ * WU-1947: Parent-child relationship tracking.
434
+ *
435
+ * @example
436
+ * await store.spawn('WU-200', 'WU-100', 'spawn-abc123');
437
+ */
438
+ async spawn(childWuId, parentWuId, spawnId) {
439
+ const event = {
440
+ type: 'spawn',
441
+ wuId: childWuId,
442
+ parentWuId,
443
+ spawnId,
444
+ timestamp: new Date().toISOString(),
445
+ };
446
+ await this._appendEvent(event);
447
+ this._applyEvent(event);
448
+ }
449
+ }
450
+ /**
451
+ * Check if a process with given PID is running
452
+ */
453
+ function isProcessRunning(pid) {
454
+ try {
455
+ // Sending signal 0 checks if process exists without affecting it
456
+ process.kill(pid, 0);
457
+ return true;
458
+ }
459
+ catch {
460
+ return false;
461
+ }
462
+ }
463
+ /**
464
+ * Check if a lock is stale (expired or dead process)
465
+ *
466
+ * WU-2240: Prepared for proper-lockfile integration
467
+ */
468
+ export function isLockStale(lockData) {
469
+ const now = Date.now();
470
+ const lockAge = now - lockData.timestamp;
471
+ // Check timeout first (5 minutes)
472
+ if (lockAge > LOCK_TIMEOUT_MS) {
473
+ return true;
474
+ }
475
+ // Check if on same host - if different host, can't check PID
476
+ if (lockData.hostname !== os.hostname()) {
477
+ // Different host, only rely on timeout
478
+ return false;
479
+ }
480
+ // Same host - check if process is still alive
481
+ return !isProcessRunning(lockData.pid);
482
+ }
483
+ /**
484
+ * Safely remove a lock file, ignoring errors
485
+ */
486
+ function safeUnlink(lockPath) {
487
+ try {
488
+ unlinkSync(lockPath);
489
+ }
490
+ catch {
491
+ // Ignore removal errors
492
+ }
493
+ }
494
+ /**
495
+ * Read and parse existing lock file
496
+ */
497
+ function readLockFile(lockPath) {
498
+ try {
499
+ const content = readFileSync(lockPath, 'utf-8');
500
+ return JSON.parse(content);
501
+ }
502
+ catch {
503
+ return null;
504
+ }
505
+ }
506
+ /**
507
+ * Handle existing lock file - returns true if should retry
508
+ */
509
+ async function handleExistingLock(lockPath) {
510
+ const existingLock = readLockFile(lockPath);
511
+ if (!existingLock) {
512
+ // Corrupted lock file - remove and retry
513
+ safeUnlink(lockPath);
514
+ return true;
515
+ }
516
+ if (isLockStale(existingLock)) {
517
+ safeUnlink(lockPath);
518
+ return true;
519
+ }
520
+ // Lock is held by active process - wait and retry
521
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
522
+ return true;
523
+ }
524
+ /**
525
+ * Try to create a lock file atomically
526
+ */
527
+ async function tryCreateLock(lockPath, lockData) {
528
+ try {
529
+ mkdirSync(path.dirname(lockPath), { recursive: true });
530
+ const fd = openSync(lockPath, 'wx');
531
+ const content = JSON.stringify(lockData);
532
+ writeFileSync(fd, content, 'utf-8');
533
+ fsyncSync(fd);
534
+ closeSync(fd);
535
+ return true;
536
+ }
537
+ catch (error) {
538
+ if (error.code === 'EEXIST') {
539
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
540
+ return false;
541
+ }
542
+ throw error;
543
+ }
544
+ }
545
+ /**
546
+ * Acquire a file lock for the events file
547
+ *
548
+ * Uses a JSON lock file containing PID, timestamp, and hostname.
549
+ * Implements stale lock detection via:
550
+ * - PID check (on same host)
551
+ * - 5-minute timeout (across hosts)
552
+ *
553
+ * WU-2240: Prepared for proper-lockfile integration
554
+ *
555
+ * @throws Error If lock cannot be acquired after retries
556
+ */
557
+ export async function acquireLock(lockPath) {
558
+ const lockData = {
559
+ pid: process.pid,
560
+ timestamp: Date.now(),
561
+ hostname: os.hostname(),
562
+ };
563
+ for (let attempt = 0; attempt < LOCK_MAX_RETRIES; attempt++) {
564
+ if (existsSync(lockPath)) {
565
+ const shouldRetry = await handleExistingLock(lockPath);
566
+ if (shouldRetry)
567
+ continue;
568
+ }
569
+ const acquired = await tryCreateLock(lockPath, lockData);
570
+ if (acquired)
571
+ return;
572
+ }
573
+ throw new Error(`Failed to acquire lock after ${LOCK_MAX_RETRIES} attempts`);
574
+ }
575
+ /**
576
+ * Release a file lock
577
+ *
578
+ * WU-2240: Prepared for proper-lockfile integration
579
+ */
580
+ export function releaseLock(lockPath) {
581
+ safeUnlink(lockPath);
582
+ }
583
+ /**
584
+ * Repair a corrupted state file by removing invalid lines.
585
+ *
586
+ * WU-2240: Corruption recovery for wu-events.jsonl
587
+ *
588
+ * Features:
589
+ * - Creates backup before repair
590
+ * - Removes malformed JSON lines
591
+ * - Removes lines that fail schema validation
592
+ * - Returns detailed repair statistics
593
+ *
594
+ * @example
595
+ * const stateFilePath = path.join(process.cwd(), '.beacon', 'state', 'wu-events.jsonl');
596
+ * const result = await repairStateFile(stateFilePath);
597
+ * if (result.success) {
598
+ * console.log(`Repaired: kept ${result.linesKept}, removed ${result.linesRemoved}`);
599
+ * }
600
+ */
601
+ export async function repairStateFile(filePath) {
602
+ const warnings = [];
603
+ let linesKept = 0;
604
+ let linesRemoved = 0;
605
+ // Check if file exists
606
+ if (!existsSync(filePath)) {
607
+ return {
608
+ success: true,
609
+ linesKept: 0,
610
+ linesRemoved: 0,
611
+ backupPath: null,
612
+ warnings: ['File does not exist, nothing to repair'],
613
+ };
614
+ }
615
+ // Read the original content
616
+ const originalContent = readFileSync(filePath, 'utf-8');
617
+ const lines = originalContent.split('\n');
618
+ // Create backup with timestamp
619
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
620
+ const backupPath = `${filePath}.backup.${timestamp}`;
621
+ writeFileSync(backupPath, originalContent, 'utf-8');
622
+ // Process each line
623
+ const validLines = [];
624
+ for (let i = 0; i < lines.length; i++) {
625
+ const line = lines[i].trim();
626
+ // Skip empty lines
627
+ if (!line) {
628
+ continue;
629
+ }
630
+ // Try to parse JSON
631
+ let parsed;
632
+ try {
633
+ parsed = JSON.parse(line);
634
+ }
635
+ catch {
636
+ linesRemoved++;
637
+ warnings.push(`Line ${i + 1}: Malformed JSON removed`);
638
+ continue;
639
+ }
640
+ // Validate against schema
641
+ const validation = validateWUEvent(parsed);
642
+ if (!validation.success) {
643
+ linesRemoved++;
644
+ const issues = validation.error.issues
645
+ .map((issue) => `${issue.path.join('.')}: ${issue.message}`)
646
+ .join(', ');
647
+ warnings.push(`Line ${i + 1}: Invalid event removed (${issues})`);
648
+ continue;
649
+ }
650
+ // Line is valid
651
+ validLines.push(line);
652
+ linesKept++;
653
+ }
654
+ // Write repaired file atomically
655
+ const tempPath = `${filePath}.tmp.${process.pid}`;
656
+ const repairedContent = validLines.length > 0 ? `${validLines.join('\n')}\n` : '';
657
+ try {
658
+ const fd = openSync(tempPath, 'w');
659
+ writeFileSync(fd, repairedContent, 'utf-8');
660
+ fsyncSync(fd);
661
+ closeSync(fd);
662
+ // Atomic rename
663
+ renameSync(tempPath, filePath);
664
+ // Fsync directory
665
+ const dirPath = path.dirname(filePath);
666
+ const dirFd = openSync(dirPath, 'r');
667
+ fsyncSync(dirFd);
668
+ closeSync(dirFd);
669
+ }
670
+ catch (error) {
671
+ // Cleanup temp file on failure
672
+ try {
673
+ unlinkSync(tempPath);
674
+ }
675
+ catch {
676
+ // Ignore cleanup errors
677
+ }
678
+ throw error;
679
+ }
680
+ // Add warning if file is now empty
681
+ if (linesKept === 0 && linesRemoved > 0) {
682
+ warnings.push('All lines were invalid - file is now empty');
683
+ }
684
+ return {
685
+ success: true,
686
+ linesKept,
687
+ linesRemoved,
688
+ backupPath,
689
+ warnings,
690
+ };
691
+ }