@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,519 @@
1
+ /**
2
+ * FileSystem Metrics Collector Adapter
3
+ *
4
+ * Hexagonal Architecture - Adapter (Infrastructure Layer)
5
+ * Implements MetricsCollector port by reading from filesystem:
6
+ * - WU YAML files (docs/04-operations/tasks/wu/)
7
+ * - status.md (active/blocked WUs)
8
+ * - telemetry files (.beacon/telemetry/)
9
+ *
10
+ * Library-First Approach:
11
+ * - fast-glob: File discovery
12
+ * - yaml: YAML parsing
13
+ * - date-fns: Time calculations
14
+ *
15
+ * @module filesystem-metrics.adapter
16
+ * @see {@link ../ports/metrics-collector.port.ts} - Port interface
17
+ * @see {@link ../domain/orchestration.types.ts} - Return types
18
+ */
19
+ import { readFile } from 'node:fs/promises';
20
+ import { join } from 'node:path';
21
+ import fg from 'fast-glob';
22
+ import { parse as parseYaml } from 'yaml';
23
+ import { differenceInMilliseconds, subHours } from 'date-fns';
24
+ import { minimatch } from 'minimatch';
25
+ import { FILESYSTEM_PATHS, DOD_TOTAL, MANDATORY_TRIGGERS, TIMELINE_WINDOW_HOURS, MAX_ALERTS_DISPLAY, } from '../domain/orchestration.constants.js';
26
+ import { scanWorktrees } from '../worktree-scanner.js';
27
+ /**
28
+ * FileSystem implementation of MetricsCollector.
29
+ *
30
+ * Reads orchestration data from local filesystem.
31
+ * Suitable for development and single-machine deployments.
32
+ *
33
+ * @example
34
+ * const collector = new FileSystemMetricsCollector('/path/to/repo');
35
+ * const status = await collector.getGlobalStatus();
36
+ * console.log(`Active WUs: ${status.activeWUs}`);
37
+ */
38
+ export class FileSystemMetricsCollector {
39
+ baseDir;
40
+ /**
41
+ * Create a new FileSystemMetricsCollector.
42
+ *
43
+ * @param baseDir - Base directory of the repository (default: process.cwd())
44
+ */
45
+ constructor(baseDir) {
46
+ this.baseDir = baseDir ?? process.cwd();
47
+ }
48
+ /**
49
+ * Get global orchestration status.
50
+ *
51
+ * Reads status.md to count active/blocked WUs.
52
+ * Reads stamp files to count completed WUs in last 24h.
53
+ * Reads WU YAMLs to check for failing gates and mandatory agents.
54
+ */
55
+ async getGlobalStatus() {
56
+ try {
57
+ const [statusContent, activeWUs, stamps] = await Promise.all([
58
+ this.readStatusFile(),
59
+ this.readAllWUs(),
60
+ this.readStamps(),
61
+ ]);
62
+ const activeWUsList = activeWUs.filter((wu) => wu.status === 'in_progress');
63
+ const blockedWUs = activeWUs.filter((wu) => wu.status === 'blocked');
64
+ // Count completed WUs in last 24 hours
65
+ const twentyFourHoursAgo = subHours(new Date(), TIMELINE_WINDOW_HOURS);
66
+ const completed24h = stamps.filter((stamp) => {
67
+ const stampDate = new Date(stamp.completedAt);
68
+ return stampDate >= twentyFourHoursAgo;
69
+ }).length;
70
+ // Find longest running WU
71
+ let longestRunning = null;
72
+ if (activeWUsList.length > 0) {
73
+ const sorted = [...activeWUsList].sort((a, b) => {
74
+ const aDuration = differenceInMilliseconds(new Date(), new Date(a.claimed_at ?? a.created));
75
+ const bDuration = differenceInMilliseconds(new Date(), new Date(b.claimed_at ?? b.created));
76
+ return bDuration - aDuration;
77
+ });
78
+ const longest = sorted[0];
79
+ if (longest) {
80
+ longestRunning = {
81
+ wuId: longest.id,
82
+ lane: longest.lane,
83
+ durationMs: differenceInMilliseconds(new Date(), new Date(longest.claimed_at ?? longest.created)),
84
+ };
85
+ }
86
+ }
87
+ // Detect pending mandatory agents
88
+ const pendingMandatory = [];
89
+ for (const wu of activeWUsList) {
90
+ const codePaths = wu.code_paths ?? [];
91
+ for (const [agentName, patterns] of Object.entries(MANDATORY_TRIGGERS)) {
92
+ const shouldTrigger = patterns.some((pattern) => codePaths.some((path) => this.matchesPattern(path, pattern)));
93
+ if (shouldTrigger) {
94
+ // Check if agent has been invoked (would be in telemetry)
95
+ const hasRun = await this.hasAgentRun(wu.id, agentName);
96
+ if (!hasRun) {
97
+ pendingMandatory.push({
98
+ wuId: wu.id,
99
+ agent: agentName,
100
+ });
101
+ }
102
+ }
103
+ }
104
+ }
105
+ // WU-1438: Read active session
106
+ const activeSession = await this.readActiveSession();
107
+ // WU-1748: Scan worktrees for uncommitted changes
108
+ const worktreesWithUncommittedChanges = await this.scanWorktreesForUncommittedChanges();
109
+ return {
110
+ activeWUs: activeWUsList.length,
111
+ completed24h,
112
+ blocked: blockedWUs.length,
113
+ gatesFailing: 0, // Gate failures will be tracked in future WU
114
+ longestRunning,
115
+ pendingMandatory,
116
+ activeSession,
117
+ worktreesWithUncommittedChanges,
118
+ };
119
+ }
120
+ catch (error) {
121
+ // Return empty status if files not found (e.g., in test fixtures)
122
+ return {
123
+ activeWUs: 0,
124
+ completed24h: 0,
125
+ blocked: 0,
126
+ gatesFailing: 0,
127
+ longestRunning: null,
128
+ pendingMandatory: [],
129
+ activeSession: null,
130
+ worktreesWithUncommittedChanges: [],
131
+ };
132
+ }
133
+ }
134
+ /**
135
+ * Get metrics for all known agents.
136
+ *
137
+ * Reads telemetry files to aggregate agent invocations.
138
+ */
139
+ async getAgentMetrics() {
140
+ const metrics = {};
141
+ try {
142
+ const telemetryEvents = await this.readTelemetry();
143
+ // Group events by agent
144
+ const agentEvents = telemetryEvents.filter((e) => e.event === 'agent');
145
+ const agentGroups = new Map();
146
+ for (const event of agentEvents) {
147
+ // Extract agent name from detail (format: "Agent {name} {result}")
148
+ const match = event.detail.match(/Agent ([a-z-]+) (passed|failed)/i);
149
+ if (!match)
150
+ continue;
151
+ const [, agentName, resultStr] = match;
152
+ const result = resultStr.toLowerCase() === 'passed' ? 'pass' : 'fail';
153
+ if (!agentGroups.has(agentName)) {
154
+ agentGroups.set(agentName, []);
155
+ }
156
+ agentGroups.get(agentName).push({
157
+ result,
158
+ timestamp: event.timestamp,
159
+ wuId: event.wuId,
160
+ durationMs: 0, // Would need to parse from telemetry
161
+ });
162
+ }
163
+ // Calculate metrics for each agent
164
+ for (const [agentName, runs] of agentGroups.entries()) {
165
+ const invoked = runs.length;
166
+ const passed = runs.filter((r) => r.result === 'pass').length;
167
+ const passRate = invoked > 0 ? (passed / invoked) * 100 : 0;
168
+ const avgDurationMs = 0; // Would need duration data from telemetry
169
+ const sortedRuns = [...runs].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
170
+ const lastRun = sortedRuns[0]
171
+ ? {
172
+ wuId: sortedRuns[0].wuId,
173
+ timestamp: sortedRuns[0].timestamp,
174
+ result: sortedRuns[0].result,
175
+ }
176
+ : null;
177
+ metrics[agentName] = {
178
+ invoked,
179
+ passRate,
180
+ avgDurationMs,
181
+ lastRun,
182
+ };
183
+ }
184
+ return metrics;
185
+ }
186
+ catch {
187
+ return {};
188
+ }
189
+ }
190
+ /**
191
+ * Get progress for all active WUs.
192
+ *
193
+ * Parses WU YAML files and calculates DoD progress.
194
+ */
195
+ async getWUProgress() {
196
+ try {
197
+ const allWUs = await this.readAllWUs();
198
+ const activeWUs = allWUs.filter((wu) => wu.status === 'in_progress' || wu.status === 'blocked');
199
+ const progress = [];
200
+ for (const wu of activeWUs) {
201
+ // Calculate DoD progress (simplified - would need actual DoD tracking)
202
+ const dodProgress = this.calculateDoDProgress(wu);
203
+ // Get agent statuses
204
+ const agents = {};
205
+ const codePaths = wu.code_paths ?? [];
206
+ for (const [agent, patterns] of Object.entries(MANDATORY_TRIGGERS)) {
207
+ const shouldTrigger = patterns.some((pattern) => codePaths.some((path) => this.matchesPattern(path, pattern)));
208
+ if (shouldTrigger) {
209
+ const hasRun = await this.hasAgentRun(wu.id, agent);
210
+ agents[agent] = hasRun ? 'pass' : 'pending';
211
+ }
212
+ }
213
+ // Generate Tufte-style headline
214
+ const headline = this.generateHeadline(wu, agents);
215
+ progress.push({
216
+ wuId: wu.id,
217
+ lane: wu.lane,
218
+ title: wu.title,
219
+ dodProgress,
220
+ dodTotal: DOD_TOTAL,
221
+ agents,
222
+ headline,
223
+ });
224
+ }
225
+ // Sort by lane then WU ID
226
+ progress.sort((a, b) => {
227
+ const laneCompare = a.lane.localeCompare(b.lane);
228
+ return laneCompare !== 0 ? laneCompare : a.wuId.localeCompare(b.wuId);
229
+ });
230
+ return progress;
231
+ }
232
+ catch {
233
+ return [];
234
+ }
235
+ }
236
+ /**
237
+ * Get timeline events since a given date.
238
+ *
239
+ * Reads and aggregates telemetry files.
240
+ */
241
+ async getTimeline(since) {
242
+ try {
243
+ const allEvents = await this.readTelemetry();
244
+ // Filter events after 'since' date
245
+ const filtered = allEvents.filter((event) => {
246
+ const eventDate = new Date(event.timestamp);
247
+ return eventDate >= since;
248
+ });
249
+ // Sort by timestamp descending
250
+ filtered.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
251
+ return filtered;
252
+ }
253
+ catch {
254
+ return [];
255
+ }
256
+ }
257
+ /**
258
+ * Get current alerts requiring attention.
259
+ *
260
+ * Generates alerts based on WU state and mandatory agents.
261
+ */
262
+ async getAlerts() {
263
+ const alerts = [];
264
+ try {
265
+ const allWUs = await this.readAllWUs();
266
+ const activeWUs = allWUs.filter((wu) => wu.status === 'in_progress');
267
+ // HIGH: Mandatory agents not invoked
268
+ for (const wu of activeWUs) {
269
+ const codePaths = wu.code_paths ?? [];
270
+ for (const [agent, patterns] of Object.entries(MANDATORY_TRIGGERS)) {
271
+ const shouldTrigger = patterns.some((pattern) => codePaths.some((path) => this.matchesPattern(path, pattern)));
272
+ if (shouldTrigger) {
273
+ const hasRun = await this.hasAgentRun(wu.id, agent);
274
+ if (!hasRun) {
275
+ alerts.push({
276
+ severity: 'high',
277
+ message: `Mandatory agent not yet invoked`,
278
+ wuId: wu.id,
279
+ action: `Run ${agent} before wu:done`,
280
+ });
281
+ }
282
+ }
283
+ }
284
+ }
285
+ // MEDIUM: WUs near completion (DoD > 8/11)
286
+ for (const wu of activeWUs) {
287
+ const dodProgress = this.calculateDoDProgress(wu);
288
+ if (dodProgress >= 8) {
289
+ alerts.push({
290
+ severity: 'medium',
291
+ message: `WU near completion - ready for review`,
292
+ wuId: wu.id,
293
+ action: `Run code-reviewer`,
294
+ });
295
+ }
296
+ }
297
+ // MEDIUM: Worktrees with uncommitted changes (WU-1748)
298
+ const worktreesWithChanges = await this.scanWorktreesForUncommittedChanges();
299
+ for (const wt of worktreesWithChanges) {
300
+ alerts.push({
301
+ severity: 'medium',
302
+ message: `Abandoned work: ${wt.uncommittedFileCount} uncommitted files`,
303
+ wuId: wt.wuId,
304
+ action: `pnpm wu:takeover --id ${wt.wuId} (see recovery workflow)`,
305
+ });
306
+ }
307
+ // LOW: Available lanes with ready WUs
308
+ const readyWUs = allWUs.filter((wu) => wu.status === 'ready');
309
+ const readyByLane = new Map();
310
+ for (const wu of readyWUs) {
311
+ readyByLane.set(wu.lane, (readyByLane.get(wu.lane) ?? 0) + 1);
312
+ }
313
+ for (const [lane, count] of readyByLane.entries()) {
314
+ if (count > 0) {
315
+ alerts.push({
316
+ severity: 'low',
317
+ message: `${count} ready WU(s) in ${lane} lane`,
318
+ wuId: readyWUs.find((wu) => wu.lane === lane).id,
319
+ action: `pnpm wu:claim --id ${readyWUs.find((wu) => wu.lane === lane).id} --lane "${lane}"`,
320
+ });
321
+ }
322
+ }
323
+ // Sort by severity (high first)
324
+ const severityOrder = { high: 0, medium: 1, low: 2 };
325
+ alerts.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
326
+ // Limit to MAX_ALERTS_DISPLAY
327
+ return alerts.slice(0, MAX_ALERTS_DISPLAY);
328
+ }
329
+ catch {
330
+ return [];
331
+ }
332
+ }
333
+ // Private helper methods
334
+ async readStatusFile() {
335
+ const statusPath = join(this.baseDir, FILESYSTEM_PATHS.STATUS_FILE);
336
+ return await readFile(statusPath, { encoding: 'utf-8' });
337
+ }
338
+ async readAllWUs() {
339
+ const wuDir = join(this.baseDir, FILESYSTEM_PATHS.WU_DIR);
340
+ const wuFiles = await fg('WU-*.yaml', { cwd: wuDir, absolute: true });
341
+ const wus = await Promise.all(wuFiles.map(async (file) => {
342
+ const content = await readFile(file, { encoding: 'utf-8' });
343
+ return parseYaml(content);
344
+ }));
345
+ return wus;
346
+ }
347
+ async readStamps() {
348
+ const stampsDir = join(this.baseDir, FILESYSTEM_PATHS.STAMPS_DIR);
349
+ const stampFiles = await fg('WU-*.done', { cwd: stampsDir, absolute: true });
350
+ const stamps = await Promise.all(stampFiles.map(async (file) => {
351
+ const content = await readFile(file, { encoding: 'utf-8' });
352
+ const data = parseYaml(content);
353
+ return {
354
+ wuId: data.id ?? '',
355
+ completedAt: data.completed_at ?? data.timestamp ?? new Date().toISOString(),
356
+ };
357
+ }));
358
+ return stamps;
359
+ }
360
+ async readTelemetry() {
361
+ const telemetryDir = join(this.baseDir, FILESYSTEM_PATHS.TELEMETRY_DIR);
362
+ const telemetryFiles = await fg('*.{ndjson,json}', {
363
+ cwd: telemetryDir,
364
+ absolute: true,
365
+ });
366
+ const events = [];
367
+ for (const file of telemetryFiles) {
368
+ const content = await readFile(file, { encoding: 'utf-8' });
369
+ // Handle NDJSON (newline-delimited JSON)
370
+ if (file.endsWith('.ndjson')) {
371
+ const lines = content.split('\n').filter((line) => line.trim());
372
+ for (const line of lines) {
373
+ try {
374
+ const event = JSON.parse(line);
375
+ if (this.isTimelineEvent(event)) {
376
+ events.push(event);
377
+ }
378
+ }
379
+ catch {
380
+ // Skip invalid JSON lines
381
+ }
382
+ }
383
+ }
384
+ else {
385
+ // Handle regular JSON
386
+ try {
387
+ const data = JSON.parse(content);
388
+ if (Array.isArray(data)) {
389
+ events.push(...data.filter((e) => this.isTimelineEvent(e)));
390
+ }
391
+ else if (this.isTimelineEvent(data)) {
392
+ events.push(data);
393
+ }
394
+ }
395
+ catch {
396
+ // Skip invalid JSON files
397
+ }
398
+ }
399
+ }
400
+ return events;
401
+ }
402
+ isTimelineEvent(obj) {
403
+ return (obj &&
404
+ typeof obj === 'object' &&
405
+ typeof obj.timestamp === 'string' &&
406
+ typeof obj.event === 'string' &&
407
+ typeof obj.wuId === 'string' &&
408
+ typeof obj.detail === 'string' &&
409
+ typeof obj.severity === 'string');
410
+ }
411
+ async hasAgentRun(wuId, agentName) {
412
+ const events = await this.readTelemetry();
413
+ return events.some((e) => e.wuId === wuId && e.event === 'agent' && e.detail.includes(agentName));
414
+ }
415
+ /**
416
+ * Match a path against a glob pattern.
417
+ * WU-1849: Replaced custom regex with minimatch library.
418
+ *
419
+ * @param path - Path to match
420
+ * @param pattern - Glob pattern
421
+ * @returns True if path matches pattern
422
+ */
423
+ matchesPattern(path, pattern) {
424
+ return minimatch(path, pattern);
425
+ }
426
+ calculateDoDProgress(wu) {
427
+ // Simplified DoD calculation
428
+ // In reality, would parse actual DoD checkpoints from WU YAML or telemetry
429
+ let progress = 0;
430
+ // Basic heuristics
431
+ if (wu.code_paths && wu.code_paths.length > 0)
432
+ progress += 2;
433
+ if (wu.test_paths && wu.test_paths.unit && wu.test_paths.unit.length > 0)
434
+ progress += 2;
435
+ if (wu.claimed_at)
436
+ progress += 2;
437
+ if (wu.worktree_path)
438
+ progress += 1;
439
+ // Cap at DOD_TOTAL
440
+ return Math.min(progress, DOD_TOTAL);
441
+ }
442
+ generateHeadline(wu, agents) {
443
+ // Tufte-style: data-dense, narrative sentence
444
+ const pendingAgents = Object.entries(agents)
445
+ .filter(([, status]) => status === 'pending')
446
+ .map(([agent]) => agent);
447
+ if (wu.status === 'blocked') {
448
+ return `Blocked: ${wu.blocked_reason ?? 'Unknown reason'}`;
449
+ }
450
+ if (pendingAgents.length > 0) {
451
+ return `Awaiting ${pendingAgents.join(', ')} - ${this.calculateDoDProgress(wu)}/${DOD_TOTAL} DoD complete`;
452
+ }
453
+ return `${this.calculateDoDProgress(wu)}/${DOD_TOTAL} DoD complete - ready for gates`;
454
+ }
455
+ /**
456
+ * Read active session from session file (WU-1438).
457
+ *
458
+ * Transforms snake_case session file format to camelCase TypeScript types.
459
+ *
460
+ * @private
461
+ * @returns Active session data or null if no session active
462
+ */
463
+ async readActiveSession() {
464
+ const sessionPath = join(this.baseDir, FILESYSTEM_PATHS.SESSION_FILE);
465
+ try {
466
+ const content = await readFile(sessionPath, { encoding: 'utf-8' });
467
+ const session = JSON.parse(content);
468
+ // Transform snake_case to camelCase (session file -> TypeScript types)
469
+ return {
470
+ sessionId: session.session_id,
471
+ wuId: session.wu_id,
472
+ started: session.started,
473
+ contextTier: session.context_tier,
474
+ incidentsLogged: session.incidents_logged ?? 0,
475
+ };
476
+ }
477
+ catch {
478
+ // Return null on any read/parse error (file may not exist, corrupted, or mid-write)
479
+ return null;
480
+ }
481
+ }
482
+ /**
483
+ * Scan worktrees for uncommitted changes (WU-1748).
484
+ *
485
+ * Uses worktree-scanner module to detect abandoned WU work.
486
+ * Extracts WU ID from worktree path/branch name.
487
+ *
488
+ * @private
489
+ * @returns Array of worktrees with uncommitted changes
490
+ */
491
+ async scanWorktreesForUncommittedChanges() {
492
+ try {
493
+ const scanResult = await scanWorktrees(this.baseDir);
494
+ const worktrees = scanResult.worktrees ?? [];
495
+ // Filter to only worktrees with uncommitted changes and extract WU ID
496
+ const result = [];
497
+ for (const wt of worktrees) {
498
+ if (!wt.hasUncommittedChanges)
499
+ continue;
500
+ // Extract WU ID from branch name (e.g., "operations-tooling-wu-1748" -> "WU-1748")
501
+ const wuIdMatch = wt.branchName.match(/wu-(\d+)/i);
502
+ if (!wuIdMatch)
503
+ continue;
504
+ const wuId = `WU-${wuIdMatch[1]}`;
505
+ result.push({
506
+ wuId,
507
+ worktreePath: wt.worktreePath,
508
+ uncommittedFileCount: wt.uncommittedFileCount,
509
+ lastActivityTimestamp: wt.lastActivityTimestamp,
510
+ });
511
+ }
512
+ return result;
513
+ }
514
+ catch {
515
+ // Non-fatal: return empty array if worktree scanning fails
516
+ return [];
517
+ }
518
+ }
519
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Terminal Dashboard Renderer Adapter
3
+ *
4
+ * Hexagonal Architecture - Infrastructure Layer
5
+ * Implements the DashboardRenderer port for terminal/CLI output.
6
+ *
7
+ * Follows Edward Tufte's data visualisation principles:
8
+ * - High data-ink ratio (minimal chartjunk)
9
+ * - Small multiples for comparison (agent metrics table)
10
+ * - 5-second scannable layout
11
+ * - Headline sentences for context
12
+ *
13
+ * Library-First Approach:
14
+ * - picocolors: Semantic ANSI colours (NOT raw escape codes)
15
+ * - cli-table3: ASCII table rendering
16
+ * - cli-progress: Progress bar rendering
17
+ *
18
+ * @module terminal-renderer.adapter
19
+ * @see {@link ../ports/dashboard-renderer.port.ts} - Port interface
20
+ * @see {@link ../domain/orchestration.types.ts} - Domain types
21
+ */
22
+ import type { IDashboardRenderer } from '../ports/dashboard-renderer.port.js';
23
+ import type { DashboardData, Suggestion, ExecutionPlan, UserChoice } from '../domain/orchestration.types.js';
24
+ /**
25
+ * Terminal Dashboard Renderer
26
+ *
27
+ * Renders orchestration dashboard data to terminal using ANSI colours and ASCII tables.
28
+ *
29
+ * @example
30
+ * const renderer = new TerminalDashboardRenderer();
31
+ * const data = await metricsCollector.collect();
32
+ * renderer.render(data);
33
+ */
34
+ export declare class TerminalDashboardRenderer implements IDashboardRenderer {
35
+ /**
36
+ * Render the complete dashboard with all 5 sections.
37
+ *
38
+ * Sections:
39
+ * 1. Global Status - High-level metrics
40
+ * 2. Agent Small Multiples - Per-agent comparison table
41
+ * 3. WU Progress - DoD progress bars with headlines
42
+ * 4. Timeline - Recent events
43
+ * 5. Alerts - Items requiring attention
44
+ *
45
+ * @param data - Complete dashboard data
46
+ */
47
+ render(data: DashboardData): void;
48
+ /**
49
+ * Render prioritised suggestions.
50
+ *
51
+ * @param suggestions - Ordered suggestions (highest priority first)
52
+ */
53
+ renderSuggestions(suggestions: Suggestion[]): void;
54
+ /**
55
+ * Render execution plan and prompt for user approval.
56
+ *
57
+ * @param plan - Proposed execution plan
58
+ * @returns User's choice (approve/reject/edit)
59
+ */
60
+ renderPlan(plan: ExecutionPlan): Promise<UserChoice>;
61
+ /**
62
+ * Clear terminal output.
63
+ *
64
+ * Uses ANSI escape sequence to clear screen.
65
+ */
66
+ clear(): void;
67
+ /**
68
+ * Render global status section.
69
+ *
70
+ * @private
71
+ */
72
+ private renderGlobalStatus;
73
+ /**
74
+ * Render agent metrics as small multiples table.
75
+ *
76
+ * @private
77
+ */
78
+ private renderAgentMetrics;
79
+ /**
80
+ * Render WU progress with DoD bars and headlines.
81
+ *
82
+ * @private
83
+ */
84
+ private renderWUProgress;
85
+ /**
86
+ * Render timeline of recent events.
87
+ *
88
+ * @private
89
+ */
90
+ private renderTimeline;
91
+ /**
92
+ * Render alerts requiring attention.
93
+ *
94
+ * @private
95
+ */
96
+ private renderAlerts;
97
+ /**
98
+ * Prompt user for execution plan approval.
99
+ *
100
+ * This is a simplified implementation for testing.
101
+ * Production version would use @inquirer/prompts for interactive input.
102
+ *
103
+ * @private
104
+ */
105
+ private promptUser;
106
+ }