@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,567 @@
1
+ /**
2
+ * WU Consistency Checker (WU-1276, WU-2412)
3
+ *
4
+ * Layer 2 defense-in-depth: Detect and repair WU state inconsistencies.
5
+ *
6
+ * Detects five types of inconsistencies:
7
+ * - YAML_DONE_STATUS_IN_PROGRESS: WU YAML done but in status.md In Progress
8
+ * - BACKLOG_DUAL_SECTION: WU in both Done and In Progress sections
9
+ * - YAML_DONE_NO_STAMP: WU YAML done but no stamp file
10
+ * - ORPHAN_WORKTREE_DONE: Done WU still has worktree
11
+ * - STAMP_EXISTS_YAML_NOT_DONE: Stamp exists but YAML status is not done (WU-2412)
12
+ *
13
+ * @see {@link ../wu-repair.mjs} CLI interface
14
+ */
15
+ import { readFile, writeFile, readdir, mkdir, access } from 'node:fs/promises';
16
+ import { constants } from 'node:fs';
17
+ import path from 'node:path';
18
+ import yaml from 'js-yaml';
19
+ import { WU_PATHS } from './wu-paths.js';
20
+ import { CONSISTENCY_TYPES, LOG_PREFIX, REMOTES, STRING_LITERALS, toKebab, WU_STATUS, YAML_OPTIONS, } from './wu-constants.js';
21
+ import { todayISO } from './date-utils.js';
22
+ import { createGitForPath } from './git-adapter.js';
23
+ /**
24
+ * Check a single WU for state inconsistencies
25
+ *
26
+ * @param {string} id - WU ID (e.g., 'WU-123')
27
+ * @param {string} [projectRoot=process.cwd()] - Project root directory
28
+ * @returns {Promise<object>} Consistency report with valid, errors, and stats
29
+ */
30
+ export async function checkWUConsistency(id, projectRoot = process.cwd()) {
31
+ const errors = [];
32
+ const wuPath = path.join(projectRoot, WU_PATHS.WU(id));
33
+ const stampPath = path.join(projectRoot, WU_PATHS.STAMP(id));
34
+ const backlogPath = path.join(projectRoot, WU_PATHS.BACKLOG());
35
+ const statusPath = path.join(projectRoot, WU_PATHS.STATUS());
36
+ // Handle missing WU YAML gracefully
37
+ try {
38
+ await access(wuPath, constants.R_OK);
39
+ }
40
+ catch {
41
+ return { valid: true, errors: [], stats: { wuExists: false } };
42
+ }
43
+ const wuContent = await readFile(wuPath, { encoding: 'utf-8' });
44
+ const wuDoc = yaml.load(wuContent);
45
+ const yamlStatus = wuDoc?.status || 'unknown';
46
+ const lane = wuDoc?.lane || '';
47
+ const title = wuDoc?.title || '';
48
+ // Check stamp existence
49
+ let hasStamp = false;
50
+ try {
51
+ await access(stampPath, constants.R_OK);
52
+ hasStamp = true;
53
+ }
54
+ catch {
55
+ hasStamp = false;
56
+ }
57
+ // Parse backlog sections
58
+ let backlogContent = '';
59
+ try {
60
+ backlogContent = await readFile(backlogPath, { encoding: 'utf-8' });
61
+ }
62
+ catch {
63
+ backlogContent = '';
64
+ }
65
+ const { inDone: backlogInDone, inProgress: backlogInProgress } = parseBacklogSections(backlogContent, id);
66
+ // Parse status.md sections
67
+ let statusContent = '';
68
+ try {
69
+ statusContent = await readFile(statusPath, { encoding: 'utf-8' });
70
+ }
71
+ catch {
72
+ statusContent = '';
73
+ }
74
+ const { inProgress: statusInProgress } = parseStatusSections(statusContent, id);
75
+ // Check for worktree
76
+ const hasWorktree = await checkWorktreeExists(id, projectRoot);
77
+ // Detection logic
78
+ // 1. YAML done but in status.md In Progress
79
+ if (yamlStatus === WU_STATUS.DONE && statusInProgress) {
80
+ errors.push({
81
+ type: CONSISTENCY_TYPES.YAML_DONE_STATUS_IN_PROGRESS,
82
+ wuId: id,
83
+ description: `WU ${id} has status '${WU_STATUS.DONE}' in YAML but still appears in status.md In Progress section`,
84
+ repairAction: 'Remove from status.md In Progress section',
85
+ canAutoRepair: true,
86
+ });
87
+ }
88
+ // 2. Backlog dual section (Done AND In Progress)
89
+ if (backlogInDone && backlogInProgress) {
90
+ errors.push({
91
+ type: CONSISTENCY_TYPES.BACKLOG_DUAL_SECTION,
92
+ wuId: id,
93
+ description: `WU ${id} appears in both Done and In Progress sections of backlog.md`,
94
+ repairAction: 'Remove from In Progress section (Done wins)',
95
+ canAutoRepair: true,
96
+ });
97
+ }
98
+ // 3. YAML done but no stamp
99
+ if (yamlStatus === WU_STATUS.DONE && !hasStamp) {
100
+ errors.push({
101
+ type: CONSISTENCY_TYPES.YAML_DONE_NO_STAMP,
102
+ wuId: id,
103
+ title,
104
+ description: `WU ${id} has status '${WU_STATUS.DONE}' but no stamp file exists`,
105
+ repairAction: 'Create stamp file',
106
+ canAutoRepair: true,
107
+ });
108
+ }
109
+ // 4. Orphan worktree for done WU
110
+ if (yamlStatus === WU_STATUS.DONE && hasWorktree) {
111
+ errors.push({
112
+ type: CONSISTENCY_TYPES.ORPHAN_WORKTREE_DONE,
113
+ wuId: id,
114
+ lane,
115
+ description: `WU ${id} has status '${WU_STATUS.DONE}' but still has an associated worktree`,
116
+ repairAction: 'Remove orphan worktree and lane branch',
117
+ canAutoRepair: true,
118
+ });
119
+ }
120
+ // 5. Stamp exists but YAML not done (inverse of YAML_DONE_NO_STAMP)
121
+ // This catches partial wu:done failures where stamp was created but YAML update failed
122
+ if (hasStamp && yamlStatus !== WU_STATUS.DONE) {
123
+ errors.push({
124
+ type: CONSISTENCY_TYPES.STAMP_EXISTS_YAML_NOT_DONE,
125
+ wuId: id,
126
+ title,
127
+ description: `WU ${id} has stamp file but YAML status is '${yamlStatus}' (not done)`,
128
+ repairAction: 'Update YAML to done+locked+completed',
129
+ canAutoRepair: true,
130
+ });
131
+ }
132
+ return {
133
+ valid: errors.length === 0,
134
+ errors,
135
+ stats: {
136
+ yamlStatus,
137
+ hasStamp,
138
+ backlogInDone,
139
+ backlogInProgress,
140
+ statusInProgress,
141
+ hasWorktree,
142
+ },
143
+ };
144
+ }
145
+ /**
146
+ * Check all WUs for consistency
147
+ *
148
+ * @param {string} [projectRoot=process.cwd()] - Project root directory
149
+ * @returns {Promise<object>} Aggregated report with valid, errors, and checked count
150
+ */
151
+ export async function checkAllWUConsistency(projectRoot = process.cwd()) {
152
+ const wuDir = path.join(projectRoot, 'docs/04-operations/tasks/wu');
153
+ try {
154
+ await access(wuDir, constants.R_OK);
155
+ }
156
+ catch {
157
+ return { valid: true, errors: [], checked: 0 };
158
+ }
159
+ const allErrors = [];
160
+ const wuFiles = (await readdir(wuDir)).filter((f) => /^WU-\d+\.yaml$/.test(f));
161
+ for (const file of wuFiles) {
162
+ const id = file.replace('.yaml', '');
163
+ const report = await checkWUConsistency(id, projectRoot);
164
+ allErrors.push(...report.errors);
165
+ }
166
+ return {
167
+ valid: allErrors.length === 0,
168
+ errors: allErrors,
169
+ checked: wuFiles.length,
170
+ };
171
+ }
172
+ /**
173
+ * Check lane for orphan done WUs (pre-flight for wu:claim)
174
+ *
175
+ * @param {string} lane - Lane name to check
176
+ * @param {string} excludeId - WU ID to exclude from check (the one being claimed)
177
+ * @param {string} [projectRoot=process.cwd()] - Project root directory
178
+ * @returns {Promise<object>} Result with valid, orphans list, and reports
179
+ */
180
+ export async function checkLaneForOrphanDoneWU(lane, excludeId, projectRoot = process.cwd()) {
181
+ const wuDir = path.join(projectRoot, 'docs/04-operations/tasks/wu');
182
+ try {
183
+ await access(wuDir, constants.R_OK);
184
+ }
185
+ catch {
186
+ return { valid: true, orphans: [] };
187
+ }
188
+ const orphans = [];
189
+ const wuFiles = (await readdir(wuDir)).filter((f) => /^WU-\d+\.yaml$/.test(f));
190
+ for (const file of wuFiles) {
191
+ const id = file.replace('.yaml', '');
192
+ if (id === excludeId)
193
+ continue;
194
+ const wuPath = path.join(wuDir, file);
195
+ let wuContent;
196
+ try {
197
+ wuContent = await readFile(wuPath, { encoding: 'utf-8' });
198
+ }
199
+ catch {
200
+ // Skip unreadable files
201
+ continue;
202
+ }
203
+ let wuDoc;
204
+ try {
205
+ wuDoc = yaml.load(wuContent);
206
+ }
207
+ catch {
208
+ // Skip malformed YAML files - they're a separate issue
209
+ continue;
210
+ }
211
+ if (wuDoc?.lane === lane && wuDoc?.status === WU_STATUS.DONE) {
212
+ const report = await checkWUConsistency(id, projectRoot);
213
+ if (!report.valid) {
214
+ orphans.push({ id, errors: report.errors });
215
+ }
216
+ }
217
+ }
218
+ return {
219
+ valid: orphans.length === 0,
220
+ orphans: orphans.map((o) => o.id),
221
+ reports: orphans,
222
+ };
223
+ }
224
+ /**
225
+ * Repair WU inconsistencies
226
+ *
227
+ * @param {object} report - Report from checkWUConsistency()
228
+ * @param {RepairWUInconsistencyOptions} [options={}] - Repair options
229
+ * @returns {Promise<object>} Result with repaired, skipped, and failed counts
230
+ */
231
+ export async function repairWUInconsistency(report, options = {}) {
232
+ const { dryRun = false, projectRoot = process.cwd() } = options;
233
+ if (report.valid) {
234
+ return { repaired: 0, skipped: 0, failed: 0 };
235
+ }
236
+ let repaired = 0;
237
+ let skipped = 0;
238
+ let failed = 0;
239
+ for (const error of report.errors) {
240
+ if (!error.canAutoRepair) {
241
+ skipped++;
242
+ continue;
243
+ }
244
+ if (dryRun) {
245
+ repaired++;
246
+ continue;
247
+ }
248
+ try {
249
+ const result = await repairSingleError(error, projectRoot);
250
+ if (result.success) {
251
+ repaired++;
252
+ }
253
+ else if (result.skipped) {
254
+ skipped++;
255
+ if (result.reason) {
256
+ console.warn(`${LOG_PREFIX.REPAIR} Skipped ${error.type}: ${result.reason}`);
257
+ }
258
+ }
259
+ else {
260
+ failed++;
261
+ }
262
+ }
263
+ catch (err) {
264
+ const errMessage = err instanceof Error ? err.message : String(err);
265
+ console.error(`${LOG_PREFIX.REPAIR} Failed to repair ${error.type}: ${errMessage}`);
266
+ failed++;
267
+ }
268
+ }
269
+ return { repaired, skipped, failed };
270
+ }
271
+ /**
272
+ * Repair a single inconsistency error
273
+ *
274
+ * @param {object} error - Error object from checkWUConsistency()
275
+ * @param {string} projectRoot - Project root directory
276
+ * @returns {Promise<RepairResult>} Result with success, skipped, and reason
277
+ */
278
+ async function repairSingleError(error, projectRoot) {
279
+ switch (error.type) {
280
+ case CONSISTENCY_TYPES.YAML_DONE_NO_STAMP:
281
+ await createStampInProject(error.wuId, error.title || `WU ${error.wuId}`, projectRoot);
282
+ return { success: true };
283
+ case CONSISTENCY_TYPES.YAML_DONE_STATUS_IN_PROGRESS:
284
+ await removeWUFromSection(path.join(projectRoot, WU_PATHS.STATUS()), error.wuId, '## In Progress');
285
+ return { success: true };
286
+ case CONSISTENCY_TYPES.BACKLOG_DUAL_SECTION:
287
+ await removeWUFromSection(path.join(projectRoot, WU_PATHS.BACKLOG()), error.wuId, '## 🔧 In progress');
288
+ return { success: true };
289
+ case CONSISTENCY_TYPES.ORPHAN_WORKTREE_DONE:
290
+ return await removeOrphanWorktree(error.wuId, error.lane, projectRoot);
291
+ case CONSISTENCY_TYPES.STAMP_EXISTS_YAML_NOT_DONE:
292
+ await updateYamlToDone(error.wuId, projectRoot);
293
+ return { success: true };
294
+ default:
295
+ return { skipped: true, reason: `Unknown error type: ${error.type}` };
296
+ }
297
+ }
298
+ /**
299
+ * Create stamp file in a specific project root
300
+ *
301
+ * @param {string} id - WU ID
302
+ * @param {string} title - WU title
303
+ * @param {string} projectRoot - Project root directory
304
+ * @returns {Promise<void>}
305
+ */
306
+ async function createStampInProject(id, title, projectRoot) {
307
+ const stampsDir = path.join(projectRoot, WU_PATHS.STAMPS_DIR());
308
+ const stampPath = path.join(projectRoot, WU_PATHS.STAMP(id));
309
+ // Ensure stamps directory exists
310
+ try {
311
+ await access(stampsDir, constants.R_OK);
312
+ }
313
+ catch {
314
+ await mkdir(stampsDir, { recursive: true });
315
+ }
316
+ // Don't overwrite existing stamp
317
+ try {
318
+ await access(stampPath, constants.R_OK);
319
+ return; // Stamp already exists
320
+ }
321
+ catch {
322
+ // Stamp doesn't exist, continue to create it
323
+ }
324
+ // Create stamp file
325
+ const body = `WU ${id} — ${title}\nCompleted: ${todayISO()}\n`;
326
+ await writeFile(stampPath, body, { encoding: 'utf-8' });
327
+ }
328
+ /**
329
+ * Update WU YAML to done+locked+completed state (WU-2412)
330
+ *
331
+ * Repairs STAMP_EXISTS_YAML_NOT_DONE by setting:
332
+ * - status: done
333
+ * - locked: true
334
+ * - completed: YYYY-MM-DD (today, unless already set)
335
+ *
336
+ * @param {string} id - WU ID
337
+ * @param {string} projectRoot - Project root directory
338
+ * @returns {Promise<void>}
339
+ */
340
+ async function updateYamlToDone(id, projectRoot) {
341
+ const wuPath = path.join(projectRoot, WU_PATHS.WU(id));
342
+ // Read current YAML
343
+ const content = await readFile(wuPath, { encoding: 'utf-8' });
344
+ const wuDoc = yaml.load(content);
345
+ if (!wuDoc) {
346
+ throw new Error(`Failed to parse WU YAML: ${wuPath}`);
347
+ }
348
+ // Update fields
349
+ wuDoc.status = WU_STATUS.DONE;
350
+ wuDoc.locked = true;
351
+ // Preserve existing completed date if present, otherwise set to today
352
+ if (!wuDoc.completed) {
353
+ wuDoc.completed = todayISO();
354
+ }
355
+ // Write updated YAML
356
+ const updatedContent = yaml.dump(wuDoc, { lineWidth: YAML_OPTIONS.LINE_WIDTH });
357
+ await writeFile(wuPath, updatedContent, { encoding: 'utf-8' });
358
+ }
359
+ /**
360
+ * Remove WU entry from a specific section in a markdown file
361
+ *
362
+ * @param {string} filePath - Path to the markdown file
363
+ * @param {string} id - WU ID to remove
364
+ * @param {string} sectionHeading - Section heading to target
365
+ * @returns {Promise<void>}
366
+ */
367
+ async function removeWUFromSection(filePath, id, sectionHeading) {
368
+ try {
369
+ await access(filePath, constants.R_OK);
370
+ }
371
+ catch {
372
+ return; // File doesn't exist
373
+ }
374
+ const content = await readFile(filePath, { encoding: 'utf-8' });
375
+ const lines = content.split(/\r?\n/);
376
+ let inTargetSection = false;
377
+ let nextSectionIdx = -1;
378
+ let sectionStartIdx = -1;
379
+ // Normalize heading for comparison (lowercase, trim)
380
+ const normalizedHeading = sectionHeading.toLowerCase().trim();
381
+ // Find section boundaries
382
+ for (let i = 0; i < lines.length; i++) {
383
+ const normalizedLine = lines[i].toLowerCase().trim();
384
+ if (normalizedLine === normalizedHeading || normalizedLine.startsWith(normalizedHeading)) {
385
+ inTargetSection = true;
386
+ sectionStartIdx = i;
387
+ continue;
388
+ }
389
+ if (inTargetSection && lines[i].trim().startsWith('## ')) {
390
+ nextSectionIdx = i;
391
+ break;
392
+ }
393
+ }
394
+ if (sectionStartIdx === -1)
395
+ return;
396
+ const endIdx = nextSectionIdx === -1 ? lines.length : nextSectionIdx;
397
+ // Filter out lines containing the WU ID in the target section
398
+ const newLines = [];
399
+ for (let i = 0; i < lines.length; i++) {
400
+ if (i > sectionStartIdx && i < endIdx && lines[i].includes(id)) {
401
+ continue; // Skip this line
402
+ }
403
+ newLines.push(lines[i]);
404
+ }
405
+ await writeFile(filePath, newLines.join(STRING_LITERALS.NEWLINE));
406
+ }
407
+ /**
408
+ * Remove orphan worktree for a done WU
409
+ *
410
+ * CRITICAL: This function includes safety guards to prevent data loss.
411
+ * See WU-1276 incident report for why these guards are essential.
412
+ *
413
+ * @param {string} id - WU ID
414
+ * @param {string} lane - Lane name
415
+ * @param {string} projectRoot - Project root directory
416
+ * @returns {Promise<object>} Result with success, skipped, and reason
417
+ */
418
+ async function removeOrphanWorktree(id, lane, projectRoot) {
419
+ // Find worktree path
420
+ const laneKebab = toKebab(lane);
421
+ const worktreeName = `${laneKebab}-${id.toLowerCase()}`;
422
+ const worktreePath = path.join(projectRoot, 'worktrees', worktreeName);
423
+ // 🚨 SAFETY GUARD 1: Check if cwd is inside worktree
424
+ const cwd = process.cwd();
425
+ if (cwd.startsWith(worktreePath)) {
426
+ return { skipped: true, reason: 'Cannot delete worktree while inside it' };
427
+ }
428
+ // 🚨 SAFETY GUARD 2: Check for uncommitted changes (if worktree exists)
429
+ try {
430
+ await access(worktreePath, constants.R_OK);
431
+ // Worktree exists, check for uncommitted changes
432
+ try {
433
+ const gitWorktree = createGitForPath(worktreePath);
434
+ const status = await gitWorktree.getStatus();
435
+ if (status.trim().length > 0) {
436
+ return { skipped: true, reason: 'Worktree has uncommitted changes' };
437
+ }
438
+ }
439
+ catch {
440
+ // Ignore errors checking status - proceed with other guards
441
+ }
442
+ }
443
+ catch {
444
+ // Worktree doesn't exist, that's fine
445
+ }
446
+ // 🚨 SAFETY GUARD 3: Check stamp exists (not rollback state)
447
+ const stampPath = path.join(projectRoot, WU_PATHS.STAMP(id));
448
+ try {
449
+ await access(stampPath, constants.R_OK);
450
+ }
451
+ catch {
452
+ return { skipped: true, reason: 'WU marked done but no stamp - possible rollback state' };
453
+ }
454
+ // Safe to proceed with cleanup
455
+ const git = createGitForPath(projectRoot);
456
+ try {
457
+ await access(worktreePath, constants.R_OK);
458
+ await git.worktreeRemove(worktreePath, { force: true });
459
+ }
460
+ catch {
461
+ // Worktree may not exist
462
+ }
463
+ // Delete lane branch
464
+ const branchName = `lane/${laneKebab}/${id.toLowerCase()}`;
465
+ try {
466
+ await git.deleteBranch(branchName, { force: true });
467
+ }
468
+ catch {
469
+ // Branch may not exist locally
470
+ }
471
+ try {
472
+ await git.raw(['push', REMOTES.ORIGIN, '--delete', branchName]);
473
+ }
474
+ catch {
475
+ // Remote branch may not exist
476
+ }
477
+ return { success: true };
478
+ }
479
+ /**
480
+ * Parse backlog.md to find which sections contain a WU ID
481
+ *
482
+ * @param {string} content - Backlog file content
483
+ * @param {string} id - WU ID to search for
484
+ * @returns {object} Object with inDone and inProgress booleans
485
+ */
486
+ function parseBacklogSections(content, id) {
487
+ const lines = content.split(/\r?\n/);
488
+ let inDone = false;
489
+ let inProgress = false;
490
+ let currentSection = null;
491
+ // Match exact WU YAML filename to prevent substring false positives
492
+ // e.g., WU-208 should not match lines containing WU-2087
493
+ const exactPattern = `(wu/${id}.yaml)`;
494
+ for (const line of lines) {
495
+ if (line.trim() === '## ✅ Done') {
496
+ currentSection = WU_STATUS.DONE;
497
+ continue;
498
+ }
499
+ if (line.trim() === '## 🔧 In progress') {
500
+ currentSection = 'in_progress';
501
+ continue;
502
+ }
503
+ if (line.trim().startsWith('## ')) {
504
+ currentSection = null;
505
+ continue;
506
+ }
507
+ if (line.includes(exactPattern)) {
508
+ if (currentSection === WU_STATUS.DONE)
509
+ inDone = true;
510
+ if (currentSection === 'in_progress')
511
+ inProgress = true;
512
+ }
513
+ }
514
+ return { inDone, inProgress };
515
+ }
516
+ /**
517
+ * Parse status.md to find if WU is in In Progress section
518
+ *
519
+ * @param {string} content - Status file content
520
+ * @param {string} id - WU ID to search for
521
+ * @returns {object} Object with inProgress boolean
522
+ */
523
+ function parseStatusSections(content, id) {
524
+ const lines = content.split(/\r?\n/);
525
+ let inProgress = false;
526
+ let currentSection = null;
527
+ // Match exact WU YAML filename to prevent substring false positives
528
+ // e.g., WU-208 should not match lines containing WU-2087
529
+ const exactPattern = `(wu/${id}.yaml)`;
530
+ for (const line of lines) {
531
+ if (line.trim() === '## In Progress') {
532
+ currentSection = 'in_progress';
533
+ continue;
534
+ }
535
+ if (line.trim().startsWith('## ')) {
536
+ currentSection = null;
537
+ continue;
538
+ }
539
+ if (currentSection === 'in_progress' && line.includes(exactPattern)) {
540
+ inProgress = true;
541
+ }
542
+ }
543
+ return { inProgress };
544
+ }
545
+ /**
546
+ * Check if a worktree exists for a given WU ID
547
+ *
548
+ * Uses word-boundary matching to avoid false positives where one WU ID
549
+ * is a prefix of another (e.g., WU-204 should not match wu-2049).
550
+ *
551
+ * @param {string} id - WU ID
552
+ * @param {string} projectRoot - Project root directory
553
+ * @returns {Promise<boolean>} True if worktree exists
554
+ */
555
+ async function checkWorktreeExists(id, projectRoot) {
556
+ try {
557
+ const git = createGitForPath(projectRoot);
558
+ const output = await git.worktreeList();
559
+ // Match WU ID followed by non-digit or end of string to prevent
560
+ // false positives (e.g., wu-204 matching wu-2049)
561
+ const pattern = new RegExp(`${id.toLowerCase()}(?![0-9])`, 'i');
562
+ return pattern.test(output);
563
+ }
564
+ catch {
565
+ return false;
566
+ }
567
+ }