@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.
- package/LICENSE +190 -0
- package/README.md +119 -0
- package/dist/active-wu-detector.d.ts +33 -0
- package/dist/active-wu-detector.js +106 -0
- package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
- package/dist/adapters/filesystem-metrics.adapter.js +519 -0
- package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
- package/dist/adapters/terminal-renderer.adapter.js +337 -0
- package/dist/arg-parser.d.ts +63 -0
- package/dist/arg-parser.js +560 -0
- package/dist/backlog-editor.d.ts +98 -0
- package/dist/backlog-editor.js +179 -0
- package/dist/backlog-generator.d.ts +111 -0
- package/dist/backlog-generator.js +381 -0
- package/dist/backlog-parser.d.ts +45 -0
- package/dist/backlog-parser.js +102 -0
- package/dist/backlog-sync-validator.d.ts +78 -0
- package/dist/backlog-sync-validator.js +294 -0
- package/dist/branch-drift.d.ts +34 -0
- package/dist/branch-drift.js +51 -0
- package/dist/cleanup-install-config.d.ts +33 -0
- package/dist/cleanup-install-config.js +37 -0
- package/dist/cleanup-lock.d.ts +139 -0
- package/dist/cleanup-lock.js +313 -0
- package/dist/code-path-validator.d.ts +146 -0
- package/dist/code-path-validator.js +537 -0
- package/dist/code-paths-overlap.d.ts +55 -0
- package/dist/code-paths-overlap.js +245 -0
- package/dist/commands-logger.d.ts +77 -0
- package/dist/commands-logger.js +254 -0
- package/dist/commit-message-utils.d.ts +25 -0
- package/dist/commit-message-utils.js +41 -0
- package/dist/compliance-parser.d.ts +150 -0
- package/dist/compliance-parser.js +507 -0
- package/dist/constants/backlog-patterns.d.ts +20 -0
- package/dist/constants/backlog-patterns.js +23 -0
- package/dist/constants/dora-constants.d.ts +49 -0
- package/dist/constants/dora-constants.js +53 -0
- package/dist/constants/gate-constants.d.ts +15 -0
- package/dist/constants/gate-constants.js +15 -0
- package/dist/constants/linter-constants.d.ts +16 -0
- package/dist/constants/linter-constants.js +16 -0
- package/dist/constants/tokenizer-constants.d.ts +15 -0
- package/dist/constants/tokenizer-constants.js +15 -0
- package/dist/core/scope-checker.d.ts +97 -0
- package/dist/core/scope-checker.js +163 -0
- package/dist/core/tool-runner.d.ts +161 -0
- package/dist/core/tool-runner.js +393 -0
- package/dist/core/tool.constants.d.ts +105 -0
- package/dist/core/tool.constants.js +101 -0
- package/dist/core/tool.schemas.d.ts +226 -0
- package/dist/core/tool.schemas.js +226 -0
- package/dist/core/worktree-guard.d.ts +130 -0
- package/dist/core/worktree-guard.js +242 -0
- package/dist/coverage-gate.d.ts +108 -0
- package/dist/coverage-gate.js +196 -0
- package/dist/date-utils.d.ts +75 -0
- package/dist/date-utils.js +140 -0
- package/dist/dependency-graph.d.ts +142 -0
- package/dist/dependency-graph.js +550 -0
- package/dist/dependency-guard.d.ts +54 -0
- package/dist/dependency-guard.js +142 -0
- package/dist/dependency-validator.d.ts +105 -0
- package/dist/dependency-validator.js +154 -0
- package/dist/docs-path-validator.d.ts +36 -0
- package/dist/docs-path-validator.js +95 -0
- package/dist/domain/orchestration.constants.d.ts +99 -0
- package/dist/domain/orchestration.constants.js +97 -0
- package/dist/domain/orchestration.schemas.d.ts +280 -0
- package/dist/domain/orchestration.schemas.js +211 -0
- package/dist/domain/orchestration.types.d.ts +133 -0
- package/dist/domain/orchestration.types.js +12 -0
- package/dist/error-handler.d.ts +116 -0
- package/dist/error-handler.js +136 -0
- package/dist/file-classifiers.d.ts +62 -0
- package/dist/file-classifiers.js +108 -0
- package/dist/gates-agent-mode.d.ts +81 -0
- package/dist/gates-agent-mode.js +94 -0
- package/dist/generate-traceability.d.ts +107 -0
- package/dist/generate-traceability.js +411 -0
- package/dist/git-adapter.d.ts +395 -0
- package/dist/git-adapter.js +649 -0
- package/dist/git-staged-validator.d.ts +32 -0
- package/dist/git-staged-validator.js +48 -0
- package/dist/hardcoded-strings.d.ts +61 -0
- package/dist/hardcoded-strings.js +270 -0
- package/dist/incremental-lint.d.ts +78 -0
- package/dist/incremental-lint.js +129 -0
- package/dist/incremental-test.d.ts +39 -0
- package/dist/incremental-test.js +61 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +61 -0
- package/dist/invariants/check-automated-tests.d.ts +50 -0
- package/dist/invariants/check-automated-tests.js +166 -0
- package/dist/invariants-runner.d.ts +103 -0
- package/dist/invariants-runner.js +527 -0
- package/dist/lane-checker.d.ts +50 -0
- package/dist/lane-checker.js +319 -0
- package/dist/lane-inference.d.ts +39 -0
- package/dist/lane-inference.js +195 -0
- package/dist/lane-lock.d.ts +211 -0
- package/dist/lane-lock.js +474 -0
- package/dist/lane-validator.d.ts +48 -0
- package/dist/lane-validator.js +114 -0
- package/dist/logs-lib.d.ts +104 -0
- package/dist/logs-lib.js +207 -0
- package/dist/lumenflow-config-schema.d.ts +272 -0
- package/dist/lumenflow-config-schema.js +207 -0
- package/dist/lumenflow-config.d.ts +95 -0
- package/dist/lumenflow-config.js +236 -0
- package/dist/manual-test-validator.d.ts +80 -0
- package/dist/manual-test-validator.js +200 -0
- package/dist/merge-lock.d.ts +115 -0
- package/dist/merge-lock.js +251 -0
- package/dist/micro-worktree.d.ts +159 -0
- package/dist/micro-worktree.js +427 -0
- package/dist/migration-deployer.d.ts +69 -0
- package/dist/migration-deployer.js +151 -0
- package/dist/orchestration-advisory-loader.d.ts +28 -0
- package/dist/orchestration-advisory-loader.js +87 -0
- package/dist/orchestration-advisory.d.ts +58 -0
- package/dist/orchestration-advisory.js +94 -0
- package/dist/orchestration-di.d.ts +48 -0
- package/dist/orchestration-di.js +57 -0
- package/dist/orchestration-rules.d.ts +57 -0
- package/dist/orchestration-rules.js +201 -0
- package/dist/orphan-detector.d.ts +131 -0
- package/dist/orphan-detector.js +226 -0
- package/dist/path-classifiers.d.ts +57 -0
- package/dist/path-classifiers.js +93 -0
- package/dist/piped-command-detector.d.ts +34 -0
- package/dist/piped-command-detector.js +64 -0
- package/dist/ports/dashboard-renderer.port.d.ts +112 -0
- package/dist/ports/dashboard-renderer.port.js +25 -0
- package/dist/ports/metrics-collector.port.d.ts +132 -0
- package/dist/ports/metrics-collector.port.js +26 -0
- package/dist/process-detector.d.ts +84 -0
- package/dist/process-detector.js +172 -0
- package/dist/prompt-linter.d.ts +72 -0
- package/dist/prompt-linter.js +312 -0
- package/dist/prompt-monitor.d.ts +15 -0
- package/dist/prompt-monitor.js +205 -0
- package/dist/rebase-artifact-cleanup.d.ts +145 -0
- package/dist/rebase-artifact-cleanup.js +433 -0
- package/dist/retry-strategy.d.ts +189 -0
- package/dist/retry-strategy.js +283 -0
- package/dist/risk-detector.d.ts +108 -0
- package/dist/risk-detector.js +252 -0
- package/dist/rollback-utils.d.ts +76 -0
- package/dist/rollback-utils.js +104 -0
- package/dist/section-headings.d.ts +43 -0
- package/dist/section-headings.js +49 -0
- package/dist/spawn-escalation.d.ts +90 -0
- package/dist/spawn-escalation.js +253 -0
- package/dist/spawn-monitor.d.ts +229 -0
- package/dist/spawn-monitor.js +672 -0
- package/dist/spawn-recovery.d.ts +82 -0
- package/dist/spawn-recovery.js +298 -0
- package/dist/spawn-registry-schema.d.ts +98 -0
- package/dist/spawn-registry-schema.js +108 -0
- package/dist/spawn-registry-store.d.ts +146 -0
- package/dist/spawn-registry-store.js +273 -0
- package/dist/spawn-tree.d.ts +121 -0
- package/dist/spawn-tree.js +285 -0
- package/dist/stamp-status-validator.d.ts +84 -0
- package/dist/stamp-status-validator.js +134 -0
- package/dist/stamp-utils.d.ts +100 -0
- package/dist/stamp-utils.js +229 -0
- package/dist/state-machine.d.ts +26 -0
- package/dist/state-machine.js +83 -0
- package/dist/system-map-validator.d.ts +80 -0
- package/dist/system-map-validator.js +272 -0
- package/dist/telemetry.d.ts +80 -0
- package/dist/telemetry.js +213 -0
- package/dist/token-counter.d.ts +51 -0
- package/dist/token-counter.js +145 -0
- package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
- package/dist/usecases/get-dashboard-data.usecase.js +61 -0
- package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
- package/dist/usecases/get-suggestions.usecase.js +153 -0
- package/dist/user-normalizer.d.ts +41 -0
- package/dist/user-normalizer.js +141 -0
- package/dist/validators/phi-constants.d.ts +97 -0
- package/dist/validators/phi-constants.js +152 -0
- package/dist/validators/phi-scanner.d.ts +58 -0
- package/dist/validators/phi-scanner.js +215 -0
- package/dist/worktree-ownership.d.ts +50 -0
- package/dist/worktree-ownership.js +74 -0
- package/dist/worktree-scanner.d.ts +103 -0
- package/dist/worktree-scanner.js +168 -0
- package/dist/worktree-symlink.d.ts +99 -0
- package/dist/worktree-symlink.js +359 -0
- package/dist/wu-backlog-updater.d.ts +17 -0
- package/dist/wu-backlog-updater.js +37 -0
- package/dist/wu-checkpoint.d.ts +124 -0
- package/dist/wu-checkpoint.js +233 -0
- package/dist/wu-claim-helpers.d.ts +26 -0
- package/dist/wu-claim-helpers.js +63 -0
- package/dist/wu-claim-resume.d.ts +106 -0
- package/dist/wu-claim-resume.js +276 -0
- package/dist/wu-consistency-checker.d.ts +95 -0
- package/dist/wu-consistency-checker.js +567 -0
- package/dist/wu-constants.d.ts +1275 -0
- package/dist/wu-constants.js +1382 -0
- package/dist/wu-create-validators.d.ts +42 -0
- package/dist/wu-create-validators.js +93 -0
- package/dist/wu-done-branch-only.d.ts +63 -0
- package/dist/wu-done-branch-only.js +191 -0
- package/dist/wu-done-messages.d.ts +119 -0
- package/dist/wu-done-messages.js +185 -0
- package/dist/wu-done-pr.d.ts +72 -0
- package/dist/wu-done-pr.js +174 -0
- package/dist/wu-done-retry-helpers.d.ts +85 -0
- package/dist/wu-done-retry-helpers.js +172 -0
- package/dist/wu-done-ui.d.ts +37 -0
- package/dist/wu-done-ui.js +69 -0
- package/dist/wu-done-validators.d.ts +411 -0
- package/dist/wu-done-validators.js +1229 -0
- package/dist/wu-done-worktree.d.ts +182 -0
- package/dist/wu-done-worktree.js +1097 -0
- package/dist/wu-helpers.d.ts +128 -0
- package/dist/wu-helpers.js +248 -0
- package/dist/wu-lint.d.ts +70 -0
- package/dist/wu-lint.js +234 -0
- package/dist/wu-paths.d.ts +171 -0
- package/dist/wu-paths.js +178 -0
- package/dist/wu-preflight-validators.d.ts +86 -0
- package/dist/wu-preflight-validators.js +251 -0
- package/dist/wu-recovery.d.ts +138 -0
- package/dist/wu-recovery.js +341 -0
- package/dist/wu-repair-core.d.ts +131 -0
- package/dist/wu-repair-core.js +669 -0
- package/dist/wu-schema-normalization.d.ts +17 -0
- package/dist/wu-schema-normalization.js +82 -0
- package/dist/wu-schema.d.ts +793 -0
- package/dist/wu-schema.js +881 -0
- package/dist/wu-spawn-helpers.d.ts +121 -0
- package/dist/wu-spawn-helpers.js +271 -0
- package/dist/wu-spawn.d.ts +158 -0
- package/dist/wu-spawn.js +1306 -0
- package/dist/wu-state-schema.d.ts +213 -0
- package/dist/wu-state-schema.js +156 -0
- package/dist/wu-state-store.d.ts +264 -0
- package/dist/wu-state-store.js +691 -0
- package/dist/wu-status-transition.d.ts +63 -0
- package/dist/wu-status-transition.js +382 -0
- package/dist/wu-status-updater.d.ts +25 -0
- package/dist/wu-status-updater.js +116 -0
- package/dist/wu-transaction-collectors.d.ts +116 -0
- package/dist/wu-transaction-collectors.js +272 -0
- package/dist/wu-transaction.d.ts +170 -0
- package/dist/wu-transaction.js +273 -0
- package/dist/wu-validation-constants.d.ts +60 -0
- package/dist/wu-validation-constants.js +66 -0
- package/dist/wu-validation.d.ts +118 -0
- package/dist/wu-validation.js +243 -0
- package/dist/wu-validator.d.ts +62 -0
- package/dist/wu-validator.js +325 -0
- package/dist/wu-yaml-fixer.d.ts +97 -0
- package/dist/wu-yaml-fixer.js +264 -0
- package/dist/wu-yaml.d.ts +86 -0
- package/dist/wu-yaml.js +222 -0
- 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
|
+
}
|